From 7957a4c018f729e47ce976fa89f065284b959a52 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 15 Mar 2017 15:38:10 -0700 Subject: [PATCH 1/6] Initial import of OSS OctoDNS --- .git_hooks_pre-commit | 11 + .github/CONTRIBUTING.md | 0 .gitignore | 11 + LICENSE | 22 + README.md | 219 + docs/assets/deploy.png | Bin 0 -> 82371 bytes docs/assets/noop.png | Bin 0 -> 75814 bytes docs/assets/pr.png | Bin 0 -> 212872 bytes octodns/__init__.py | 8 + octodns/cmds/__init__.py | 6 + octodns/cmds/args.py | 69 + octodns/cmds/compare.py | 29 + octodns/cmds/dump.py | 25 + octodns/cmds/report.py | 99 + octodns/cmds/sync.py | 37 + octodns/cmds/validate.py | 22 + octodns/manager.py | 309 ++ octodns/provider/__init__.py | 6 + octodns/provider/base.py | 116 + octodns/provider/cloudflare.py | 249 + octodns/provider/dnsimple.py | 349 ++ octodns/provider/dyn.py | 651 +++ octodns/provider/powerdns.py | 361 ++ octodns/provider/route53.py | 651 +++ octodns/provider/yaml.py | 82 + octodns/record.py | 549 +++ octodns/source/__init__.py | 6 + octodns/source/base.py | 33 + octodns/source/tinydns.py | 208 + octodns/yaml.py | 79 + octodns/zone.py | 117 + requirements-dev.txt | 6 + requirements.txt | 17 + script/bootstrap | 35 + script/cibuild | 30 + script/coverage | 30 + script/lint | 21 + script/sdist | 15 + script/test | 28 + setup.py | 46 + tests/config/bad-provider-class-module.yaml | 4 + .../config/bad-provider-class-no-module.yaml | 4 + tests/config/bad-provider-class.yaml | 4 + tests/config/empty.yaml | 1 + tests/config/missing-provider-class.yaml | 3 + tests/config/missing-provider-config.yaml | 4 + tests/config/missing-provider-env.yaml | 6 + tests/config/missing-sources.yaml | 3 + tests/config/no-dump.yaml | 13 + tests/config/simple-validate.yaml | 13 + tests/config/simple.yaml | 35 + tests/config/subzone.unit.tests.yaml | 10 + tests/config/unit.tests.yaml | 108 + tests/config/unknown-provider.yaml | 28 + tests/config/unordered.yaml | 8 + .../cloudflare-dns_records-page-1.json | 188 + .../cloudflare-dns_records-page-2.json | 116 + tests/fixtures/cloudflare-zones-page-1.json | 140 + tests/fixtures/cloudflare-zones-page-2.json | 140 + tests/fixtures/dnsimple-invalid-content.json | 106 + tests/fixtures/dnsimple-page-1.json | 314 ++ tests/fixtures/dnsimple-page-2.json | 138 + tests/fixtures/dyn-traffic-director-get.json | 4190 +++++++++++++++++ tests/fixtures/powerdns-full-data.json | 235 + tests/helpers.py | 69 + tests/test_octodns_manager.py | 203 + tests/test_octodns_provider_base.py | 170 + tests/test_octodns_provider_cloudflare.py | 273 ++ tests/test_octodns_provider_dnsimple.py | 202 + tests/test_octodns_provider_dyn.py | 1155 +++++ tests/test_octodns_provider_powerdns.py | 290 ++ tests/test_octodns_provider_route53.py | 1145 +++++ tests/test_octodns_provider_yaml.py | 111 + tests/test_octodns_record.py | 765 +++ tests/test_octodns_source_tinydns.py | 176 + tests/test_octodns_yaml.py | 61 + tests/test_octodns_zone.py | 174 + tests/zones/.is-needed-for-tests | Bin 0 -> 1024 bytes tests/zones/example.com | 48 + tests/zones/other.foo | 7 + 80 files changed, 15212 insertions(+) create mode 100755 .git_hooks_pre-commit create mode 100644 .github/CONTRIBUTING.md create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 docs/assets/deploy.png create mode 100644 docs/assets/noop.png create mode 100644 docs/assets/pr.png create mode 100644 octodns/__init__.py create mode 100644 octodns/cmds/__init__.py create mode 100644 octodns/cmds/args.py create mode 100755 octodns/cmds/compare.py create mode 100755 octodns/cmds/dump.py create mode 100755 octodns/cmds/report.py create mode 100755 octodns/cmds/sync.py create mode 100755 octodns/cmds/validate.py create mode 100644 octodns/manager.py create mode 100644 octodns/provider/__init__.py create mode 100644 octodns/provider/base.py create mode 100644 octodns/provider/cloudflare.py create mode 100644 octodns/provider/dnsimple.py create mode 100644 octodns/provider/dyn.py create mode 100644 octodns/provider/powerdns.py create mode 100644 octodns/provider/route53.py create mode 100644 octodns/provider/yaml.py create mode 100644 octodns/record.py create mode 100644 octodns/source/__init__.py create mode 100644 octodns/source/base.py create mode 100644 octodns/source/tinydns.py create mode 100644 octodns/yaml.py create mode 100644 octodns/zone.py create mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100755 script/bootstrap create mode 100755 script/cibuild create mode 100755 script/coverage create mode 100755 script/lint create mode 100755 script/sdist create mode 100755 script/test create mode 100644 setup.py create mode 100644 tests/config/bad-provider-class-module.yaml create mode 100644 tests/config/bad-provider-class-no-module.yaml create mode 100644 tests/config/bad-provider-class.yaml create mode 100644 tests/config/empty.yaml create mode 100644 tests/config/missing-provider-class.yaml create mode 100644 tests/config/missing-provider-config.yaml create mode 100644 tests/config/missing-provider-env.yaml create mode 100644 tests/config/missing-sources.yaml create mode 100644 tests/config/no-dump.yaml create mode 100644 tests/config/simple-validate.yaml create mode 100644 tests/config/simple.yaml create mode 100644 tests/config/subzone.unit.tests.yaml create mode 100644 tests/config/unit.tests.yaml create mode 100644 tests/config/unknown-provider.yaml create mode 100644 tests/config/unordered.yaml create mode 100644 tests/fixtures/cloudflare-dns_records-page-1.json create mode 100644 tests/fixtures/cloudflare-dns_records-page-2.json create mode 100644 tests/fixtures/cloudflare-zones-page-1.json create mode 100644 tests/fixtures/cloudflare-zones-page-2.json create mode 100644 tests/fixtures/dnsimple-invalid-content.json create mode 100644 tests/fixtures/dnsimple-page-1.json create mode 100644 tests/fixtures/dnsimple-page-2.json create mode 100644 tests/fixtures/dyn-traffic-director-get.json create mode 100644 tests/fixtures/powerdns-full-data.json create mode 100644 tests/helpers.py create mode 100644 tests/test_octodns_manager.py create mode 100644 tests/test_octodns_provider_base.py create mode 100644 tests/test_octodns_provider_cloudflare.py create mode 100644 tests/test_octodns_provider_dnsimple.py create mode 100644 tests/test_octodns_provider_dyn.py create mode 100644 tests/test_octodns_provider_powerdns.py create mode 100644 tests/test_octodns_provider_route53.py create mode 100644 tests/test_octodns_provider_yaml.py create mode 100644 tests/test_octodns_record.py create mode 100644 tests/test_octodns_source_tinydns.py create mode 100644 tests/test_octodns_yaml.py create mode 100644 tests/test_octodns_zone.py create mode 100644 tests/zones/.is-needed-for-tests create mode 100644 tests/zones/example.com create mode 100644 tests/zones/other.foo diff --git a/.git_hooks_pre-commit b/.git_hooks_pre-commit new file mode 100755 index 0000000..9cb854a --- /dev/null +++ b/.git_hooks_pre-commit @@ -0,0 +1,11 @@ +#!/bin/sh + +set -e + +HOOKS=`dirname $0` +GIT=`dirname $HOOKS` +ROOT=`dirname $GIT` + +source $ROOT/env/bin/activate +$ROOT/script/lint +$ROOT/script/test diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..842a688 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.pyc +.coverage +.env +coverage.xml +dist/ +env/ +htmlcov/ +nosetests.xml +octodns.egg-info/ +output/ +tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..acc8e6f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2017 GitHub, Inc. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index e69de29..30c70c4 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,219 @@ +# OctoDns + +## DNS as code - Tools for managing DNS across multiple providers + +In the vein of [infrastructure as +code](https://en.wikipedia.org/wiki/Infrastructure_as_Code) OctoDNS provides a set of tools & patterns that make it easy to manage your DNS records across multiple providers. The resulting config can live in a repository and be [deployed](https://github.com/blog/1241-deploying-at-github) just like the rest of your code, maintaining a clear history and using your existing review & workflow. + +The architecture is pluggable and tooling flexible intending to be applicable to a wide variety of use-cases. Effort has been made to make adding new providers as easy as possible. In the simple case that involves writing of a single `class` and a couple hundred lines of code, most of which is translating between the provider's schema and OctoDNS's. More on some of the ways we use it and how to go about extending it below and in the [/docs directory](/docs). + +It is similar to [Netflix/denominator](https://github.com/Netflix/denominator). + +## Getting started + +### Workspace + +Running through the following commands will install the latest release of OctoDNS and set up a place for your config files to live. + +``` +$ mkdir dns +$ cd dns +$ virtualenv env +... +$ source env/bin/activate +$ pip install octodns +$ mkdir config +``` + +### Config + +So first we need to create the primary config file to tell OctoDns about our providers and the zone(s) we want it to manage. Below we're setting up a `YamlProvider` to source records from our config files and both a `Route53Provider` and `DynProvider` to serve as the targets for those records. You can have any number of zones set up and any number of sources of data and targets for records for each. You can also have multiple config files, that make use of separate accounts and each manage a distinct set of zones. A good example of this this might be `./config/staging.yaml` & `./config/production.yaml`. We'll focus on a `config/production.yaml`. + +```yaml +--- +providers: + config: + class: octodns.provider.yaml.YamlProvider + directory: ./config + dyn: + class: octodns.provider.dyn.DynProvider + customer: 1234 + username: 'username' + password: env/DYN_PASSWORD + route53: + class: octodns.provider.route53.Route53Provider + access_key_id: env/AWS_ACCESS_KEY_ID + secret_access_key: env/AWS_SECRET_ACCESS_KEY + +zones: + github.com.: + sources: + - config + targets: + - dyn + - route53 +``` + +`class` is a special key that tells OctoDNS what python class should be loaded. Any other keys will be passed as configuration values to that provider. In general any sensitive or frequently rotated values should come from environmental variables. When OctoDns sees a value that starts with `env/` it will look for that value in the process's environment and pass the result along. + +So now that we have something to tell OctoDns about our providers & zones we need to tell it about or records. We'll keep it simple for now and just create a single `A` record at the top-level of the domain. + +`config/github.com.yaml` + +```yaml +--- +'': + ttl: 60 + type: A + values: + - 1.2.3.4 + - 1.2.3.5 +``` + +### Noop + +So now we're ready to do a dry-run with our new setup to see what changes it would make. Since we're pretending here we'll act like there are no existing records for `github.com.` in our accounts on either provider. + +``` +$ octodns-sync --config-file=./config/production.yaml +... +******************************************************************************** +* github.com. +******************************************************************************** +* route53 (Route53Provider) +* Create +* Summary: Creates=1, Updates=0, Deletes=0, Existing Records=0 +* dyn (DynProvider) +* Create +* Summary: Creates=1, Updates=0, Deletes=0, Existing Records=0 +******************************************************************************** +... +``` + +There will be other logging information presented on the screen, but successful runs of sync will always end with a summary like the above for any providers & zones with changes. If there are no changes a message saying so will be printed instead. Above we're creating a new zone in both providers so they show the same change, but that doesn't always have to be the case. If to start one of them had a different state you would see the changes OctoDns intends to make to sync them up. + +### Making changes + +**WARNING**: OctoDns assumes ownership of any domain you point it to. When you tell it to act it will do whatever is necessary to try and match up states including deleting any unexpected records. Be careful when playing around with OctoDNS. It's best to experiment with a fake zone or one without any data that matters until your comfortable with the system. + +Now it's time to tell OctoDns to make things happen. We'll invoke it again with the same options and add a `--doit` on the end to tell it this time we actually want it to try and make the specified changes. + +``` +$ octodns-sync --config-file=./config/production.yaml --doit +... +``` + +The output here would be the same as before with a few more log lines at the end as it makes the actual changes. After which the config in Route53 and Dyn should match what's in the yaml file. + +### Workflow + +In the above case we manually ran OctoDns from the command line. That works and it's a lot better than heading into the provider GUIs and making changes by clicking around, but OctoDns is designed to be run as part of a deploy process. The implementation of that is well beyond the scope of this README, but I will walk through the process we use. It works a lot like how we [branch deploy GitHub](https://githubengineering.com/deploying-branches-to-github-com/). + +The first step is to create a PR with your changes. + +![](/docs/assets/pr.png) + +Assuming the code tests and config validation statuses are green the next step is to do a noop deploy and verify that the changes OctoDns plans to make are the ones you expect. + +![](/docs/assets/noop.png) + +After that comes a set of reviews. One from a teammate who should have full context on what you're trying to accomplish and visibility in to the changes you're making to do it. The other is from a member of the team here at GitHub that owns DNS, mostly as a sanity check and to make sure that best practices are being followed. As much of that as possible is baked into `octodns-validate`. + +After the reviews it's time to branch deploy the change. + +![](/docs/assets/deploy.png) + +If that goes smoothly, you again see the expected changes, and verify them with `dig` and/or `octodns-report` you're good to hit the merge button. If there are problems you can quickly do a `.deploy dns/master` to go back to the previous state. + +### Bootstrapping config files + +Very few situations will involve starting with a blank slate which is why there's tooling built in to pull existing data out of providers into a matching config file. + +``` +$ octodns-dump --config-file=config/production.yaml --output-dir=tmp/ github.com. route53 +2017-03-15T13:33:34 INFO Manager __init__: config_file=tmp/production.yaml +2017-03-15T13:33:34 INFO Manager dump: zone=github.com., sources=('route53',) +2017-03-15T13:33:36 INFO Route53Provider[route53] populate: found 64 records +2017-03-15T13:33:36 INFO YamlProvider[dump] plan: desired=github.com. +2017-03-15T13:33:36 INFO YamlProvider[dump] plan: Creates=64, Updates=0, Deletes=0, Existing Records=0 +2017-03-15T13:33:36 INFO YamlProvider[dump] apply: making changes +``` + +The above command pulled the existing data out of Route53 and placed the results into `tmp/github.com.yaml`. That file can be inspected and moved into `config/` to become the new source. If things are working as designed a subsequent noop sync should show zero changes. + +## Custom Sources and Providers + +You can check out the [source](/octodns/source/) and [provider](/octodns/provider/) directory to see what's currently supported. Sources as the name implies can only act as a source of record information. TinyDnsProvider is currently the only OSS source, though we have several others internally that are specific to our environment. These include something to pull host data from [gPanel](https://githubengineering.com/githubs-metal-cloud/) and a similar provider that sources information about our network gear to create both `A` & `PTR` records for their interfaces. Things that might make good OSS sources might include an `ElbSource` that pulls information about [AWS Elastic Load Balancers](https://aws.amazon.com/elasticloadbalancing/) and dynamically creates `CNAME`s for them, or `Ec2Source` that pulls instance information so that records can be created for hosts similar to our our `GPanelProvider` works. An `AxfrSource` could be really interesting as well. Another case where a source may make sense is if you'd like to export data from a legacy service that you have no plans to push changes back into. + +Most of the things included in OctoDNS are providers, the obvious difference being that they can serve as both sources and targets of data. We'd really like to see this list grow over time so if you use an unsupported provider PRs welcome. The existing providers should serve as reasonable examples. Those that have no GeoDNS support are relatively straightforward. Unfortunately most of the APIs involved to do GeoDNS style traffic management are complex and somewhat inconsistent so adding support for that function would be nice, but is optional and best done in a separate pass. + +The `class` key in the providers config section can be used to point to arbitrary classes in the python path so internal or 3rd party providers can easily be included with no coordiation beyond getting them into PYTHONPATH, most likely installed into the virtualenv with OctoDNS. + +## Other Uses + +### Syncing between providers + +While the primary use-case is to sync a set of yaml config files up to one or more DNS providers the piece of OctoDNS have been built in such a way that you can easily source and target things arbitrarily. As a quick example the config below would sync `githubtest.net.` from Route53 to Dyn. + +```yaml +--- +providers: + route53: + class: octodns.provider.route53.Route53Provider + access_key_id: env/AWS_ACCESS_KEY_ID + secret_access_key: env/AWS_SECRET_ACCESS_KEY + dyn: + class: octodns.provider.dyn.DynProvider + customer: env/DYN_CUSTOMER + username: env/DYN_USERNAME + password: env/DYN_PASSWORD + +zones: + + githubtest.net.: + sources: + - route53 + targets: + - dyn +``` + +### Dynamic sources + +Internally we use custom sources to create records based on dynamic data that changes frequently without direct human intervention. An example of that might look something like the following. For hosts this mechanisms is janitorial, run periodically, making sure the correct records exists so long as the host is alive and ensuring they go away after the host does. The host provisioning and destruction processes do the actual work to create and destroy the records. + +```yaml +--- +providers: + gpanel-site: + class: github.octodns.source.gpanel.GPanelProvider + host: 'gpanel.site.github.foo' + token: env/GPANEL_SITE_TOKEN + powerdns-site: + class: octodns.provider.powerdns.PowerDnsProvider + host: 'internal-dns.site.github.foo' + api_key: env/POWERDNS_SITE_API_KEY + +zones: + + hosts.site.github.foo.: + sources: + - gpanel-site + targets: + - powerdns-site +``` + +## Contributing + +Please see our [contributing document](/.github/CONTRIBUTING.md) if you would like to participate! + +## Getting help + +If you have a problem or suggestion, please [open an issue](https://github.com/github/octodns/issues/new) in this repository, and we will do our best to help. Please note that this project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#GitHub%20OctoDNS/opensource@github.com). + +## License + +OctoDNS is licensed under the [MIT license](LICENSE). + +## Authors + +OctoDNS was designed and authored by [Ross McFarland](https://github.com/ross) and is now maintained, reviewed, and tested by Ross and the rest of the Site Reliability Engineering team at GitHub. diff --git a/docs/assets/deploy.png b/docs/assets/deploy.png new file mode 100644 index 0000000000000000000000000000000000000000..701eff524d53d74b168e4aa4a1ecbc2c77ecdc61 GIT binary patch literal 82371 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

W}}4EX9K)h)_JiHiBz@UV8w4Y(E2ZfLuYFGBf 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 Q8Ua7gB;=kHKGOI5FORwHRsaA1 literal 0 HcmV?d00001 diff --git a/docs/assets/noop.png b/docs/assets/noop.png new file mode 100644 index 0000000000000000000000000000000000000000..42c3722bae71ecb12ee3e9559c468cfef0371d94 GIT binary patch literal 75814 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 OcwjNdjb9#i2>TCaY4SJ# literal 0 HcmV?d00001 diff --git a/docs/assets/pr.png b/docs/assets/pr.png new file mode 100644 index 0000000000000000000000000000000000000000..463e2ea7067cf7abf95959b64e04a6ab35a2aa8c GIT binary patch literal 212872 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 literal 0 HcmV?d00001 diff --git a/octodns/__init__.py b/octodns/__init__.py new file mode 100644 index 0000000..4806766 --- /dev/null +++ b/octodns/__init__.py @@ -0,0 +1,8 @@ +''' +OctoDNS: DNS as code - Tools for managing DNS across multiple providers +''' + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +__VERSION__ = '0.8.0' diff --git a/octodns/cmds/__init__.py b/octodns/cmds/__init__.py new file mode 100644 index 0000000..14ccf18 --- /dev/null +++ b/octodns/cmds/__init__.py @@ -0,0 +1,6 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/octodns/cmds/args.py b/octodns/cmds/args.py new file mode 100644 index 0000000..daec5c9 --- /dev/null +++ b/octodns/cmds/args.py @@ -0,0 +1,69 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from argparse import ArgumentParser as _Base +from logging import DEBUG, INFO, WARN, Formatter, StreamHandler, \ + getLogger +from logging.handlers import SysLogHandler +from sys import stderr, stdout + + +class ArgumentParser(_Base): + ''' + Manages argument parsing and adds some defaults and takes action on them. + + Also manages logging setup. + ''' + + def __init__(self, *args, **kwargs): + super(ArgumentParser, self).__init__(*args, **kwargs) + + def parse_args(self, default_log_level=INFO): + self.add_argument('--log-stream-stdout', action='store_true', + default=False, + help='Log to stdout instead of stderr') + _help = 'Send logging data to syslog in addition to stderr' + self.add_argument('--log-syslog', action='store_true', default=False, + help=_help) + self.add_argument('--syslog-device', default='/dev/log', + help='Syslog device') + self.add_argument('--syslog-facility', default='local0', + help='Syslog facility') + + _help = 'Increase verbosity to get details and help track down issues' + self.add_argument('--debug', action='store_true', default=False, + help=_help) + + args = super(ArgumentParser, self).parse_args() + self._setup_logging(args, default_log_level) + return args + + def _setup_logging(self, args, default_log_level): + # TODO: if/when things are multi-threaded add [%(thread)d] in to the + # format + fmt = '%(asctime)s %(levelname)-5s %(name)s %(message)s' + formatter = Formatter(fmt=fmt, datefmt='%Y-%m-%dT%H:%M:%S ') + stream = stdout if args.log_stream_stdout else stderr + handler = StreamHandler(stream=stream) + handler.setFormatter(formatter) + logger = getLogger() + logger.addHandler(handler) + + if args.log_syslog: + fmt = 'octodns[%(process)-5s:%(thread)d]: %(name)s ' \ + '%(levelname)-5s %(message)s' + handler = SysLogHandler(address=args.syslog_device, + facility=args.syslog_facility) + handler.setFormatter(Formatter(fmt=fmt)) + logger.addHandler(handler) + + logger.level = DEBUG if args.debug else default_log_level + + # boto is noisy, set it to warn + getLogger('botocore').level = WARN + # DynectSession is noisy too + getLogger('DynectSession').level = WARN diff --git a/octodns/cmds/compare.py b/octodns/cmds/compare.py new file mode 100755 index 0000000..b05ca9c --- /dev/null +++ b/octodns/cmds/compare.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +''' +Octo-DNS Comparator +''' + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from pprint import pprint + +from octodns.cmds.args import ArgumentParser +from octodns.manager import Manager + +parser = ArgumentParser(description=__doc__.split('\n')[1]) + +parser.add_argument('--config-file', required=True, + help='The Manager configuration file to use') +parser.add_argument('--a', nargs='+', required=True, + help='First source(s) to pull data from') +parser.add_argument('--b', nargs='+', required=True, + help='Second source(s) to pull data from') +parser.add_argument('--zone', default=None, required=True, + help='Zone to compare') + +args = parser.parse_args() + +manager = Manager(args.config_file) +changes = manager.compare(args.a, args.b, args.zone) +pprint(changes) diff --git a/octodns/cmds/dump.py b/octodns/cmds/dump.py new file mode 100755 index 0000000..b58a59d --- /dev/null +++ b/octodns/cmds/dump.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +''' +Octo-DNS Dumper +''' + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from octodns.cmds.args import ArgumentParser +from octodns.manager import Manager + +parser = ArgumentParser(description=__doc__.split('\n')[1]) + +parser.add_argument('--config-file', required=True, + help='The Manager configuration file to use') +parser.add_argument('--output-dir', required=True, + help='The directory into which the results will be ' + 'written (Note: will overwrite existing files)') +parser.add_argument('zone', help='Zone to dump') +parser.add_argument('source', nargs='+', help='Source(s) to pull data from') + +args = parser.parse_args() + +manager = Manager(args.config_file) +manager.dump(args.zone, args.output_dir, *args.source) diff --git a/octodns/cmds/report.py b/octodns/cmds/report.py new file mode 100755 index 0000000..062e5e7 --- /dev/null +++ b/octodns/cmds/report.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +''' +Octo-DNS Reporter +''' + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from concurrent.futures import ThreadPoolExecutor +from dns.exception import Timeout +from dns.resolver import NXDOMAIN, NoAnswer, NoNameservers, Resolver, query +from logging import getLogger +from sys import stdout +import re + +from octodns.cmds.args import ArgumentParser +from octodns.manager import Manager +from octodns.zone import Zone + + +class AsyncResolver(Resolver): + + def __init__(self, num_workers, *args, **kwargs): + super(AsyncResolver, self).__init__(*args, **kwargs) + self.executor = ThreadPoolExecutor(max_workers=num_workers) + + def query(self, *args, **kwargs): + return self.executor.submit(super(AsyncResolver, self).query, *args, + **kwargs) + + +parser = ArgumentParser(description=__doc__.split('\n')[1]) + +parser.add_argument('--config-file', required=True, + help='The Manager configuration file to use') +parser.add_argument('--zone', required=True, help='Zone to dump') +parser.add_argument('--source', required=True, default=[], action='append', + help='Source(s) to pull data from') +parser.add_argument('--num-workers', default=4, + help='Number of background workers') +parser.add_argument('--timeout', default=1, + help='Number seconds to wait for an answer') +parser.add_argument('server', nargs='+', help='Servers to query') + +args = parser.parse_args() + +manager = Manager(args.config_file) + +log = getLogger('report') + +try: + sources = [manager.providers[source] for source in args.source] +except KeyError as e: + raise Exception('Unknown source: {}'.format(e.args[0])) + +zone = Zone(args.zone, manager.configured_sub_zones(args.zone)) +for source in sources: + source.populate(zone) + +print('name,type,ttl,{},consistent'.format(','.join(args.server))) +resolvers = [] +ip_addr_re = re.compile(r'^[\d\.]+$') +for server in args.server: + resolver = AsyncResolver(configure=False, + num_workers=int(args.num_workers)) + if not ip_addr_re.match(server): + server = str(query(server, 'A')[0]) + log.info('server=%s', server) + resolver.nameservers = [server] + resolver.lifetime = int(args.timeout) + resolvers.append(resolver) + +queries = {} +for record in sorted(zone.records): + queries[record] = [r.query(record.fqdn, record._type) + for r in resolvers] + +for record, futures in sorted(queries.items(), key=lambda d: d[0]): + stdout.write(record.fqdn) + stdout.write(',') + stdout.write(record._type) + stdout.write(',') + stdout.write(str(record.ttl)) + compare = {} + for future in futures: + stdout.write(',') + try: + answers = [str(r) for r in future.result()] + except (NoAnswer, NoNameservers): + answers = ['*no answer*'] + except NXDOMAIN: + answers = ['*does not exist*'] + except Timeout: + answers = ['*timeout*'] + stdout.write(' '.join(answers)) + # sorting to ignore order + answers = '*:*'.join(sorted(answers)).lower() + compare[answers] = True + stdout.write(',True\n' if len(compare) == 1 else ',False\n') diff --git a/octodns/cmds/sync.py b/octodns/cmds/sync.py new file mode 100755 index 0000000..5002743 --- /dev/null +++ b/octodns/cmds/sync.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +''' +Octo-DNS Multiplexer +''' + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from octodns.cmds.args import ArgumentParser +from octodns.manager import Manager + +parser = ArgumentParser(description=__doc__.split('\n')[1]) + +parser.add_argument('--config-file', required=True, + help='The Manager configuration file to use') +parser.add_argument('--doit', action='store_true', default=False, + help='Whether to take action or just show what would ' + 'change') +parser.add_argument('--force', action='store_true', default=False, + help='Acknowledge that significant changes are being made ' + 'and do them') + +parser.add_argument('zone', nargs='*', default=[], + help='Limit sync to the specified zone(s)') + +# --sources isn't an option here b/c filtering sources out would be super +# dangerous since you could eaily end up with an empty zone and delete +# everything, or even just part of things when there are multiple sources + +parser.add_argument('--target', default=[], action='append', + help='Limit sync to the specified target(s)') + +args = parser.parse_args() + +manager = Manager(args.config_file) +manager.sync(eligible_zones=args.zone, eligible_targets=args.target, + dry_run=not args.doit, force=args.force) diff --git a/octodns/cmds/validate.py b/octodns/cmds/validate.py new file mode 100755 index 0000000..576e96e --- /dev/null +++ b/octodns/cmds/validate.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +''' +Octo-DNS Validator +''' + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from logging import WARN + +from octodns.cmds.args import ArgumentParser +from octodns.manager import Manager + +parser = ArgumentParser(description=__doc__.split('\n')[1]) + +parser.add_argument('--config-file', default='./config/production.yaml', + help='The Manager configuration file to use') + +args = parser.parse_args(WARN) + +manager = Manager(args.config_file) +manager.validate_configs() diff --git a/octodns/manager.py b/octodns/manager.py new file mode 100644 index 0000000..684ed07 --- /dev/null +++ b/octodns/manager.py @@ -0,0 +1,309 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from StringIO import StringIO +from importlib import import_module +from os import environ +import logging + +from .provider.base import BaseProvider +from .provider.yaml import YamlProvider +from .yaml import safe_load +from .zone import Zone + + +class _AggregateTarget(object): + id = 'aggregate' + + def __init__(self, targets): + self.targets = targets + + def supports(self, record): + for target in self.targets: + if not target.supports(record): + return False + return True + + @property + def SUPPORTS_GEO(self): + for target in self.targets: + if not target.SUPPORTS_GEO: + return False + return True + + +class Manager(object): + log = logging.getLogger('Manager') + + def __init__(self, config_file): + self.log.info('__init__: config_file=%s', config_file) + + # Read our config file + with open(config_file, 'r') as fh: + self.config = safe_load(fh, enforce_order=False) + + self.log.debug('__init__: configuring providers') + self.providers = {} + for provider_name, provider_config in self.config['providers'].items(): + # Get our class and remove it from the provider_config + try: + _class = provider_config.pop('class') + except KeyError: + raise Exception('Provider {} is missing class' + .format(provider_name)) + _class = self._get_provider_class(_class) + # Build up the arguments we need to pass to the provider + kwargs = {} + for k, v in provider_config.items(): + try: + if v.startswith('env/'): + try: + env_var = v[4:] + v = environ[env_var] + except KeyError: + raise Exception('Incorrect provider config, ' + 'missing env var {}' + .format(env_var)) + except AttributeError: + pass + kwargs[k] = v + try: + self.providers[provider_name] = _class(provider_name, **kwargs) + except TypeError: + raise Exception('Incorrect provider config for {}' + .format(provider_name)) + + zone_tree = {} + # sort by reversed strings so that parent zones always come first + for name in sorted(self.config['zones'].keys(), key=lambda s: s[::-1]): + # ignore trailing dots, and reverse + pieces = name[:-1].split('.')[::-1] + # where starts out at the top + where = zone_tree + # for all the pieces + for piece in pieces: + try: + where = where[piece] + # our current piece already exists, just point where at + # it's value + except KeyError: + # our current piece doesn't exist, create it + where[piece] = {} + # and then point where at it's newly created value + where = where[piece] + self.zone_tree = zone_tree + + def _get_provider_class(self, _class): + try: + module_name, class_name = _class.rsplit('.', 1) + module = import_module(module_name) + except (ImportError, ValueError): + self.log.error('_get_provider_class: Unable to import module %s', + _class) + raise Exception('Unknown provider class: {}'.format(_class)) + try: + return getattr(module, class_name) + except AttributeError: + self.log.error('_get_provider_class: Unable to get class %s from ' + 'module %s', class_name, module) + raise Exception('Unknown provider class: {}'.format(_class)) + + def configured_sub_zones(self, zone_name): + # Reversed pieces of the zone name + pieces = zone_name[:-1].split('.')[::-1] + # Point where at the root of the tree + where = self.zone_tree + # Until we've hit the bottom of this zone + try: + while pieces: + # Point where at the value of our current piece + where = where[pieces.pop(0)] + except KeyError: + self.log.debug('configured_sub_zones: unknown zone, %s, no subs', + zone_name) + return set() + # We're not pointed at the dict for our name, the keys of which will be + # any subzones + sub_zone_names = where.keys() + self.log.debug('configured_sub_zones: subs=%s', sub_zone_names) + return set(sub_zone_names) + + def sync(self, eligible_zones=[], eligible_targets=[], dry_run=True, + force=False): + self.log.info('sync: eligible_zones=%s, eligible_targets=%s, ' + 'dry_run=%s, force=%s', eligible_zones, eligible_targets, + dry_run, force) + + zones = self.config['zones'].items() + if eligible_zones: + zones = filter(lambda d: d[0] in eligible_zones, zones) + + plans = [] + for zone_name, config in zones: + self.log.info('sync: zone=%s', zone_name) + try: + sources = config['sources'] + except KeyError: + raise Exception('Zone {} is missing sources'.format(zone_name)) + + try: + targets = config['targets'] + except KeyError: + raise Exception('Zone {} is missing targets'.format(zone_name)) + if eligible_targets: + targets = filter(lambda d: d in eligible_targets, targets) + + self.log.info('sync: sources=%s -> targets=%s', sources, targets) + + try: + sources = [self.providers[source] for source in sources] + except KeyError: + raise Exception('Zone {}, unknown source: {}'.format(zone_name, + source)) + + try: + trgs = [] + for target in targets: + trg = self.providers[target] + if not isinstance(trg, BaseProvider): + raise Exception('{} - "{}" does not support targeting' + .format(trg, target)) + trgs.append(trg) + targets = trgs + except KeyError: + raise Exception('Zone {}, unknown target: {}'.format(zone_name, + target)) + + self.log.debug('sync: populating') + zone = Zone(zone_name, + sub_zones=self.configured_sub_zones(zone_name)) + for source in sources: + source.populate(zone) + + self.log.debug('sync: planning') + for target in targets: + plan = target.plan(zone) + if plan: + plans.append((target, plan)) + + hr = '*************************************************************' \ + '*******************\n' + buf = StringIO() + buf.write('\n') + if plans: + current_zone = None + for target, plan in plans: + if plan.desired.name != current_zone: + current_zone = plan.desired.name + buf.write(hr) + buf.write('* ') + buf.write(current_zone) + buf.write('\n') + buf.write(hr) + + buf.write('* ') + buf.write(target.id) + buf.write(' (') + buf.write(target) + buf.write(')\n* ') + for change in plan.changes: + buf.write(change.__repr__(leader='* ')) + buf.write('\n* ') + + buf.write('Summary: ') + buf.write(plan) + buf.write('\n') + else: + buf.write(hr) + buf.write('No changes were planned\n') + buf.write(hr) + buf.write('\n') + self.log.info(buf.getvalue()) + + if not force: + self.log.debug('sync: checking safety') + for target, plan in plans: + plan.raise_if_unsafe() + + if dry_run or config.get('always-dry-run', False): + return 0 + + total_changes = 0 + self.log.debug('sync: applying') + for target, plan in plans: + total_changes += target.apply(plan) + + self.log.info('sync: %d total changes', total_changes) + return total_changes + + def compare(self, a, b, zone): + ''' + Compare zone data between 2 sources. + + Note: only things supported by both sources will be considered + ''' + self.log.info('compare: a=%s, b=%s, zone=%s', a, b, zone) + + try: + a = [self.providers[source] for source in a] + b = [self.providers[source] for source in b] + except KeyError as e: + raise Exception('Unknown source: {}'.format(e.args[0])) + + sub_zones = self.configured_sub_zones(zone) + za = Zone(zone, sub_zones) + for source in a: + source.populate(za) + + zb = Zone(zone, sub_zones) + for source in b: + source.populate(zb) + + return zb.changes(za, _AggregateTarget(a + b)) + + def dump(self, zone, output_dir, source, *sources): + ''' + Dump zone data from the specified source + ''' + self.log.info('dump: zone=%s, sources=%s', zone, sources) + + # We broke out source to force at least one to be passed, add it to any + # others we got. + sources = [source] + list(sources) + + try: + sources = [self.providers[s] for s in sources] + except KeyError as e: + raise Exception('Unknown source: {}'.format(e.args[0])) + + target = YamlProvider('dump', output_dir) + + zone = Zone(zone, self.configured_sub_zones(zone)) + for source in sources: + source.populate(zone) + + plan = target.plan(zone) + target.apply(plan) + + def validate_configs(self): + for zone_name, config in self.config['zones'].items(): + zone = Zone(zone_name, self.configured_sub_zones(zone_name)) + + try: + sources = config['sources'] + except KeyError: + raise Exception('Zone {} is missing sources'.format(zone_name)) + + try: + sources = [self.providers[source] for source in sources] + except KeyError: + raise Exception('Zone {}, unknown source: {}'.format(zone_name, + source)) + + for source in sources: + if isinstance(source, YamlProvider): + source.populate(zone) diff --git a/octodns/provider/__init__.py b/octodns/provider/__init__.py new file mode 100644 index 0000000..14ccf18 --- /dev/null +++ b/octodns/provider/__init__.py @@ -0,0 +1,6 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/octodns/provider/base.py b/octodns/provider/base.py new file mode 100644 index 0000000..d839d59 --- /dev/null +++ b/octodns/provider/base.py @@ -0,0 +1,116 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from ..source.base import BaseSource +from ..zone import Zone + + +class UnsafePlan(Exception): + pass + + +class Plan(object): + MAX_SAFE_UPDATES = 4 + MAX_SAFE_DELETES = 4 + + def __init__(self, existing, desired, changes): + self.existing = existing + self.desired = desired + self.changes = changes + + change_counts = { + 'Create': 0, + 'Delete': 0, + 'Update': 0 + } + for change in changes: + change_counts[change.__class__.__name__] += 1 + self.change_counts = change_counts + + def raise_if_unsafe(self): + # TODO: what is safe really? + if self.change_counts['Update'] > self.MAX_SAFE_UPDATES: + raise UnsafePlan('Too many updates') + if self.change_counts['Delete'] > self.MAX_SAFE_DELETES: + raise UnsafePlan('Too many deletes') + + def __repr__(self): + return 'Creates={}, Updates={}, Deletes={}, Existing Records={}' \ + .format(self.change_counts['Create'], self.change_counts['Update'], + self.change_counts['Delete'], + len(self.existing.records)) + + +class BaseProvider(BaseSource): + + def __init__(self, id, apply_disabled=False): + super(BaseProvider, self).__init__(id) + self.log.debug('__init__: id=%s, apply_disabled=%s', id, + apply_disabled) + self.apply_disabled = apply_disabled + + def _include_change(self, change): + ''' + An opportunity for providers to filter out false positives due to + pecularities in their implementation. E.g. minimum TTLs. + ''' + return True + + def _extra_changes(self, existing, changes): + ''' + An opportunity for providers to add extra changes to the plan that are + necessary to update ancilary record data or configure the zone. E.g. + base NS records. + ''' + return [] + + def plan(self, desired): + self.log.info('plan: desired=%s', desired.name) + + existing = Zone(desired.name, desired.sub_zones) + self.populate(existing, target=True) + + # compute the changes at the zone/record level + changes = existing.changes(desired, self) + + # allow the provider to filter out false positives + before = len(changes) + changes = filter(self._include_change, changes) + after = len(changes) + if before != after: + self.log.info('plan: filtered out %s changes', before - after) + + # allow the provider to add extra changes it needs + extra = self._extra_changes(existing, changes) + if extra: + self.log.info('plan: extra changes\n %s', '\n ' + .join([str(c) for c in extra])) + changes += extra + + if changes: + plan = Plan(existing, desired, changes) + self.log.info('plan: %s', plan) + return plan + self.log.info('plan: No changes') + return None + + def apply(self, plan): + ''' + Submits actual planned changes to the provider. Returns the number of + changes made + ''' + if self.apply_disabled: + self.log.info('apply: disabled') + return 0 + + self.log.info('apply: making changes') + self._apply(plan) + return len(plan.changes) + + def _apply(self, plan): + raise NotImplementedError('Abstract base class, _apply method ' + 'missing') diff --git a/octodns/provider/cloudflare.py b/octodns/provider/cloudflare.py new file mode 100644 index 0000000..95df47a --- /dev/null +++ b/octodns/provider/cloudflare.py @@ -0,0 +1,249 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from collections import defaultdict +from logging import getLogger +from requests import Session + +from ..record import Record, Update +from .base import BaseProvider + + +class CloudflareAuthenticationError(Exception): + + def __init__(self, data): + try: + message = data['errors'][0]['message'] + except (IndexError, KeyError): + message = 'Authentication error' + super(CloudflareAuthenticationError, self).__init__(message) + + +class CloudflareProvider(BaseProvider): + SUPPORTS_GEO = False + # TODO: support SRV + UNSUPPORTED_TYPES = ('NAPTR', 'PTR', 'SOA', 'SRV', 'SSHFP') + + MIN_TTL = 120 + TIMEOUT = 15 + + def __init__(self, id, email, token, *args, **kwargs): + self.log = getLogger('CloudflareProvider[{}]'.format(id)) + self.log.debug('__init__: id=%s, email=%s, token=***', id, email) + super(CloudflareProvider, self).__init__(id, *args, **kwargs) + + sess = Session() + sess.headers.update({ + 'X-Auth-Email': email, + 'X-Auth-Key': token, + }) + self._sess = sess + + self._zones = None + self._zone_records = {} + + def supports(self, record): + return record._type not in self.UNSUPPORTED_TYPES + + def _request(self, method, path, params=None, data=None): + self.log.debug('_request: method=%s, path=%s', method, path) + + url = 'https://api.cloudflare.com/client/v4{}'.format(path) + resp = self._sess.request(method, url, params=params, json=data, + timeout=self.TIMEOUT) + self.log.debug('_request: status=%d', resp.status_code) + if resp.status_code == 403: + raise CloudflareAuthenticationError(resp.json()) + resp.raise_for_status() + return resp.json() + + @property + def zones(self): + if self._zones is None: + page = 1 + zones = [] + while page: + resp = self._request('GET', '/zones', params={'page': page}) + zones += resp['result'] + info = resp['result_info'] + if info['count'] > 0 and info['count'] == info['per_page']: + page += 1 + else: + page = None + + self._zones = {'{}.'.format(z['name']): z['id'] for z in zones} + + return self._zones + + def _data_for_multiple(self, _type, records): + return { + 'ttl': records[0]['ttl'], + 'type': _type, + 'values': [r['content'] for r in records], + } + + _data_for_A = _data_for_multiple + _data_for_AAAA = _data_for_multiple + _data_for_SPF = _data_for_multiple + _data_for_TXT = _data_for_multiple + + def _data_for_CNAME(self, _type, records): + only = records[0] + return { + 'ttl': only['ttl'], + 'type': _type, + 'value': '{}.'.format(only['content']) + } + + def _data_for_MX(self, _type, records): + values = [] + for r in records: + values.append({ + 'priority': r['priority'], + 'value': '{}.'.format(r['content']), + }) + return { + 'ttl': records[0]['ttl'], + 'type': _type, + 'values': values, + } + + def _data_for_NS(self, _type, records): + return { + 'ttl': records[0]['ttl'], + 'type': _type, + 'values': ['{}.'.format(r['content']) for r in records], + } + + def zone_records(self, zone): + if zone.name not in self._zone_records: + zone_id = self.zones.get(zone.name, False) + if not zone_id: + return [] + + records = [] + path = '/zones/{}/dns_records'.format(zone_id) + page = 1 + while page: + resp = self._request('GET', path, params={'page': page}) + records += resp['result'] + info = resp['result_info'] + if info['count'] > 0 and info['count'] == info['per_page']: + page += 1 + else: + page = None + + self._zone_records[zone.name] = records + + return self._zone_records[zone.name] + + def populate(self, zone, target=False): + self.log.debug('populate: name=%s', zone.name) + + before = len(zone.records) + records = self.zone_records(zone) + if records: + values = defaultdict(lambda: defaultdict(list)) + for record in records: + name = zone.hostname_from_fqdn(record['name']) + _type = record['type'] + if _type not in self.UNSUPPORTED_TYPES: + values[name][record['type']].append(record) + + for name, types in values.items(): + for _type, records in types.items(): + data_for = getattr(self, '_data_for_{}'.format(_type)) + data = data_for(_type, records) + record = Record.new(zone, name, data, source=self) + zone.add_record(record) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _include_change(self, change): + if isinstance(change, Update): + existing = change.existing.data + new = change.new.data + new['ttl'] = max(120, new['ttl']) + if new == existing: + return False + return True + + def _contents_for_multiple(self, record): + for value in record.values: + yield {'content': value} + + _contents_for_A = _contents_for_multiple + _contents_for_AAAA = _contents_for_multiple + _contents_for_NS = _contents_for_multiple + _contents_for_SPF = _contents_for_multiple + _contents_for_TXT = _contents_for_multiple + + def _contents_for_CNAME(self, record): + yield {'content': record.value} + + def _contents_for_MX(self, record): + for value in record.values: + yield { + 'priority': value.priority, + 'content': value.value + } + + def _apply_Create(self, change): + new = change.new + zone_id = self.zones[new.zone.name] + contents_for = getattr(self, '_contents_for_{}'.format(new._type)) + path = '/zones/{}/dns_records'.format(zone_id) + name = new.fqdn[:-1] + for content in contents_for(change.new): + content.update({ + 'name': name, + 'type': new._type, + # Cloudflare has a min ttl of 120s + 'ttl': max(self.MIN_TTL, new.ttl), + }) + self._request('POST', path, data=content) + + def _apply_Update(self, change): + # Create the new and delete the old + self._apply_Create(change) + self._apply_Delete(change) + + def _apply_Delete(self, change): + existing = change.existing + existing_name = existing.fqdn[:-1] + for record in self.zone_records(existing.zone): + if existing_name == record['name'] and \ + existing._type == record['type']: + path = '/zones/{}/dns_records/{}'.format(record['zone_id'], + record['id']) + self._request('DELETE', path) + + def _apply(self, plan): + desired = plan.desired + changes = plan.changes + self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + + name = desired.name + if name not in self.zones: + self.log.debug('_apply: no matching zone, creating') + data = { + 'name': name[:-1], + 'jump_start': False, + } + resp = self._request('POST', '/zones', data=data) + zone_id = resp['result']['id'] + self.zones[name] = zone_id + self._zone_records[name] = {} + + for change in changes: + class_name = change.__class__.__name__ + getattr(self, '_apply_{}'.format(class_name))(change) + + # clear the cache + self._zone_records.pop(name, None) diff --git a/octodns/provider/dnsimple.py b/octodns/provider/dnsimple.py new file mode 100644 index 0000000..4c9663c --- /dev/null +++ b/octodns/provider/dnsimple.py @@ -0,0 +1,349 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from collections import defaultdict +from requests import Session +import logging + +from ..record import Record +from .base import BaseProvider + + +class DnsimpleClientException(Exception): + pass + + +class DnsimpleClientNotFound(DnsimpleClientException): + + def __init__(self): + super(DnsimpleClientNotFound, self).__init__('Not found') + + +class DnsimpleClientUnauthorized(DnsimpleClientException): + + def __init__(self): + super(DnsimpleClientUnauthorized, self).__init__('Unauthorized') + + +class DnsimpleClient(object): + BASE = 'https://api.dnsimple.com/v2/' + + def __init__(self, token, account): + self.account = account + sess = Session() + sess.headers.update({'Authorization': 'Bearer {}'.format(token)}) + self._sess = sess + + def _request(self, method, path, params=None, data=None): + url = '{}{}{}'.format(self.BASE, self.account, path) + resp = self._sess.request(method, url, params=params, json=data) + if resp.status_code == 401: + raise DnsimpleClientUnauthorized() + if resp.status_code == 404: + raise DnsimpleClientNotFound() + resp.raise_for_status() + return resp + + def domain(self, name): + path = '/domains/{}'.format(name) + return self._request('GET', path).json() + + def domain_create(self, name): + return self._request('POST', '/domains', data={'name': name}) + + def records(self, zone_name): + ret = [] + + page = 1 + while True: + data = self._request('GET', '/zones/{}/records'.format(zone_name), + {'page': page}).json() + ret += data['data'] + pagination = data['pagination'] + if page >= pagination['total_pages']: + break + page += 1 + + return ret + + def record_create(self, zone_name, params): + path = '/zones/{}/records'.format(zone_name) + self._request('POST', path, data=params) + + def record_delete(self, zone_name, record_id): + path = '/zones/{}/records/{}'.format(zone_name, record_id) + self._request('DELETE', path) + + +class DnsimpleProvider(BaseProvider): + SUPPORTS_GEO = False + + def __init__(self, id, token, account, *args, **kwargs): + self.log = logging.getLogger('DnsimpleProvider[{}]'.format(id)) + self.log.debug('__init__: id=%s, token=***, account=%s', id, account) + super(DnsimpleProvider, self).__init__(id, *args, **kwargs) + self._client = DnsimpleClient(token, account) + + self._zone_records = {} + + def _data_for_multiple(self, _type, records): + return { + 'ttl': records[0]['ttl'], + 'type': _type, + 'values': [r['content'] for r in records] + } + + _data_for_A = _data_for_multiple + _data_for_AAAA = _data_for_multiple + _data_for_SPF = _data_for_multiple + _data_for_TXT = _data_for_multiple + + def _data_for_CNAME(self, _type, records): + record = records[0] + return { + 'ttl': record['ttl'], + 'type': _type, + 'value': '{}.'.format(record['content']) + } + + def _data_for_MX(self, _type, records): + values = [] + for record in records: + values.append({ + 'priority': record['priority'], + 'value': '{}.'.format(record['content']) + }) + return { + 'ttl': records[0]['ttl'], + 'type': _type, + 'values': values + } + + def _data_for_NAPTR(self, _type, records): + values = [] + for record in records: + try: + order, preference, flags, service, regexp, replacement = \ + record['content'].split(' ', 5) + except ValueError: + # their api will let you create invalid records, this + # essnetially handles that by ignoring them for values + # purposes. That will cause updates to happen to delete them if + # they shouldn't exist or update them if they're wrong + continue + values.append({ + 'flags': flags[1:-1], + 'order': order, + 'preference': preference, + 'regexp': regexp[1:-1], + 'replacement': replacement, + 'service': service[1:-1], + }) + return { + 'type': _type, + 'ttl': records[0]['ttl'], + 'values': values + } + + def _data_for_NS(self, _type, records): + values = [] + for record in records: + content = record['content'] + if content[-1] != '.': + content = '{}.'.format(content) + values.append(content) + return { + 'ttl': records[0]['ttl'], + 'type': _type, + 'values': values, + } + + def _data_for_PTR(self, _type, records): + record = records[0] + return { + 'ttl': record['ttl'], + 'type': _type, + 'value': record['content'] + } + + def _data_for_SRV(self, _type, records): + values = [] + for record in records: + try: + weight, port, target = record['content'].split(' ', 2) + except ValueError: + # see _data_for_NAPTR's continue + continue + values.append({ + 'port': port, + 'priority': record['priority'], + 'target': '{}.'.format(target), + 'weight': weight + }) + return { + 'type': _type, + 'ttl': records[0]['ttl'], + 'values': values + } + + def _data_for_SSHFP(self, _type, records): + values = [] + for record in records: + try: + algorithm, fingerprint_type, fingerprint = \ + record['content'].split(' ', 2) + except ValueError: + # see _data_for_NAPTR's continue + continue + values.append({ + 'algorithm': algorithm, + 'fingerprint': fingerprint, + 'fingerprint_type': fingerprint_type + }) + return { + 'type': _type, + 'ttl': records[0]['ttl'], + 'values': values + } + + def zone_records(self, zone): + if zone.name not in self._zone_records: + try: + self._zone_records[zone.name] = \ + self._client.records(zone.name[:-1]) + except DnsimpleClientNotFound: + return [] + + return self._zone_records[zone.name] + + def populate(self, zone, target=False): + self.log.debug('populate: name=%s', zone.name) + + values = defaultdict(lambda: defaultdict(list)) + for record in self.zone_records(zone): + _type = record['type'] + if _type == 'SOA': + continue + values[record['name']][record['type']].append(record) + + before = len(zone.records) + for name, types in values.items(): + for _type, records in types.items(): + data_for = getattr(self, '_data_for_{}'.format(_type)) + record = Record.new(zone, name, data_for(_type, records)) + zone.add_record(record) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _params_for_multiple(self, record): + for value in record.values: + yield { + 'content': value, + 'name': record.name, + 'ttl': record.ttl, + 'type': record._type, + } + + _params_for_A = _params_for_multiple + _params_for_AAAA = _params_for_multiple + _params_for_NS = _params_for_multiple + _params_for_SPF = _params_for_multiple + _params_for_TXT = _params_for_multiple + + def _params_for_single(self, record): + yield { + 'content': record.value, + 'name': record.name, + 'ttl': record.ttl, + 'type': record._type + } + + _params_for_CNAME = _params_for_single + _params_for_PTR = _params_for_single + + def _params_for_MX(self, record): + for value in record.values: + yield { + 'content': value.value, + 'name': record.name, + 'priority': value.priority, + 'ttl': record.ttl, + 'type': record._type + } + + def _params_for_NAPTR(self, record): + for value in record.values: + content = '{} {} "{}" "{}" "{}" {}' \ + .format(value.order, value.preference, value.flags, + value.service, value.regexp, value.replacement) + yield { + 'content': content, + 'name': record.name, + 'ttl': record.ttl, + 'type': record._type + } + + def _params_for_SRV(self, record): + for value in record.values: + yield { + 'content': '{} {} {}'.format(value.weight, value.port, + value.target), + 'name': record.name, + 'priority': value.priority, + 'ttl': record.ttl, + 'type': record._type + } + + def _params_for_SSHFP(self, record): + for value in record.values: + yield { + 'content': '{} {} {}'.format(value.algorithm, + value.fingerprint_type, + value.fingerprint), + 'name': record.name, + 'ttl': record.ttl, + 'type': record._type + } + + def _apply_Create(self, change): + new = change.new + params_for = getattr(self, '_params_for_{}'.format(new._type)) + for params in params_for(new): + self._client.record_create(new.zone.name[:-1], params) + + def _apply_Update(self, change): + self._apply_Create(change) + self._apply_Delete(change) + + def _apply_Delete(self, change): + existing = change.existing + zone = existing.zone + for record in self.zone_records(zone): + if existing.name == record['name'] and \ + existing._type == record['type']: + self._client.record_delete(zone.name[:-1], record['id']) + + def _apply(self, plan): + desired = plan.desired + changes = plan.changes + self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + + domain_name = desired.name[:-1] + try: + self._client.domain(domain_name) + except DnsimpleClientNotFound: + self.log.debug('_apply: no matching zone, creating domain') + self._client.domain_create(domain_name) + + for change in changes: + class_name = change.__class__.__name__ + getattr(self, '_apply_{}'.format(class_name))(change) + + # Clear out the cache if any + self._zone_records.pop(desired.name, None) diff --git a/octodns/provider/dyn.py b/octodns/provider/dyn.py new file mode 100644 index 0000000..e0a86fa --- /dev/null +++ b/octodns/provider/dyn.py @@ -0,0 +1,651 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from collections import defaultdict +from dyn.tm.errors import DynectGetError +from dyn.tm.services.dsf import DSFARecord, DSFAAAARecord, DSFFailoverChain, \ + DSFMonitor, DSFNode, DSFRecordSet, DSFResponsePool, DSFRuleset, \ + TrafficDirector, get_all_dsf_monitors, get_all_dsf_services, \ + get_response_pool +from dyn.tm.session import DynectSession +from dyn.tm.zones import Zone as DynZone +from logging import getLogger +from uuid import uuid4 + +from ..record import Record +from .base import BaseProvider + + +class _CachingDynZone(DynZone): + log = getLogger('_CachingDynZone') + + _cache = {} + + @classmethod + def get(cls, zone_name, create=False): + cls.log.debug('get: zone_name=%s, create=%s', zone_name, create) + # This works in dyn zone names, without the trailing . + try: + dyn_zone = cls._cache[zone_name] + cls.log.debug('get: cache hit') + except KeyError: + cls.log.debug('get: cache miss') + try: + dyn_zone = _CachingDynZone(zone_name) + cls.log.debug('get: fetched') + except DynectGetError: + if not create: + cls.log.debug("get: does't exist") + return None + # this value shouldn't really matter, it's not tied to + # whois or anything + hostname = 'hostmaster@{}'.format(zone_name[:-1]) + # Try again with the params necessary to create + dyn_zone = _CachingDynZone(zone_name, ttl=3600, + contact=hostname, + serial_style='increment') + cls.log.debug('get: created') + cls._cache[zone_name] = dyn_zone + + return dyn_zone + + @classmethod + def flush_zone(cls, zone_name): + '''Flushes the zone cache, if there is one''' + cls.log.debug('flush_zone: zone_name=%s', zone_name) + try: + del cls._cache[zone_name] + except KeyError: + pass + + def __init__(self, zone_name, *args, **kwargs): + super(_CachingDynZone, self).__init__(zone_name, *args, **kwargs) + self.flush_cache() + + def flush_cache(self): + self._cached_records = None + + def get_all_records(self): + if self._cached_records is None: + self._cached_records = \ + super(_CachingDynZone, self).get_all_records() + return self._cached_records + + def publish(self): + super(_CachingDynZone, self).publish() + self.flush_cache() + + +class DynProvider(BaseProvider): + RECORDS_TO_TYPE = { + 'a_records': 'A', + 'aaaa_records': 'AAAA', + 'cname_records': 'CNAME', + 'mx_records': 'MX', + 'naptr_records': 'NAPTR', + 'ns_records': 'NS', + 'ptr_records': 'PTR', + 'sshfp_records': 'SSHFP', + 'spf_records': 'SPF', + 'srv_records': 'SRV', + 'txt_records': 'TXT', + } + TYPE_TO_RECORDS = { + 'A': 'a_records', + 'AAAA': 'aaaa_records', + 'CNAME': 'cname_records', + 'MX': 'mx_records', + 'NAPTR': 'naptr_records', + 'NS': 'ns_records', + 'PTR': 'ptr_records', + 'SSHFP': 'sshfp_records', + 'SPF': 'spf_records', + 'SRV': 'srv_records', + 'TXT': 'txt_records', + } + + # https://help.dyn.com/predefined-geotm-regions-groups/ + REGION_CODES = { + 'NA': 11, # Continental North America + 'SA': 12, # Continental South America + 'EU': 13, # Contentinal Europe + 'AF': 14, # Continental Africa + 'AS': 15, # Contentinal Asia + 'OC': 16, # Contentinal Austrailia/Oceania + 'AN': 17, # Continental Antartica + } + + # Going to be lazy loaded b/c it makes a (slow) request, global + _dyn_sess = None + + def __init__(self, id, customer, username, password, + traffic_directors_enabled=False, *args, **kwargs): + self.log = getLogger('DynProvider[{}]'.format(id)) + self.log.debug('__init__: id=%s, customer=%s, username=%s, ' + 'password=***, traffic_directors_enabled=%s', id, + customer, username, traffic_directors_enabled) + # we have to set this before calling super b/c SUPPORTS_GEO requires it + self.traffic_directors_enabled = traffic_directors_enabled + super(DynProvider, self).__init__(id, *args, **kwargs) + self.customer = customer + self.username = username + self.password = password + + self._cache = {} + self._traffic_directors = None + self._traffic_director_monitors = None + + @property + def SUPPORTS_GEO(self): + return self.traffic_directors_enabled + + def _check_dyn_sess(self): + if self._dyn_sess: + self.log.debug('_check_dyn_sess: exists') + return + + self.log.debug('_check_dyn_sess: creating') + # Dynect's client is ugly, you create a session object, but then don't + # use it for anything. It just makes the other objects work behind the + # scences. :-( That probably means we can only support a single set of + # dynect creds + self._dyn_sess = DynectSession(self.customer, self.username, + self.password) + + def _data_for_A(self, _type, records): + return { + 'type': _type, + 'ttl': records[0].ttl, + 'values': [r.address for r in records] + } + + _data_for_AAAA = _data_for_A + + def _data_for_CNAME(self, _type, records): + record = records[0] + return { + 'type': _type, + 'ttl': record.ttl, + 'value': record.cname, + } + + def _data_for_MX(self, _type, records): + return { + 'type': _type, + 'ttl': records[0].ttl, + 'values': [{'priority': r.preference, 'value': r.exchange} + for r in records], + } + + def _data_for_NAPTR(self, _type, records): + return { + 'type': _type, + 'ttl': records[0].ttl, + 'values': [{ + 'order': r.order, + 'preference': r.preference, + 'flags': r.flags, + 'service': r.services, + 'regexp': r.regexp, + 'replacement': r.replacement, + } for r in records] + } + + def _data_for_NS(self, _type, records): + return { + 'type': _type, + 'ttl': records[0].ttl, + 'values': [r.nsdname for r in records] + } + + def _data_for_PTR(self, _type, records): + record = records[0] + return { + 'type': _type, + 'ttl': record.ttl, + 'value': record.ptrdname, + } + + def _data_for_SPF(self, _type, records): + record = records[0] + return { + 'type': _type, + 'ttl': record.ttl, + 'values': [r.txtdata for r in records] + } + + _data_for_TXT = _data_for_SPF + + def _data_for_SSHFP(self, _type, records): + return { + 'type': _type, + 'ttl': records[0].ttl, + 'values': [{ + 'algorithm': r.algorithm, + 'fingerprint_type': r.fptype, + 'fingerprint': r.fingerprint, + } for r in records], + } + + def _data_for_SRV(self, _type, records): + return { + 'type': _type, + 'ttl': records[0].ttl, + 'values': [{ + 'priority': r.priority, + 'weight': r.weight, + 'port': r.port, + 'target': r.target, + } for r in records], + } + + @property + def traffic_directors(self): + if self._traffic_directors is None: + self._check_dyn_sess() + + tds = defaultdict(dict) + for td in get_all_dsf_services(): + try: + fqdn, _type = td.label.split(':', 1) + except ValueError: + continue + tds[fqdn][_type] = td + self._traffic_directors = dict(tds) + + return self._traffic_directors + + def _populate_traffic_directors(self, zone): + self.log.debug('_populate_traffic_directors: zone=%s', zone.name) + td_records = set() + for fqdn, types in self.traffic_directors.items(): + # TODO: skip subzones + if not fqdn.endswith(zone.name): + continue + + for _type, td in types.items(): + # critical to call rulesets once, each call loads them :-( + rulesets = td.rulesets + + # We start out with something that will always change show + # change in case this is a busted TD. This will prevent us from + # creating a duplicate td. We'll overwrite this with real data + # provide we have it + geo = {} + data = { + 'geo': geo, + 'type': _type, + 'ttl': td.ttl, + 'values': ['0.0.0.0'] + } + for ruleset in rulesets: + try: + record_set = ruleset.response_pools[0].rs_chains[0] \ + .record_sets[0] + except IndexError: + # problems indicate a malformed ruleset, ignore it + continue + _type = record_set.rdata_class + if ruleset.label.startswith('default:'): + data_for = getattr(self, '_data_for_{}'.format(_type)) + data.update(data_for(_type, record_set.records)) + else: + # We've stored the geo in label + try: + code, _ = ruleset.label.split(':', 1) + except ValueError: + continue + values = [r.address for r in record_set.records] + geo[code] = values + + name = zone.hostname_from_fqdn(fqdn) + record = Record.new(zone, name, data, source=self) + zone.add_record(record) + td_records.add(record) + + return td_records + + def populate(self, zone, target=False): + self.log.info('populate: zone=%s', zone.name) + before = len(zone.records) + + self._check_dyn_sess() + + td_records = set() + if self.traffic_directors_enabled: + td_records = self._populate_traffic_directors(zone) + + dyn_zone = _CachingDynZone.get(zone.name[:-1]) + + if dyn_zone: + values = defaultdict(lambda: defaultdict(list)) + for _type, records in dyn_zone.get_all_records().items(): + if _type == 'soa_records': + continue + _type = self.RECORDS_TO_TYPE[_type] + for record in records: + record_name = zone.hostname_from_fqdn(record.fqdn) + values[record_name][_type].append(record) + + for name, types in values.items(): + for _type, records in types.items(): + data_for = getattr(self, '_data_for_{}'.format(_type)) + data = data_for(_type, records) + record = Record.new(zone, name, data, source=self) + if record not in td_records: + zone.add_record(record) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _kwargs_for_A(self, record): + return [{ + 'address': v, + 'ttl': record.ttl, + } for v in record.values] + + _kwargs_for_AAAA = _kwargs_for_A + + def _kwargs_for_CNAME(self, record): + return [{ + 'cname': record.value, + 'ttl': record.ttl, + }] + + def _kwargs_for_MX(self, record): + return [{ + 'preference': v.priority, + 'exchange': v.value, + 'ttl': record.ttl, + } for v in record.values] + + def _kwargs_for_NAPTR(self, record): + return [{ + 'flags': v.flags, + 'order': v.order, + 'preference': v.preference, + 'regexp': v.regexp, + 'replacement': v.replacement, + 'services': v.service, + 'ttl': record.ttl, + } for v in record.values] + + def _kwargs_for_NS(self, record): + return [{ + 'nsdname': v, + 'ttl': record.ttl, + } for v in record.values] + + def _kwargs_for_PTR(self, record): + return [{ + 'ptrdname': record.value, + 'ttl': record.ttl, + }] + + def _kwargs_for_SSHFP(self, record): + return [{ + 'algorithm': v.algorithm, + 'fptype': v.fingerprint_type, + 'fingerprint': v.fingerprint, + } for v in record.values] + + def _kwargs_for_SPF(self, record): + return [{ + 'txtdata': v, + 'ttl': record.ttl, + } for v in record.values] + + def _kwargs_for_SRV(self, record): + return [{ + 'port': v.port, + 'priority': v.priority, + 'target': v.target, + 'weight': v.weight, + 'ttl': record.ttl, + } for v in record.values] + + _kwargs_for_TXT = _kwargs_for_SPF + + def _traffic_director_monitor(self, fqdn): + if self._traffic_director_monitors is None: + self._traffic_director_monitors = \ + {m.label: m for m in get_all_dsf_monitors()} + + try: + return self._traffic_director_monitors[fqdn] + except KeyError: + monitor = DSFMonitor(fqdn, protocol='HTTPS', response_count=2, + probe_interval=60, retries=2, port=443, + active='Y', host=fqdn[:-1], timeout=10, + path='/_dns') + self._traffic_director_monitors[fqdn] = monitor + return monitor + + def _find_or_create_pool(self, td, pools, label, _type, values, + monitor_id=None): + for pool in pools: + if pool.label != label: + continue + records = pool.rs_chains[0].record_sets[0].records + record_values = sorted([r.address for r in records]) + if record_values == values: + # it's a match + return pool + # we need to create the pool + _class = { + 'A': DSFARecord, + 'AAAA': DSFAAAARecord + }[_type] + records = [_class(v) for v in values] + record_set = DSFRecordSet(_type, label, serve_count=len(records), + records=records, dsf_monitor_id=monitor_id) + chain = DSFFailoverChain(label, record_sets=[record_set]) + pool = DSFResponsePool(label, rs_chains=[chain]) + pool.create(td) + return pool + + def _mod_rulesets(self, td, change): + new = change.new + + # Response Pools + pools = {} + + # Get existing pools. This should be simple, but it's not b/c the dyn + # api is a POS. We need all response pools so we can GC and check to + # make sure that what we're after doesn't already exist. + # td.all_response_pools just returns thin objects that don't include + # their rs_chains (and children down to actual records.) We could just + # foreach over those turning them into full DSFResponsePool objects + # with get_response_pool, but that'd be N round-trips. We can avoid + # those round trips in cases where the pools are in use in rules where + # they're already full objects. + + # First up populate all the full pools we have under rules, the _ + # prevents a td.refresh we don't need :-( seriously? + existing_rulesets = td._rulesets + for ruleset in existing_rulesets: + for pool in ruleset.response_pools: + pools[pool.response_pool_id] = pool + # Now we need to find any pools that aren't referenced by rules + for pool in td.all_response_pools: + rpid = pool.response_pool_id + if rpid not in pools: + # we want this one, but it's thin, inflate it + pools[rpid] = get_response_pool(rpid, td) + # now that we have full objects for the complete set of existing pools, + # a list will be more useful + pools = pools.values() + + # Rulesets + + # add the default + label = 'default:{}'.format(uuid4().hex) + ruleset = DSFRuleset(label, 'always', []) + ruleset.create(td, index=0) + pool = self._find_or_create_pool(td, pools, 'default', new._type, + new.values) + # There's no way in the client lib to create a ruleset with an existing + # pool (ref'd by id) so we have to do this round-a-bout. + active_pools = { + 'default': pool.response_pool_id + } + ruleset.add_response_pool(pool.response_pool_id) + + monitor_id = self._traffic_director_monitor(new.fqdn).dsf_monitor_id + # Geos ordered least to most specific so that parents will always be + # created before their children (and thus can be referenced + geos = sorted(new.geo.items(), key=lambda d: d[0]) + for _, geo in geos: + if geo.subdivision_code: + criteria = { + 'country': geo.country_code, + 'province': geo.subdivision_code + } + elif geo.country_code: + criteria = { + 'country': geo.country_code + } + else: + criteria = { + 'region': self.REGION_CODES[geo.continent_code] + } + + label = '{}:{}'.format(geo.code, uuid4().hex) + ruleset = DSFRuleset(label, 'geoip', [], { + 'geoip': criteria + }) + # Something you have to call create others the constructor does it + ruleset.create(td, index=0) + + first = geo.values[0] + pool = self._find_or_create_pool(td, pools, first, new._type, + geo.values, monitor_id) + active_pools[geo.code] = pool.response_pool_id + ruleset.add_response_pool(pool.response_pool_id) + + # look for parent rulesets we can add in the chain + for code in geo.parents: + try: + pool_id = active_pools[code] + # looking at client lib code, index > exists appends + ruleset.add_response_pool(pool_id, index=999) + except KeyError: + pass + # and always add default as the last + pool_id = active_pools['default'] + ruleset.add_response_pool(pool_id, index=999) + + # we're done with active_pools as a lookup, convert it in to a set of + # the ids in use + active_pools = set(active_pools.values()) + # Clean up unused response_pools + for pool in pools: + if pool.response_pool_id in active_pools: + continue + pool.delete() + + # Clean out the old rulesets + for ruleset in existing_rulesets: + ruleset.delete() + + def _mod_geo_Create(self, dyn_zone, change): + new = change.new + fqdn = new.fqdn + _type = new._type + label = '{}:{}'.format(fqdn, _type) + node = DSFNode(new.zone.name, fqdn) + td = TrafficDirector(label, ttl=new.ttl, nodes=[node], publish='Y') + self.log.debug('_mod_geo_Create: td=%s', td.service_id) + self._mod_rulesets(td, change) + self.traffic_directors[fqdn] = { + _type: td + } + + def _mod_geo_Update(self, dyn_zone, change): + new = change.new + if not new.geo: + # New record doesn't have geo we're going from a TD to a regular + # record + self._mod_Create(dyn_zone, change) + self._mod_geo_Delete(dyn_zone, change) + return + try: + td = self.traffic_directors[new.fqdn][new._type] + except KeyError: + # There's no td, this is actually a create, we must be going from a + # non-geo to geo record so delete the regular record as well + self._mod_geo_Create(dyn_zone, change) + self._mod_Delete(dyn_zone, change) + return + self._mod_rulesets(td, change) + + def _mod_geo_Delete(self, dyn_zone, change): + existing = change.existing + fqdn_tds = self.traffic_directors[existing.fqdn] + _type = existing._type + fqdn_tds[_type].delete() + del fqdn_tds[_type] + + def _mod_Create(self, dyn_zone, change): + new = change.new + kwargs_for = getattr(self, '_kwargs_for_{}'.format(new._type)) + for kwargs in kwargs_for(new): + dyn_zone.add_record(new.name, new._type, **kwargs) + + def _mod_Delete(self, dyn_zone, change): + existing = change.existing + if existing.name: + target = '{}.{}'.format(existing.name, existing.zone.name[:-1]) + else: + target = existing.zone.name[:-1] + _type = self.TYPE_TO_RECORDS[existing._type] + for rec in dyn_zone.get_all_records()[_type]: + if rec.fqdn == target: + rec.delete() + + def _mod_Update(self, dyn_zone, change): + self._mod_Delete(dyn_zone, change) + self._mod_Create(dyn_zone, change) + + def _apply_traffic_directors(self, desired, changes, dyn_zone): + self.log.debug('_apply_traffic_directors: zone=%s', desired.name) + unhandled_changes = [] + for c in changes: + # we only mess with changes that have geo info somewhere + if getattr(c.new, 'geo', False) or getattr(c.existing, 'geo', + False): + mod = getattr(self, '_mod_geo_{}'.format(c.__class__.__name__)) + mod(dyn_zone, c) + else: + unhandled_changes.append(c) + + return unhandled_changes + + def _apply_regular(self, desired, changes, dyn_zone): + self.log.debug('_apply_regular: zone=%s', desired.name) + for c in changes: + mod = getattr(self, '_mod_{}'.format(c.__class__.__name__)) + mod(dyn_zone, c) + + # TODO: detect "extra" changes when monitors are out of date or failover + # chains are wrong etc. + + def _apply(self, plan): + desired = plan.desired + changes = plan.changes + self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + + dyn_zone = _CachingDynZone.get(desired.name[:-1], create=True) + + if self.traffic_directors_enabled: + # any changes left over don't involve geo + changes = self._apply_traffic_directors(desired, changes, dyn_zone) + + self._apply_regular(desired, changes, dyn_zone) + + dyn_zone.publish() diff --git a/octodns/provider/powerdns.py b/octodns/provider/powerdns.py new file mode 100644 index 0000000..25e7313 --- /dev/null +++ b/octodns/provider/powerdns.py @@ -0,0 +1,361 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from requests import HTTPError, Session +import logging + +from ..record import Create, Record +from .base import BaseProvider + + +class PowerDnsBaseProvider(BaseProvider): + SUPPORTS_GEO = False + TIMEOUT = 5 + + def __init__(self, id, host, api_key, port=8081, *args, **kwargs): + super(PowerDnsBaseProvider, self).__init__(id, *args, **kwargs) + + self.host = host + self.port = port + + sess = Session() + sess.headers.update({'X-API-Key': api_key}) + self._sess = sess + + def _request(self, method, path, data=None): + self.log.debug('_request: method=%s, path=%s', method, path) + + url = 'http://{}:{}/api/v1/servers/localhost/{}' \ + .format(self.host, self.port, path) + resp = self._sess.request(method, url, json=data, timeout=self.TIMEOUT) + self.log.debug('_request: status=%d', resp.status_code) + resp.raise_for_status() + return resp + + def _get(self, path, data=None): + return self._request('GET', path, data=data) + + def _post(self, path, data=None): + return self._request('POST', path, data=data) + + def _patch(self, path, data=None): + return self._request('PATCH', path, data=data) + + def _data_for_multiple(self, rrset): + # TODO: geo not supported + return { + 'type': rrset['type'], + 'values': [r['content'] for r in rrset['records']], + 'ttl': rrset['ttl'] + } + + _data_for_A = _data_for_multiple + _data_for_AAAA = _data_for_multiple + _data_for_NS = _data_for_multiple + + def _data_for_single(self, rrset): + return { + 'type': rrset['type'], + 'value': rrset['records'][0]['content'], + 'ttl': rrset['ttl'] + } + + _data_for_CNAME = _data_for_single + _data_for_PTR = _data_for_single + + def _data_for_quoted(self, rrset): + return { + 'type': rrset['type'], + 'values': [r['content'][1:-1] for r in rrset['records']], + 'ttl': rrset['ttl'] + } + + _data_for_SPF = _data_for_quoted + _data_for_TXT = _data_for_quoted + + def _data_for_MX(self, rrset): + values = [] + for record in rrset['records']: + priority, value = record['content'].split(' ', 1) + values.append({ + 'priority': priority, + 'value': value, + }) + return { + 'type': rrset['type'], + 'values': values, + 'ttl': rrset['ttl'] + } + + def _data_for_NAPTR(self, rrset): + values = [] + for record in rrset['records']: + order, preference, flags, service, regexp, replacement = \ + record['content'].split(' ', 5) + values.append({ + 'order': order, + 'preference': preference, + 'flags': flags[1:-1], + 'service': service[1:-1], + 'regexp': regexp[1:-1], + 'replacement': replacement, + }) + return { + 'type': rrset['type'], + 'values': values, + 'ttl': rrset['ttl'] + } + + def _data_for_SSHFP(self, rrset): + values = [] + for record in rrset['records']: + algorithm, fingerprint_type, fingerprint = \ + record['content'].split(' ', 2) + values.append({ + 'algorithm': algorithm, + 'fingerprint_type': fingerprint_type, + 'fingerprint': fingerprint, + }) + return { + 'type': rrset['type'], + 'values': values, + 'ttl': rrset['ttl'] + } + + def _data_for_SRV(self, rrset): + values = [] + for record in rrset['records']: + priority, weight, port, target = \ + record['content'].split(' ', 3) + values.append({ + 'priority': priority, + 'weight': weight, + 'port': port, + 'target': target, + }) + return { + 'type': rrset['type'], + 'values': values, + 'ttl': rrset['ttl'] + } + + def populate(self, zone, target=False): + self.log.debug('populate: name=%s', zone.name) + + resp = None + try: + resp = self._get('zones/{}'.format(zone.name)) + self.log.debug('populate: loaded') + except HTTPError as e: + if e.response.status_code == 401: + # Nicer error message for auth problems + raise Exception('PowerDNS unauthorized host={}' + .format(self.host)) + elif e.response.status_code == 422: + # 422 means powerdns doesn't know anything about the requsted + # domain. We'll just ignore it here and leave the zone + # untouched. + pass + else: + # just re-throw + raise + + before = len(zone.records) + + if resp: + for rrset in resp.json()['rrsets']: + _type = rrset['type'] + if _type == 'SOA': + continue + data_for = getattr(self, '_data_for_{}'.format(_type)) + record_name = zone.hostname_from_fqdn(rrset['name']) + record = Record.new(zone, record_name, data_for(rrset), + source=self) + zone.add_record(record) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _records_for_multiple(self, record): + return [{'content': v, 'disabled': False} + for v in record.values] + + _records_for_A = _records_for_multiple + _records_for_AAAA = _records_for_multiple + _records_for_NS = _records_for_multiple + + def _records_for_single(self, record): + return [{'content': record.value, 'disabled': False}] + + _records_for_CNAME = _records_for_single + _records_for_PTR = _records_for_single + + def _records_for_quoted(self, record): + return [{'content': '"{}"'.format(v), 'disabled': False} + for v in record.values] + + _records_for_SPF = _records_for_quoted + _records_for_TXT = _records_for_quoted + + def _records_for_MX(self, record): + return [{ + 'content': '{} {}'.format(v.priority, v.value), + 'disabled': False + } for v in record.values] + + def _records_for_NAPTR(self, record): + return [{ + 'content': '{} {} "{}" "{}" "{}" {}'.format(v.order, v.preference, + v.flags, v.service, + v.regexp, + v.replacement), + 'disabled': False + } for v in record.values] + + def _records_for_SSHFP(self, record): + return [{ + 'content': '{} {} {}'.format(v.algorithm, v.fingerprint_type, + v.fingerprint), + 'disabled': False + } for v in record.values] + + def _records_for_SRV(self, record): + return [{ + 'content': '{} {} {} {}'.format(v.priority, v.weight, v.port, + v.target), + 'disabled': False + } for v in record.values] + + def _mod_Create(self, change): + new = change.new + records_for = getattr(self, '_records_for_{}'.format(new._type)) + return { + 'name': new.fqdn, + 'type': new._type, + 'ttl': new.ttl, + 'changetype': 'REPLACE', + 'records': records_for(new) + } + + _mod_Update = _mod_Create + + def _mod_Delete(self, change): + existing = change.existing + records_for = getattr(self, '_records_for_{}'.format(existing._type)) + return { + 'name': existing.fqdn, + 'type': existing._type, + 'ttl': existing.ttl, + 'changetype': 'DELETE', + 'records': records_for(existing) + } + + def _get_nameserver_record(self, existing): + return None + + def _extra_changes(self, existing, _): + self.log.debug('_extra_changes: zone=%s', existing.name) + + ns = self._get_nameserver_record(existing) + if not ns: + return [] + + # sorting mostly to make things deterministic for testing, but in + # theory it let us find what we're after quickier (though sorting would + # ve more exepensive.) + for record in sorted(existing.records): + if record == ns: + # We've found the top-level NS record, return any changes + change = record.changes(ns, self) + self.log.debug('_extra_changes: change=%s', change) + if change: + # We need to modify an existing record + return [change] + # No change is necessary + return [] + # No existing top-level NS + self.log.debug('_extra_changes: create') + return [Create(ns)] + + def _get_error(self, http_error): + try: + return http_error.response.json()['error'] + except Exception: + return '' + + def _apply(self, plan): + desired = plan.desired + changes = plan.changes + self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + + mods = [] + for change in changes: + class_name = change.__class__.__name__ + mods.append(getattr(self, '_mod_{}'.format(class_name))(change)) + self.log.debug('_apply: sending change request') + + try: + self._patch('zones/{}'.format(desired.name), + data={'rrsets': mods}) + self.log.debug('_apply: patched') + except HTTPError as e: + error = self._get_error(e) + if e.response.status_code != 422 or \ + not error.startswith('Could not find domain '): + self.log.error('_apply: status=%d, text=%s', + e.response.status_code, + e.response.text) + raise + self.log.info('_apply: creating zone=%s', desired.name) + # 422 means powerdns doesn't know anything about the requsted + # domain. We'll try to create it with the correct records instead + # of update. Hopefully all the mods are creates :-) + data = { + 'name': desired.name, + 'kind': 'Master', + 'masters': [], + 'nameservers': [], + 'rrsets': mods, + 'soa_edit_api': 'INCEPTION-INCREMENT', + 'serial': 0, + } + try: + self._post('zones', data) + except HTTPError as e: + self.log.error('_apply: status=%d, text=%s', + e.response.status_code, + e.response.text) + raise + self.log.debug('_apply: created') + + self.log.debug('_apply: complete') + + +class PowerDnsProvider(PowerDnsBaseProvider): + + def __init__(self, id, host, api_key, port=8081, nameserver_values=None, + nameserver_ttl=600, *args, **kwargs): + self.log = logging.getLogger('PowerDnsProvider[{}]'.format(id)) + self.log.debug('__init__: id=%s, host=%s, port=%d, ' + 'nameserver_values=%s, nameserver_ttl=%d', + id, host, port, nameserver_values, nameserver_ttl) + super(PowerDnsProvider, self).__init__(id, host=host, api_key=api_key, + port=port, *args, **kwargs) + + self.nameserver_values = nameserver_values + self.nameserver_ttl = nameserver_ttl + + def _get_nameserver_record(self, existing): + if self.nameserver_values: + return Record.new(existing, '', { + 'type': 'NS', + 'ttl': self.nameserver_ttl, + 'values': self.nameserver_values, + }, source=self) + + return super(PowerDnsProvider, self)._get_nameserver_record(existing) diff --git a/octodns/provider/route53.py b/octodns/provider/route53.py new file mode 100644 index 0000000..634c526 --- /dev/null +++ b/octodns/provider/route53.py @@ -0,0 +1,651 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from boto3 import client +from collections import defaultdict +from incf.countryutils.transformations import cca_to_ctca2 +from uuid import uuid4 +import logging +import re + +from ..record import Record, Update +from .base import BaseProvider + + +class _Route53Record(object): + + def __init__(self, fqdn, _type, ttl, record=None, values=None, geo=None, + health_check_id=None): + self.fqdn = fqdn + self._type = _type + self.ttl = ttl + # From here on things are a little ugly, it works, but would be nice to + # clean up someday. + if record: + values_for = getattr(self, '_values_for_{}'.format(self._type)) + self.values = values_for(record) + else: + self.values = values + self.geo = geo + self.health_check_id = health_check_id + self.is_geo_default = False + + @property + def _geo_code(self): + return getattr(self.geo, 'code', '') + + def _values_for_values(self, record): + return record.values + + _values_for_A = _values_for_values + _values_for_AAAA = _values_for_values + _values_for_NS = _values_for_values + + def _values_for_value(self, record): + return [record.value] + + _values_for_CNAME = _values_for_value + _values_for_PTR = _values_for_value + + def _values_for_MX(self, record): + return ['{} {}'.format(v.priority, v.value) for v in record.values] + + def _values_for_NAPTR(self, record): + return ['{} {} "{}" "{}" "{}" {}' + .format(v.order, v.preference, + v.flags if v.flags else '', + v.service if v.service else '', + v.regexp if v.regexp else '', + v.replacement) + for v in record.values] + + def _values_for_quoted(self, record): + return ['"{}"'.format(v.replace('"', '\\"')) + for v in record.values] + + _values_for_SPF = _values_for_quoted + _values_for_TXT = _values_for_quoted + + def _values_for_SRV(self, record): + return ['{} {} {} {}'.format(v.priority, v.weight, v.port, + v.target) + for v in record.values] + + def mod(self, action): + rrset = { + 'Name': self.fqdn, + 'Type': self._type, + 'TTL': self.ttl, + 'ResourceRecords': [{'Value': v} for v in self.values], + } + if self.is_geo_default: + rrset['GeoLocation'] = { + 'CountryCode': '*' + } + rrset['SetIdentifier'] = 'default' + elif self.geo: + geo = self.geo + rrset['SetIdentifier'] = geo.code + if self.health_check_id: + rrset['HealthCheckId'] = self.health_check_id + if geo.subdivision_code: + rrset['GeoLocation'] = { + 'CountryCode': geo.country_code, + 'SubdivisionCode': geo.subdivision_code + } + elif geo.country_code: + rrset['GeoLocation'] = { + 'CountryCode': geo.country_code + } + else: + rrset['GeoLocation'] = { + 'ContinentCode': geo.continent_code + } + + return { + 'Action': action, + 'ResourceRecordSet': rrset, + } + + # NOTE: we're using __hash__ and __cmp__ methods that consider + # _Route53Records equivalent if they have the same fqdn, _type, and + # geo.ident. Values are ignored. This is usful when computing + # diffs/changes. + + def __hash__(self): + return '{}:{}:{}'.format(self.fqdn, self._type, + self._geo_code).__hash__() + + def __cmp__(self, other): + return 0 if (self.fqdn == other.fqdn and + self._type == other._type and + self._geo_code == other._geo_code) else 1 + + def __repr__(self): + return '_Route53Record<{} {:>5} {:8} {}>' \ + .format(self.fqdn, self._type, self._geo_code, self.values) + + +octal_re = re.compile(r'\\(\d\d\d)') + + +def _octal_replace(s): + # See http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ + # DomainNameFormat.html + return octal_re.sub(lambda m: chr(int(m.group(1), 8)), s) + + +class Route53Provider(BaseProvider): + SUPPORTS_GEO = True + + # This should be bumped when there are underlying changes made to the + # health check config. + HEALTH_CHECK_VERSION = '0000' + + def __init__(self, id, access_key_id, secret_access_key, max_changes=1000, + *args, **kwargs): + self.max_changes = max_changes + self.log = logging.getLogger('Route53Provider[{}]'.format(id)) + self.log.debug('__init__: id=%s, access_key_id=%s, ' + 'secret_access_key=***', id, access_key_id) + super(Route53Provider, self).__init__(id, *args, **kwargs) + self._conn = client('route53', aws_access_key_id=access_key_id, + aws_secret_access_key=secret_access_key) + + self._r53_zones = None + self._r53_rrsets = {} + self._health_checks = None + + def supports(self, record): + return record._type != 'SSHFP' + + @property + def r53_zones(self): + if self._r53_zones is None: + self.log.debug('r53_zones: loading') + zones = {} + more = True + start = {} + while more: + resp = self._conn.list_hosted_zones() + for z in resp['HostedZones']: + zones[z['Name']] = z['Id'] + more = resp['IsTruncated'] + start['Marker'] = resp.get('NextMarker', None) + + self._r53_zones = zones + + return self._r53_zones + + def _get_zone_id(self, name, create=False): + self.log.debug('_get_zone_id: name=%s', name) + if name in self.r53_zones: + id = self.r53_zones[name] + self.log.debug('_get_zone_id: id=%s', id) + return id + if create: + ref = uuid4().hex + self.log.debug('_get_zone_id: no matching zone, creating, ' + 'ref=%s', ref) + resp = self._conn.create_hosted_zone(Name=name, + CallerReference=ref) + self.r53_zones[name] = id = resp['HostedZone']['Id'] + return id + return None + + def _parse_geo(self, rrset): + try: + loc = rrset['GeoLocation'] + except KeyError: + # No geo loc + return + try: + return loc['ContinentCode'] + except KeyError: + # Must be country + cc = loc['CountryCode'] + if cc == '*': + # This is the default + return + cn = cca_to_ctca2(cc) + try: + return '{}-{}-{}'.format(cn, cc, loc['SubdivisionCode']) + except KeyError: + return '{}-{}'.format(cn, cc) + + def _data_for_geo(self, rrset): + ret = { + 'type': rrset['Type'], + 'values': [v['Value'] for v in rrset['ResourceRecords']], + 'ttl': int(rrset['TTL']) + } + geo = self._parse_geo(rrset) + if geo: + ret['geo'] = geo + return ret + + _data_for_A = _data_for_geo + _data_for_AAAA = _data_for_geo + + def _data_for_single(self, rrset): + return { + 'type': rrset['Type'], + 'value': rrset['ResourceRecords'][0]['Value'], + 'ttl': int(rrset['TTL']) + } + + _data_for_PTR = _data_for_single + _data_for_CNAME = _data_for_single + + def _data_for_quoted(self, rrset): + return { + 'type': rrset['Type'], + 'values': [rr['Value'][1:-1] for rr in rrset['ResourceRecords']], + 'ttl': int(rrset['TTL']) + } + + _data_for_TXT = _data_for_quoted + _data_for_SPF = _data_for_quoted + + def _data_for_MX(self, rrset): + values = [] + for rr in rrset['ResourceRecords']: + priority, value = rr['Value'].split(' ') + values.append({ + 'priority': priority, + 'value': value, + }) + return { + 'type': rrset['Type'], + 'values': values, + 'ttl': int(rrset['TTL']) + } + + def _data_for_NAPTR(self, rrset): + values = [] + for rr in rrset['ResourceRecords']: + order, preference, flags, service, regexp, replacement = \ + rr['Value'].split(' ') + flags = flags[1:-1] + service = service[1:-1] + regexp = regexp[1:-1] + values.append({ + 'order': order, + 'preference': preference, + 'flags': flags if flags else None, + 'service': service if service else None, + 'regexp': regexp if regexp else None, + 'replacement': replacement if replacement else None, + }) + return { + 'type': rrset['Type'], + 'values': values, + 'ttl': int(rrset['TTL']) + } + + def _data_for_NS(self, rrset): + return { + 'type': rrset['Type'], + 'values': [v['Value'] for v in rrset['ResourceRecords']], + 'ttl': int(rrset['TTL']) + } + + def _data_for_SRV(self, rrset): + values = [] + for rr in rrset['ResourceRecords']: + priority, weight, port, target = rr['Value'].split(' ') + values.append({ + 'priority': priority, + 'weight': weight, + 'port': port, + 'target': target, + }) + return { + 'type': rrset['Type'], + 'values': values, + 'ttl': int(rrset['TTL']) + } + + def _load_records(self, zone_id): + if zone_id not in self._r53_rrsets: + self.log.debug('_load_records: zone_id=%s loading', zone_id) + rrsets = [] + more = True + start = {} + while more: + resp = \ + self._conn.list_resource_record_sets(HostedZoneId=zone_id, + **start) + rrsets += resp['ResourceRecordSets'] + more = resp['IsTruncated'] + if more: + start = { + 'StartRecordName': resp['NextRecordName'], + 'StartRecordType': resp['NextRecordType'], + } + try: + start['StartRecordIdentifier'] = \ + resp['NextRecordIdentifier'] + except KeyError: + pass + + self._r53_rrsets[zone_id] = rrsets + + return self._r53_rrsets[zone_id] + + def populate(self, zone, target=False): + self.log.debug('populate: name=%s', zone.name) + before = len(zone.records) + + zone_id = self._get_zone_id(zone.name) + if zone_id: + records = defaultdict(lambda: defaultdict(list)) + for rrset in self._load_records(zone_id): + record_name = zone.hostname_from_fqdn(rrset['Name']) + record_name = _octal_replace(record_name) + record_type = rrset['Type'] + if record_type == 'SOA': + continue + data = getattr(self, '_data_for_{}'.format(record_type))(rrset) + records[record_name][record_type].append(data) + + for name, types in records.items(): + for _type, data in types.items(): + if len(data) > 1: + # Multiple data indicates a record with GeoDNS, convert + # them data into the format we need + geo = {} + for d in data: + try: + geo[d['geo']] = d['values'] + except KeyError: + primary = d + data = primary + data['geo'] = geo + else: + data = data[0] + record = Record.new(zone, name, data, source=self) + zone.add_record(record) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _gen_mods(self, action, records): + ''' + Turns `_Route53Record`s in to `change_resource_record_sets` `Changes` + ''' + return [r.mod(action) for r in records] + + @property + def health_checks(self): + if self._health_checks is None: + # need to do the first load + self.log.debug('health_checks: loading') + checks = {} + more = True + start = {} + while more: + resp = self._conn.list_health_checks(**start) + for health_check in resp['HealthChecks']: + # our format for CallerReference is dddd:hex-uuid + ref = health_check.get('CallerReference', 'xxxxx') + if len(ref) > 4 and ref[4] != ':': + # ignore anything else + continue + checks[health_check['Id']] = health_check + more = resp['IsTruncated'] + start['Marker'] = resp.get('NextMarker', None) + + self._health_checks = checks + + # We've got a cached version use it + return self._health_checks + + def _get_health_check_id(self, record, ident, geo): + # fqdn & the first value are special, we use them to match up health + # checks to their records. Route53 health checks check a single ip and + # we're going to assume that ips are interchangeable to avoid + # health-checking each one independently + fqdn = record.fqdn + first_value = geo.values[0] + self.log.debug('_get_health_check_id: fqdn=%s, type=%s, geo=%s, ' + 'first_value=%s', fqdn, record._type, ident, + first_value) + + # health check host can't end with a . + host = fqdn[:-1] + # we're looking for a healthcheck with the current version & our record + # type, we'll ignore anything else + expected_version_and_type = '{}:{}:'.format(self.HEALTH_CHECK_VERSION, + record._type) + for id, health_check in self.health_checks.items(): + if not health_check['CallerReference'] \ + .startswith(expected_version_and_type): + # not a version & type match, ignore + continue + config = health_check['HealthCheckConfig'] + if host == config['FullyQualifiedDomainName'] and \ + first_value == config['IPAddress']: + # this is the health check we're looking for + return id + + # no existing matches, we need to create a new health check + config = { + 'EnableSNI': True, + 'FailureThreshold': 6, + 'FullyQualifiedDomainName': host, + 'IPAddress': first_value, + 'MeasureLatency': True, + 'Port': 443, + 'RequestInterval': 10, + 'ResourcePath': '/_dns', + 'Type': 'HTTPS', + } + ref = '{}:{}:{}'.format(self.HEALTH_CHECK_VERSION, record._type, + uuid4().hex[:16]) + resp = self._conn.create_health_check(CallerReference=ref, + HealthCheckConfig=config) + health_check = resp['HealthCheck'] + id = health_check['Id'] + # store the new health check so that we'll be able to find it in the + # future + self._health_checks[id] = health_check + self.log.info('_get_health_check_id: created id=%s, host=%s, ' + 'first_value=%s', id, host, first_value) + return id + + def _gc_health_checks(self, record, new): + self.log.debug('_gc_health_checks: record=%s', record) + # Find the health checks we're using for the new route53 records + in_use = set() + for r in new: + if r.health_check_id: + in_use.add(r.health_check_id) + self.log.debug('_gc_health_checks: in_use=%s', in_use) + # Now we need to run through ALL the health checks looking for those + # that apply to this record, deleting any that do and are no longer in + # use + host = record.fqdn[:-1] + for id, health_check in self.health_checks.items(): + config = health_check['HealthCheckConfig'] + _type = health_check['CallerReference'].split(':', 2)[1] + # if host and the pulled out type match it applies + if host == config['FullyQualifiedDomainName'] and \ + _type == record._type and id not in in_use: + # this is a health check for our fqdn & type but not one we're + # planning to use going forward + self.log.info('_gc_health_checks: deleting id=%s', id) + self._conn.delete_health_check(HealthCheckId=id) + + def _gen_records(self, record, new=False): + ''' + Turns an octodns.Record into one or more `_Route53Record`s + ''' + records = set() + base = _Route53Record(record.fqdn, record._type, record.ttl, + record=record) + records.add(base) + if getattr(record, 'geo', False): + base.is_geo_default = True + for ident, geo in record.geo.items(): + if new: + # only find health checks for records that are going to be + # around after the run + health_check_id = self._get_health_check_id(record, ident, + geo) + else: + health_check_id = None + records.add(_Route53Record(record.fqdn, record._type, + record.ttl, values=geo.values, + geo=geo, + health_check_id=health_check_id)) + + return records + + def _mod_Create(self, change): + # New is the stuff that needs to be created + new_records = self._gen_records(change.new, True) + # Now is a good time to clear out any unused health checks since we + # know what we'll be using going forward + self._gc_health_checks(change.new, new_records) + return self._gen_mods('CREATE', new_records) + + def _mod_Update(self, change): + # See comments in _Route53Record for how the set math is made to do our + # bidding here. + existing_records = self._gen_records(change.existing) + new_records = self._gen_records(change.new, True) + # Now is a good time to clear out any unused health checks since we + # know what we'll be using going forward + self._gc_health_checks(change.new, new_records) + # Things in existing, but not new are deletes + deletes = existing_records - new_records + # Things in new, but not existing are the creates + creates = new_records - existing_records + # Things in both need updating, we could optimize this and filter out + # things that haven't actually changed, but that's for another day. + upserts = existing_records & new_records + return self._gen_mods('DELETE', deletes) + \ + self._gen_mods('CREATE', creates) + \ + self._gen_mods('UPSERT', upserts) + + def _mod_Delete(self, change): + # Existing is the thing that needs to be deleted + existing_records = self._gen_records(change.existing) + # Now is a good time to clear out all the health checks since we know + # we're done with them + self._gc_health_checks(change.existing, []) + return self._gen_mods('DELETE', existing_records) + + def _extra_changes(self, existing, changes): + self.log.debug('_extra_changes: existing=%s', existing.name) + zone_id = self._get_zone_id(existing.name) + if not zone_id: + # zone doesn't exist so no extras to worry about + return [] + # we'll skip extra checking for anything we're already going to change + changed = set([c.record for c in changes]) + # ok, now it's time for the reason we're here, we need to go over all + # the existing records + extra = [] + for record in existing.records: + if record in changed: + # already have a change for it, skipping + continue + if not getattr(record, 'geo', False): + # record doesn't support geo, we don't need to inspect it + continue + # OK this is a record we don't have change for that does have geo + # information. We need to look and see if it needs to be updated + # b/c of a health check version bump + self.log.debug('_extra_changes: inspecting=%s, %s', record.fqdn, + record._type) + fqdn = record.fqdn + # loop through all the r53 rrsets + for rrset in self._load_records(zone_id): + if fqdn != rrset['Name'] or record._type != rrset['Type']: + # not a name and type match + continue + if rrset.get('GeoLocation', {}) \ + .get('CountryCode', False) == '*': + # it's a default record + continue + # we expect a healtcheck now + try: + health_check_id = rrset['HealthCheckId'] + caller_ref = \ + self.health_checks[health_check_id]['CallerReference'] + if caller_ref.startswith(self.HEALTH_CHECK_VERSION): + # it has the right health check + continue + except KeyError: + # no health check id or one that isn't the right version + pass + # no good, doesn't have the right health check, needs an update + self.log.debug('_extra_changes: health-check caused ' + 'update') + extra.append(Update(record, record)) + # We don't need to process this record any longer + break + + return extra + + def _apply(self, plan): + desired = plan.desired + changes = plan.changes + self.log.info('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + + batch = [] + batch_rs_count = 0 + zone_id = self._get_zone_id(desired.name, True) + for c in changes: + mods = getattr(self, '_mod_{}'.format(c.__class__.__name__))(c) + mods_rs_count = sum( + [len(m['ResourceRecordSet']['ResourceRecords']) for m in mods] + ) + + if mods_rs_count > self.max_changes: + # a single mod resulted in too many ResourceRecords changes + raise Exception('Too many modifications: {}' + .format(mods_rs_count)) + + # r53 limits changesets to 1000 entries + if (batch_rs_count + mods_rs_count) < self.max_changes: + # append to the batch + batch += mods + batch_rs_count += mods_rs_count + else: + self.log.info('_apply: sending change request for batch of ' + '%d mods, %d ResourceRecords', len(batch), + batch_rs_count) + # send the batch + self._really_apply(batch, zone_id) + # start a new batch with the lefovers + batch = mods + batch_rs_count = mods_rs_count + + # the way the above process works there will always be something left + # over in batch to process. In the case that we submit a batch up there + # it was always the case that there was something pushing us over + # max_changes and thus left over to submit. + self.log.info('_apply: sending change request for batch of %d mods,' + ' %d ResourceRecords', len(batch), + batch_rs_count) + self._really_apply(batch, zone_id) + + def _really_apply(self, batch, zone_id): + uuid = uuid4().hex + batch = { + 'Comment': 'Change: {}'.format(uuid), + 'Changes': batch, + } + self.log.debug('_really_apply: sending change request, comment=%s', + batch['Comment']) + resp = self._conn.change_resource_record_sets( + HostedZoneId=zone_id, ChangeBatch=batch) + self.log.debug('_really_apply: change info=%s', resp['ChangeInfo']) diff --git a/octodns/provider/yaml.py b/octodns/provider/yaml.py new file mode 100644 index 0000000..d335d85 --- /dev/null +++ b/octodns/provider/yaml.py @@ -0,0 +1,82 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from collections import defaultdict +from os import makedirs +from os.path import isdir, join +import logging + +from ..record import Record +from ..yaml import safe_load, safe_dump +from .base import BaseProvider + + +class YamlProvider(BaseProvider): + SUPPORTS_GEO = True + + def __init__(self, id, directory, default_ttl=3600, *args, **kwargs): + self.log = logging.getLogger('YamlProvider[{}]'.format(id)) + self.log.debug('__init__: id=%s, directory=%s, default_ttl=%d', id, + directory, default_ttl) + super(YamlProvider, self).__init__(id, *args, **kwargs) + self.directory = directory + self.default_ttl = default_ttl + + def populate(self, zone, target=False): + self.log.debug('populate: zone=%s, target=%s', zone.name, target) + if target: + # When acting as a target we ignore any existing records so that we + # create a completely new copy + return + + before = len(zone.records) + filename = join(self.directory, '{}yaml'.format(zone.name)) + with open(filename, 'r') as fh: + yaml_data = safe_load(fh) + if yaml_data: + for name, data in yaml_data.items(): + if not isinstance(data, list): + data = [data] + for d in data: + if 'ttl' not in d: + d['ttl'] = self.default_ttl + record = Record.new(zone, name, d, source=self) + zone.add_record(record) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _apply(self, plan): + desired = plan.desired + changes = plan.changes + self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, + len(changes)) + # Since we don't have existing we'll only see creates + records = [c.new for c in changes] + # Order things alphabetically (records sort that way + records.sort() + data = defaultdict(list) + for record in records: + d = record.data + d['type'] = record._type + if record.ttl == self.default_ttl: + # ttl is the default, we don't need to store it + del d['ttl'] + data[record.name].append(d) + + # Flatten single element lists + for k in data.keys(): + if len(data[k]) == 1: + data[k] = data[k][0] + + if not isdir(self.directory): + makedirs(self.directory) + + filename = join(self.directory, '{}yaml'.format(desired.name)) + self.log.debug('_apply: writing filename=%s', filename) + with open(filename, 'w') as fh: + safe_dump(dict(data), fh) diff --git a/octodns/record.py b/octodns/record.py new file mode 100644 index 0000000..570988b --- /dev/null +++ b/octodns/record.py @@ -0,0 +1,549 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from ipaddress import IPv4Address, IPv6Address +from logging import getLogger +import re + + +class Change(object): + + def __init__(self, existing, new): + self.existing = existing + self.new = new + + @property + def record(self): + 'Returns new if we have one, existing otherwise' + return self.new or self.existing + + +class Create(Change): + + def __init__(self, new): + super(Create, self).__init__(None, new) + + def __repr__(self, leader=''): + source = self.new.source.id if self.new.source else '' + return 'Create {} ({})'.format(self.new, source) + + +class Update(Change): + + # Leader is just to allow us to work around heven eating leading whitespace + # in our output. When we call this from the Manager.sync plan summary + # section we'll pass in a leader, otherwise we'll just let it default and + # do nothing + def __repr__(self, leader=''): + source = self.new.source.id if self.new.source else '' + return 'Update\n{leader} {existing} ->\n{leader} {new} ({src})' \ + .format(existing=self.existing, new=self.new, leader=leader, + src=source) + + +class Delete(Change): + + def __init__(self, existing): + super(Delete, self).__init__(existing, None) + + def __repr__(self, leader=''): + return 'Delete {}'.format(self.existing) + + +_unescaped_semicolon_re = re.compile(r'\w;') + + +class Record(object): + log = getLogger('Record') + + @classmethod + def new(cls, zone, name, data, source=None): + try: + _type = data['type'] + except KeyError: + fqdn = '{}.{}'.format(name, zone.name) if name else zone.name + raise Exception('Invalid record {}, missing type'.format(fqdn)) + try: + _type = { + 'A': ARecord, + 'AAAA': AaaaRecord, + # alias + # cert + 'CNAME': CnameRecord, + # dhcid + # dname + # dnskey + # ds + # ipseckey + # key + # kx + # loc + 'MX': MxRecord, + 'NAPTR': NaptrRecord, + 'NS': NsRecord, + # nsap + 'PTR': PtrRecord, + # px + # rp + # soa - would it even make sense? + 'SPF': SpfRecord, + 'SRV': SrvRecord, + 'SSHFP': SshfpRecord, + 'TXT': TxtRecord, + # url + }[_type] + except KeyError: + raise Exception('Unknown record type: "{}"'.format(_type)) + return _type(zone, name, data, source=source) + + def __init__(self, zone, name, data, source=None): + self.log.debug('__init__: zone.name=%s, type=%11s, name=%s', zone.name, + self.__class__.__name__, name) + self.zone = zone + # force everything lower-case just to be safe + self.name = str(name).lower() if name else name + try: + self.ttl = int(data['ttl']) + except KeyError: + raise Exception('Invalid record {}, missing ttl'.format(self.fqdn)) + self.source = source + + def _data(self): + return {'ttl': self.ttl} + + @property + def data(self): + return self._data() + + @property + def fqdn(self): + if self.name: + return '{}.{}'.format(self.name, self.zone.name) + return self.zone.name + + def changes(self, other, target): + # We're assuming we have the same name and type if we're being compared + if self.ttl != other.ttl: + return Update(self, other) + + # NOTE: we're using __hash__ and __cmp__ methods that consider Records + # equivalent if they have the same name & _type. Values are ignored. This + # is usful when computing diffs/changes. + + def __hash__(self): + return '{}:{}'.format(self.name, self._type).__hash__() + + def __cmp__(self, other): + a = '{}:{}'.format(self.name, self._type) + b = '{}:{}'.format(other.name, other._type) + return cmp(a, b) + + def __repr__(self): + # Make sure this is always overridden + raise NotImplementedError('Abstract base class, __repr__ required') + + +class GeoValue(object): + geo_re = re.compile(r'^(?P\w\w)(-(?P\w\w)' + r'(-(?P\w\w))?)?$') + + def __init__(self, geo, values): + match = self.geo_re.match(geo) + if not match: + raise Exception('Invalid geo "{}"'.format(geo)) + self.code = geo + self.continent_code = match.group('continent_code') + self.country_code = match.group('country_code') + self.subdivision_code = match.group('subdivision_code') + self.values = values + + @property + def parents(self): + bits = self.code.split('-')[:-1] + while bits: + yield '-'.join(bits) + bits.pop() + + def __cmp__(self, other): + return 0 if (self.continent_code == other.continent_code and + self.country_code == other.country_code and + self.subdivision_code == other.subdivision_code and + self.values == other.values) else 1 + + def __repr__(self): + return "'Geo {} {} {} {}'".format(self.continent_code, + self.country_code, + self.subdivision_code, self.values) + + +class _ValuesMixin(object): + + def __init__(self, zone, name, data, source=None): + super(_ValuesMixin, self).__init__(zone, name, data, source=source) + try: + self.values = sorted(self._process_values(data['values'])) + except KeyError: + try: + self.values = self._process_values([data['value']]) + except KeyError: + raise Exception('Invalid record {}, missing value(s)' + .format(self.fqdn)) + + def changes(self, other, target): + if self.values != other.values: + return Update(self, other) + return super(_ValuesMixin, self).changes(other, target) + + def _data(self): + ret = super(_ValuesMixin, self)._data() + if len(self.values) > 1: + ret['values'] = [getattr(v, 'data', v) for v in self.values] + else: + v = self.values[0] + ret['value'] = getattr(v, 'data', v) + return ret + + def __repr__(self): + return '<{} {} {}, {}, {}>'.format(self.__class__.__name__, + self._type, self.ttl, + self.fqdn, self.values) + + +class _GeoMixin(_ValuesMixin): + ''' + Adds GeoDNS support to a record. + + Must be included before `Record`. + ''' + + # TODO: move away from "data" hash to strict params, it's kind of leaking + # the yaml implementation into here and then forcing it back out into + # non-yaml providers during input + def __init__(self, zone, name, data, *args, **kwargs): + super(_GeoMixin, self).__init__(zone, name, data, *args, **kwargs) + try: + self.geo = dict(data['geo']) + except KeyError: + self.geo = {} + for k, vs in self.geo.items(): + vs = sorted(self._process_values(vs)) + self.geo[k] = GeoValue(k, vs) + + def _data(self): + ret = super(_GeoMixin, self)._data() + if self.geo: + geo = {} + for code, value in self.geo.items(): + geo[code] = value.values + ret['geo'] = geo + return ret + + def changes(self, other, target): + if target.SUPPORTS_GEO: + if self.geo != other.geo: + return Update(self, other) + return super(_GeoMixin, self).changes(other, target) + + def __repr__(self): + if self.geo: + return '<{} {} {}, {}, {}, {}>'.format(self.__class__.__name__, + self._type, self.ttl, + self.fqdn, self.values, + self.geo) + return super(_GeoMixin, self).__repr__() + + +class ARecord(_GeoMixin, Record): + _type = 'A' + + def _process_values(self, values): + for ip in values: + try: + IPv4Address(unicode(ip)) + except Exception: + raise Exception('Invalid record {}, value {} not a valid ip' + .format(self.fqdn, ip)) + return values + + +class AaaaRecord(_GeoMixin, Record): + _type = 'AAAA' + + def _process_values(self, values): + ret = [] + for ip in values: + try: + IPv6Address(unicode(ip)) + ret.append(ip.lower()) + except Exception: + raise Exception('Invalid record {}, value {} not a valid ip' + .format(self.fqdn, ip)) + return ret + + +class _ValueMixin(object): + + def __init__(self, zone, name, data, source=None): + super(_ValueMixin, self).__init__(zone, name, data, source=source) + try: + self.value = self._process_value(data['value']) + except KeyError: + raise Exception('Invalid record {}, missing value' + .format(self.fqdn)) + + def changes(self, other, target): + if self.value != other.value: + return Update(self, other) + return super(_ValueMixin, self).changes(other, target) + + def _data(self): + ret = super(_ValueMixin, self)._data() + ret['value'] = getattr(self.value, 'data', self.value) + return ret + + def __repr__(self): + return '<{} {} {}, {}, {}>'.format(self.__class__.__name__, + self._type, self.ttl, + self.fqdn, self.value) + + +class CnameRecord(_ValueMixin, Record): + _type = 'CNAME' + + def _process_value(self, value): + if not value.endswith('.'): + raise Exception('Invalid record {}, value {} missing trailing .' + .format(self.fqdn, value)) + return value.lower() + + +class MxValue(object): + + def __init__(self, value): + # TODO: rename preference + self.priority = int(value['priority']) + # TODO: rename to exchange? + self.value = value['value'].lower() + + @property + def data(self): + return { + 'priority': self.priority, + 'value': self.value, + } + + def __cmp__(self, other): + if self.priority == other.priority: + return cmp(self.value, other.value) + return cmp(self.priority, other.priority) + + def __repr__(self): + return "'{} {}'".format(self.priority, self.value) + + +class MxRecord(_ValuesMixin, Record): + _type = 'MX' + + def _process_values(self, values): + ret = [] + for value in values: + try: + ret.append(MxValue(value)) + except KeyError as e: + raise Exception('Invalid value in record {}, missing {}' + .format(self.fqdn, e.args[0])) + return ret + + +class NaptrValue(object): + + def __init__(self, value): + self.order = int(value['order']) + self.preference = int(value['preference']) + self.flags = value['flags'] + self.service = value['service'] + self.regexp = value['regexp'] + self.replacement = value['replacement'] + + @property + def data(self): + return { + 'order': self.order, + 'preference': self.preference, + 'flags': self.flags, + 'service': self.service, + 'regexp': self.regexp, + 'replacement': self.replacement, + } + + def __cmp__(self, other): + if self.order != other.order: + return cmp(self.order, other.order) + elif self.preference != other.preference: + return cmp(self.preference, other.preference) + elif self.flags != other.flags: + return cmp(self.flags, other.flags) + elif self.service != other.service: + return cmp(self.service, other.service) + elif self.regexp != other.regexp: + return cmp(self.regexp, other.regexp) + return cmp(self.replacement, other.replacement) + + def __repr__(self): + flags = self.flags if self.flags is not None else '' + service = self.service if self.service is not None else '' + regexp = self.regexp if self.regexp is not None else '' + return "'{} {} \"{}\" \"{}\" \"{}\" {}'" \ + .format(self.order, self.preference, flags, service, regexp, + self.replacement) + + +class NaptrRecord(_ValuesMixin, Record): + _type = 'NAPTR' + + def _process_values(self, values): + ret = [] + for value in values: + try: + ret.append(NaptrValue(value)) + except KeyError as e: + raise Exception('Invalid value in record {}, missing {}' + .format(self.fqdn, e.args[0])) + return ret + + +class NsRecord(_ValuesMixin, Record): + _type = 'NS' + + def _process_values(self, values): + ret = [] + for ns in values: + if not ns.endswith('.'): + raise Exception('Invalid record {}, value {} missing ' + 'trailing .'.format(self.fqdn, ns)) + ret.append(ns.lower()) + return ret + + +class PtrRecord(_ValueMixin, Record): + _type = 'PTR' + + def _process_value(self, value): + if not value.endswith('.'): + raise Exception('Invalid record {}, value {} missing trailing .' + .format(self.fqdn, value)) + return value.lower() + + +class SshfpValue(object): + + def __init__(self, value): + self.algorithm = int(value['algorithm']) + self.fingerprint_type = int(value['fingerprint_type']) + self.fingerprint = value['fingerprint'] + + @property + def data(self): + return { + 'algorithm': self.algorithm, + 'fingerprint_type': self.fingerprint_type, + 'fingerprint': self.fingerprint, + } + + def __cmp__(self, other): + if self.algorithm != other.algorithm: + return cmp(self.algorithm, other.algorithm) + elif self.fingerprint_type != other.fingerprint_type: + return cmp(self.fingerprint_type, other.fingerprint_type) + return cmp(self.fingerprint, other.fingerprint) + + def __repr__(self): + return "'{} {} {}'".format(self.algorithm, self.fingerprint_type, + self.fingerprint) + + +class SshfpRecord(_ValuesMixin, Record): + _type = 'SSHFP' + + def _process_values(self, values): + ret = [] + for value in values: + try: + ret.append(SshfpValue(value)) + except KeyError as e: + raise Exception('Invalid value in record {}, missing {}' + .format(self.fqdn, e.args[0])) + return ret + + +class SpfRecord(_ValuesMixin, Record): + _type = 'SPF' + + def _process_values(self, values): + return values + + +class SrvValue(object): + + def __init__(self, value): + self.priority = int(value['priority']) + self.weight = int(value['weight']) + self.port = int(value['port']) + self.target = value['target'].lower() + + @property + def data(self): + return { + 'priority': self.priority, + 'weight': self.weight, + 'port': self.port, + 'target': self.target, + } + + def __cmp__(self, other): + if self.priority != other.priority: + return cmp(self.priority, other.priority) + elif self.weight != other.weight: + return cmp(self.weight, other.weight) + elif self.port != other.port: + return cmp(self.port, other.port) + return cmp(self.target, other.target) + + def __repr__(self): + return "'{} {} {} {}'".format(self.priority, self.weight, self.port, + self.target) + + +class SrvRecord(_ValuesMixin, Record): + _type = 'SRV' + _name_re = re.compile(r'^_[^\.]+\.[^\.]+') + + def __init__(self, zone, name, data, source=None): + if not self._name_re.match(name): + raise Exception('Invalid name {}.{}'.format(name, zone.name)) + super(SrvRecord, self).__init__(zone, name, data, source) + + def _process_values(self, values): + ret = [] + for value in values: + try: + ret.append(SrvValue(value)) + except KeyError as e: + raise Exception('Invalid value in record {}, missing {}' + .format(self.fqdn, e.args[0])) + return ret + + +class TxtRecord(_ValuesMixin, Record): + _type = 'TXT' + + def _process_values(self, values): + for value in values: + if _unescaped_semicolon_re.search(value): + raise Exception('Invalid record {}, unescaped ;' + .format(self.fqdn)) + return values diff --git a/octodns/source/__init__.py b/octodns/source/__init__.py new file mode 100644 index 0000000..14ccf18 --- /dev/null +++ b/octodns/source/__init__.py @@ -0,0 +1,6 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/octodns/source/base.py b/octodns/source/base.py new file mode 100644 index 0000000..72ebaab --- /dev/null +++ b/octodns/source/base.py @@ -0,0 +1,33 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +class BaseSource(object): + + def __init__(self, id): + self.id = id + if not getattr(self, 'log', False): + raise NotImplementedError('Abstract base class, log property ' + 'missing') + if not hasattr(self, 'SUPPORTS_GEO'): + raise NotImplementedError('Abstract base class, SUPPORTS_GEO ' + 'property missing') + + def populate(self, zone, target=False): + ''' + Loads all zones the provider knows about + ''' + raise NotImplementedError('Abstract base class, populate method ' + 'missing') + + def supports(self, record): + # Unless overriden and handled appropriaitely we'll assume that all + # record types are supported + return True + + def __repr__(self): + return self.__class__.__name__ diff --git a/octodns/source/tinydns.py b/octodns/source/tinydns.py new file mode 100644 index 0000000..7d66998 --- /dev/null +++ b/octodns/source/tinydns.py @@ -0,0 +1,208 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from collections import defaultdict +from ipaddress import ip_address +from os import listdir +from os.path import join +import logging +import re + +from ..record import Record +from ..zone import DuplicateRecordException, SubzoneRecordException +from .base import BaseSource + + +class TinyDnsSource(BaseSource): + SUPPORTS_GEO = False + + split_re = re.compile(r':+') + + def __init__(self, id, default_ttl=3600): + super(TinyDnsSource, self).__init__(id) + self.default_ttl = default_ttl + + def _data_for_A(self, _type, records): + values = [] + for record in records: + if record[0] != '0.0.0.0': + values.append(record[0]) + if len(values) == 0: + return + try: + ttl = records[0][1] + except IndexError: + ttl = self.default_ttl + return { + 'ttl': ttl, + 'type': _type, + 'values': values, + } + + def _data_for_CNAME(self, _type, records): + first = records[0] + try: + ttl = first[1] + except IndexError: + ttl = self.default_ttl + return { + 'ttl': ttl, + 'type': _type, + 'value': '{}.'.format(first[0]) + } + + def _data_for_MX(self, _type, records): + try: + ttl = records[0][2] + except IndexError: + ttl = self.default_ttl + return { + 'ttl': ttl, + 'type': _type, + 'values': [{ + 'priority': r[1], + 'value': '{}.'.format(r[0]) + } for r in records] + } + + def _data_for_NS(self, _type, records): + try: + ttl = records[0][1] + except IndexError: + ttl = self.default_ttl + return { + 'ttl': ttl, + 'type': _type, + 'values': ['{}.'.format(r[0]) for r in records] + } + + def populate(self, zone, target=False): + self.log.debug('populate: zone=%s', zone.name) + before = len(zone.records) + + if zone.name.endswith('in-addr.arpa.'): + self._populate_in_addr_arpa(zone) + else: + self._populate_normal(zone) + + self.log.info('populate: found %s records', + len(zone.records) - before) + + def _populate_normal(self, zone): + type_map = { + '=': 'A', + '^': None, + '.': 'NS', + 'C': 'CNAME', + '+': 'A', + '@': 'MX', + } + name_re = re.compile('((?P.+)\.)?{}$'.format(zone.name[:-1])) + + data = defaultdict(lambda: defaultdict(list)) + for line in self._lines(): + _type = line[0] + if _type not in type_map: + # Something we don't care about + continue + _type = type_map[_type] + if not _type: + continue + + # Skip type, remove trailing comments, and omit newline + line = line[1:].split('#', 1)[0] + # Split on :'s including :: and strip leading/trailing ws + line = [p.strip() for p in self.split_re.split(line)] + match = name_re.match(line[0]) + if not match: + continue + name = zone.hostname_from_fqdn(line[0]) + data[name][_type].append(line[1:]) + + for name, types in data.items(): + for _type, d in types.items(): + data_for = getattr(self, '_data_for_{}'.format(_type)) + data = data_for(_type, d) + if data: + record = Record.new(zone, name, data, source=self) + try: + zone.add_record(record) + except SubzoneRecordException: + self.log.debug('_populate_normal: skipping subzone ' + 'record=%s', record) + + def _populate_in_addr_arpa(self, zone): + name_re = re.compile('(?P.+)\.{}$'.format(zone.name[:-1])) + + for line in self._lines(): + _type = line[0] + # We're only interested in = (A+PTR), and ^ (PTR) records + if _type not in ('=', '^'): + continue + + # Skip type, remove trailing comments, and omit newline + line = line[1:].split('#', 1)[0] + # Split on :'s including :: and strip leading/trailing ws + line = [p.strip() for p in self.split_re.split(line)] + + if line[0].endswith('in-addr.arpa'): + # since it's already in in-addr.arpa format + match = name_re.match(line[0]) + value = '{}.'.format(line[1]) + else: + addr = ip_address(line[1]) + match = name_re.match(addr.reverse_pointer) + value = '{}.'.format(line[0]) + + if match: + try: + ttl = line[2] + except IndexError: + ttl = self.default_ttl + + name = match.group('name') + record = Record.new(zone, name, { + 'ttl': ttl, + 'type': 'PTR', + 'value': value + }, source=self) + try: + zone.add_record(record) + except DuplicateRecordException: + self.log.warn('Duplicate PTR record for {}, ' + 'skipping'.format(addr)) + + +class TinyDnsFileSource(TinyDnsSource): + ''' + A basic TinyDNS zonefile importer created to import legacy data. + + NOTE: timestamps & lo fields are ignored if present. + ''' + def __init__(self, id, directory, default_ttl=3600): + self.log = logging.getLogger('TinyDnsFileSource[{}]'.format(id)) + self.log.debug('__init__: id=%s, directory=%s, default_ttl=%d', id, + directory, default_ttl) + super(TinyDnsFileSource, self).__init__(id, default_ttl) + self.directory = directory + self._cache = None + + def _lines(self): + if self._cache is None: + # We unfortunately don't know where to look since tinydns stuff can + # be defined anywhere so we'll just read all files + lines = [] + for filename in listdir(self.directory): + if filename[0] == '.': + # Ignore hidden files + continue + with open(join(self.directory, filename), 'r') as fh: + lines += filter(lambda l: l, fh.read().split('\n')) + + self._cache = lines + + return self._cache diff --git a/octodns/yaml.py b/octodns/yaml.py new file mode 100644 index 0000000..b6c6379 --- /dev/null +++ b/octodns/yaml.py @@ -0,0 +1,79 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from yaml import SafeDumper, SafeLoader, load, dump +from yaml.constructor import ConstructorError +import re + + +# zero-padded sort, simplified version of +# https://www.xormedia.com/natural-sort-order-with-zero-padding/ +_pad_re = re.compile('\d+') + + +def _zero_pad(match): + return '{:04d}'.format(int(match.group(0))) + + +def _zero_padded_numbers(s): + try: + int(s) + except ValueError: + return _pad_re.sub(lambda d: _zero_pad(d), s) + + +# Found http://stackoverflow.com/a/21912744 which guided me on how to hook in +# here +class SortEnforcingLoader(SafeLoader): + + def __init__(self, *args, **kwargs): + super(SortEnforcingLoader, self).__init__(*args, **kwargs) + self.add_constructor(self.DEFAULT_MAPPING_TAG, self._construct) + + def _construct(self, _, node): + self.flatten_mapping(node) + ret = self.construct_pairs(node) + keys = [d[0] for d in ret] + if keys != sorted(keys, key=_zero_padded_numbers): + raise ConstructorError(None, None, "keys out of order: {}" + .format(', '.join(keys)), node.start_mark) + return dict(ret) + + +def safe_load(stream, enforce_order=True): + return load(stream, SortEnforcingLoader if enforce_order else SafeLoader) + + +class SortingDumper(SafeDumper): + ''' + This sorts keys alphanumerically in a "natural" manner where things with + the number 2 come before the number 12. + + See https://www.xormedia.com/natural-sort-order-with-zero-padding/ for + more info + ''' + + def __init__(self, *args, **kwargs): + super(SortingDumper, self).__init__(*args, **kwargs) + self.add_representer(dict, self._representer) + + def _representer(self, _, data): + data = data.items() + data.sort(key=lambda d: _zero_padded_numbers(d[0])) + return self.represent_mapping(self.DEFAULT_MAPPING_TAG, data) + + +def safe_dump(data, fh, **options): + kwargs = { + 'canonical': False, + 'indent': 2, + 'default_style': '', + 'default_flow_style': False, + 'explicit_start': True + } + kwargs.update(options) + dump(data, fh, SortingDumper, **kwargs) diff --git a/octodns/zone.py b/octodns/zone.py new file mode 100644 index 0000000..e9c64b4 --- /dev/null +++ b/octodns/zone.py @@ -0,0 +1,117 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from logging import getLogger +import re + +from .record import Create, Delete + + +class SubzoneRecordException(Exception): + pass + + +class DuplicateRecordException(Exception): + pass + + +def _is_eligible(record): + # Should this record be considered when computing changes + # We ignore all top-level NS records + return record._type != 'NS' or record.name != '' + + +class Zone(object): + log = getLogger('Zone') + + def __init__(self, name, sub_zones): + if not name[-1] == '.': + raise Exception('Invalid zone name {}, missing ending dot' + .format(name)) + # Force everyting to lowercase just to be safe + self.name = str(name).lower() if name else name + self.sub_zones = sub_zones + self.records = set() + # optional leading . to match empty hostname + # optional trailing . b/c some sources don't have it on their fqdn + self._name_re = re.compile('\.?{}?$'.format(name)) + + self.log.debug('__init__: zone=%s, sub_zones=%s', self, sub_zones) + + def hostname_from_fqdn(self, fqdn): + return self._name_re.sub('', fqdn) + + def add_record(self, record): + name = record.name + last = name.split('.')[-1] + if last in self.sub_zones: + if name != last: + # it's a record for something under a sub-zone + raise SubzoneRecordException('Record {} is under a ' + 'managed subzone' + .format(record.fqdn)) + elif record._type != 'NS': + # It's a non NS record for exactly a sub-zone + raise SubzoneRecordException('Record {} a managed sub-zone ' + 'and not of type NS' + .format(record.fqdn)) + if record in self.records: + raise DuplicateRecordException('Duplicate record {}, type {}' + .format(record.fqdn, record._type)) + self.records.add(record) + + def changes(self, desired, target): + self.log.debug('changes: zone=%s, target=%s', self, target) + + # Build up a hash of the desired records, thanks to our special + # __hash__ and __cmp__ on Record we'll be able to look up records that + # match name and _type with it + desired_records = {r: r for r in desired.records} + + changes = [] + + # Find diffs & removes + for record in filter(_is_eligible, self.records): + try: + desired_record = desired_records[record] + except KeyError: + if not target.supports(record): + self.log.debug('changes: skipping record=%s %s - %s does ' + 'not support it', record.fqdn, record._type, + target.id) + continue + # record has been removed + self.log.debug('changes: zone=%s, removed record=%s', self, + record) + changes.append(Delete(record)) + else: + change = record.changes(desired_record, target) + if change: + self.log.debug('changes: zone=%s, modified\n' + ' existing=%s,\n desired=%s', self, + record, desired_record) + changes.append(change) + else: + self.log.debug('changes: zone=%s, n.c. record=%s', self, + record) + + # Find additions, things that are in desired, but missing in ourselves. + # This uses set math and our special __hash__ and __cmp__ functions as + # well + for record in filter(_is_eligible, desired.records - self.records): + if not target.supports(record): + self.log.debug('changes: skipping record=%s %s - %s does not ' + 'support it', record.fqdn, record._type, + target.id) + continue + self.log.debug('changes: zone=%s, create record=%s', self, record) + changes.append(Create(record)) + + return changes + + def __repr__(self): + return 'Zone<{}>'.format(self.name) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..265f407 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,6 @@ +coverage +mock +nose +pep8 +pyflakes +requests_mock diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a248347 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +# These are known good versions. You're free to use others and things will +# likely work, but no promises are made, especilly if you go older. +PyYaml==3.12 +boto3==1.4.4 +botocore==1.5.4 +dnspython==1.15.0 +docutils==0.13.1 +dyn==1.7.10 +futures==3.0.5 +incf.countryutils==1.0 +ipaddress==1.0.18 +jmespath==0.9.0 +python-dateutil==2.6.0 +requests==2.13.0 +s3transfer==0.1.10 +six==1.10.0 +yamllint==1.6.0 diff --git a/script/bootstrap b/script/bootstrap new file mode 100755 index 0000000..1f76914 --- /dev/null +++ b/script/bootstrap @@ -0,0 +1,35 @@ +#!/bin/bash +# Usage: script/bootstrap +# Ensures all dependencies are installed locally. + +set -e + +cd "$(dirname $0)"/.. +ROOT=$(pwd) + +if [ -z "$VENV_NAME" ]; then + VENV_NAME="env" +fi + +if [ ! -d "$VENV_NAME" ]; then + if [ -z "$VENV_PYTHON" ]; then + VENV_PYTHON=`which python` + fi + virtualenv --python=$VENV_PYTHON $VENV_NAME +fi +. "$VENV_NAME/bin/activate" + +pip install -U -r requirements.txt + +if [ "$ENV" != "production" ]; then + pip install -U -r requirements-dev.txt +fi + +if [ ! -L ".git/hooks/pre-commit" ]; then + ln -s "$ROOT/.git_hooks_pre-commit" ".git/hooks/pre-commit" +fi + +echo "" +echo "Run source env/bin/activate to get your shell in to the virtualenv" +echo "See README.md for more information." +echo "" diff --git a/script/cibuild b/script/cibuild new file mode 100755 index 0000000..d048e8e --- /dev/null +++ b/script/cibuild @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +cd "$(dirname "$0")/.." + +echo "## bootstrap ###################################################################" +script/bootstrap + +echo "## environment & versions ######################################################" +python --version +pip --version +VVER=$(virtualenv --version) +echo "virtualenv $VVER" + +if [ -z "$VENV_NAME" ]; then + VENV_NAME="env" +fi + +. "$VENV_NAME/bin/activate" + +echo "## clean up ####################################################################" +find octodns tests -name "*.pyc" -exec rm {} \; +rm -f *.pyc +echo "## begin #######################################################################" +# For now it's just lint... +echo "## lint ########################################################################" +script/lint +echo "## tests/coverage ##############################################################" +script/coverage +echo "## complete ####################################################################" diff --git a/script/coverage b/script/coverage new file mode 100755 index 0000000..ca5d693 --- /dev/null +++ b/script/coverage @@ -0,0 +1,30 @@ +#!/bin/sh +set -e + +cd "$(dirname "$0")/.." + +if [ -z "$VENV_NAME" ]; then + VENV_NAME="env" +fi + +ACTIVATE="$VENV_NAME/bin/activate" +if [ ! -f "$ACTIVATE" ]; then + echo "$ACTIVATE does not exist, run ./script/bootstrap" >&2 + exit 1 +fi +. "$ACTIVATE" + +# Just to be sure/safe +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export CLOUDFLARE_EMAIL= +export CLOUDFLARE_TOKEN= +export DNSIMPLE_ACCOUNT= +export DNSIMPLE_TOKEN= +export DYN_CUSTOMER= +export DYN_PASSWORD= +export DYN_USERNAME= + +coverage run --branch --source=octodns `which nosetests` --with-xunit "$@" +coverage html +coverage xml diff --git a/script/lint b/script/lint new file mode 100755 index 0000000..91e6d60 --- /dev/null +++ b/script/lint @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +cd "$(dirname "$0")/.." +ROOT=$(pwd) + +if [ -z "$VENV_NAME" ]; then + VENV_NAME="env" +fi + +ACTIVATE="$VENV_NAME/bin/activate" +if [ ! -f "$ACTIVATE" ]; then + echo "$ACTIVATE does not exist, run ./script/bootstrap" >&2 + exit 1 +fi +. "$ACTIVATE" + +SOURCES="*.py octodns/*.py octodns/*/*.py tests/*.py" + +pep8 --ignore=E221,E241,E251 $SOURCES +pyflakes $SOURCES diff --git a/script/sdist b/script/sdist new file mode 100755 index 0000000..f244363 --- /dev/null +++ b/script/sdist @@ -0,0 +1,15 @@ +#!/bin/bash + +set -e + +if ! git diff-index --quiet HEAD --; then + echo "Changes in local directory, commit or clear" + exit 1 +fi + +SHA=$(git rev-parse HEAD) +python setup.py sdist +TARBALL=dist/octodns-$SHA.tar.gz +mv dist/octodns-0.*.tar.gz $TARBALL + +echo "Created $TARBALL" diff --git a/script/test b/script/test new file mode 100755 index 0000000..3ee6e9c --- /dev/null +++ b/script/test @@ -0,0 +1,28 @@ +#!/bin/sh +set -e + +cd "$(dirname "$0")/.." + +if [ -z "$VENV_NAME" ]; then + VENV_NAME="env" +fi + +ACTIVATE="$VENV_NAME/bin/activate" +if [ ! -f "$ACTIVATE" ]; then + echo "$ACTIVATE does not exist, run ./script/bootstrap" >&2 + exit 1 +fi +. "$ACTIVATE" + +# Just to be sure/safe +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export CLOUDFLARE_EMAIL= +export CLOUDFLARE_TOKEN= +export DNSIMPLE_ACCOUNT= +export DNSIMPLE_TOKEN= +export DYN_CUSTOMER= +export DYN_PASSWORD= +export DYN_USERNAME= + +nosetests "$@" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f1c44c9 --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python + +from os.path import dirname, join +import octodns + +try: + from setuptools import find_packages, setup +except ImportError: + from distutils.core import find_packages, setup + +cmds = ( + 'compare', + 'dump', + 'report', + 'sync', + 'validate' +) +cmds_dir = join(dirname(__file__), 'octodns', 'cmds') +console_scripts = { + 'octodns-{name} = octodns.cmds.{name}'.format(name=filename[:-3]) + for filename in cmds +} + +setup( + author='Ross McFarland', + author_email='rwmcfa1@gmail.com', + description=octodns.__doc__, + entry_points={ + 'console_scripts': console_scripts, + }, + install_requires=[ + 'PyYaml>=3.12', + 'dnspython>=1.15.0', + 'incf.countryutils>=1.0', + 'ipaddress>=1.0.18', + 'python-dateutil>=2.6.0', + 'requests>=2.13.0', + 'yamllint>=1.6.0' + ], + license='MIT', + long_description=open('README.md').read(), + name='octodns', + packages=find_packages(), + url='https://github.com/github/octodns', + version=octodns.__VERSION__, +) diff --git a/tests/config/bad-provider-class-module.yaml b/tests/config/bad-provider-class-module.yaml new file mode 100644 index 0000000..4c6f262 --- /dev/null +++ b/tests/config/bad-provider-class-module.yaml @@ -0,0 +1,4 @@ +providers: + dne: + class: octodns.provider.yaml.DoesntExistProvider +zones: {} diff --git a/tests/config/bad-provider-class-no-module.yaml b/tests/config/bad-provider-class-no-module.yaml new file mode 100644 index 0000000..ef6ca1c --- /dev/null +++ b/tests/config/bad-provider-class-no-module.yaml @@ -0,0 +1,4 @@ +providers: + dne: + class: DoesntExistProvider +zones: {} diff --git a/tests/config/bad-provider-class.yaml b/tests/config/bad-provider-class.yaml new file mode 100644 index 0000000..c5de382 --- /dev/null +++ b/tests/config/bad-provider-class.yaml @@ -0,0 +1,4 @@ +providers: + dne: + class: foo.bar.DoesntExistProvider +zones: {} diff --git a/tests/config/empty.yaml b/tests/config/empty.yaml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/tests/config/empty.yaml @@ -0,0 +1 @@ +--- diff --git a/tests/config/missing-provider-class.yaml b/tests/config/missing-provider-class.yaml new file mode 100644 index 0000000..ce27542 --- /dev/null +++ b/tests/config/missing-provider-class.yaml @@ -0,0 +1,3 @@ +providers: + yaml: {} +zones: {} diff --git a/tests/config/missing-provider-config.yaml b/tests/config/missing-provider-config.yaml new file mode 100644 index 0000000..548e708 --- /dev/null +++ b/tests/config/missing-provider-config.yaml @@ -0,0 +1,4 @@ +providers: + yaml: + class: octodns.provider.yaml.YamlProvider +zones: {} diff --git a/tests/config/missing-provider-env.yaml b/tests/config/missing-provider-env.yaml new file mode 100644 index 0000000..1985e3f --- /dev/null +++ b/tests/config/missing-provider-env.yaml @@ -0,0 +1,6 @@ +providers: + yaml: + class: octodns.provider.yaml.YamlProvider + int: 42 + directory: env/DOES_NOT_EXIST +zones: {} diff --git a/tests/config/missing-sources.yaml b/tests/config/missing-sources.yaml new file mode 100644 index 0000000..2a562a1 --- /dev/null +++ b/tests/config/missing-sources.yaml @@ -0,0 +1,3 @@ +providers: {} +zones: + missing.sources.: {} diff --git a/tests/config/no-dump.yaml b/tests/config/no-dump.yaml new file mode 100644 index 0000000..c10f839 --- /dev/null +++ b/tests/config/no-dump.yaml @@ -0,0 +1,13 @@ +providers: + in: + class: octodns.provider.yaml.YamlProvider + directory: tests/config + out: + class: octodns.provider.yaml.YamlProvider + directory: /tmp/foo +zones: + unit.tests.: + sources: + - in + targets: + - out diff --git a/tests/config/simple-validate.yaml b/tests/config/simple-validate.yaml new file mode 100644 index 0000000..d5ccd50 --- /dev/null +++ b/tests/config/simple-validate.yaml @@ -0,0 +1,13 @@ +providers: + in: + class: octodns.provider.yaml.YamlProvider + directory: tests/config + simple: + class: helpers.SimpleProvider +zones: + unit.tests.: + sources: + - in + - simple + targets: + - dump diff --git a/tests/config/simple.yaml b/tests/config/simple.yaml new file mode 100644 index 0000000..604b772 --- /dev/null +++ b/tests/config/simple.yaml @@ -0,0 +1,35 @@ +providers: + in: + class: octodns.provider.yaml.YamlProvider + directory: tests/config + dump: + class: octodns.provider.yaml.YamlProvider + directory: env/YAML_TMP_DIR + # This is sort of ugly, but it shouldn't hurt anything. It'll just write out + # the target file twice where it and dump are both used + dump2: + class: octodns.provider.yaml.YamlProvider + directory: env/YAML_TMP_DIR + simple: + class: helpers.SimpleProvider + geo: + class: helpers.GeoProvider + nosshfp: + class: helpers.NoSshFpProvider +zones: + unit.tests.: + sources: + - in + targets: + - dump + subzone.unit.tests.: + sources: + - in + targets: + - dump + - dump2 + empty.: + sources: + - in + targets: + - dump diff --git a/tests/config/subzone.unit.tests.yaml b/tests/config/subzone.unit.tests.yaml new file mode 100644 index 0000000..932ac28 --- /dev/null +++ b/tests/config/subzone.unit.tests.yaml @@ -0,0 +1,10 @@ +--- +2: + type: A + value: 2.4.4.4 +12: + type: A + value: 12.4.4.4 +test: + type: A + value: 4.4.4.4 diff --git a/tests/config/unit.tests.yaml b/tests/config/unit.tests.yaml new file mode 100644 index 0000000..90604ae --- /dev/null +++ b/tests/config/unit.tests.yaml @@ -0,0 +1,108 @@ +--- +? '' +: - geo: + AF: + - 2.2.3.4 + - 2.2.3.5 + AS-JP: + - 3.2.3.4 + - 3.2.3.5 + NA-US: + - 4.2.3.4 + - 4.2.3.5 + NA-US-CA: + - 5.2.3.4 + - 5.2.3.5 + ttl: 300 + type: A + values: + - 1.2.3.4 + - 1.2.3.5 + - ttl: 3600 + type: SSHFP + values: + - algorithm: 1 + fingerprint: bf6b6825d2977c511a475bbefb88aad54a92ac73 + fingerprint_type: 1 + - algorithm: 1 + fingerprint: 7491973e5f8b39d5327cd4e08bc81b05f7710b49 + fingerprint_type: 1 + - type: NS + values: + - 6.2.3.4. + - 7.2.3.4. +_srv._tcp: + ttl: 600 + type: SRV + values: + - port: 30 + priority: 12 + target: foo-2.unit.tests. + weight: 20 + - port: 30 + priority: 10 + target: foo-1.unit.tests. + weight: 20 +aaaa: + ttl: 600 + type: AAAA + value: 2601:644:500:e210:62f8:1dff:feb8:947a +cname: + ttl: 300 + type: CNAME + value: unit.tests. +mx: + ttl: 300 + type: MX + values: + - priority: 40 + value: smtp-1.unit.tests. + - priority: 20 + value: smtp-2.unit.tests. + - priority: 30 + value: smtp-3.unit.tests. + - priority: 10 + value: smtp-4.unit.tests. +naptr: + ttl: 600 + type: NAPTR + values: + - flags: U + order: 100 + preference: 100 + regexp: '!^.*$!sip:info@bar.example.com!' + replacement: . + service: SIP+D2U + - flags: S + order: 10 + preference: 100 + regexp: '!^.*$!sip:info@bar.example.com!' + replacement: . + service: SIP+D2U +ptr: + ttl: 300 + type: PTR + value: foo.bar.com. +spf: + ttl: 600 + type: SPF + value: v=spf1 ip4:192.168.0.1/16-all +sub: + type: 'NS' + values: + - 6.2.3.4. + - 7.2.3.4. +txt: + ttl: 600 + type: TXT + values: + - Bah bah black sheep + - have you any wool. +www: + ttl: 300 + type: A + value: 2.2.3.6 +www.sub: + ttl: 300 + type: A + value: 2.2.3.6 diff --git a/tests/config/unknown-provider.yaml b/tests/config/unknown-provider.yaml new file mode 100644 index 0000000..9071046 --- /dev/null +++ b/tests/config/unknown-provider.yaml @@ -0,0 +1,28 @@ +providers: + yaml: + class: octodns.provider.yaml.YamlProvider + directory: ./config + simple_source: + class: helpers.SimpleSource +zones: + missing.sources.: + targets: + - yaml + missing.targets.: + sources: + - yaml + unknown.source.: + sources: + - not-there + targets: + - yaml + unknown.target.: + sources: + - yaml + targets: + - not-there-either + not.targetable.: + sources: + - yaml + targets: + - simple_source diff --git a/tests/config/unordered.yaml b/tests/config/unordered.yaml new file mode 100644 index 0000000..e641c3b --- /dev/null +++ b/tests/config/unordered.yaml @@ -0,0 +1,8 @@ +--- +abc: + type: A + value: 9.9.9.9 +xyz: + # t comes before v + value: 9.9.9.9 + type: A diff --git a/tests/fixtures/cloudflare-dns_records-page-1.json b/tests/fixtures/cloudflare-dns_records-page-1.json new file mode 100644 index 0000000..d67dfb3 --- /dev/null +++ b/tests/fixtures/cloudflare-dns_records-page-1.json @@ -0,0 +1,188 @@ +{ + "result": [ + { + "id": "fc12ab34cd5611334422ab3322997650", + "type": "A", + "name": "unit.tests", + "content": "1.2.3.4", + "proxiable": true, + "proxied": false, + "ttl": 300, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.054409Z", + "created_on": "2017-03-11T18:01:43.054409Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997651", + "type": "A", + "name": "unit.tests", + "content": "1.2.3.5", + "proxiable": true, + "proxied": false, + "ttl": 300, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.160148Z", + "created_on": "2017-03-11T18:01:43.160148Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997653", + "type": "A", + "name": "www.unit.tests", + "content": "2.2.3.6", + "proxiable": true, + "proxied": false, + "ttl": 300, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.420689Z", + "created_on": "2017-03-11T18:01:43.420689Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997654", + "type": "A", + "name": "www.sub.unit.tests", + "content": "2.2.3.6", + "proxiable": true, + "proxied": false, + "ttl": 300, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:44.030044Z", + "created_on": "2017-03-11T18:01:44.030044Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997655", + "type": "AAAA", + "name": "aaaa.unit.tests", + "content": "2601:644:500:e210:62f8:1dff:feb8:947a", + "proxiable": true, + "proxied": false, + "ttl": 600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.843594Z", + "created_on": "2017-03-11T18:01:43.843594Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997656", + "type": "CNAME", + "name": "cname.unit.tests", + "content": "unit.tests", + "proxiable": true, + "proxied": false, + "ttl": 300, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.940682Z", + "created_on": "2017-03-11T18:01:43.940682Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997657", + "type": "MX", + "name": "mx.unit.tests", + "content": "smtp-1.unit.tests", + "proxiable": false, + "proxied": false, + "ttl": 300, + "priority": 40, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.764273Z", + "created_on": "2017-03-11T18:01:43.764273Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997658", + "type": "MX", + "name": "mx.unit.tests", + "content": "smtp-2.unit.tests", + "proxiable": false, + "proxied": false, + "ttl": 300, + "priority": 20, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.586007Z", + "created_on": "2017-03-11T18:01:43.586007Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997659", + "type": "MX", + "name": "mx.unit.tests", + "content": "smtp-3.unit.tests", + "proxiable": false, + "proxied": false, + "ttl": 300, + "priority": 30, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.670592Z", + "created_on": "2017-03-11T18:01:43.670592Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997660", + "type": "MX", + "name": "mx.unit.tests", + "content": "smtp-4.unit.tests", + "proxiable": false, + "proxied": false, + "ttl": 300, + "priority": 10, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.505671Z", + "created_on": "2017-03-11T18:01:43.505671Z", + "meta": { + "auto_added": false + } + } + ], + "result_info": { + "page": 1, + "per_page": 10, + "total_pages": 2, + "count": 10, + "total_count": 16 + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/fixtures/cloudflare-dns_records-page-2.json b/tests/fixtures/cloudflare-dns_records-page-2.json new file mode 100644 index 0000000..4995c51 --- /dev/null +++ b/tests/fixtures/cloudflare-dns_records-page-2.json @@ -0,0 +1,116 @@ +{ + "result": [ + { + "id": "fc12ab34cd5611334422ab3322997661", + "type": "NS", + "name": "under.unit.tests", + "content": "ns1.unit.tests", + "proxiable": false, + "proxied": false, + "ttl": 3600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:42.599878Z", + "created_on": "2017-03-11T18:01:42.599878Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997662", + "type": "NS", + "name": "under.unit.tests", + "content": "ns2.unit.tests", + "proxiable": false, + "proxied": false, + "ttl": 3600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:42.727011Z", + "created_on": "2017-03-11T18:01:42.727011Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997663", + "type": "SPF", + "name": "spf.unit.tests", + "content": "v=spf1 ip4:192.168.0.1/16-all", + "proxiable": false, + "proxied": false, + "ttl": 600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:44.112568Z", + "created_on": "2017-03-11T18:01:44.112568Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997664", + "type": "TXT", + "name": "txt.unit.tests", + "content": "Bah bah black sheep", + "proxiable": false, + "proxied": false, + "ttl": 600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:42.837282Z", + "created_on": "2017-03-11T18:01:42.837282Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997665", + "type": "TXT", + "name": "txt.unit.tests", + "content": "have you any wool.", + "proxiable": false, + "proxied": false, + "ttl": 600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:42.961566Z", + "created_on": "2017-03-11T18:01:42.961566Z", + "meta": { + "auto_added": false + } + }, + { + "id": "fc12ab34cd5611334422ab3322997666", + "type": "SOA", + "name": "unit.tests", + "content": "ignored", + "proxiable": false, + "proxied": false, + "ttl": 600, + "locked": false, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:42.961566Z", + "created_on": "2017-03-11T18:01:42.961566Z", + "meta": { + "auto_added": false + } + } + ], + "result_info": { + "page": 2, + "per_page": 10, + "total_pages": 2, + "count": 6, + "total_count": 16 + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/fixtures/cloudflare-zones-page-1.json b/tests/fixtures/cloudflare-zones-page-1.json new file mode 100644 index 0000000..86f3b21 --- /dev/null +++ b/tests/fixtures/cloudflare-zones-page-1.json @@ -0,0 +1,140 @@ +{ + "result": [ + { + "id": "234234243423aaabb334342aaa343433", + "name": "github.com", + "status": "pending", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "alice.ns.cloudflare.com", + "tom.ns.cloudflare.com" + ], + "original_name_servers": [], + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2017-02-20T03:57:03.753292Z", + "created_on": "2017-02-20T03:53:59.274170Z", + "meta": { + "step": 4, + "wildcard_proxiable": false, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false, + "multiple_railguns_allowed": false + }, + "owner": { + "type": "user", + "id": "334234243423aaabb334342aaa343433", + "email": "noreply@github.com" + }, + "permissions": [ + "#analytics:read", + "#billing:edit", + "#billing:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#lb:edit", + "#lb:read", + "#logs:read", + "#organization:edit", + "#organization:read", + "#ssl:edit", + "#ssl:read", + "#waf:edit", + "#waf:read", + "#zone:edit", + "#zone:read", + "#zone_settings:edit", + "#zone_settings:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": true, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "234234243423aaabb334342aaa343434", + "name": "github.io", + "status": "pending", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "alice.ns.cloudflare.com", + "tom.ns.cloudflare.com" + ], + "original_name_servers": [], + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2017-02-20T04:12:00.732827Z", + "created_on": "2017-02-20T04:11:58.250696Z", + "meta": { + "step": 4, + "wildcard_proxiable": false, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false, + "multiple_railguns_allowed": false + }, + "owner": { + "type": "user", + "id": "334234243423aaabb334342aaa343433", + "email": "noreply@github.com" + }, + "permissions": [ + "#analytics:read", + "#billing:edit", + "#billing:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#lb:edit", + "#lb:read", + "#logs:read", + "#organization:edit", + "#organization:read", + "#ssl:edit", + "#ssl:read", + "#waf:edit", + "#waf:read", + "#zone:edit", + "#zone:read", + "#zone_settings:edit", + "#zone_settings:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": true, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + } + ], + "result_info": { + "page": 1, + "per_page": 2, + "total_pages": 2, + "count": 2, + "total_count": 4 + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/fixtures/cloudflare-zones-page-2.json b/tests/fixtures/cloudflare-zones-page-2.json new file mode 100644 index 0000000..bc4abdf --- /dev/null +++ b/tests/fixtures/cloudflare-zones-page-2.json @@ -0,0 +1,140 @@ +{ + "result": [ + { + "id": "234234243423aaabb334342aaa343434", + "name": "githubusercontent.com", + "status": "pending", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "alice.ns.cloudflare.com", + "tom.ns.cloudflare.com" + ], + "original_name_servers": [], + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2017-02-20T04:06:46.019706Z", + "created_on": "2017-02-20T04:05:51.683040Z", + "meta": { + "step": 4, + "wildcard_proxiable": false, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false, + "multiple_railguns_allowed": false + }, + "owner": { + "type": "user", + "id": "334234243423aaabb334342aaa343433", + "email": "noreply@github.com" + }, + "permissions": [ + "#analytics:read", + "#billing:edit", + "#billing:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#lb:edit", + "#lb:read", + "#logs:read", + "#organization:edit", + "#organization:read", + "#ssl:edit", + "#ssl:read", + "#waf:edit", + "#waf:read", + "#zone:edit", + "#zone:read", + "#zone_settings:edit", + "#zone_settings:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": true, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + }, + { + "id": "234234243423aaabb334342aaa343435", + "name": "unit.tests", + "status": "pending", + "paused": false, + "type": "full", + "development_mode": 0, + "name_servers": [ + "alice.ns.cloudflare.com", + "tom.ns.cloudflare.com" + ], + "original_name_servers": [], + "original_registrar": null, + "original_dnshost": null, + "modified_on": "2017-02-20T04:10:23.687329Z", + "created_on": "2017-02-20T04:10:18.294562Z", + "meta": { + "step": 4, + "wildcard_proxiable": false, + "custom_certificate_quota": 0, + "page_rule_quota": 3, + "phishing_detected": false, + "multiple_railguns_allowed": false + }, + "owner": { + "type": "user", + "id": "334234243423aaabb334342aaa343433", + "email": "noreply@github.com" + }, + "permissions": [ + "#analytics:read", + "#billing:edit", + "#billing:read", + "#cache_purge:edit", + "#dns_records:edit", + "#dns_records:read", + "#lb:edit", + "#lb:read", + "#logs:read", + "#organization:edit", + "#organization:read", + "#ssl:edit", + "#ssl:read", + "#waf:edit", + "#waf:read", + "#zone:edit", + "#zone:read", + "#zone_settings:edit", + "#zone_settings:read" + ], + "plan": { + "id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "name": "Free Website", + "price": 0, + "currency": "USD", + "frequency": "", + "is_subscribed": true, + "can_subscribe": false, + "legacy_id": "free", + "legacy_discount": false, + "externally_managed": false + } + } + ], + "result_info": { + "page": 2, + "per_page": 2, + "total_pages": 2, + "count": 2, + "total_count": 4 + }, + "success": true, + "errors": [], + "messages": [] +} diff --git a/tests/fixtures/dnsimple-invalid-content.json b/tests/fixtures/dnsimple-invalid-content.json new file mode 100644 index 0000000..4e6e10b --- /dev/null +++ b/tests/fixtures/dnsimple-invalid-content.json @@ -0,0 +1,106 @@ +{ + "data": [ + { + "id": 11189898, + "zone_id": "unit.tests", + "parent_id": null, + "name": "naptr", + "content": "", + "ttl": 600, + "priority": null, + "type": "NAPTR", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:11Z", + "updated_at": "2017-03-09T15:55:11Z" + }, + { + "id": 11189899, + "zone_id": "unit.tests", + "parent_id": null, + "name": "naptr", + "content": "100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", + "ttl": 600, + "priority": null, + "type": "NAPTR", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:11Z", + "updated_at": "2017-03-09T15:55:11Z" + }, + { + "id": 11189878, + "zone_id": "unit.tests", + "parent_id": null, + "name": "_srv._tcp", + "content": "", + "ttl": 600, + "priority": 10, + "type": "SRV", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189879, + "zone_id": "unit.tests", + "parent_id": null, + "name": "_srv._tcp", + "content": "20 foo-2.unit.tests", + "ttl": 600, + "priority": 12, + "type": "SRV", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189882, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "", + "ttl": 3600, + "priority": null, + "type": "SSHFP", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189883, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "1 1", + "ttl": 3600, + "priority": null, + "type": "SSHFP", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + } + ], + "pagination": { + "current_page": 1, + "per_page": 20, + "total_entries": 6, + "total_pages": 1 + } +} diff --git a/tests/fixtures/dnsimple-page-1.json b/tests/fixtures/dnsimple-page-1.json new file mode 100644 index 0000000..5418898 --- /dev/null +++ b/tests/fixtures/dnsimple-page-1.json @@ -0,0 +1,314 @@ +{ + "data": [ + { + "id": 11189873, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "ns1.dnsimple.com admin.dnsimple.com 1489074932 86400 7200 604800 300", + "ttl": 3600, + "priority": null, + "type": "SOA", + "regions": [ + "global" + ], + "system_record": true, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:56:21Z" + }, + { + "id": 11189874, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "ns1.dnsimple.com", + "ttl": 3600, + "priority": null, + "type": "NS", + "regions": [ + "global" + ], + "system_record": true, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189875, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "ns2.dnsimple.com", + "ttl": 3600, + "priority": null, + "type": "NS", + "regions": [ + "global" + ], + "system_record": true, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189876, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "ns3.dnsimple.com", + "ttl": 3600, + "priority": null, + "type": "NS", + "regions": [ + "global" + ], + "system_record": true, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189877, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "ns4.dnsimple.com", + "ttl": 3600, + "priority": null, + "type": "NS", + "regions": [ + "global" + ], + "system_record": true, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189878, + "zone_id": "unit.tests", + "parent_id": null, + "name": "_srv._tcp", + "content": "20 30 foo-1.unit.tests", + "ttl": 600, + "priority": 10, + "type": "SRV", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189879, + "zone_id": "unit.tests", + "parent_id": null, + "name": "_srv._tcp", + "content": "20 30 foo-2.unit.tests", + "ttl": 600, + "priority": 12, + "type": "SRV", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189880, + "zone_id": "unit.tests", + "parent_id": null, + "name": "under", + "content": "ns1.unit.tests.", + "ttl": 3600, + "priority": null, + "type": "NS", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189881, + "zone_id": "unit.tests", + "parent_id": null, + "name": "under", + "content": "ns2.unit.tests.", + "ttl": 3600, + "priority": null, + "type": "NS", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189882, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49", + "ttl": 3600, + "priority": null, + "type": "SSHFP", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:08Z", + "updated_at": "2017-03-09T15:55:08Z" + }, + { + "id": 11189883, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73", + "ttl": 3600, + "priority": null, + "type": "SSHFP", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + }, + { + "id": 11189884, + "zone_id": "unit.tests", + "parent_id": null, + "name": "txt", + "content": "Bah bah black sheep", + "ttl": 600, + "priority": null, + "type": "TXT", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + }, + { + "id": 11189885, + "zone_id": "unit.tests", + "parent_id": null, + "name": "txt", + "content": "have you any wool.", + "ttl": 600, + "priority": null, + "type": "TXT", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + }, + { + "id": 11189886, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "1.2.3.4", + "ttl": 300, + "priority": null, + "type": "A", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + }, + { + "id": 11189887, + "zone_id": "unit.tests", + "parent_id": null, + "name": "", + "content": "1.2.3.5", + "ttl": 300, + "priority": null, + "type": "A", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + }, + { + "id": 11189889, + "zone_id": "unit.tests", + "parent_id": null, + "name": "www", + "content": "2.2.3.6", + "ttl": 300, + "priority": null, + "type": "A", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:09Z", + "updated_at": "2017-03-09T15:55:09Z" + }, + { + "id": 11189890, + "zone_id": "unit.tests", + "parent_id": null, + "name": "mx", + "content": "smtp-4.unit.tests", + "ttl": 300, + "priority": 10, + "type": "MX", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189891, + "zone_id": "unit.tests", + "parent_id": null, + "name": "mx", + "content": "smtp-2.unit.tests", + "ttl": 300, + "priority": 20, + "type": "MX", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189892, + "zone_id": "unit.tests", + "parent_id": null, + "name": "mx", + "content": "smtp-3.unit.tests", + "ttl": 300, + "priority": 30, + "type": "MX", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + } + ], + "pagination": { + "current_page": 1, + "per_page": 20, + "total_entries": 27, + "total_pages": 2 + } +} diff --git a/tests/fixtures/dnsimple-page-2.json b/tests/fixtures/dnsimple-page-2.json new file mode 100644 index 0000000..3f65133 --- /dev/null +++ b/tests/fixtures/dnsimple-page-2.json @@ -0,0 +1,138 @@ +{ + "data": [ + { + "id": 11189893, + "zone_id": "unit.tests", + "parent_id": null, + "name": "mx", + "content": "smtp-1.unit.tests", + "ttl": 300, + "priority": 40, + "type": "MX", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189894, + "zone_id": "unit.tests", + "parent_id": null, + "name": "aaaa", + "content": "2601:644:500:e210:62f8:1dff:feb8:947a", + "ttl": 600, + "priority": null, + "type": "AAAA", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189895, + "zone_id": "unit.tests", + "parent_id": null, + "name": "cname", + "content": "unit.tests", + "ttl": 300, + "priority": null, + "type": "CNAME", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189896, + "zone_id": "unit.tests", + "parent_id": null, + "name": "ptr", + "content": "foo.bar.com.", + "ttl": 300, + "priority": null, + "type": "PTR", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189897, + "zone_id": "unit.tests", + "parent_id": null, + "name": "www.sub", + "content": "2.2.3.6", + "ttl": 300, + "priority": null, + "type": "A", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:10Z", + "updated_at": "2017-03-09T15:55:10Z" + }, + { + "id": 11189898, + "zone_id": "unit.tests", + "parent_id": null, + "name": "naptr", + "content": "10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", + "ttl": 600, + "priority": null, + "type": "NAPTR", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:11Z", + "updated_at": "2017-03-09T15:55:11Z" + }, + { + "id": 11189899, + "zone_id": "unit.tests", + "parent_id": null, + "name": "naptr", + "content": "100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", + "ttl": 600, + "priority": null, + "type": "NAPTR", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:11Z", + "updated_at": "2017-03-09T15:55:11Z" + }, + { + "id": 11189900, + "zone_id": "unit.tests", + "parent_id": null, + "name": "spf", + "content": "v=spf1 ip4:192.168.0.1/16-all", + "ttl": 600, + "priority": null, + "type": "SPF", + "regions": [ + "global" + ], + "system_record": false, + "created_at": "2017-03-09T15:55:11Z", + "updated_at": "2017-03-09T15:55:11Z" + } + ], + "pagination": { + "current_page": 2, + "per_page": 20, + "total_entries": 27, + "total_pages": 2 + } +} diff --git a/tests/fixtures/dyn-traffic-director-get.json b/tests/fixtures/dyn-traffic-director-get.json new file mode 100644 index 0000000..38f2602 --- /dev/null +++ b/tests/fixtures/dyn-traffic-director-get.json @@ -0,0 +1,4190 @@ +{ + "status": "success", + "data": { + "notifiers": [], + "rulesets": [ + { + "dsf_ruleset_id": "9e4lSkD33d8x1mCBChQcbOc6O2o", + "response_pools": [ + { + "status": "ok", + "rs_chains": [ + { + "dsf_response_pool_id": "vX54Ck2p2c6fSmIAkG_JD3nY5OI", + "core": "false", + "status": "ok", + "dsf_record_set_failover_chain_id": "VUFenKGhvlmDZDIcAXE-qxoxJb0", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "record_sets": [ + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "-ZT5yekuV4em0ADl7533_ulTk7g", + "dsf_monitor_id": "htWFldFj4R8dLWiG6dbMbDL0EOE", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "-ZT5yekuV4em0ADl7533_ulTk7g", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "5.2.3.4", + "dsf_record_id": "4WiI3ymCsLpsRfWwftix0lBZDoE", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "5.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "5.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "-ZT5yekuV4em0ADl7533_ulTk7g", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "5.2.3.5", + "dsf_record_id": "OuF6eTQFcURtRht9H0cOjUa8uRM", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "5.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "5.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879663", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "5.2.3.4", + "serve_count": "2" + }, + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "5Xt8xYzj5Yic3lydKyPeoihJow4", + "dsf_monitor_id": "", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "5Xt8xYzj5Yic3lydKyPeoihJow4", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.4", + "dsf_record_id": "VQSQMyjakhz1X5c63NV-PqXl8Z4", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "5Xt8xYzj5Yic3lydKyPeoihJow4", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.5", + "dsf_record_id": "pYP70Y9_EyAXdNtUOHC4dZa1NSo", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879663", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "default", + "serve_count": "2" + } + ], + "label": "5.2.3.4" + } + ], + "automation": "auto", + "last_monitored": "1487879663", + "pending_change": "", + "eligible": "true", + "rulesets": [ + { + "dsf_ruleset_id": "9e4lSkD33d8x1mCBChQcbOc6O2o", + "response_pools": [], + "criteria": { + "geoip": { + "province": [], + "country": [ + "US", + "CA" + ], + "region": [] + } + }, + "ordering": "1", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "NA-US-CA", + "criteria_type": "geoip" + } + ], + "label": "5.2.3.4", + "dsf_response_pool_id": "vX54Ck2p2c6fSmIAkG_JD3nY5OI", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [ + "US", + "CA" + ], + "region": [] + } + }, + "ordering": "1", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "NA-US-CA:b2c71ae53a5645cc813c51db2b6571c7", + "criteria_type": "geoip" + }, + { + "dsf_ruleset_id": "Eg5aPCgcvdhrCCpqK1zCSd-oE2c", + "response_pools": [ + { + "status": "ok", + "rs_chains": [ + { + "dsf_response_pool_id": "zyoEkzz1znnhQk3J1gcMFpEngeI", + "core": "false", + "status": "ok", + "dsf_record_set_failover_chain_id": "G4ZdfXTa45jrlNVaHSBY5kHVX8k", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "record_sets": [ + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "FJZxSLJnFSS4KJfdZ2yyjwM8_hY", + "dsf_monitor_id": "htWFldFj4R8dLWiG6dbMbDL0EOE", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "FJZxSLJnFSS4KJfdZ2yyjwM8_hY", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "4.2.3.4", + "dsf_record_id": "Es9g_4E_aIoN-tFTStQDNSE8-aY", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "4.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "4.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "FJZxSLJnFSS4KJfdZ2yyjwM8_hY", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "4.2.3.5", + "dsf_record_id": "Mnz7WakCZ91XRKpbcQ6dkZ6d-QI", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "4.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "4.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879665", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "4.2.3.4", + "serve_count": "2" + }, + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "YGakLwcCC22_5rXg2t8XuWL8lqc", + "dsf_monitor_id": "", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "YGakLwcCC22_5rXg2t8XuWL8lqc", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.4", + "dsf_record_id": "aduilHaDNN_8HD8BppL_wVmMerk", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "YGakLwcCC22_5rXg2t8XuWL8lqc", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.5", + "dsf_record_id": "TOn5jK5oq1ZwmkbPpYwPcBUJuVo", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879665", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "default", + "serve_count": "2" + } + ], + "label": "4.2.3.4" + } + ], + "automation": "auto", + "last_monitored": "1487879665", + "pending_change": "", + "eligible": "true", + "rulesets": [ + { + "dsf_ruleset_id": "Eg5aPCgcvdhrCCpqK1zCSd-oE2c", + "response_pools": [], + "criteria": { + "geoip": { + "province": [], + "country": [ + "US" + ], + "region": [] + } + }, + "ordering": "2", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "NA-US", + "criteria_type": "geoip" + } + ], + "label": "4.2.3.4", + "dsf_response_pool_id": "zyoEkzz1znnhQk3J1gcMFpEngeI", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [ + "US" + ], + "region": [] + } + }, + "ordering": "2", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "NA-US:b2c71ae53a5645cc813c51db2b6571c7", + "criteria_type": "geoip" + }, + { + "dsf_ruleset_id": "gi96PlJik9qf36PyNXJPT-5CRM0", + "response_pools": [ + { + "status": "ok", + "rs_chains": [ + { + "dsf_response_pool_id": "0xUYeWq92OrQg6ImuFZAdFz8gOs", + "core": "false", + "status": "ok", + "dsf_record_set_failover_chain_id": "MSuION06EZhgHBjHkBqVhtFTJq8", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "record_sets": [ + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "TeTYCMo1INWcHfs8tTjGScx2uv8", + "dsf_monitor_id": "htWFldFj4R8dLWiG6dbMbDL0EOE", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "TeTYCMo1INWcHfs8tTjGScx2uv8", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "3.2.3.4", + "dsf_record_id": "AltgsqLdUKOXi3ESlr3KLcw-vUs", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "3.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "3.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "TeTYCMo1INWcHfs8tTjGScx2uv8", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "3.2.3.5", + "dsf_record_id": "IH6_EamKifOs3p4WJWWWX04xQAQ", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "3.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "3.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879668", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "3.2.3.4", + "serve_count": "2" + }, + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "YI41kUebHBMejXqBcF3BnkjT1bQ", + "dsf_monitor_id": "", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "YI41kUebHBMejXqBcF3BnkjT1bQ", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.4", + "dsf_record_id": "ZgSaaoX-OzHI_FpmID_O6CfSFiQ", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "YI41kUebHBMejXqBcF3BnkjT1bQ", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.5", + "dsf_record_id": "S0NUfNRx-MYClfOkHRR8NCPzLhw", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879668", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "default", + "serve_count": "2" + } + ], + "label": "3.2.3.4" + } + ], + "automation": "auto", + "last_monitored": "1487879668", + "pending_change": "", + "eligible": "true", + "rulesets": [ + { + "dsf_ruleset_id": "gi96PlJik9qf36PyNXJPT-5CRM0", + "response_pools": [], + "criteria": { + "geoip": { + "province": [], + "country": [ + "JP" + ], + "region": [] + } + }, + "ordering": "3", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "AS-JP", + "criteria_type": "geoip" + } + ], + "label": "3.2.3.4", + "dsf_response_pool_id": "0xUYeWq92OrQg6ImuFZAdFz8gOs", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [ + "JP" + ], + "region": [] + } + }, + "ordering": "3", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "AS-JP:b2c71ae53a5645cc813c51db2b6571c7", + "criteria_type": "geoip" + }, + { + "dsf_ruleset_id": "lVWDeD6bwuXZtqJu_cwuQJcYVmE", + "response_pools": [ + { + "status": "ok", + "rs_chains": [ + { + "dsf_response_pool_id": "GQ2VYQM9kBPQkLur6IlzZJeCICU", + "core": "false", + "status": "ok", + "dsf_record_set_failover_chain_id": "dYiwBxOAdUjEr638SJ8FycTkolA", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "record_sets": [ + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "ek1O6mp2SnOPxGA4XQfmp9Xkr4o", + "dsf_monitor_id": "htWFldFj4R8dLWiG6dbMbDL0EOE", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "ek1O6mp2SnOPxGA4XQfmp9Xkr4o", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "2.2.3.4", + "dsf_record_id": "s8FCJSXvOch_cHa0wqHPRNIki-U", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "2.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "2.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "ek1O6mp2SnOPxGA4XQfmp9Xkr4o", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "2.2.3.5", + "dsf_record_id": "JXA71fL_4DGdE38A8tIDZ9FnoBU", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "2.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "2.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879670", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "2.2.3.4", + "serve_count": "2" + }, + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "34UoNhEdeOqF61XEW1MsBZ5jyRQ", + "dsf_monitor_id": "", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "34UoNhEdeOqF61XEW1MsBZ5jyRQ", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.4", + "dsf_record_id": "JjeVu2S5kBT6puXAeWyv9GqqV0o", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "34UoNhEdeOqF61XEW1MsBZ5jyRQ", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.5", + "dsf_record_id": "GfOulen8R_E1TwSD0WrUHfiMepk", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879670", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "default", + "serve_count": "2" + } + ], + "label": "2.2.3.4" + } + ], + "automation": "auto", + "last_monitored": "1487879670", + "pending_change": "", + "eligible": "true", + "rulesets": [ + { + "dsf_ruleset_id": "lVWDeD6bwuXZtqJu_cwuQJcYVmE", + "response_pools": [], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [ + "14" + ] + } + }, + "ordering": "4", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "AF", + "criteria_type": "geoip" + } + ], + "label": "2.2.3.4", + "dsf_response_pool_id": "GQ2VYQM9kBPQkLur6IlzZJeCICU", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [ + "14" + ] + } + }, + "ordering": "4", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "AF:b2c71ae53a5645cc813c51db2b6571c7", + "criteria_type": "geoip" + }, + { + "dsf_ruleset_id": "MZFKVbrOa112kb-JyCRHVSmh8NA", + "response_pools": [ + { + "status": "ok", + "rs_chains": [ + { + "dsf_response_pool_id": "KOKo8sVfakgJ1HpqqZ84A7QkGTk", + "core": "false", + "status": "ok", + "dsf_record_set_failover_chain_id": "Vfem7bn-aO_a3l1mhZxYrds-BUg", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "record_sets": [ + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "EdrNCZ6gM8FCeiXGamxxT39AkkA", + "dsf_monitor_id": "", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "EdrNCZ6gM8FCeiXGamxxT39AkkA", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.4", + "dsf_record_id": "14DpdlaAi81FWR2qB3DW2HYI9YM", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "EdrNCZ6gM8FCeiXGamxxT39AkkA", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.5", + "dsf_record_id": "wlLEe3F4vqEmH-OCcPicDE0K1I0", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879672", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "default", + "serve_count": "2" + } + ], + "label": "default" + } + ], + "automation": "auto", + "last_monitored": "1487879672", + "pending_change": "", + "eligible": "true", + "rulesets": [ + { + "dsf_ruleset_id": "MZFKVbrOa112kb-JyCRHVSmh8NA", + "response_pools": [], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [] + } + }, + "ordering": "5", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "default", + "criteria_type": "always" + } + ], + "label": "default", + "dsf_response_pool_id": "KOKo8sVfakgJ1HpqqZ84A7QkGTk", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [] + } + }, + "ordering": "5", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "other format", + "criteria_type": "always" + }, + { + "dsf_ruleset_id": "MZFKVbrOa112kb-JyCRHVSmh8NA", + "response_pools": [ + { + "status": "ok", + "rs_chains": [], + "automation": "auto", + "last_monitored": "1487879672", + "pending_change": "", + "eligible": "true", + "rulesets": [], + "label": "NA-US-FL:norules", + "dsf_response_pool_id": "KOKo8sVfakgJ1HpqqZ84A7QkGTk", + "service_id": "xIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [] + } + }, + "ordering": "5", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "other format", + "criteria_type": "always" + }, + { + "dsf_ruleset_id": "MZFKVbrOa112kb-JyCRHVSmh8NA", + "response_pools": [ + { + "status": "ok", + "rs_chains": [ + { + "dsf_response_pool_id": "KOKo8sVfakgJ1HpqqZ84A7QkGTk", + "core": "false", + "status": "ok", + "dsf_record_set_failover_chain_id": "Vfem7bn-aO_a3l1mhZxYrds-BUg", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "record_sets": [ + { + "status": "unknown", + "rdata_class": "A", + "eligible": "true", + "dsf_record_set_id": "EdrNCZ6gM8FCeiXGamxxT39AkkA", + "dsf_monitor_id": "", + "automation": "auto", + "trouble_count": "0", + "records": [ + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "EdrNCZ6gM8FCeiXGamxxT39AkkA", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.4", + "dsf_record_id": "14DpdlaAi81FWR2qB3DW2HYI9YM", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.4" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.4" + ], + "response_time": 4294967295 + }, + { + "status": "unknown", + "torpidity": 4294967295, + "rdata_class": "A", + "weight": 1, + "eligible": "true", + "dsf_record_set_id": "EdrNCZ6gM8FCeiXGamxxT39AkkA", + "ttl": "300", + "endpoint_up_count": 1, + "label": "", + "automation": "auto", + "master_line": "1.2.3.5", + "dsf_record_id": "wlLEe3F4vqEmH-OCcPicDE0K1I0", + "last_monitored": 0, + "rdata": [ + { + "type": "A", + "data": { + "rdata_kx": { + "preference": 0, + "exchange": "" + }, + "rdata_srv": { + "priority": 0, + "port": 0, + "weight": 0, + "target": "" + }, + "rdata_policy": { + "gui_url": "", + "policy": "", + "rtype": "", + "api_url": "", + "name": "" + }, + "rdata_soa": { + "rname": "", + "retry": 0, + "mname": "", + "minimum": 0, + "refresh": 0, + "expire": 0, + "serial": 0 + }, + "rdata_key": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_ipseckey": { + "public_key": "", + "precedence": 0, + "gatetype": 0, + "algorithm": 0, + "gateway": "" + }, + "rdata_cname": { + "cname": "" + }, + "rdata_loc": { + "horiz_pre": 0, + "altitude": 0, + "longitude": "", + "version": 0, + "vert_pre": 0, + "latitude": "", + "size": 0 + }, + "rdata_spf": { + "txtdata": "" + }, + "rdata_ptr": { + "ptrdname": "" + }, + "rdata_alias": { + "alias": "" + }, + "rdata_ds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_naptr": { + "flags": "", + "preference": 0, + "services": "", + "regexp": "", + "order": 0, + "replacement": "" + }, + "rdata_sshfp": { + "fptype": 0, + "algorithm": 0, + "fingerprint": "" + }, + "rdata_aaaa": { + "address": "" + }, + "rdata_nsap": { + "nsap": "" + }, + "rdata_dhcid": { + "digest": "" + }, + "rdata_dnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + }, + "rdata_cds": { + "keytag": 0, + "digest": "", + "algorithm": 0, + "digtype": 0 + }, + "rdata_txt": { + "txtdata": "" + }, + "rdata_ns": { + "nsdname": "" + }, + "rdata_dname": { + "dname": "" + }, + "rdata_csync": { + "soa_serial": 0, + "flags": "", + "types": "" + }, + "rdata_px": { + "mapx400": "", + "map822": "", + "preference": 0 + }, + "rdata_a": { + "address": "1.2.3.5" + }, + "rdata_cert": { + "tag": 0, + "certificate": "", + "algorithm": 0, + "format": 0 + }, + "rdata_rp": { + "mbox": "", + "txtdname": "" + }, + "rdata_tlsa": { + "cert_usage": 0, + "match_type": 0, + "certificate": "", + "selector": 0 + }, + "rdata_mx": { + "preference": 0, + "exchange": "" + }, + "rdata_cdnskey": { + "protocol": 0, + "flags": 0, + "algorithm": 0, + "public_key": "" + } + }, + "ttl": "" + } + ], + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "endpoints": [ + "1.2.3.5" + ], + "response_time": 4294967295 + } + ], + "fail_count": "1", + "torpidity_max": "0", + "ttl_derived": "300", + "last_monitored": "1487879672", + "ttl": "", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "label": "default", + "serve_count": "2" + } + ], + "label": "default" + } + ], + "automation": "auto", + "last_monitored": "1487879672", + "pending_change": "", + "eligible": "true", + "rulesets": [ + { + "dsf_ruleset_id": "MZFKVbrOa112kb-JyCRHVSmh8NA", + "response_pools": [], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [] + } + }, + "ordering": "5", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "default", + "criteria_type": "always" + } + ], + "label": "default", + "dsf_response_pool_id": "KOKo8sVfakgJ1HpqqZ84A7QkGTk", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "core_set_count": "1", + "notifier": "" + } + ], + "criteria": { + "geoip": { + "province": [], + "country": [], + "region": [] + } + }, + "ordering": "5", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "pending_change": "", + "eligible": "", + "label": "default:b2c71ae53a5645cc813c51db2b6571c7", + "criteria_type": "always" + } + ], + "ttl": "300", + "active": "Y", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "nodes": [ + { + "fqdn": "unit.tests", + "zone": "unit.tests" + } + ], + "pending_change": "", + "label": "unit.tests.:A" + }, + "job_id": 3376642606, + "msgs": [ + { + "INFO": "detail: Here is your service", + "LVL": "INFO", + "ERR_CD": null, + "SOURCE": "BLL" + } + ] +} diff --git a/tests/fixtures/powerdns-full-data.json b/tests/fixtures/powerdns-full-data.json new file mode 100644 index 0000000..efb82c2 --- /dev/null +++ b/tests/fixtures/powerdns-full-data.json @@ -0,0 +1,235 @@ +{ + "account": "", + "dnssec": false, + "id": "unit.tests.", + "kind": "Master", + "last_check": 0, + "masters": [], + "name": "unit.tests.", + "notified_serial": 2017012803, + "rrsets": [ + { + "comments": [], + "name": "mx.unit.tests.", + "records": [ + { + "content": "40 smtp-1.unit.tests.", + "disabled": false + }, + { + "content": "20 smtp-2.unit.tests.", + "disabled": false + }, + { + "content": "30 smtp-3.unit.tests.", + "disabled": false + }, + { + "content": "10 smtp-4.unit.tests.", + "disabled": false + } + ], + "ttl": 300, + "type": "MX" + }, + { + "comments": [], + "name": "sub.unit.tests.", + "records": [ + { + "content": "6.2.3.4.", + "disabled": false + }, { + "content": "7.2.3.4.", + "disabled": false + } + ], + "ttl": 3600, + "type": "NS" + }, + { + "comments": [], + "name": "www.unit.tests.", + "records": [ + { + "content": "2.2.3.6", + "disabled": false + } + ], + "ttl": 300, + "type": "A" + }, + { + "comments": [], + "name": "_srv._tcp.unit.tests.", + "records": [ + { + "content": "10 20 30 foo-1.unit.tests.", + "disabled": false + }, + { + "content": "12 20 30 foo-2.unit.tests.", + "disabled": false + } + ], + "ttl": 600, + "type": "SRV" + }, + { + "comments": [], + "name": "txt.unit.tests.", + "records": [ + { + "content": "\"Bah bah black sheep\"", + "disabled": false + }, + { + "content": "\"have you any wool.\"", + "disabled": false + } + ], + "ttl": 600, + "type": "TXT" + }, + { + "comments": [], + "name": "naptr.unit.tests.", + "records": [ + { + "content": "10 100 \"S\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", + "disabled": false + }, + { + "content": "100 100 \"U\" \"SIP+D2U\" \"!^.*$!sip:info@bar.example.com!\" .", + "disabled": false + } + ], + "ttl": 600, + "type": "NAPTR" + }, + { + "comments": [], + "name": "ptr.unit.tests.", + "records": [ + { + "content": "foo.bar.com.", + "disabled": false + } + ], + "ttl": 300, + "type": "PTR" + }, + { + "comments": [], + "name": "spf.unit.tests.", + "records": [ + { + "content": "\"v=spf1 ip4:192.168.0.1/16-all\"", + "disabled": false + } + ], + "ttl": 600, + "type": "SPF" + }, + { + "comments": [], + "name": "cname.unit.tests.", + "records": [ + { + "content": "unit.tests.", + "disabled": false + } + ], + "ttl": 300, + "type": "CNAME" + }, + { + "comments": [], + "name": "www.sub.unit.tests.", + "records": [ + { + "content": "2.2.3.6", + "disabled": false + } + ], + "ttl": 300, + "type": "A" + }, + { + "comments": [], + "name": "aaaa.unit.tests.", + "records": [ + { + "content": "2601:644:500:e210:62f8:1dff:feb8:947a", + "disabled": false + } + ], + "ttl": 600, + "type": "AAAA" + }, + { + "comments": [], + "name": "unit.tests.", + "records": [ + { + "content": "1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49", + "disabled": false + }, + { + "content": "1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73", + "disabled": false + } + ], + "ttl": 3600, + "type": "SSHFP" + }, + { + "comments": [], + "name": "unit.tests.", + "records": [ + { + "content": "ns1.ext.unit.tests. hostmaster.unit.tests. 2017012803 3600 600 604800 60", + "disabled": false + } + ], + "ttl": 3600, + "type": "SOA" + }, + { + "comments": [], + "name": "unit.tests.", + "records": [ + { + "content": "1.1.1.1.", + "disabled": false + }, + { + "content": "4.4.4.4.", + "disabled": false + } + ], + "ttl": 600, + "type": "NS" + }, + { + "comments": [], + "name": "unit.tests.", + "records": [ + { + "content": "1.2.3.5", + "disabled": false + }, + { + "content": "1.2.3.4", + "disabled": false + } + ], + "ttl": 300, + "type": "A" + } + ], + "serial": 2017012803, + "soa_edit": "", + "soa_edit_api": "INCEPTION-INCREMENT", + "url": "api/v1/servers/localhost/zones/unit.tests." +} diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..df74e84 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,69 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from shutil import rmtree +from tempfile import mkdtemp + + +class SimpleSource(object): + + def __init__(self, id='test'): + pass + + +class SimpleProvider(object): + SUPPORTS_GEO = False + + def __init__(self, id='test'): + pass + + def populate(self, zone, source=True): + pass + + def supports(self, record): + return True + + def __repr__(self): + return self.__class__.__name__ + + +class GeoProvider(object): + SUPPORTS_GEO = True + + def __init__(self, id='test'): + pass + + def populate(self, zone, source=True): + pass + + def supports(self, record): + return True + + def __repr__(self): + return self.__class__.__name__ + + +class NoSshFpProvider(SimpleProvider): + + def supports(self, record): + return record._type != 'SSHFP' + + +class TemporaryDirectory(object): + + def __init__(self, delete_on_exit=True): + self.delete_on_exit = delete_on_exit + + def __enter__(self): + self.dirname = mkdtemp() + return self + + def __exit__(self, *args, **kwargs): + if self.delete_on_exit: + rmtree(self.dirname) + else: + raise Exception(self.dirname) diff --git a/tests/test_octodns_manager.py b/tests/test_octodns_manager.py new file mode 100644 index 0000000..7ee4fbd --- /dev/null +++ b/tests/test_octodns_manager.py @@ -0,0 +1,203 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from os import environ +from os.path import dirname, join +from unittest import TestCase + +from octodns.record import Record +from octodns.manager import _AggregateTarget, Manager +from octodns.zone import Zone + +from helpers import GeoProvider, NoSshFpProvider, SimpleProvider, \ + TemporaryDirectory + +config_dir = join(dirname(__file__), 'config') + + +def get_config_filename(which): + return join(config_dir, which) + + +class TestManager(TestCase): + + def test_missing_provider_class(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('missing-provider-class.yaml')).sync() + self.assertTrue('missing class' in ctx.exception.message) + + def test_bad_provider_class(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('bad-provider-class.yaml')).sync() + self.assertTrue('Unknown provider class' in ctx.exception.message) + + def test_bad_provider_class_module(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('bad-provider-class-module.yaml')) \ + .sync() + self.assertTrue('Unknown provider class' in ctx.exception.message) + + def test_bad_provider_class_no_module(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('bad-provider-class-no-module.yaml')) \ + .sync() + self.assertTrue('Unknown provider class' in ctx.exception.message) + + def test_missing_provider_config(self): + # Missing provider config + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('missing-provider-config.yaml')).sync() + self.assertTrue('provider config' in ctx.exception.message) + + def test_missing_env_config(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('missing-provider-env.yaml')).sync() + self.assertTrue('missing env var' in ctx.exception.message) + + def test_missing_source(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('unknown-provider.yaml')) \ + .sync(['missing.sources.']) + self.assertTrue('missing sources' in ctx.exception.message) + + def test_missing_targets(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('unknown-provider.yaml')) \ + .sync(['missing.targets.']) + self.assertTrue('missing targets' in ctx.exception.message) + + def test_unknown_source(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('unknown-provider.yaml')) \ + .sync(['unknown.source.']) + self.assertTrue('unknown source' in ctx.exception.message) + + def test_unknown_target(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('unknown-provider.yaml')) \ + .sync(['unknown.target.']) + self.assertTrue('unknown target' in ctx.exception.message) + + def test_source_only_as_a_target(self): + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('unknown-provider.yaml')) \ + .sync(['not.targetable.']) + self.assertTrue('does not support targeting' in ctx.exception.message) + + def test_simple(self): + with TemporaryDirectory() as tmpdir: + environ['YAML_TMP_DIR'] = tmpdir.dirname + tc = Manager(get_config_filename('simple.yaml')) \ + .sync(dry_run=False) + self.assertEquals(19, tc) + + # try with just one of the zones + tc = Manager(get_config_filename('simple.yaml')) \ + .sync(dry_run=False, eligible_zones=['unit.tests.']) + self.assertEquals(13, tc) + + # the subzone, with 2 targets + tc = Manager(get_config_filename('simple.yaml')) \ + .sync(dry_run=False, eligible_zones=['subzone.unit.tests.']) + self.assertEquals(6, tc) + + # and finally the empty zone + tc = Manager(get_config_filename('simple.yaml')) \ + .sync(dry_run=False, eligible_zones=['empty.']) + self.assertEquals(0, tc) + + # Again with force + tc = Manager(get_config_filename('simple.yaml')) \ + .sync(dry_run=False, force=True) + self.assertEquals(19, tc) + + def test_eligible_targets(self): + with TemporaryDirectory() as tmpdir: + environ['YAML_TMP_DIR'] = tmpdir.dirname + # Only allow a target that doesn't exist + tc = Manager(get_config_filename('simple.yaml')) \ + .sync(eligible_targets=['foo']) + self.assertEquals(0, tc) + + def test_compare(self): + with TemporaryDirectory() as tmpdir: + environ['YAML_TMP_DIR'] = tmpdir.dirname + manager = Manager(get_config_filename('simple.yaml')) + + changes = manager.compare(['in'], ['in'], 'unit.tests.') + self.assertEquals([], changes) + + # Create an empty unit.test zone config + with open(join(tmpdir.dirname, 'unit.tests.yaml'), 'w') as fh: + fh.write('---\n{}') + + changes = manager.compare(['in'], ['dump'], 'unit.tests.') + self.assertEquals(13, len(changes)) + + # Compound sources with varying support + changes = manager.compare(['in', 'nosshfp'], + ['dump'], + 'unit.tests.') + self.assertEquals(12, len(changes)) + + with self.assertRaises(Exception) as ctx: + manager.compare(['nope'], ['dump'], 'unit.tests.') + self.assertEquals('Unknown source: nope', ctx.exception.message) + + def test_aggregate_target(self): + simple = SimpleProvider() + geo = GeoProvider() + nosshfp = NoSshFpProvider() + + self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_GEO) + self.assertFalse(_AggregateTarget([simple, geo]).SUPPORTS_GEO) + self.assertFalse(_AggregateTarget([geo, simple]).SUPPORTS_GEO) + self.assertTrue(_AggregateTarget([geo, geo]).SUPPORTS_GEO) + + zone = Zone('unit.tests.', []) + record = Record.new(zone, 'sshfp', { + 'ttl': 60, + 'type': 'SSHFP', + 'value': { + 'algorithm': 1, + 'fingerprint_type': 1, + 'fingerprint': 'abcdefg', + }, + }) + self.assertTrue(simple.supports(record)) + self.assertFalse(nosshfp.supports(record)) + self.assertTrue(_AggregateTarget([simple, simple]).supports(record)) + self.assertFalse(_AggregateTarget([simple, nosshfp]).supports(record)) + + def test_dump(self): + with TemporaryDirectory() as tmpdir: + environ['YAML_TMP_DIR'] = tmpdir.dirname + manager = Manager(get_config_filename('simple.yaml')) + + with self.assertRaises(Exception) as ctx: + manager.dump('unit.tests.', tmpdir.dirname, 'nope') + self.assertEquals('Unknown source: nope', ctx.exception.message) + + manager.dump('unit.tests.', tmpdir.dirname, 'in') + + # make sure this fails with an IOError and not a KeyError when + # tyring to find sub zones + with self.assertRaises(IOError): + manager.dump('unknown.zone.', tmpdir.dirname, 'in') + + def test_validate_configs(self): + Manager(get_config_filename('simple-validate.yaml')).validate_configs() + + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('missing-sources.yaml')) \ + .validate_configs() + self.assertTrue('missing sources' in ctx.exception.message) + + with self.assertRaises(Exception) as ctx: + Manager(get_config_filename('unknown-provider.yaml')) \ + .validate_configs() + self.assertTrue('unknown source' in ctx.exception.message) diff --git a/tests/test_octodns_provider_base.py b/tests/test_octodns_provider_base.py new file mode 100644 index 0000000..22ea935 --- /dev/null +++ b/tests/test_octodns_provider_base.py @@ -0,0 +1,170 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from logging import getLogger +from unittest import TestCase + +from octodns.record import Create, Delete, Record, Update +from octodns.provider.base import BaseProvider, Plan, UnsafePlan +from octodns.zone import Zone + + +class HelperProvider(BaseProvider): + log = getLogger('HelperProvider') + + def __init__(self, extra_changes, apply_disabled=False, + include_change_callback=None): + self.__extra_changes = extra_changes + self.apply_disabled = apply_disabled + self.include_change_callback = include_change_callback + + def populate(self, zone, target=False): + pass + + def _include_change(self, change): + return not self.include_change_callback or \ + self.include_change_callback(change) + + def _extra_changes(self, existing, changes): + return self.__extra_changes + + def _apply(self, plan): + pass + + +class TestBaseProvider(TestCase): + + def test_base_provider(self): + with self.assertRaises(NotImplementedError) as ctx: + BaseProvider('base') + self.assertEquals('Abstract base class, log property missing', + ctx.exception.message) + + class HasLog(BaseProvider): + log = getLogger('HasLog') + + with self.assertRaises(NotImplementedError) as ctx: + HasLog('haslog') + self.assertEquals('Abstract base class, SUPPORTS_GEO property missing', + ctx.exception.message) + + class HasSupportsGeo(HasLog): + SUPPORTS_GEO = False + + zone = Zone('unit.tests.', []) + with self.assertRaises(NotImplementedError) as ctx: + HasSupportsGeo('hassupportesgeo').populate(zone) + self.assertEquals('Abstract base class, populate method missing', + ctx.exception.message) + + class HasPopulate(HasSupportsGeo): + + def populate(self, zone, target=False): + zone.add_record(Record.new(zone, '', { + 'ttl': 60, + 'type': 'A', + 'value': '2.3.4.5' + })) + zone.add_record(Record.new(zone, 'going', { + 'ttl': 60, + 'type': 'A', + 'value': '3.4.5.6' + })) + + zone.add_record(Record.new(zone, '', { + 'ttl': 60, + 'type': 'A', + 'value': '1.2.3.4' + })) + + self.assertTrue(HasSupportsGeo('hassupportesgeo') + .supports(list(zone.records)[0])) + + plan = HasPopulate('haspopulate').plan(zone) + self.assertEquals(2, len(plan.changes)) + + with self.assertRaises(NotImplementedError) as ctx: + HasPopulate('haspopulate').apply(plan) + self.assertEquals('Abstract base class, _apply method missing', + ctx.exception.message) + + def test_plan(self): + ignored = Zone('unit.tests.', []) + + # No change, thus no plan + provider = HelperProvider([]) + self.assertEquals(None, provider.plan(ignored)) + + record = Record.new(ignored, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + }) + provider = HelperProvider([Create(record)]) + plan = provider.plan(ignored) + self.assertTrue(plan) + self.assertEquals(1, len(plan.changes)) + + def test_apply(self): + ignored = Zone('unit.tests.', []) + + record = Record.new(ignored, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + }) + provider = HelperProvider([Create(record)], apply_disabled=True) + plan = provider.plan(ignored) + provider.apply(plan) + + provider.apply_disabled = False + self.assertEquals(1, provider.apply(plan)) + + def test_include_change(self): + zone = Zone('unit.tests.', []) + + record = Record.new(zone, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + }) + zone.add_record(record) + provider = HelperProvider([], include_change_callback=lambda c: False) + plan = provider.plan(zone) + # We filtered out the only change + self.assertFalse(plan) + + def test_safe(self): + ignored = Zone('unit.tests.', []) + + # No changes is safe + Plan(None, None, []).raise_if_unsafe() + + # Creates are safe + record = Record.new(ignored, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + }) + Plan(None, None, [Create(record) for i in range(10)]).raise_if_unsafe() + + # max Updates is safe + changes = [Update(record, record) + for i in range(Plan.MAX_SAFE_UPDATES)] + Plan(None, None, changes).raise_if_unsafe() + # but max + 1 isn't + with self.assertRaises(UnsafePlan): + changes.append(Update(record, record)) + Plan(None, None, changes).raise_if_unsafe() + + # max Deletes is safe + changes = [Delete(record) for i in range(Plan.MAX_SAFE_DELETES)] + Plan(None, None, changes).raise_if_unsafe() + # but max + 1 isn't + with self.assertRaises(UnsafePlan): + changes.append(Delete(record)) + Plan(None, None, changes).raise_if_unsafe() diff --git a/tests/test_octodns_provider_cloudflare.py b/tests/test_octodns_provider_cloudflare.py new file mode 100644 index 0000000..229ff5a --- /dev/null +++ b/tests/test_octodns_provider_cloudflare.py @@ -0,0 +1,273 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from mock import Mock, call +from os.path import dirname, join +from requests import HTTPError +from requests_mock import ANY, mock as requests_mock +from unittest import TestCase + +from octodns.record import Record +from octodns.provider.cloudflare import CloudflareProvider +from octodns.provider.yaml import YamlProvider +from octodns.zone import Zone + + +class TestCloudflareProvider(TestCase): + expected = Zone('unit.tests.', []) + source = YamlProvider('test', join(dirname(__file__), 'config')) + source.populate(expected) + + # Our test suite differs a bit, add our NS and remove the simple one + expected.add_record(Record.new(expected, 'under', { + 'ttl': 3600, + 'type': 'NS', + 'values': [ + 'ns1.unit.tests.', + 'ns2.unit.tests.', + ] + })) + for record in list(expected.records): + if record.name == 'sub' and record._type == 'NS': + expected.records.remove(record) + break + + empty = {'result': [], 'result_info': {'count': 0, 'per_page': 0}} + + def test_populate(self): + provider = CloudflareProvider('test', 'email', 'token') + + # Bad auth + with requests_mock() as mock: + mock.get(ANY, status_code=403, + text='{"success":false,"errors":[{"code":9103,' + '"message":"Unknown X-Auth-Key or X-Auth-Email"}],' + '"messages":[],"result":null}') + + with self.assertRaises(Exception) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', + ctx.exception.message) + + # Bad auth, unknown resp + with requests_mock() as mock: + mock.get(ANY, status_code=403, text='{}') + + with self.assertRaises(Exception) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals('Authentication error', ctx.exception.message) + + # General error + with requests_mock() as mock: + mock.get(ANY, status_code=502, text='Things caught fire') + + with self.assertRaises(HTTPError) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(502, ctx.exception.response.status_code) + + # Non-existant zone doesn't populate anything + with requests_mock() as mock: + mock.get(ANY, status_code=200, json=self.empty) + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(set(), zone.records) + + # re-populating the same non-existant zone uses cache and makes no + # calls + again = Zone('unit.tests.', []) + provider.populate(again) + self.assertEquals(set(), again.records) + + # bust zone cache + provider._zones = None + + # existing zone with data + with requests_mock() as mock: + base = 'https://api.cloudflare.com/client/v4/zones' + + # zones + with open('tests/fixtures/cloudflare-zones-page-1.json') as fh: + mock.get('{}?page=1'.format(base), status_code=200, + text=fh.read()) + with open('tests/fixtures/cloudflare-zones-page-2.json') as fh: + mock.get('{}?page=2'.format(base), status_code=200, + text=fh.read()) + mock.get('{}?page=3'.format(base), status_code=200, + json={'result': [], 'result_info': {'count': 0, + 'per_page': 0}}) + + # records + base = '{}/234234243423aaabb334342aaa343435/dns_records' \ + .format(base) + with open('tests/fixtures/cloudflare-dns_records-' + 'page-1.json') as fh: + mock.get('{}?page=1'.format(base), status_code=200, + text=fh.read()) + with open('tests/fixtures/cloudflare-dns_records-' + 'page-2.json') as fh: + mock.get('{}?page=2'.format(base), status_code=200, + text=fh.read()) + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(9, len(zone.records)) + + changes = self.expected.changes(zone, provider) + self.assertEquals(0, len(changes)) + + # re-populating the same zone/records comes out of cache, no calls + again = Zone('unit.tests.', []) + provider.populate(again) + self.assertEquals(9, len(again.records)) + + def test_apply(self): + provider = CloudflareProvider('test', 'email', 'token') + + provider._request = Mock() + + provider._request.side_effect = [ + self.empty, # no zones + { + 'result': { + 'id': 42, + } + }, # zone create + ] + [None] * 15 # individual record creates + + # non-existant zone, create everything + plan = provider.plan(self.expected) + self.assertEquals(9, len(plan.changes)) + self.assertEquals(9, provider.apply(plan)) + + provider._request.assert_has_calls([ + # created the domain + call('POST', '/zones', data={ + 'jump_start': False, + 'name': 'unit.tests' + }), + # created at least one of the record with expected data + call('POST', '/zones/42/dns_records', data={ + 'content': 'ns1.unit.tests.', + 'type': 'NS', + 'name': 'under.unit.tests', + 'ttl': 3600 + }), + ]) + # expected number of total calls + self.assertEquals(17, provider._request.call_count) + + provider._request.reset_mock() + + provider.zone_records = Mock(return_value=[ + { + "id": "fc12ab34cd5611334422ab3322997653", + "type": "A", + "name": "www.unit.tests", + "content": "1.2.3.4", + "proxiable": True, + "proxied": False, + "ttl": 300, + "locked": False, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:43.420689Z", + "created_on": "2017-03-11T18:01:43.420689Z", + "meta": { + "auto_added": False + } + }, + { + "id": "fc12ab34cd5611334422ab3322997654", + "type": "A", + "name": "www.unit.tests", + "content": "2.2.3.4", + "proxiable": True, + "proxied": False, + "ttl": 300, + "locked": False, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:44.030044Z", + "created_on": "2017-03-11T18:01:44.030044Z", + "meta": { + "auto_added": False + } + }, + { + "id": "fc12ab34cd5611334422ab3322997655", + "type": "A", + "name": "nc.unit.tests", + "content": "3.2.3.4", + "proxiable": True, + "proxied": False, + "ttl": 120, + "locked": False, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:44.030044Z", + "created_on": "2017-03-11T18:01:44.030044Z", + "meta": { + "auto_added": False + } + }, + { + "id": "fc12ab34cd5611334422ab3322997655", + "type": "A", + "name": "ttl.unit.tests", + "content": "4.2.3.4", + "proxiable": True, + "proxied": False, + "ttl": 600, + "locked": False, + "zone_id": "ff12ab34cd5611334422ab3322997650", + "zone_name": "unit.tests", + "modified_on": "2017-03-11T18:01:44.030044Z", + "created_on": "2017-03-11T18:01:44.030044Z", + "meta": { + "auto_added": False + } + }, + ]) + + # we don't care about the POST/create return values + provider._request.return_value = {} + provider._request.side_effect = None + + wanted = Zone('unit.tests.', []) + wanted.add_record(Record.new(wanted, 'nc', { + 'ttl': 60, # TTL is below their min + 'type': 'A', + 'value': '3.2.3.4' + })) + wanted.add_record(Record.new(wanted, 'ttl', { + 'ttl': 300, # TTL change + 'type': 'A', + 'value': '3.2.3.4' + })) + + plan = provider.plan(wanted) + # only see the delete & ttl update, below min-ttl is filtered out + self.assertEquals(2, len(plan.changes)) + self.assertEquals(2, provider.apply(plan)) + # recreate for update, and deletes for the 2 parts of the other + provider._request.assert_has_calls([ + call('POST', '/zones/42/dns_records', data={ + 'content': '3.2.3.4', + 'type': 'A', + 'name': 'ttl.unit.tests', + 'ttl': 300}), + call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' + 'dns_records/fc12ab34cd5611334422ab3322997655'), + call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' + 'dns_records/fc12ab34cd5611334422ab3322997653'), + call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/' + 'dns_records/fc12ab34cd5611334422ab3322997654') + ]) diff --git a/tests/test_octodns_provider_dnsimple.py b/tests/test_octodns_provider_dnsimple.py new file mode 100644 index 0000000..3de297d --- /dev/null +++ b/tests/test_octodns_provider_dnsimple.py @@ -0,0 +1,202 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from mock import Mock, call +from os.path import dirname, join +from requests import HTTPError +from requests_mock import ANY, mock as requests_mock +from unittest import TestCase + +from octodns.record import Record +from octodns.provider.dnsimple import DnsimpleClientNotFound, DnsimpleProvider +from octodns.provider.yaml import YamlProvider +from octodns.zone import Zone + + +class TestDnsimpleProvider(TestCase): + expected = Zone('unit.tests.', []) + source = YamlProvider('test', join(dirname(__file__), 'config')) + source.populate(expected) + + # Our test suite differs a bit, add our NS and remove the simple one + expected.add_record(Record.new(expected, 'under', { + 'ttl': 3600, + 'type': 'NS', + 'values': [ + 'ns1.unit.tests.', + 'ns2.unit.tests.', + ] + })) + for record in list(expected.records): + if record.name == 'sub' and record._type == 'NS': + expected.records.remove(record) + break + + def test_populate(self): + provider = DnsimpleProvider('test', 'token', 42) + + # Bad auth + with requests_mock() as mock: + mock.get(ANY, status_code=401, + text='{"message": "Authentication failed"}') + + with self.assertRaises(Exception) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals('Unauthorized', ctx.exception.message) + + # General error + with requests_mock() as mock: + mock.get(ANY, status_code=502, text='Things caught fire') + + with self.assertRaises(HTTPError) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(502, ctx.exception.response.status_code) + + # Non-existant zone doesn't populate anything + with requests_mock() as mock: + mock.get(ANY, status_code=404, + text='{"message": "Domain `foo.bar` not found"}') + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(set(), zone.records) + + # No diffs == no changes + with requests_mock() as mock: + base = 'https://api.dnsimple.com/v2/42/zones/unit.tests/' \ + 'records?page=' + with open('tests/fixtures/dnsimple-page-1.json') as fh: + mock.get('{}{}'.format(base, 1), text=fh.read()) + with open('tests/fixtures/dnsimple-page-2.json') as fh: + mock.get('{}{}'.format(base, 2), text=fh.read()) + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(14, len(zone.records)) + changes = self.expected.changes(zone, provider) + self.assertEquals(0, len(changes)) + + # 2nd populate makes no network calls/all from cache + again = Zone('unit.tests.', []) + provider.populate(again) + self.assertEquals(14, len(again.records)) + + # bust the cache + del provider._zone_records[zone.name] + + # test handling of invalid content + with requests_mock() as mock: + with open('tests/fixtures/dnsimple-invalid-content.json') as fh: + mock.get(ANY, text=fh.read()) + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(set([ + Record.new(zone, '', { + 'ttl': 3600, + 'type': 'SSHFP', + 'values': [] + }), + Record.new(zone, '_srv._tcp', { + 'ttl': 600, + 'type': 'SRV', + 'values': [] + }), + Record.new(zone, 'naptr', { + 'ttl': 600, + 'type': 'NAPTR', + 'values': [] + }), + ]), zone.records) + + def test_apply(self): + provider = DnsimpleProvider('test', 'token', 42) + + resp = Mock() + resp.json = Mock() + provider._client._request = Mock(return_value=resp) + + # non-existant domain, create everything + resp.json.side_effect = [ + DnsimpleClientNotFound, # no zone in populate + DnsimpleClientNotFound, # no domain during apply + ] + plan = provider.plan(self.expected) + + # No root NS + n = len(self.expected.records) - 1 + self.assertEquals(n, len(plan.changes)) + self.assertEquals(n, provider.apply(plan)) + + provider._client._request.assert_has_calls([ + # created the domain + call('POST', '/domains', data={'name': 'unit.tests'}), + # created at least one of the record with expected data + call('POST', '/zones/unit.tests/records', data={ + 'content': '20 30 foo-1.unit.tests.', + 'priority': 10, + 'type': 'SRV', + 'name': '_srv._tcp', + 'ttl': 600 + }), + ]) + # expected number of total calls + self.assertEquals(25, provider._client._request.call_count) + + provider._client._request.reset_mock() + + # delete 1 and update 1 + provider._client.records = Mock(return_value=[ + { + 'id': 11189897, + 'name': 'www', + 'content': '1.2.3.4', + 'ttl': 300, + 'type': 'A', + }, + { + 'id': 11189898, + 'name': 'www', + 'content': '2.2.3.4', + 'ttl': 300, + 'type': 'A', + }, + { + 'id': 11189899, + 'name': 'ttl', + 'content': '3.2.3.4', + 'ttl': 600, + 'type': 'A', + } + ]) + # Domain exists, we don't care about return + resp.json.side_effect = ['{}'] + + wanted = Zone('unit.tests.', []) + wanted.add_record(Record.new(wanted, 'ttl', { + 'ttl': 300, + 'type': 'A', + 'value': '3.2.3.4' + })) + + plan = provider.plan(wanted) + self.assertEquals(2, len(plan.changes)) + self.assertEquals(2, provider.apply(plan)) + # recreate for update, and deletes for the 2 parts of the other + provider._client._request.assert_has_calls([ + call('POST', '/zones/unit.tests/records', data={ + 'content': '3.2.3.4', + 'type': 'A', + 'name': 'ttl', + 'ttl': 300 + }), + call('DELETE', '/zones/unit.tests/records/11189899'), + call('DELETE', '/zones/unit.tests/records/11189897'), + call('DELETE', '/zones/unit.tests/records/11189898') + ]) diff --git a/tests/test_octodns_provider_dyn.py b/tests/test_octodns_provider_dyn.py new file mode 100644 index 0000000..94fa216 --- /dev/null +++ b/tests/test_octodns_provider_dyn.py @@ -0,0 +1,1155 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from dyn.tm.errors import DynectGetError +from dyn.tm.services.dsf import DSFResponsePool +from json import loads +from mock import MagicMock, call, patch +from unittest import TestCase + +from octodns.record import Create, Delete, Record, Update +from octodns.provider.base import Plan +from octodns.provider.dyn import DynProvider, _CachingDynZone +from octodns.zone import Zone + +from helpers import SimpleProvider + + +class _DummyPool(object): + + def __init__(self, response_pool_id): + self.response_pool_id = response_pool_id + self.deleted = False + + def delete(self): + self.deleted = True + + +class TestDynProvider(TestCase): + expected = Zone('unit.tests.', []) + for name, data in ( + ('', { + 'type': 'A', + 'ttl': 300, + 'values': ['1.2.3.4'] + }), + ('cname', { + 'type': 'CNAME', + 'ttl': 301, + 'value': 'unit.tests.' + }), + ('', { + 'type': 'MX', + 'ttl': 302, + 'values': [{ + 'priority': 10, + 'value': 'smtp-1.unit.tests.' + }, { + 'priority': 20, + 'value': 'smtp-2.unit.tests.' + }] + }), + ('naptr', { + 'type': 'NAPTR', + 'ttl': 303, + 'values': [{ + 'order': 100, + 'preference': 101, + 'flags': 'U', + 'service': 'SIP+D2U', + 'regexp': '!^.*$!sip:info@foo.example.com!', + 'replacement': '.', + }, { + 'order': 200, + 'preference': 201, + 'flags': 'U', + 'service': 'SIP+D2U', + 'regexp': '!^.*$!sip:info@bar.example.com!', + 'replacement': '.', + }] + }), + ('sub', { + 'type': 'NS', + 'ttl': 3600, + 'values': ['ns3.p10.dynect.net.', 'ns3.p10.dynect.net.'], + }), + ('ptr', { + 'type': 'PTR', + 'ttl': 304, + 'value': 'xx.unit.tests.' + }), + ('spf', { + 'type': 'SPF', + 'ttl': 305, + 'values': ['v=spf1 ip4:192.168.0.1/16-all', 'v=spf1 -all'], + }), + ('', { + 'type': 'SSHFP', + 'ttl': 306, + 'value': { + 'algorithm': 1, + 'fingerprint_type': 1, + 'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73', + } + }), + ('_srv._tcp', { + 'type': 'SRV', + 'ttl': 307, + 'values': [{ + 'priority': 11, + 'weight': 12, + 'port': 10, + 'target': 'foo-1.unit.tests.' + }, { + 'priority': 21, + 'weight': 22, + 'port': 20, + 'target': 'foo-2.unit.tests.' + }]})): + expected.add_record(Record.new(expected, name, data)) + + @classmethod + def setUpClass(self): + # Get the DynectSession creation out of the way so that tests can + # ignore it + with patch('dyn.core.SessionEngine.execute', + return_value={'status': 'success'}): + provider = DynProvider('test', 'cust', 'user', 'pass') + provider._check_dyn_sess() + + def setUp(self): + # Flush our zone to ensure we start fresh + _CachingDynZone.flush_zone(self.expected.name[:-1]) + + @patch('dyn.core.SessionEngine.execute') + def test_populate_non_existent(self, execute_mock): + provider = DynProvider('test', 'cust', 'user', 'pass') + + # Test Zone create + execute_mock.side_effect = [ + DynectGetError('foo'), + ] + got = Zone('unit.tests.', []) + provider.populate(got) + execute_mock.assert_has_calls([ + call('/Zone/unit.tests/', 'GET', {}), + ]) + self.assertEquals(set(), got.records) + + @patch('dyn.core.SessionEngine.execute') + def test_populate(self, execute_mock): + provider = DynProvider('test', 'cust', 'user', 'pass') + + # Test Zone create + execute_mock.side_effect = [ + # get Zone + {'data': {}}, + # get_all_records + {'data': { + 'a_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'address': '1.2.3.4'}, + 'record_id': 1, + 'record_type': 'A', + 'ttl': 300, + 'zone': 'unit.tests', + }], + 'cname_records': [{ + 'fqdn': 'cname.unit.tests', + 'rdata': {'cname': 'unit.tests.'}, + 'record_id': 2, + 'record_type': 'CNAME', + 'ttl': 301, + 'zone': 'unit.tests', + }], + 'ns_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'nsdname': 'ns1.p10.dynect.net.'}, + 'record_id': 254597562, + 'record_type': 'NS', + 'service_class': '', + 'ttl': 3600, + 'zone': 'unit.tests' + }, { + 'fqdn': 'unit.tests', + 'rdata': {'nsdname': 'ns2.p10.dynect.net.'}, + 'record_id': 254597563, + 'record_type': 'NS', + 'service_class': '', + 'ttl': 3600, + 'zone': 'unit.tests' + }, { + 'fqdn': 'unit.tests', + 'rdata': {'nsdname': 'ns3.p10.dynect.net.'}, + 'record_id': 254597564, + 'record_type': 'NS', + 'service_class': '', + 'ttl': 3600, + 'zone': 'unit.tests' + }, { + 'fqdn': 'unit.tests', + 'rdata': {'nsdname': 'ns4.p10.dynect.net.'}, + 'record_id': 254597565, + 'record_type': 'NS', + 'service_class': '', + 'ttl': 3600, + 'zone': 'unit.tests' + }, { + 'fqdn': 'sub.unit.tests', + 'rdata': {'nsdname': 'ns3.p10.dynect.net.'}, + 'record_id': 254597564, + 'record_type': 'NS', + 'service_class': '', + 'ttl': 3600, + 'zone': 'unit.tests' + }, { + 'fqdn': 'sub.unit.tests', + 'rdata': {'nsdname': 'ns3.p10.dynect.net.'}, + 'record_id': 254597564, + 'record_type': 'NS', + 'service_class': '', + 'ttl': 3600, + 'zone': 'unit.tests' + }], + 'mx_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'exchange': 'smtp-1.unit.tests.', + 'preference': 10}, + 'record_id': 3, + 'record_type': 'MX', + 'ttl': 302, + 'zone': 'unit.tests', + }, { + 'fqdn': 'unit.tests', + 'rdata': {'exchange': 'smtp-2.unit.tests.', + 'preference': 20}, + 'record_id': 4, + 'record_type': 'MX', + 'ttl': 302, + 'zone': 'unit.tests', + }], + 'naptr_records': [{ + 'fqdn': 'naptr.unit.tests', + 'rdata': {'flags': 'U', + 'order': 100, + 'preference': 101, + 'regexp': '!^.*$!sip:info@foo.example.com!', + 'replacement': '.', + 'services': 'SIP+D2U'}, + 'record_id': 5, + 'record_type': 'MX', + 'ttl': 303, + 'zone': 'unit.tests', + }, { + 'fqdn': 'naptr.unit.tests', + 'rdata': {'flags': 'U', + 'order': 200, + 'preference': 201, + 'regexp': '!^.*$!sip:info@bar.example.com!', + 'replacement': '.', + 'services': 'SIP+D2U'}, + 'record_id': 6, + 'record_type': 'MX', + 'ttl': 303, + 'zone': 'unit.tests', + }], + 'ptr_records': [{ + 'fqdn': 'ptr.unit.tests', + 'rdata': {'ptrdname': 'xx.unit.tests.'}, + 'record_id': 7, + 'record_type': 'PTR', + 'ttl': 304, + 'zone': 'unit.tests', + }], + 'soa_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'txtdata': 'ns1.p16.dynect.net. ' + 'hostmaster.unit.tests. 4 3600 600 604800 1800'}, + 'record_id': 99, + 'record_type': 'SOA', + 'ttl': 299, + 'zone': 'unit.tests', + }], + 'spf_records': [{ + 'fqdn': 'spf.unit.tests', + 'rdata': {'txtdata': 'v=spf1 ip4:192.168.0.1/16-all'}, + 'record_id': 8, + 'record_type': 'SPF', + 'ttl': 305, + 'zone': 'unit.tests', + }, { + 'fqdn': 'spf.unit.tests', + 'rdata': {'txtdata': 'v=spf1 -all'}, + 'record_id': 8, + 'record_type': 'SPF', + 'ttl': 305, + 'zone': 'unit.tests', + }], + 'sshfp_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'algorithm': 1, + 'fingerprint': + 'bf6b6825d2977c511a475bbefb88aad54a92ac73', + 'fptype': 1}, + 'record_id': 9, + 'record_type': 'SSHFP', + 'ttl': 306, + 'zone': 'unit.tests', + }], + 'srv_records': [{ + 'fqdn': '_srv._tcp.unit.tests', + 'rdata': {'port': 10, + 'priority': 11, + 'target': u'foo-1.unit.tests.', + 'weight': 12}, + 'record_id': 10, + 'record_type': 'SRV', + 'ttl': 307, + 'zone': 'unit.tests', + }, { + 'fqdn': '_srv._tcp.unit.tests', + 'rdata': {'port': 20, + 'priority': 21, + 'target': u'foo-2.unit.tests.', + 'weight': 22}, + 'record_id': 11, + 'record_type': 'SRV', + 'ttl': 307, + 'zone': 'unit.tests', + }], + }} + ] + got = Zone('unit.tests.', []) + provider.populate(got) + execute_mock.assert_has_calls([ + call('/Zone/unit.tests/', 'GET', {}), + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}) + ]) + changes = self.expected.changes(got, SimpleProvider()) + self.assertEquals([], changes) + + @patch('dyn.core.SessionEngine.execute') + def test_sync(self, execute_mock): + provider = DynProvider('test', 'cust', 'user', 'pass') + + # Test Zone create + execute_mock.side_effect = [ + # No such zone, during populate + DynectGetError('foo'), + # No such zone, during sync + DynectGetError('foo'), + # get empty Zone + {'data': {}}, + # get zone we can modify & delete with + {'data': { + # A top-level to delete + 'a_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'address': '1.2.3.4'}, + 'record_id': 1, + 'record_type': 'A', + 'ttl': 30, + 'zone': 'unit.tests', + }, { + 'fqdn': 'a.unit.tests', + 'rdata': {'address': '2.3.4.5'}, + 'record_id': 2, + 'record_type': 'A', + 'ttl': 30, + 'zone': 'unit.tests', + }], + # A node to delete + 'cname_records': [{ + 'fqdn': 'cname.unit.tests', + 'rdata': {'cname': 'unit.tests.'}, + 'record_id': 3, + 'record_type': 'CNAME', + 'ttl': 30, + 'zone': 'unit.tests', + }], + # A record to leave alone + 'ptr_records': [{ + 'fqdn': 'ptr.unit.tests', + 'rdata': {'ptrdname': 'xx.unit.tests.'}, + 'record_id': 4, + 'record_type': 'PTR', + 'ttl': 30, + 'zone': 'unit.tests', + }], + # A record to modify + 'srv_records': [{ + 'fqdn': '_srv._tcp.unit.tests', + 'rdata': {'port': 10, + 'priority': 11, + 'target': u'foo-1.unit.tests.', + 'weight': 12}, + 'record_id': 5, + 'record_type': 'SRV', + 'ttl': 30, + 'zone': 'unit.tests', + }, { + 'fqdn': '_srv._tcp.unit.tests', + 'rdata': {'port': 20, + 'priority': 21, + 'target': u'foo-2.unit.tests.', + 'weight': 22}, + 'record_id': 6, + 'record_type': 'SRV', + 'ttl': 30, + 'zone': 'unit.tests', + }], + }} + ] + + # No existing records, create all + with patch('dyn.tm.zones.Zone.add_record') as add_mock: + with patch('dyn.tm.zones.Zone._update') as update_mock: + plan = provider.plan(self.expected) + update_mock.assert_not_called() + provider.apply(plan) + update_mock.assert_called() + add_mock.assert_called() + # Once for each dyn record (8 Records, 2 of which have dual values) + self.assertEquals(14, len(add_mock.call_args_list)) + execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}), + call('/Zone/unit.tests/', 'GET', {})]) + self.assertEquals(9, len(plan.changes)) + + execute_mock.reset_mock() + + # Delete one and modify another + new = Zone('unit.tests.', []) + for name, data in ( + ('a', { + 'type': 'A', + 'ttl': 30, + 'value': '2.3.4.5' + }), + ('ptr', { + 'type': 'PTR', + 'ttl': 30, + 'value': 'xx.unit.tests.' + }), + ('_srv._tcp', { + 'type': 'SRV', + 'ttl': 30, + 'values': [{ + 'priority': 31, + 'weight': 12, + 'port': 10, + 'target': 'foo-1.unit.tests.' + }, { + 'priority': 21, + 'weight': 22, + 'port': 20, + 'target': 'foo-2.unit.tests.' + }]})): + new.add_record(Record.new(new, name, data)) + + with patch('dyn.tm.zones.Zone.add_record') as add_mock: + with patch('dyn.tm.records.DNSRecord.delete') as delete_mock: + with patch('dyn.tm.zones.Zone._update') as update_mock: + plan = provider.plan(new) + provider.apply(plan) + update_mock.assert_called() + # we expect 4 deletes, 2 from actual deletes and 2 from + # updates which delete and recreate + self.assertEquals(4, len(delete_mock.call_args_list)) + # the 2 (re)creates + self.assertEquals(2, len(add_mock.call_args_list)) + execute_mock.assert_has_calls([ + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}) + ]) + self.assertEquals(3, len(plan.changes)) + + +class TestDynProviderGeo(TestCase): + + with open('./tests/fixtures/dyn-traffic-director-get.json') as fh: + traffic_director_response = loads(fh.read()) + + @property + def traffic_directors_reponse(self): + return { + 'data': [{ + 'active': 'Y', + 'label': 'unit.tests.:A', + 'nodes': [], + 'notifiers': [], + 'pending_change': '', + 'rulesets': [], + 'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM', + 'ttl': '300' + }, { + 'active': 'Y', + 'label': 'some.other.:A', + 'nodes': [], + 'notifiers': [], + 'pending_change': '', + 'rulesets': [], + 'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM', + 'ttl': '300' + }, { + 'active': 'Y', + 'label': 'other format', + 'nodes': [], + 'notifiers': [], + 'pending_change': '', + 'rulesets': [], + 'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM', + 'ttl': '300' + }] + } + + # Doing this as a property so that we get a fresh copy each time, dyn's + # client lib messes with the return value and prevents it from working on + # subsequent uses otherwise + @property + def records_response(self): + return { + 'data': { + 'a_records': [{ + 'fqdn': 'unit.tests', + 'rdata': {'address': '1.2.3.4'}, + 'record_id': 1, + 'record_type': 'A', + 'ttl': 301, + 'zone': 'unit.tests', + }], + } + } + + monitor_id = '42a' + monitors_response = { + 'data': [{ + 'active': 'Y', + 'dsf_monitor_id': monitor_id, + 'endpoints': [], + 'label': 'unit.tests.', + 'notifier': '', + 'options': { + 'expected': '', + 'header': '', + 'host': 'unit.tests', + 'path': '/_dns', + 'port': '443', + 'timeout': '10'}, + 'probe_interval': '60', + 'protocol': 'HTTPS', + 'response_count': '2', + 'retries': '2' + }], + 'job_id': 3376281406, + 'msgs': [{ + 'ERR_CD': None, + 'INFO': 'DSFMonitor_get: Here are your monitors', + 'LVL': 'INFO', + 'SOURCE': 'BLL' + }], + 'status': 'success' + } + + expected_geo = Zone('unit.tests.', []) + geo_record = Record.new(expected_geo, '', { + 'geo': { + 'AF': ['2.2.3.4', '2.2.3.5'], + 'AS-JP': ['3.2.3.4', '3.2.3.5'], + 'NA-US': ['4.2.3.4', '4.2.3.5'], + 'NA-US-CA': ['5.2.3.4', '5.2.3.5'] + }, + 'ttl': 300, + 'type': 'A', + 'values': ['1.2.3.4', '1.2.3.5'], + }) + expected_geo.add_record(geo_record) + expected_regular = Zone('unit.tests.', []) + regular_record = Record.new(expected_regular, '', { + 'ttl': 301, + 'type': 'A', + 'value': '1.2.3.4', + }) + expected_regular.add_record(regular_record) + + def setUp(self): + # Flush our zone to ensure we start fresh + _CachingDynZone.flush_zone('unit.tests') + + @patch('dyn.core.SessionEngine.execute') + def test_traffic_directors(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', True) + # short-circuit session checking + provider._dyn_sess = True + + # no tds + mock.side_effect = [{'data': []}] + self.assertEquals({}, provider.traffic_directors) + + # a supported td and an ingored one + response = { + 'data': [{ + 'active': 'Y', + 'label': 'unit.tests.:A', + 'nodes': [], + 'notifiers': [], + 'pending_change': '', + 'rulesets': [], + 'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM', + 'ttl': '300' + }, { + 'active': 'Y', + 'label': 'geo.unit.tests.:A', + 'nodes': [], + 'notifiers': [], + 'pending_change': '', + 'rulesets': [], + 'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM', + 'ttl': '300' + }, { + 'active': 'Y', + 'label': 'something else', + 'nodes': [], + 'notifiers': [], + 'pending_change': '', + 'rulesets': [], + 'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM', + 'ttl': '300' + }], + 'job_id': 3376164583, + 'status': 'success' + } + mock.side_effect = [response] + # first make sure that we get the empty version from cache + self.assertEquals({}, provider.traffic_directors) + # reach in and bust the cache + provider._traffic_directors = None + tds = provider.traffic_directors + self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']), + set(tds.keys())) + self.assertEquals(['A'], tds['unit.tests.'].keys()) + self.assertEquals(['A'], tds['geo.unit.tests.'].keys()) + + @patch('dyn.core.SessionEngine.execute') + def test_traffic_director_monitor(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', True) + # short-circuit session checking + provider._dyn_sess = True + + # no monitors, will try and create + geo_monitor_id = '42x' + mock.side_effect = [self.monitors_response, { + 'data': { + u'active': u'Y', + u'dsf_monitor_id': geo_monitor_id, + u'endpoints': [], + u'label': u'geo.unit.tests.', + u'notifier': u'', + u'options': { + u'expected': u'', + u'header': u'', + u'host': u'geo.unit.tests.', + u'path': u'/_dns', + u'port': u'443', + u'timeout': u'10' + }, + u'probe_interval': u'60', + u'protocol': u'HTTPS', + u'response_count': u'2', + u'retries': u'2' + }, + u'job_id': 3376259461, + u'msgs': [{u'ERR_CD': None, + u'INFO': u'add: Here is the new monitor', + u'LVL': u'INFO', + u'SOURCE': u'BLL'}], + u'status': u'success' + }] + + # ask for a monitor that doesn't exist + monitor = provider._traffic_director_monitor('geo.unit.tests.') + self.assertEquals(geo_monitor_id, monitor.dsf_monitor_id) + # should see a request for the list and a create + mock.assert_has_calls([ + call('/DSFMonitor/', 'GET', {'detail': 'Y'}), + call('/DSFMonitor/', 'POST', { + 'retries': 2, + 'protocol': u'HTTPS', + 'response_count': 2, + 'label': u'geo.unit.tests.', + 'probe_interval': 60, + 'active': 'Y', + 'options': { + 'path': u'/_dns', + 'host': u'geo.unit.tests', + 'port': 443, + 'timeout': 10 + } + }) + ]) + # created monitor is now cached + self.assertTrue('geo.unit.tests.' in + provider._traffic_director_monitors) + # pre-existing one is there too + self.assertTrue('unit.tests.' in + provider._traffic_director_monitors) + + # now ask for a monitor that does exist + mock.reset_mock() + monitor = provider._traffic_director_monitor('unit.tests.') + self.assertEquals(self.monitor_id, monitor.dsf_monitor_id) + # should have resulted in no calls b/c exists & we've cached the list + mock.assert_not_called() + + @patch('dyn.core.SessionEngine.execute') + def test_populate_traffic_directors_empty(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # empty all around + mock.side_effect = [ + # get traffic directors + {'data': []}, + # get zone + {'data': {}}, + # get records + {'data': {}}, + ] + got = Zone('unit.tests.', []) + provider.populate(got) + self.assertEquals(0, len(got.records)) + mock.assert_has_calls([ + call('/DSF/', 'GET', {'detail': 'Y'}), + call('/Zone/unit.tests/', 'GET', {}), + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), + ]) + + @patch('dyn.core.SessionEngine.execute') + def test_populate_traffic_directors_td(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # only traffic director + mock.side_effect = [ + # get traffic directors + self.traffic_directors_reponse, + # get traffic director + self.traffic_director_response, + # get zone + {'data': {}}, + # get records + {'data': {}}, + ] + got = Zone('unit.tests.', []) + provider.populate(got) + self.assertEquals(1, len(got.records)) + self.assertFalse(self.expected_geo.changes(got, provider)) + mock.assert_has_calls([ + call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET', + {'pending_changes': 'Y'}), + call('/Zone/unit.tests/', 'GET', {}), + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), + ]) + + @patch('dyn.core.SessionEngine.execute') + def test_populate_traffic_directors_regular(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # only regular + mock.side_effect = [ + # get traffic directors + {'data': []}, + # get zone + {'data': {}}, + # get records + self.records_response + ] + got = Zone('unit.tests.', []) + provider.populate(got) + self.assertEquals(1, len(got.records)) + self.assertFalse(self.expected_regular.changes(got, provider)) + mock.assert_has_calls([ + call('/DSF/', 'GET', {'detail': 'Y'}), + call('/Zone/unit.tests/', 'GET', {}), + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), + ]) + + @patch('dyn.core.SessionEngine.execute') + def test_populate_traffic_directors_both(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # both traffic director and regular, regular is ignored + mock.side_effect = [ + # get traffic directors + self.traffic_directors_reponse, + # get traffic director + self.traffic_director_response, + # get zone + {'data': {}}, + # get records + self.records_response + ] + got = Zone('unit.tests.', []) + provider.populate(got) + self.assertEquals(1, len(got.records)) + self.assertFalse(self.expected_geo.changes(got, provider)) + mock.assert_has_calls([ + call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET', + {'pending_changes': 'Y'}), + call('/Zone/unit.tests/', 'GET', {}), + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), + ]) + + @patch('dyn.core.SessionEngine.execute') + def test_populate_traffic_director_busted(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + busted_traffic_director_response = { + "status": "success", + "data": { + "notifiers": [], + "rulesets": [], + "ttl": "300", + "active": "Y", + "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI", + "nodes": [{ + "fqdn": "unit.tests", + "zone": "unit.tests" + }], + "pending_change": "", + "label": "unit.tests.:A" + }, + "job_id": 3376642606, + "msgs": [{ + "INFO": "detail: Here is your service", + "LVL": "INFO", + "ERR_CD": None, + "SOURCE": "BLL" + }] + } + # busted traffic director + mock.side_effect = [ + # get traffic directors + self.traffic_directors_reponse, + # get traffic director + busted_traffic_director_response, + # get zone + {'data': {}}, + # get records + {'data': {}}, + ] + got = Zone('unit.tests.', []) + provider.populate(got) + self.assertEquals(1, len(got.records)) + # we expect a change here for the record, the values aren't important, + # so just compare set contents (which does name and type) + self.assertEquals(self.expected_geo.records, got.records) + mock.assert_has_calls([ + call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET', + {'pending_changes': 'Y'}), + call('/Zone/unit.tests/', 'GET', {}), + call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}), + ]) + + @patch('dyn.core.SessionEngine.execute') + def test_apply_traffic_director(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # stubbing these out to avoid a lot of messy mocking, they'll be tested + # individually, we'll check for expected calls + provider._mod_geo_Create = MagicMock() + provider._mod_geo_Update = MagicMock() + provider._mod_geo_Delete = MagicMock() + provider._mod_Create = MagicMock() + provider._mod_Update = MagicMock() + provider._mod_Delete = MagicMock() + + # busted traffic director + mock.side_effect = [ + # get zone + {'data': {}}, + # accept publish + {'data': {}}, + ] + desired = Zone('unit.tests.', []) + geo = self.geo_record + regular = self.regular_record + + changes = [ + Create(geo), + Create(regular), + Update(geo, geo), + Update(regular, regular), + Delete(geo), + Delete(regular), + ] + plan = Plan(None, desired, changes) + provider._apply(plan) + mock.assert_has_calls([ + call('/Zone/unit.tests/', 'GET', {}), + call('/Zone/unit.tests/', 'PUT', {'publish': True}) + ]) + # should have seen 1 call to each + provider._mod_geo_Create.assert_called_once() + provider._mod_geo_Update.assert_called_once() + provider._mod_geo_Delete.assert_called_once() + provider._mod_Create.assert_called_once() + provider._mod_Update.assert_called_once() + provider._mod_Delete.assert_called_once() + + @patch('dyn.core.SessionEngine.execute') + def test_mod_geo_create(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # will be tested seperately + provider._mod_rulesets = MagicMock() + + mock.side_effect = [ + # create traffic director + self.traffic_director_response, + # get traffic directors + self.traffic_directors_reponse + ] + provider._mod_geo_Create(None, Create(self.geo_record)) + # td now lives in cache + self.assertTrue('A' in provider.traffic_directors['unit.tests.']) + # should have seen 1 gen call + provider._mod_rulesets.assert_called_once() + + def test_mod_geo_update_geo_geo(self): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # update of an existing td + + # pre-populate the cache with our mock td + provider._traffic_directors = { + 'unit.tests.': { + 'A': 42, + } + } + # mock _mod_rulesets + provider._mod_rulesets = MagicMock() + + geo = self.geo_record + change = Update(geo, geo) + provider._mod_geo_Update(None, change) + # still in cache + self.assertTrue('A' in provider.traffic_directors['unit.tests.']) + # should have seen 1 gen call + provider._mod_rulesets.assert_called_once_with(42, change) + + @patch('dyn.core.SessionEngine.execute') + def test_mod_geo_update_geo_regular(self, _): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # convert a td to a regular record + + provider._mod_Create = MagicMock() + provider._mod_geo_Delete = MagicMock() + + change = Update(self.geo_record, self.regular_record) + provider._mod_geo_Update(42, change) + # should have seen a call to create the new regular record + provider._mod_Create.assert_called_once_with(42, change) + # should have seen a call to delete the old td record + provider._mod_geo_Delete.assert_called_once_with(42, change) + + @patch('dyn.core.SessionEngine.execute') + def test_mod_geo_update_regular_geo(self, _): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + # convert a regular record to a td + + provider._mod_geo_Create = MagicMock() + provider._mod_Delete = MagicMock() + + change = Update(self.regular_record, self.geo_record) + provider._mod_geo_Update(42, change) + # should have seen a call to create the new geo record + provider._mod_geo_Create.assert_called_once_with(42, change) + # should have seen a call to delete the old regular record + provider._mod_Delete.assert_called_once_with(42, change) + + @patch('dyn.core.SessionEngine.execute') + def test_mod_geo_delete(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + td_mock = MagicMock() + provider._traffic_directors = { + 'unit.tests.': { + 'A': td_mock, + } + } + provider._mod_geo_Delete(None, Delete(self.geo_record)) + # delete called + td_mock.delete.assert_called_once() + # removed from cache + self.assertFalse('A' in provider.traffic_directors['unit.tests.']) + + @patch('dyn.tm.services.DSFResponsePool.create') + def test_find_or_create_pool(self, mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + td = 42 + + # no candidates cache miss, so create + values = ['1.2.3.4', '1.2.3.5'] + pool = provider._find_or_create_pool(td, [], 'default', 'A', values) + self.assertIsInstance(pool, DSFResponsePool) + self.assertEquals(1, len(pool.rs_chains)) + records = pool.rs_chains[0].record_sets[0].records + self.assertEquals(values, [r.address for r in records]) + mock.assert_called_once_with(td) + + # cache hit, use the one we just created + mock.reset_mock() + pools = [pool] + cached = provider._find_or_create_pool(td, pools, 'default', 'A', + values) + self.assertEquals(pool, cached) + mock.assert_not_called() + + # cache miss, non-matching label + mock.reset_mock() + miss = provider._find_or_create_pool(td, pools, 'NA-US-CA', 'A', + values) + self.assertNotEquals(pool, miss) + self.assertEquals('NA-US-CA', miss.label) + mock.assert_called_once_with(td) + + # cache miss, non-matching label + mock.reset_mock() + values = ['2.2.3.4.', '2.2.3.5'] + miss = provider._find_or_create_pool(td, pools, 'default', 'A', values) + self.assertNotEquals(pool, miss) + mock.assert_called_once_with(td) + + @patch('dyn.tm.services.DSFRuleset.add_response_pool') + @patch('dyn.tm.services.DSFRuleset.create') + # just lets us ignore the pool.create calls + @patch('dyn.tm.services.DSFResponsePool.create') + def test_mod_rulesets_create(self, _, ruleset_create_mock, + add_response_pool_mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + td_mock = MagicMock() + td_mock._rulesets = [] + provider._traffic_director_monitor = MagicMock() + provider._find_or_create_pool = MagicMock() + + td_mock.all_response_pools = [] + + provider._find_or_create_pool.side_effect = [ + _DummyPool('default'), + _DummyPool(1), + _DummyPool(2), + _DummyPool(3), + _DummyPool(4), + ] + + change = Create(self.geo_record) + provider._mod_rulesets(td_mock, change) + ruleset_create_mock.assert_has_calls(( + call(td_mock, index=0), + call(td_mock, index=0), + call(td_mock, index=0), + call(td_mock, index=0), + call(td_mock, index=0), + )) + add_response_pool_mock.assert_has_calls(( + # default + call('default'), + # first geo and it's fallback + call(1), + call('default', index=999), + # 2nd geo and it's fallback + call(2), + call('default', index=999), + # 3nd geo and it's fallback + call(3), + call('default', index=999), + # 4th geo and it's 2 levels of fallback + call(4), + call(3, index=999), + call('default', index=999), + )) + + # have to patch the place it's imported into, not where it lives + @patch('octodns.provider.dyn.get_response_pool') + @patch('dyn.tm.services.DSFRuleset.add_response_pool') + @patch('dyn.tm.services.DSFRuleset.create') + # just lets us ignore the pool.create calls + @patch('dyn.tm.services.DSFResponsePool.create') + def test_mod_rulesets_existing(self, _, ruleset_create_mock, + add_response_pool_mock, + get_response_pool_mock): + provider = DynProvider('test', 'cust', 'user', 'pass', + traffic_directors_enabled=True) + + ruleset_mock = MagicMock() + ruleset_mock.response_pools = [_DummyPool(3)] + + td_mock = MagicMock() + td_mock._rulesets = [ + ruleset_mock, + ] + provider._traffic_director_monitor = MagicMock() + provider._find_or_create_pool = MagicMock() + + unused_pool = _DummyPool('unused') + td_mock.all_response_pools = \ + ruleset_mock.response_pools + [unused_pool] + get_response_pool_mock.return_value = unused_pool + + provider._find_or_create_pool.side_effect = [ + _DummyPool('default'), + _DummyPool(1), + _DummyPool(2), + ruleset_mock.response_pools[0], + _DummyPool(4), + ] + + change = Create(self.geo_record) + provider._mod_rulesets(td_mock, change) + ruleset_create_mock.assert_has_calls(( + call(td_mock, index=0), + call(td_mock, index=0), + call(td_mock, index=0), + call(td_mock, index=0), + call(td_mock, index=0), + )) + add_response_pool_mock.assert_has_calls(( + # default + call('default'), + # first geo and it's fallback + call(1), + call('default', index=999), + # 2nd geo and it's fallback + call(2), + call('default', index=999), + # 3nd geo, from existing, and it's fallback + call(3), + call('default', index=999), + # 4th geo and it's 2 levels of fallback + call(4), + call(3, index=999), + call('default', index=999), + )) + # unused poll should have been deleted + self.assertTrue(unused_pool.deleted) + # old ruleset ruleset should be deleted, it's pool will have been + # reused + ruleset_mock.delete.assert_called_once() diff --git a/tests/test_octodns_provider_powerdns.py b/tests/test_octodns_provider_powerdns.py new file mode 100644 index 0000000..fd2752c --- /dev/null +++ b/tests/test_octodns_provider_powerdns.py @@ -0,0 +1,290 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from json import loads, dumps +from os.path import dirname, join +from requests import HTTPError +from requests_mock import ANY, mock as requests_mock +from unittest import TestCase + +from octodns.record import Record +from octodns.provider.powerdns import PowerDnsProvider +from octodns.provider.yaml import YamlProvider +from octodns.zone import Zone + +EMPTY_TEXT = ''' +{ + "account": "", + "dnssec": false, + "id": "xunit.tests.", + "kind": "Master", + "last_check": 0, + "masters": [], + "name": "xunit.tests.", + "notified_serial": 0, + "rrsets": [], + "serial": 2017012801, + "soa_edit": "", + "soa_edit_api": "INCEPTION-INCREMENT", + "url": "api/v1/servers/localhost/zones/xunit.tests." +} +''' + +with open('./tests/fixtures/powerdns-full-data.json') as fh: + FULL_TEXT = fh.read() + + +class TestPowerDnsProvider(TestCase): + + def test_provider(self): + provider = PowerDnsProvider('test', 'non.existant', 'api-key', + nameserver_values=['8.8.8.8.', + '9.9.9.9.']) + + # Bad auth + with requests_mock() as mock: + mock.get(ANY, status_code=401, text='Unauthorized') + + with self.assertRaises(Exception) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + print(ctx.exception.message) + self.assertTrue('unauthorized' in ctx.exception.message) + + # General error + with requests_mock() as mock: + mock.get(ANY, status_code=502, text='Things caught fire') + + with self.assertRaises(HTTPError) as ctx: + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(502, ctx.exception.response.status_code) + + # Non-existant zone doesn't populate anything + with requests_mock() as mock: + mock.get(ANY, status_code=422, + json={'error': "Could not find domain 'unit.tests.'"}) + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(set(), zone.records) + + # The rest of this is messy/complicated b/c it's dealing with mocking + + expected = Zone('unit.tests.', []) + source = YamlProvider('test', join(dirname(__file__), 'config')) + source.populate(expected) + self.assertEquals(14, len(expected.records)) + + # No diffs == no changes + with requests_mock() as mock: + mock.get(ANY, status_code=200, text=FULL_TEXT) + + zone = Zone('unit.tests.', []) + provider.populate(zone) + self.assertEquals(14, len(zone.records)) + changes = expected.changes(zone, provider) + self.assertEquals(0, len(changes)) + + # Used in a minute + def assert_rrsets_callback(request, context): + data = loads(request.body) + self.assertEquals(len(expected.records), len(data['rrsets'])) + return '' + + # No existing records -> creates for every record in expected + with requests_mock() as mock: + mock.get(ANY, status_code=200, text=EMPTY_TEXT) + # post 201, is reponse to the create with data + mock.patch(ANY, status_code=201, text=assert_rrsets_callback) + + plan = provider.plan(expected) + self.assertEquals(len(expected.records), len(plan.changes)) + self.assertEquals(len(expected.records), provider.apply(plan)) + + # Non-existent zone -> creates for every record in expected + # OMG this is fucking ugly, probably better to ditch requests_mocks and + # just mock things for real as it doesn't seem to provide a way to get + # at the request params or verify that things were called from what I + # can tell + not_found = {'error': "Could not find domain 'unit.tests.'"} + with requests_mock() as mock: + # get 422's, unknown zone + mock.get(ANY, status_code=422, text='') + # patch 422's, unknown zone + mock.patch(ANY, status_code=422, text=dumps(not_found)) + # post 201, is reponse to the create with data + mock.post(ANY, status_code=201, text=assert_rrsets_callback) + + plan = provider.plan(expected) + self.assertEquals(len(expected.records), len(plan.changes)) + self.assertEquals(len(expected.records), provider.apply(plan)) + + with requests_mock() as mock: + # get 422's, unknown zone + mock.get(ANY, status_code=422, text='') + # patch 422's, + data = {'error': "Key 'name' not present or not a String"} + mock.patch(ANY, status_code=422, text=dumps(data)) + + with self.assertRaises(HTTPError) as ctx: + plan = provider.plan(expected) + provider.apply(plan) + response = ctx.exception.response + self.assertEquals(422, response.status_code) + self.assertTrue('error' in response.json()) + + with requests_mock() as mock: + # get 422's, unknown zone + mock.get(ANY, status_code=422, text='') + # patch 500's, things just blew up + mock.patch(ANY, status_code=500, text='') + + with self.assertRaises(HTTPError): + plan = provider.plan(expected) + provider.apply(plan) + + with requests_mock() as mock: + # get 422's, unknown zone + mock.get(ANY, status_code=422, text='') + # patch 500's, things just blew up + mock.patch(ANY, status_code=422, text=dumps(not_found)) + # post 422's, something wrong with create + mock.post(ANY, status_code=422, text='Hello Word!') + + with self.assertRaises(HTTPError): + plan = provider.plan(expected) + provider.apply(plan) + + def test_small_change(self): + provider = PowerDnsProvider('test', 'non.existant', 'api-key') + + expected = Zone('unit.tests.', []) + source = YamlProvider('test', join(dirname(__file__), 'config')) + source.populate(expected) + self.assertEquals(14, len(expected.records)) + + # A small change to a single record + with requests_mock() as mock: + mock.get(ANY, status_code=200, text=FULL_TEXT) + + missing = Zone(expected.name, []) + # Find and delete the SPF record + for record in expected.records: + if record._type != 'SPF': + missing.add_record(record) + + def assert_delete_callback(request, context): + self.assertEquals({ + 'rrsets': [{ + 'records': [ + {'content': '"v=spf1 ip4:192.168.0.1/16-all"', + 'disabled': False} + ], + 'changetype': 'DELETE', + 'type': 'SPF', + 'name': 'spf.unit.tests.', + 'ttl': 600 + }] + }, loads(request.body)) + return '' + + mock.patch(ANY, status_code=201, text=assert_delete_callback) + + plan = provider.plan(missing) + self.assertEquals(1, len(plan.changes)) + self.assertEquals(1, provider.apply(plan)) + + def test_existing_nameservers(self): + ns_values = ['8.8.8.8.', '9.9.9.9.'] + provider = PowerDnsProvider('test', 'non.existant', 'api-key', + nameserver_values=ns_values) + + expected = Zone('unit.tests.', []) + ns_record = Record.new(expected, '', { + 'type': 'NS', + 'ttl': 600, + 'values': ns_values + }) + expected.add_record(ns_record) + + # no changes + with requests_mock() as mock: + data = { + 'rrsets': [{ + 'comments': [], + 'name': 'unit.tests.', + 'records': [ + { + 'content': '8.8.8.8.', + 'disabled': False + }, + { + 'content': '9.9.9.9.', + 'disabled': False + } + ], + 'ttl': 600, + 'type': 'NS' + }, { + 'comments': [], + 'name': 'unit.tests.', + 'records': [{ + 'content': '1.2.3.4', + 'disabled': False, + }], + 'ttl': 60, + 'type': 'A' + }] + } + mock.get(ANY, status_code=200, json=data) + + unrelated_record = Record.new(expected, '', { + 'type': 'A', + 'ttl': 60, + 'value': '1.2.3.4' + }) + expected.add_record(unrelated_record) + plan = provider.plan(expected) + self.assertFalse(plan) + # remove it now that we don't need the unrelated change any longer + expected.records.remove(unrelated_record) + + # ttl diff + with requests_mock() as mock: + data = { + 'rrsets': [{ + 'comments': [], + 'name': 'unit.tests.', + 'records': [ + { + 'content': '8.8.8.8.', + 'disabled': False + }, + { + 'content': '9.9.9.9.', + 'disabled': False + }, + ], + 'ttl': 3600, + 'type': 'NS' + }] + } + mock.get(ANY, status_code=200, json=data) + + plan = provider.plan(expected) + self.assertEquals(1, len(plan.changes)) + + # create + with requests_mock() as mock: + data = { + 'rrsets': [] + } + mock.get(ANY, status_code=200, json=data) + + plan = provider.plan(expected) + self.assertEquals(1, len(plan.changes)) diff --git a/tests/test_octodns_provider_route53.py b/tests/test_octodns_provider_route53.py new file mode 100644 index 0000000..2dbc509 --- /dev/null +++ b/tests/test_octodns_provider_route53.py @@ -0,0 +1,1145 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from botocore.exceptions import ClientError +from botocore.stub import ANY, Stubber +from unittest import TestCase +from mock import patch + +from octodns.record import Create, Delete, Record, Update +from octodns.provider.route53 import _Route53Record, Route53Provider, \ + _octal_replace +from octodns.zone import Zone + +from helpers import GeoProvider + + +class TestOctalReplace(TestCase): + + def test_basic(self): + for expected, s in ( + ('', ''), + ('abc', 'abc'), + ('123', '123'), + ('abc123', 'abc123'), + ('*', '\\052'), + ('abc*', 'abc\\052'), + ('*abc', '\\052abc'), + ('123*', '123\\052'), + ('*123', '\\052123'), + ('**', '\\052\\052'), + ): + self.assertEquals(expected, _octal_replace(s)) + + +class TestRoute53Provider(TestCase): + expected = Zone('unit.tests.', []) + for name, data in ( + ('simple', + {'ttl': 60, 'type': 'A', 'values': ['1.2.3.4', '2.2.3.4']}), + ('', + {'ttl': 61, 'type': 'A', 'values': ['2.2.3.4', '3.2.3.4'], + 'geo': { + 'AF': ['4.2.3.4'], + 'NA-US': ['5.2.3.4', '6.2.3.4'], + 'NA-US-CA': ['7.2.3.4']}}), + ('cname', {'ttl': 62, 'type': 'CNAME', 'value': 'unit.tests.'}), + ('txt', {'ttl': 63, 'type': 'TXT', 'values': ['Hello World!', + 'Goodbye World?']}), + ('', {'ttl': 64, 'type': 'MX', + 'values': [{ + 'priority': 10, + 'value': 'smtp-1.unit.tests.', + }, { + 'priority': 20, + 'value': 'smtp-2.unit.tests.', + }]}), + ('naptr', {'ttl': 65, 'type': 'NAPTR', + 'value': { + 'order': 10, + 'preference': 20, + 'flags': 'U', + 'service': 'SIP+D2U', + 'regexp': '!^.*$!sip:info@bar.example.com!', + 'replacement': '.', + }}), + ('_srv._tcp', {'ttl': 66, 'type': 'SRV', 'value': { + 'priority': 10, + 'weight': 20, + 'port': 30, + 'target': 'cname.unit.tests.' + }}), + ('', + {'ttl': 67, 'type': 'NS', 'values': ['8.2.3.4.', '9.2.3.4.']}), + ('sub', + {'ttl': 68, 'type': 'NS', 'values': ['5.2.3.4.', '6.2.3.4.']}), + ): + record = Record.new(expected, name, data) + expected.add_record(record) + + caller_ref = '{}:A:1324'.format(Route53Provider.HEALTH_CHECK_VERSION) + health_checks = [{ + 'Id': '42', + 'CallerReference': caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '4.2.3.4', + }, + 'HealthCheckVersion': 2, + }, { + 'Id': 'ignored-also', + 'CallerReference': 'something-else', + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '5.2.3.4', + }, + 'HealthCheckVersion': 42, + }, { + 'Id': '43', + 'CallerReference': caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '5.2.3.4', + }, + 'HealthCheckVersion': 2, + }, { + 'Id': '44', + 'CallerReference': caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '7.2.3.4', + }, + 'HealthCheckVersion': 2, + }, { + 'Id': '45', + # won't match anything based on type + 'CallerReference': caller_ref.replace(':A:', ':AAAA:'), + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '7.2.3.4', + }, + 'HealthCheckVersion': 2, + }] + + def _get_stubbed_provider(self): + provider = Route53Provider('test', 'abc', '123') + + # Use the stubber + stubber = Stubber(provider._conn) + stubber.activate() + + return (provider, stubber) + + def test_populate(self): + provider, stubber = self._get_stubbed_provider() + + got = Zone('unit.tests.', []) + with self.assertRaises(ClientError): + stubber.add_client_error('list_hosted_zones') + provider.populate(got) + + with self.assertRaises(ClientError): + list_hosted_zones_resp = { + 'HostedZones': [{ + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, + {}) + stubber.add_client_error('list_resource_record_sets', + expected_params={'HostedZoneId': u'z42'}) + provider.populate(got) + stubber.assert_no_pending_responses() + + # list_hosted_zones has been cached from now on so we don't have to + # worry about stubbing it + + list_resource_record_sets_resp_p1 = { + 'ResourceRecordSets': [{ + 'Name': 'simple.unit.tests.', + 'Type': 'A', + 'ResourceRecords': [{ + 'Value': '1.2.3.4', + }, { + 'Value': '2.2.3.4', + }], + 'TTL': 60, + }, { + 'Name': 'unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'CountryCode': '*', + }, + 'ResourceRecords': [{ + 'Value': '2.2.3.4', + }, { + 'Value': '3.2.3.4', + }], + 'TTL': 61, + }, { + 'Name': 'unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'ContinentCode': 'AF', + }, + 'ResourceRecords': [{ + 'Value': '4.2.3.4', + }], + 'TTL': 61, + }, { + 'Name': 'unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'CountryCode': 'US', + }, + 'ResourceRecords': [{ + 'Value': '5.2.3.4', + }, { + 'Value': '6.2.3.4', + }], + 'TTL': 61, + }, { + 'Name': 'unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'CountryCode': 'US', + 'SubdivisionCode': 'CA', + }, + 'ResourceRecords': [{ + 'Value': '7.2.3.4', + }], + 'TTL': 61, + }], + 'IsTruncated': True, + 'NextRecordName': 'next_name', + 'NextRecordType': 'next_type', + 'MaxItems': '100', + } + stubber.add_response('list_resource_record_sets', + list_resource_record_sets_resp_p1, + {'HostedZoneId': 'z42'}) + + list_resource_record_sets_resp_p2 = { + 'ResourceRecordSets': [{ + 'Name': 'cname.unit.tests.', + 'Type': 'CNAME', + 'ResourceRecords': [{ + 'Value': 'unit.tests.', + }], + 'TTL': 62, + }, { + 'Name': 'txt.unit.tests.', + 'Type': 'TXT', + 'ResourceRecords': [{ + 'Value': '"Hello World!"', + }, { + 'Value': '"Goodbye World?"', + }], + 'TTL': 63, + }, { + 'Name': 'unit.tests.', + 'Type': 'MX', + 'ResourceRecords': [{ + 'Value': '10 smtp-1.unit.tests.', + }, { + 'Value': '20 smtp-2.unit.tests.', + }], + 'TTL': 64, + }, { + 'Name': 'naptr.unit.tests.', + 'Type': 'NAPTR', + 'ResourceRecords': [{ + 'Value': '10 20 "U" "SIP+D2U" ' + '"!^.*$!sip:info@bar.example.com!" .', + }], + 'TTL': 65, + }, { + 'Name': '_srv._tcp.unit.tests.', + 'Type': 'SRV', + 'ResourceRecords': [{ + 'Value': '10 20 30 cname.unit.tests.', + }], + 'TTL': 66, + }, { + 'Name': 'unit.tests.', + 'Type': 'NS', + 'ResourceRecords': [{ + 'Value': 'ns1.unit.tests.', + }], + 'TTL': 67, + }, { + 'Name': 'sub.unit.tests.', + 'Type': 'NS', + 'GeoLocation': { + 'ContinentCode': 'AF', + }, + 'ResourceRecords': [{ + 'Value': '5.2.3.4.', + }, { + 'Value': '6.2.3.4.', + }], + 'TTL': 68, + }, { + 'Name': 'soa.unit.tests.', + 'Type': 'SOA', + 'ResourceRecords': [{ + 'Value': 'ns1.unit.tests.', + }], + 'TTL': 69, + }], + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_resource_record_sets', + list_resource_record_sets_resp_p2, + {'HostedZoneId': 'z42', + 'StartRecordName': 'next_name', + 'StartRecordType': 'next_type'}) + + # Load everything + provider.populate(got) + # Make sure we got what we expected + changes = self.expected.changes(got, GeoProvider()) + self.assertEquals(0, len(changes)) + stubber.assert_no_pending_responses() + + # Populate a zone that doesn't exist + noexist = Zone('does.not.exist.', []) + provider.populate(noexist) + self.assertEquals(set(), noexist.records) + + def test_sync(self): + provider, stubber = self._get_stubbed_provider() + + list_hosted_zones_resp = { + 'HostedZones': [{ + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, + {}) + list_resource_record_sets_resp = { + 'ResourceRecordSets': [], + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_resource_record_sets', + list_resource_record_sets_resp, + {'HostedZoneId': 'z42'}) + + plan = provider.plan(self.expected) + self.assertEquals(8, len(plan.changes)) + for change in plan.changes: + self.assertIsInstance(change, Create) + stubber.assert_no_pending_responses() + + stubber.add_response('list_health_checks', + { + 'HealthChecks': self.health_checks, + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + stubber.add_response('change_resource_record_sets', + {'ChangeInfo': { + 'Id': 'id', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + }}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY}) + + self.assertEquals(8, provider.apply(plan)) + stubber.assert_no_pending_responses() + + # Delete by monkey patching in a populate that includes an extra record + def add_extra_populate(existing, target): + for record in self.expected.records: + existing.records.add(record) + record = Record.new(existing, 'extra', + {'ttl': 99, 'type': 'A', + 'values': ['9.9.9.9']}) + existing.records.add(record) + + provider.populate = add_extra_populate + change_resource_record_sets_params = { + 'ChangeBatch': { + 'Changes': [{ + 'Action': 'DELETE', 'ResourceRecordSet': { + 'Name': 'extra.unit.tests.', + 'ResourceRecords': [{'Value': u'9.9.9.9'}], + 'TTL': 99, + 'Type': 'A' + }}], + u'Comment': ANY + }, + 'HostedZoneId': u'z42' + } + stubber.add_response('change_resource_record_sets', + {'ChangeInfo': { + 'Id': 'id', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + }}, change_resource_record_sets_params) + plan = provider.plan(self.expected) + self.assertEquals(1, len(plan.changes)) + self.assertIsInstance(plan.changes[0], Delete) + self.assertEquals(1, provider.apply(plan)) + stubber.assert_no_pending_responses() + + # Update by monkey patching in a populate that modifies the A record + # with geos + def mod_geo_populate(existing, target): + for record in self.expected.records: + if record._type != 'A' or not record.geo: + existing.records.add(record) + record = Record.new(existing, '', { + 'ttl': 61, + 'type': 'A', + 'values': ['8.2.3.4', '3.2.3.4'], + 'geo': { + 'AF': ['4.2.3.4'], + 'NA-US': ['5.2.3.4', '6.2.3.4'], + 'NA-US-KY': ['7.2.3.4'] + } + }) + existing.records.add(record) + + provider.populate = mod_geo_populate + change_resource_record_sets_params = { + 'ChangeBatch': { + 'Changes': [{ + 'Action': 'DELETE', + 'ResourceRecordSet': { + 'GeoLocation': {'CountryCode': 'US', + 'SubdivisionCode': 'KY'}, + 'Name': 'unit.tests.', + 'ResourceRecords': [{'Value': '7.2.3.4'}], + 'SetIdentifier': 'NA-US-KY', + 'TTL': 61, + 'Type': 'A' + } + }, { + 'Action': 'CREATE', + 'ResourceRecordSet': { + 'GeoLocation': {'CountryCode': 'US', + 'SubdivisionCode': 'CA'}, + 'HealthCheckId': u'44', + 'Name': 'unit.tests.', + 'ResourceRecords': [{'Value': '7.2.3.4'}], + 'SetIdentifier': 'NA-US-CA', + 'TTL': 61, + 'Type': 'A' + } + }, { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'GeoLocation': {'ContinentCode': 'AF'}, + 'Name': 'unit.tests.', + 'HealthCheckId': u'42', + 'ResourceRecords': [{'Value': '4.2.3.4'}], + 'SetIdentifier': 'AF', + 'TTL': 61, + 'Type': 'A' + } + }, { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'GeoLocation': {'CountryCode': '*'}, + 'Name': 'unit.tests.', + 'ResourceRecords': [{'Value': '2.2.3.4'}, + {'Value': '3.2.3.4'}], + 'SetIdentifier': 'default', + 'TTL': 61, + 'Type': 'A' + } + }, { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'GeoLocation': {'CountryCode': 'US'}, + 'HealthCheckId': u'43', + 'Name': 'unit.tests.', + 'ResourceRecords': [{'Value': '5.2.3.4'}, + {'Value': '6.2.3.4'}], + 'SetIdentifier': 'NA-US', + 'TTL': 61, + 'Type': 'A' + } + }], + 'Comment': ANY + }, + 'HostedZoneId': 'z42' + } + stubber.add_response('change_resource_record_sets', + {'ChangeInfo': { + 'Id': 'id', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + }}, change_resource_record_sets_params) + plan = provider.plan(self.expected) + self.assertEquals(1, len(plan.changes)) + self.assertIsInstance(plan.changes[0], Update) + self.assertEquals(1, provider.apply(plan)) + stubber.assert_no_pending_responses() + + def test_sync_create(self): + provider, stubber = self._get_stubbed_provider() + + got = Zone('unit.tests.', []) + + list_hosted_zones_resp = { + 'HostedZones': [], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, + {}) + + plan = provider.plan(self.expected) + self.assertEquals(8, len(plan.changes)) + for change in plan.changes: + self.assertIsInstance(change, Create) + stubber.assert_no_pending_responses() + + create_hosted_zone_resp = { + 'HostedZone': { + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }, + 'ChangeInfo': { + 'Id': 'a12', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + 'Comment': 'hrm', + }, + 'DelegationSet': { + 'Id': 'b23', + 'CallerReference': 'blip', + 'NameServers': [ + 'n12.unit.tests.', + ], + }, + 'Location': 'us-east-1', + } + stubber.add_response('create_hosted_zone', + create_hosted_zone_resp, { + 'Name': got.name, + 'CallerReference': ANY, + }) + + stubber.add_response('list_health_checks', + { + 'HealthChecks': self.health_checks, + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + + stubber.add_response('change_resource_record_sets', + {'ChangeInfo': { + 'Id': 'id', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + }}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY}) + + self.assertEquals(8, provider.apply(plan)) + stubber.assert_no_pending_responses() + + def test_health_checks_pagination(self): + provider, stubber = self._get_stubbed_provider() + + health_checks_p1 = [{ + 'Id': '42', + 'CallerReference': self.caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '4.2.3.4', + }, + 'HealthCheckVersion': 2, + }, { + 'Id': '43', + 'CallerReference': 'abc123', + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '9.2.3.4', + }, + 'HealthCheckVersion': 2, + }] + stubber.add_response('list_health_checks', + { + 'HealthChecks': health_checks_p1, + 'IsTruncated': True, + 'MaxItems': '2', + 'Marker': '', + 'NextMarker': 'moar', + }) + + health_checks_p2 = [{ + 'Id': '44', + 'CallerReference': self.caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '8.2.3.4', + }, + 'HealthCheckVersion': 2, + }] + stubber.add_response('list_health_checks', + { + 'HealthChecks': health_checks_p2, + 'IsTruncated': False, + 'MaxItems': '2', + 'Marker': 'moar', + }, {'Marker': 'moar'}) + + health_checks = provider.health_checks + self.assertEquals({ + '42': health_checks_p1[0], + '44': health_checks_p2[0], + }, health_checks) + stubber.assert_no_pending_responses() + + # get without create + record = Record.new(self.expected, '', { + 'ttl': 61, + 'type': 'A', + 'values': ['2.2.3.4', '3.2.3.4'], + 'geo': { + 'AF': ['4.2.3.4'], + } + }) + id = provider._get_health_check_id(record, 'AF', record.geo['AF']) + self.assertEquals('42', id) + + def test_health_check_create(self): + provider, stubber = self._get_stubbed_provider() + + # No match based on type + caller_ref = \ + '{}:AAAA:foo1234'.format(Route53Provider.HEALTH_CHECK_VERSION) + health_checks = [{ + 'Id': '42', + # No match based on version + 'CallerReference': '9999:A:foo1234', + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '4.2.3.4', + }, + 'HealthCheckVersion': 2, + }, { + 'Id': '43', + 'CallerReference': caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '4.2.3.4', + }, + 'HealthCheckVersion': 2, + }] + stubber.add_response('list_health_checks', { + 'HealthChecks': health_checks, + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + + health_check_config = { + 'EnableSNI': True, + 'FailureThreshold': 6, + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '4.2.3.4', + 'MeasureLatency': True, + 'Port': 443, + 'RequestInterval': 10, + 'ResourcePath': '/_dns', + 'Type': 'HTTPS' + } + stubber.add_response('create_health_check', { + 'HealthCheck': { + 'Id': '42', + 'CallerReference': self.caller_ref, + 'HealthCheckConfig': health_check_config, + 'HealthCheckVersion': 1, + }, + 'Location': 'http://url', + }, { + 'CallerReference': ANY, + 'HealthCheckConfig': health_check_config, + }) + + record = Record.new(self.expected, '', { + 'ttl': 61, + 'type': 'A', + 'values': ['2.2.3.4', '3.2.3.4'], + 'geo': { + 'AF': ['4.2.3.4'], + } + }) + id = provider._get_health_check_id(record, 'AF', record.geo['AF']) + self.assertEquals('42', id) + stubber.assert_no_pending_responses() + + def test_health_check_gc(self): + provider, stubber = self._get_stubbed_provider() + + stubber.add_response('list_health_checks', { + 'HealthChecks': self.health_checks, + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + + record = Record.new(self.expected, '', { + 'ttl': 61, + 'type': 'A', + 'values': ['2.2.3.4', '3.2.3.4'], + 'geo': { + 'AF': ['4.2.3.4'], + 'NA-US': ['5.2.3.4', '6.2.3.4'], + # removed one geo + } + }) + + class DummyRecord(object): + + def __init__(self, health_check_id): + self.health_check_id = health_check_id + + # gc no longer in_use records (directly) + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': '44', + }) + provider._gc_health_checks(record, [ + DummyRecord('42'), + DummyRecord('43'), + ]) + stubber.assert_no_pending_responses() + + # gc through _mod_Create + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': '44', + }) + change = Create(record) + provider._mod_Create(change) + stubber.assert_no_pending_responses() + + # gc through _mod_Update + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': '44', + }) + # first record is ignored for our purposes, we have to pass something + change = Update(record, record) + provider._mod_Create(change) + stubber.assert_no_pending_responses() + + # gc through _mod_Delete, expect 3 to go away, can't check order + # b/c it's not deterministic + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': ANY, + }) + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': ANY, + }) + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': ANY, + }) + change = Delete(record) + provider._mod_Delete(change) + stubber.assert_no_pending_responses() + + # gc only AAAA, leave the A's alone + stubber.add_response('delete_health_check', {}, { + 'HealthCheckId': '45', + }) + record = Record.new(self.expected, '', { + 'ttl': 61, + 'type': 'AAAA', + 'value': '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b' + }) + provider._gc_health_checks(record, []) + stubber.assert_no_pending_responses() + + def test_no_extra_changes(self): + provider, stubber = self._get_stubbed_provider() + + list_hosted_zones_resp = { + 'HostedZones': [{ + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) + + # empty is empty + existing = Zone('unit.tests.', []) + extra = provider._extra_changes(existing, []) + self.assertEquals([], extra) + stubber.assert_no_pending_responses() + + # single record w/o geo is empty + existing = Zone('unit.tests.', []) + record = Record.new(existing, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + }) + existing.add_record(record) + extra = provider._extra_changes(existing, []) + self.assertEquals([], extra) + stubber.assert_no_pending_responses() + + # short-circuit for unknown zone + other = Zone('other.tests.', []) + extra = provider._extra_changes(other, []) + self.assertEquals([], extra) + stubber.assert_no_pending_responses() + + def test_extra_change_no_health_check(self): + provider, stubber = self._get_stubbed_provider() + + list_hosted_zones_resp = { + 'HostedZones': [{ + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) + + # record with geo and no health check returns change + existing = Zone('unit.tests.', []) + record = Record.new(existing, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + 'geo': { + 'NA': ['2.2.3.4'], + } + }) + existing.add_record(record) + list_resource_record_sets_resp = { + 'ResourceRecordSets': [{ + 'Name': 'a.unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'ContinentCode': 'NA', + }, + 'ResourceRecords': [{ + 'Value': '2.2.3.4', + }], + 'TTL': 61, + }], + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_resource_record_sets', + list_resource_record_sets_resp, + {'HostedZoneId': 'z42'}) + extra = provider._extra_changes(existing, []) + self.assertEquals(1, len(extra)) + stubber.assert_no_pending_responses() + + def test_extra_change_has_wrong_health_check(self): + provider, stubber = self._get_stubbed_provider() + + list_hosted_zones_resp = { + 'HostedZones': [{ + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) + + # record with geo and no health check returns change + existing = Zone('unit.tests.', []) + record = Record.new(existing, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + 'geo': { + 'NA': ['2.2.3.4'], + } + }) + existing.add_record(record) + list_resource_record_sets_resp = { + 'ResourceRecordSets': [{ + 'Name': 'a.unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'ContinentCode': 'NA', + }, + 'ResourceRecords': [{ + 'Value': '2.2.3.4', + }], + 'TTL': 61, + 'HealthCheckId': '42', + }], + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_resource_record_sets', + list_resource_record_sets_resp, + {'HostedZoneId': 'z42'}) + stubber.add_response('list_health_checks', { + 'HealthChecks': [{ + 'Id': '42', + 'CallerReference': 'foo', + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '2.2.3.4', + }, + 'HealthCheckVersion': 2, + }], + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + extra = provider._extra_changes(existing, []) + self.assertEquals(1, len(extra)) + stubber.assert_no_pending_responses() + + for change in (Create(record), Update(record, record), Delete(record)): + extra = provider._extra_changes(existing, [change]) + self.assertEquals(0, len(extra)) + stubber.assert_no_pending_responses() + + def test_extra_change_has_health_check(self): + provider, stubber = self._get_stubbed_provider() + + list_hosted_zones_resp = { + 'HostedZones': [{ + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {}) + + # record with geo and no health check returns change + existing = Zone('unit.tests.', []) + record = Record.new(existing, 'a', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + 'geo': { + 'NA': ['2.2.3.4'], + } + }) + existing.add_record(record) + list_resource_record_sets_resp = { + 'ResourceRecordSets': [{ + # other name + 'Name': 'unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'CountryCode': '*', + }, + 'ResourceRecords': [{ + 'Value': '1.2.3.4', + }], + 'TTL': 61, + }, { + # matching name, other type + 'Name': 'a.unit.tests.', + 'Type': 'AAAA', + 'ResourceRecords': [{ + 'Value': '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b' + }], + 'TTL': 61, + }, { + # default geo + 'Name': 'a.unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'CountryCode': '*', + }, + 'ResourceRecords': [{ + 'Value': '1.2.3.4', + }], + 'TTL': 61, + }, { + # match w/correct geo + 'Name': 'a.unit.tests.', + 'Type': 'A', + 'GeoLocation': { + 'ContinentCode': 'NA', + }, + 'ResourceRecords': [{ + 'Value': '2.2.3.4', + }], + 'TTL': 61, + 'HealthCheckId': '42', + }], + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_resource_record_sets', + list_resource_record_sets_resp, + {'HostedZoneId': 'z42'}) + stubber.add_response('list_health_checks', { + 'HealthChecks': [{ + 'Id': '42', + 'CallerReference': self.caller_ref, + 'HealthCheckConfig': { + 'Type': 'HTTPS', + 'FullyQualifiedDomainName': 'unit.tests', + 'IPAddress': '2.2.3.4', + }, + 'HealthCheckVersion': 2, + }], + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + extra = provider._extra_changes(existing, []) + self.assertEquals(0, len(extra)) + stubber.assert_no_pending_responses() + + def test_route_53_record(self): + # Just make sure it doesn't blow up + _Route53Record('foo.unit.tests.', 'A', 30).__repr__() + + def _get_test_plan(self, max_changes): + + provider = Route53Provider('test', 'abc', '123', max_changes) + + # Use the stubber + stubber = Stubber(provider._conn) + stubber.activate() + + got = Zone('unit.tests.', []) + + list_hosted_zones_resp = { + 'HostedZones': [], + 'Marker': 'm', + 'IsTruncated': False, + 'MaxItems': '100', + } + stubber.add_response('list_hosted_zones', list_hosted_zones_resp, + {}) + + create_hosted_zone_resp = { + 'HostedZone': { + 'Name': 'unit.tests.', + 'Id': 'z42', + 'CallerReference': 'abc', + }, + 'ChangeInfo': { + 'Id': 'a12', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + 'Comment': 'hrm', + }, + 'DelegationSet': { + 'Id': 'b23', + 'CallerReference': 'blip', + 'NameServers': [ + 'n12.unit.tests.', + ], + }, + 'Location': 'us-east-1', + } + stubber.add_response('create_hosted_zone', + create_hosted_zone_resp, { + 'Name': got.name, + 'CallerReference': ANY, + }) + + stubber.add_response('list_health_checks', + { + 'HealthChecks': self.health_checks, + 'IsTruncated': False, + 'MaxItems': '100', + 'Marker': '', + }) + + stubber.add_response('change_resource_record_sets', + {'ChangeInfo': { + 'Id': 'id', + 'Status': 'PENDING', + 'SubmittedAt': '2017-01-29T01:02:03Z', + }}, {'HostedZoneId': 'z42', 'ChangeBatch': ANY}) + + plan = provider.plan(self.expected) + + return provider, plan + + # _get_test_plan() returns a plan with 11 modifications, 17 RRs + + @patch('octodns.provider.route53.Route53Provider._really_apply') + def test_apply_1(self, really_apply_mock): + + # 17 RRs with max of 18 should only get applied in one call + provider, plan = self._get_test_plan(18) + provider.apply(plan) + really_apply_mock.assert_called_once() + + @patch('octodns.provider.route53.Route53Provider._really_apply') + def test_apply_2(self, really_apply_mock): + + # 17 RRs with max of 17 should only get applied in two calls + provider, plan = self._get_test_plan(17) + provider.apply(plan) + self.assertEquals(2, really_apply_mock.call_count) + + @patch('octodns.provider.route53.Route53Provider._really_apply') + def test_apply_3(self, really_apply_mock): + + # with a max of seven modifications, four calls + provider, plan = self._get_test_plan(7) + provider.apply(plan) + self.assertEquals(4, really_apply_mock.call_count) + + @patch('octodns.provider.route53.Route53Provider._really_apply') + def test_apply_4(self, really_apply_mock): + + # with a max of 11 modifications, two calls + provider, plan = self._get_test_plan(11) + provider.apply(plan) + self.assertEquals(2, really_apply_mock.call_count) + + @patch('octodns.provider.route53.Route53Provider._really_apply') + def test_apply_bad(self, really_apply_mock): + + # with a max of 1 modifications, fail + provider, plan = self._get_test_plan(1) + with self.assertRaises(Exception) as ctx: + provider.apply(plan) + self.assertTrue('modifications' in ctx.exception.message) diff --git a/tests/test_octodns_provider_yaml.py b/tests/test_octodns_provider_yaml.py new file mode 100644 index 0000000..a557bb3 --- /dev/null +++ b/tests/test_octodns_provider_yaml.py @@ -0,0 +1,111 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from os.path import dirname, isfile, join +from unittest import TestCase +from yaml import safe_load +from yaml.constructor import ConstructorError + +from octodns.record import Create +from octodns.provider.yaml import YamlProvider +from octodns.zone import SubzoneRecordException, Zone + +from helpers import TemporaryDirectory + + +class TestYamlProvider(TestCase): + + def test_provider(self): + source = YamlProvider('test', join(dirname(__file__), 'config')) + + zone = Zone('unit.tests.', []) + + # With target we don't add anything + source.populate(zone, target=source) + self.assertEquals(0, len(zone.records)) + + # without it we see everything + source.populate(zone) + self.assertEquals(14, len(zone.records)) + + # Assumption here is that a clean round-trip means that everything + # worked as expected, data that went in came back out and could be + # pulled in yet again and still match up. That assumes that the input + # data completely exercises things. This assumption can be tested by + # relatively well by running + # ./script/coverage tests/test_octodns_provider_yaml.py and + # looking at the coverage file + # ./htmlcov/octodns_provider_yaml_py.html + + with TemporaryDirectory() as td: + # Add some subdirs to make sure that it can create them + directory = join(td.dirname, 'sub', 'dir') + yaml_file = join(directory, 'unit.tests.yaml') + target = YamlProvider('test', directory) + + # We add everything + plan = target.plan(zone) + self.assertEquals(13, len(filter(lambda c: isinstance(c, Create), + plan.changes))) + self.assertFalse(isfile(yaml_file)) + + # Now actually do it + self.assertEquals(13, target.apply(plan)) + self.assertTrue(isfile(yaml_file)) + + # There should be no changes after the round trip + reloaded = Zone('unit.tests.', []) + target.populate(reloaded) + self.assertFalse(zone.changes(reloaded, target=source)) + + # A 2nd sync should still create everything + plan = target.plan(zone) + self.assertEquals(13, len(filter(lambda c: isinstance(c, Create), + plan.changes))) + + with open(yaml_file) as fh: + data = safe_load(fh.read()) + + # these are stored as plural 'values' + for r in data['']: + self.assertTrue('values' in r) + self.assertTrue('values' in data['mx']) + self.assertTrue('values' in data['naptr']) + self.assertTrue('values' in data['_srv._tcp']) + self.assertTrue('values' in data['txt']) + # these are stored as singular 'value' + self.assertTrue('value' in data['aaaa']) + self.assertTrue('value' in data['ptr']) + self.assertTrue('value' in data['spf']) + self.assertTrue('value' in data['www']) + + def test_empty(self): + source = YamlProvider('test', join(dirname(__file__), 'config')) + + zone = Zone('empty.', []) + + # without it we see everything + source.populate(zone) + self.assertEquals(0, len(zone.records)) + + def test_unsorted(self): + source = YamlProvider('test', join(dirname(__file__), 'config')) + + zone = Zone('unordered.', []) + + with self.assertRaises(ConstructorError): + source.populate(zone) + + def test_subzone_handling(self): + source = YamlProvider('test', join(dirname(__file__), 'config')) + + # If we add `sub` as a sub-zone we'll reject `www.sub` + zone = Zone('unit.tests.', ['sub']) + with self.assertRaises(SubzoneRecordException) as ctx: + source.populate(zone) + self.assertEquals('Record www.sub.unit.tests. is under a managed ' + 'subzone', ctx.exception.message) diff --git a/tests/test_octodns_record.py b/tests/test_octodns_record.py new file mode 100644 index 0000000..491b278 --- /dev/null +++ b/tests/test_octodns_record.py @@ -0,0 +1,765 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from octodns.record import ARecord, AaaaRecord, CnameRecord, Create, Delete, \ + GeoValue, MxRecord, NaptrRecord, NaptrValue, NsRecord, PtrRecord, Record, \ + SshfpRecord, SpfRecord, SrvRecord, TxtRecord, Update +from octodns.zone import Zone + +from helpers import GeoProvider, SimpleProvider + + +class TestRecord(TestCase): + zone = Zone('unit.tests.', []) + + def test_lowering(self): + record = ARecord(self.zone, 'MiXeDcAsE', { + 'ttl': 30, + 'type': 'A', + 'value': '1.2.3.4', + }) + self.assertEquals('mixedcase', record.name) + + def test_a_and_record(self): + a_values = ['1.2.3.4', '2.2.3.4'] + a_data = {'ttl': 30, 'values': a_values} + a = ARecord(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_values, a.values) + self.assertEquals(a_data, a.data) + + b_value = '3.2.3.4' + b_data = {'ttl': 30, 'value': b_value} + b = ARecord(self.zone, 'b', b_data) + self.assertEquals([b_value], b.values) + self.assertEquals(b_data, b.data) + + # missing ttl + with self.assertRaises(Exception) as ctx: + ARecord(self.zone, None, {'value': '1.1.1.1'}) + self.assertTrue('missing ttl' in ctx.exception.message) + # missing values & value + with self.assertRaises(Exception) as ctx: + ARecord(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value(s)' in ctx.exception.message) + + # top-level + data = {'ttl': 30, 'value': '4.2.3.4'} + self.assertEquals(self.zone.name, ARecord(self.zone, '', data).fqdn) + self.assertEquals(self.zone.name, ARecord(self.zone, None, data).fqdn) + + # ARecord equate with itself + self.assertTrue(a == a) + # Records with differing names and same type don't equate + self.assertFalse(a == b) + # Records with same name & type equate even if ttl is different + self.assertTrue(a == ARecord(self.zone, 'a', + {'ttl': 31, 'values': a_values})) + # Records with same name & type equate even if values are different + self.assertTrue(a == ARecord(self.zone, 'a', + {'ttl': 30, 'value': b_value})) + + target = SimpleProvider() + # no changes if self + self.assertFalse(a.changes(a, target)) + # no changes if clone + other = ARecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) + self.assertFalse(a.changes(other, target)) + # changes if ttl modified + other.ttl = 31 + update = a.changes(other, target) + self.assertEquals(a, update.existing) + self.assertEquals(other, update.new) + # changes if values modified + other.ttl = a.ttl + other.values = ['4.4.4.4'] + update = a.changes(other, target) + self.assertEquals(a, update.existing) + self.assertEquals(other, update.new) + + # Hashing + records = set() + records.add(a) + self.assertTrue(a in records) + self.assertFalse(b in records) + records.add(b) + self.assertTrue(b in records) + + # __repr__ doesn't blow up + a.__repr__() + # Record.__repr__ does + with self.assertRaises(NotImplementedError): + class DummyRecord(Record): + + def __init__(self): + pass + + DummyRecord().__repr__() + + def test_invalid_a(self): + with self.assertRaises(Exception) as ctx: + ARecord(self.zone, 'a', { + 'ttl': 30, + 'value': 'foo', + }) + self.assertTrue('Invalid record' in ctx.exception.message) + with self.assertRaises(Exception) as ctx: + ARecord(self.zone, 'a', { + 'ttl': 30, + 'values': ['1.2.3.4', 'bar'], + }) + self.assertTrue('Invalid record' in ctx.exception.message) + + def test_geo(self): + geo_data = {'ttl': 42, 'values': ['5.2.3.4', '6.2.3.4'], + 'geo': {'AF': ['1.1.1.1'], + 'AS-JP': ['2.2.2.2', '3.3.3.3'], + 'NA-US': ['4.4.4.4', '5.5.5.5'], + 'NA-US-CA': ['6.6.6.6', '7.7.7.7']}} + geo = ARecord(self.zone, 'geo', geo_data) + self.assertEquals(geo_data, geo.data) + + other_data = {'ttl': 42, 'values': ['5.2.3.4', '6.2.3.4'], + 'geo': {'AF': ['1.1.1.1'], + 'AS-JP': ['2.2.2.2', '3.3.3.3'], + 'NA-US': ['4.4.4.4', '5.5.5.5'], + 'NA-US-CA': ['6.6.6.6', '7.7.7.7']}} + other = ARecord(self.zone, 'geo', other_data) + self.assertEquals(other_data, other.data) + + simple_target = SimpleProvider() + geo_target = GeoProvider() + + # Geo provider doesn't consider identical geo to be changes + self.assertFalse(geo.changes(geo, geo_target)) + + # geo values don't impact equality + other.geo['AF'].values = ['9.9.9.9'] + self.assertTrue(geo == other) + # Non-geo supporting provider doesn't consider geo diffs to be changes + self.assertFalse(geo.changes(other, simple_target)) + # Geo provider does consider geo diffs to be changes + self.assertTrue(geo.changes(other, geo_target)) + + # Object without geo doesn't impact equality + other.geo = {} + self.assertTrue(geo == other) + # Non-geo supporting provider doesn't consider lack of geo a diff + self.assertFalse(geo.changes(other, simple_target)) + # Geo provider does consider lack of geo diffs to be changes + self.assertTrue(geo.changes(other, geo_target)) + + # invalid geo code + with self.assertRaises(Exception) as ctx: + ARecord(self.zone, 'geo', {'ttl': 42, + 'values': ['5.2.3.4', '6.2.3.4'], + 'geo': {'abc': ['1.1.1.1']}}) + self.assertEquals('Invalid geo "abc"', ctx.exception.message) + + with self.assertRaises(Exception) as ctx: + ARecord(self.zone, 'geo', {'ttl': 42, + 'values': ['5.2.3.4', '6.2.3.4'], + 'geo': {'NA-US': ['1.1.1']}}) + self.assertTrue('not a valid ip' in ctx.exception.message) + + # __repr__ doesn't blow up + geo.__repr__() + + def assertMultipleValues(self, _type, a_values, b_value): + a_data = {'ttl': 30, 'values': a_values} + a = _type(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_values, a.values) + self.assertEquals(a_data, a.data) + + b_data = {'ttl': 30, 'value': b_value} + b = _type(self.zone, 'b', b_data) + self.assertEquals([b_value], b.values) + self.assertEquals(b_data, b.data) + + # missing values & value + with self.assertRaises(Exception) as ctx: + _type(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value(s)' in ctx.exception.message) + + def test_aaaa(self): + a_values = ['2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b', + '2001:0db8:3c4d:0015:0000:0000:1a2f:1a3b'] + b_value = '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b' + self.assertMultipleValues(AaaaRecord, a_values, b_value) + + with self.assertRaises(Exception) as ctx: + AaaaRecord(self.zone, 'a', { + 'ttl': 30, + 'value': 'foo', + }) + self.assertTrue('Invalid record' in ctx.exception.message) + with self.assertRaises(Exception) as ctx: + AaaaRecord(self.zone, 'a', { + 'ttl': 30, + 'values': [b_value, 'bar'], + }) + self.assertTrue('Invalid record' in ctx.exception.message) + + def assertSingleValue(self, _type, a_value, b_value): + a_data = {'ttl': 30, 'value': a_value} + a = _type(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_value, a.value) + self.assertEquals(a_data, a.data) + + b_data = {'ttl': 30, 'value': b_value} + b = _type(self.zone, 'b', b_data) + self.assertEquals(b_value, b.value) + self.assertEquals(b_data, b.data) + + # missing value + with self.assertRaises(Exception) as ctx: + _type(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value' in ctx.exception.message) + + target = SimpleProvider() + # No changes with self + self.assertFalse(a.changes(a, target)) + # Diff in value causes change + other = _type(self.zone, 'a', {'ttl': 30, 'value': b_value}) + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + + # __repr__ doesn't blow up + a.__repr__() + + def test_cname(self): + self.assertSingleValue(CnameRecord, 'target.foo.com.', + 'other.foo.com.') + + with self.assertRaises(Exception) as ctx: + CnameRecord(self.zone, 'a', { + 'ttl': 30, + 'value': 'foo', + }) + self.assertTrue('Invalid record' in ctx.exception.message) + with self.assertRaises(Exception) as ctx: + CnameRecord(self.zone, 'a', { + 'ttl': 30, + 'values': ['foo.com.', 'bar.com'], + }) + self.assertTrue('Invalid record' in ctx.exception.message) + + def test_mx(self): + a_values = [{ + 'priority': 10, + 'value': 'smtp1' + }, { + 'priority': 20, + 'value': 'smtp2' + }] + a_data = {'ttl': 30, 'values': a_values} + a = MxRecord(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_values[0]['priority'], a.values[0].priority) + self.assertEquals(a_values[0]['value'], a.values[0].value) + self.assertEquals(a_values[1]['priority'], a.values[1].priority) + self.assertEquals(a_values[1]['value'], a.values[1].value) + self.assertEquals(a_data, a.data) + + b_value = { + 'priority': 12, + 'value': 'smtp3', + } + b_data = {'ttl': 30, 'value': b_value} + b = MxRecord(self.zone, 'b', b_data) + self.assertEquals(b_value['priority'], b.values[0].priority) + self.assertEquals(b_value['value'], b.values[0].value) + self.assertEquals(b_data, b.data) + + # missing value + with self.assertRaises(Exception) as ctx: + MxRecord(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value(s)' in ctx.exception.message) + # invalid value + with self.assertRaises(Exception) as ctx: + MxRecord(self.zone, None, {'ttl': 42, 'value': {}}) + self.assertTrue('Invalid value' in ctx.exception.message) + + target = SimpleProvider() + # No changes with self + self.assertFalse(a.changes(a, target)) + # Diff in priority causes change + other = MxRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) + other.values[0].priority = 22 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in value causes change + other.values[0].priority = a.values[0].priority + other.values[0].value = 'smtpX' + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + + # __repr__ doesn't blow up + a.__repr__() + + def test_naptr(self): + a_values = [{ + 'order': 10, + 'preference': 11, + 'flags': 'X', + 'service': 'Y', + 'regexp': 'Z', + 'replacement': '.', + }, { + 'order': 20, + 'preference': 21, + 'flags': 'A', + 'service': 'B', + 'regexp': 'C', + 'replacement': 'foo.com', + }] + a_data = {'ttl': 30, 'values': a_values} + a = NaptrRecord(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + for i in (0, 1): + for k in a_values[0].keys(): + self.assertEquals(a_values[i][k], getattr(a.values[i], k)) + self.assertEquals(a_data, a.data) + + b_value = { + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + } + b_data = {'ttl': 30, 'value': b_value} + b = NaptrRecord(self.zone, 'b', b_data) + for k in a_values[0].keys(): + self.assertEquals(b_value[k], getattr(b.values[0], k)) + self.assertEquals(b_data, b.data) + + # missing value + with self.assertRaises(Exception) as ctx: + NaptrRecord(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value' in ctx.exception.message) + # invalid value + with self.assertRaises(Exception) as ctx: + NaptrRecord(self.zone, None, {'ttl': 42, 'value': {}}) + self.assertTrue('Invalid value' in ctx.exception.message) + + target = SimpleProvider() + # No changes with self + self.assertFalse(a.changes(a, target)) + # Diff in priority causes change + other = NaptrRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) + other.values[0].order = 22 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in replacement causes change + other.values[0].order = a.values[0].order + other.values[0].replacement = 'smtpX' + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + + # full sorting + # equivilent + b_naptr_value = b.values[0] + self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value)) + # by order + self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 10, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + }))) + self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 40, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + }))) + # by preference + self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 10, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + }))) + self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 40, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + }))) + # by flags + self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'A', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + }))) + self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'Z', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'x', + }))) + # by service + self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'A', + 'regexp': 'O', + 'replacement': 'x', + }))) + self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'Z', + 'regexp': 'O', + 'replacement': 'x', + }))) + # by regexp + self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'A', + 'replacement': 'x', + }))) + self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'Z', + 'replacement': 'x', + }))) + # by replacement + self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'a', + }))) + self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({ + 'order': 30, + 'preference': 31, + 'flags': 'M', + 'service': 'N', + 'regexp': 'O', + 'replacement': 'z', + }))) + + # __repr__ doesn't blow up + a.__repr__() + + def test_ns(self): + a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.'] + a_data = {'ttl': 30, 'values': a_values} + a = NsRecord(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_values, a.values) + self.assertEquals(a_data, a.data) + + b_value = '9.8.7.6.' + b_data = {'ttl': 30, 'value': b_value} + b = NsRecord(self.zone, 'b', b_data) + self.assertEquals([b_value], b.values) + self.assertEquals(b_data, b.data) + + # missing values & value + with self.assertRaises(Exception) as ctx: + NsRecord(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value(s)' in ctx.exception.message) + + with self.assertRaises(Exception) as ctx: + NsRecord(self.zone, 'a', { + 'ttl': 30, + 'value': 'foo', + }) + self.assertTrue('Invalid record' in ctx.exception.message) + with self.assertRaises(Exception) as ctx: + NsRecord(self.zone, 'a', { + 'ttl': 30, + 'values': ['foo.com.', 'bar.com'], + }) + self.assertTrue('Invalid record' in ctx.exception.message) + + def test_ptr(self): + self.assertSingleValue(PtrRecord, 'foo.bar.com.', 'other.bar.com.') + with self.assertRaises(Exception) as ctx: + PtrRecord(self.zone, 'a', { + 'ttl': 30, + 'value': 'foo', + }) + self.assertTrue('Invalid record' in ctx.exception.message) + + def test_sshfp(self): + a_values = [{ + 'algorithm': 10, + 'fingerprint_type': 11, + 'fingerprint': 'abc123', + }, { + 'algorithm': 20, + 'fingerprint_type': 21, + 'fingerprint': 'def456', + }] + a_data = {'ttl': 30, 'values': a_values} + a = SshfpRecord(self.zone, 'a', a_data) + self.assertEquals('a', a.name) + self.assertEquals('a.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_values[0]['algorithm'], a.values[0].algorithm) + self.assertEquals(a_values[0]['fingerprint_type'], + a.values[0].fingerprint_type) + self.assertEquals(a_values[0]['fingerprint'], a.values[0].fingerprint) + self.assertEquals(a_data, a.data) + + b_value = { + 'algorithm': 30, + 'fingerprint_type': 31, + 'fingerprint': 'ghi789', + } + b_data = {'ttl': 30, 'value': b_value} + b = SshfpRecord(self.zone, 'b', b_data) + self.assertEquals(b_value['algorithm'], b.values[0].algorithm) + self.assertEquals(b_value['fingerprint_type'], + b.values[0].fingerprint_type) + self.assertEquals(b_value['fingerprint'], b.values[0].fingerprint) + self.assertEquals(b_data, b.data) + + # missing value + with self.assertRaises(Exception) as ctx: + SshfpRecord(self.zone, None, {'ttl': 42}) + self.assertTrue('missing value(s)' in ctx.exception.message) + # invalid value + with self.assertRaises(Exception) as ctx: + SshfpRecord(self.zone, None, {'ttl': 42, 'value': {}}) + self.assertTrue('Invalid value' in ctx.exception.message) + + target = SimpleProvider() + # No changes with self + self.assertFalse(a.changes(a, target)) + # Diff in algorithm causes change + other = SshfpRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) + other.values[0].algorithm = 22 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in fingerprint_type causes change + other = SshfpRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) + other.values[0].algorithm = a.values[0].algorithm + other.values[0].fingerprint_type = 22 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in fingerprint causes change + other = SshfpRecord(self.zone, 'a', {'ttl': 30, 'values': a_values}) + other.values[0].fingerprint_type = a.values[0].fingerprint_type + other.values[0].fingerprint = 22 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + + # __repr__ doesn't blow up + a.__repr__() + + def test_spf(self): + a_values = ['spf1 -all', 'spf1 -hrm'] + b_value = 'spf1 -other' + self.assertMultipleValues(SpfRecord, a_values, b_value) + + def test_srv(self): + a_values = [{ + 'priority': 10, + 'weight': 11, + 'port': 12, + 'target': 'server1', + }, { + 'priority': 20, + 'weight': 21, + 'port': 22, + 'target': 'server2', + }] + a_data = {'ttl': 30, 'values': a_values} + a = SrvRecord(self.zone, '_a._tcp', a_data) + self.assertEquals('_a._tcp', a.name) + self.assertEquals('_a._tcp.unit.tests.', a.fqdn) + self.assertEquals(30, a.ttl) + self.assertEquals(a_values[0]['priority'], a.values[0].priority) + self.assertEquals(a_values[0]['weight'], a.values[0].weight) + self.assertEquals(a_values[0]['port'], a.values[0].port) + self.assertEquals(a_values[0]['target'], a.values[0].target) + self.assertEquals(a_data, a.data) + + b_value = { + 'priority': 30, + 'weight': 31, + 'port': 32, + 'target': 'server3', + } + b_data = {'ttl': 30, 'value': b_value} + b = SrvRecord(self.zone, '_b._tcp', b_data) + self.assertEquals(b_value['priority'], b.values[0].priority) + self.assertEquals(b_value['weight'], b.values[0].weight) + self.assertEquals(b_value['port'], b.values[0].port) + self.assertEquals(b_value['target'], b.values[0].target) + self.assertEquals(b_data, b.data) + + # invalid name + with self.assertRaises(Exception) as ctx: + SrvRecord(self.zone, 'bad', {'ttl': 42}) + self.assertEquals('Invalid name bad.unit.tests.', + ctx.exception.message) + + # missing value + with self.assertRaises(Exception) as ctx: + SrvRecord(self.zone, '_missing._tcp', {'ttl': 42}) + self.assertTrue('missing value(s)' in ctx.exception.message) + # invalid value + with self.assertRaises(Exception) as ctx: + SrvRecord(self.zone, '_missing._udp', {'ttl': 42, 'value': {}}) + self.assertTrue('Invalid value' in ctx.exception.message) + + target = SimpleProvider() + # No changes with self + self.assertFalse(a.changes(a, target)) + # Diff in priority causes change + other = SrvRecord(self.zone, '_a._icmp', + {'ttl': 30, 'values': a_values}) + other.values[0].priority = 22 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in weight causes change + other.values[0].priority = a.values[0].priority + other.values[0].weight = 33 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in port causes change + other.values[0].weight = a.values[0].weight + other.values[0].port = 44 + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + # Diff in target causes change + other.values[0].port = a.values[0].port + other.values[0].target = 'serverX' + change = a.changes(other, target) + self.assertEqual(change.existing, a) + self.assertEqual(change.new, other) + + # __repr__ doesn't blow up + a.__repr__() + + def test_txt(self): + a_values = ['a one', 'a two'] + b_value = 'b other' + self.assertMultipleValues(TxtRecord, a_values, b_value) + + Record.new(self.zone, 'txt', { + 'ttl': 44, + 'type': 'TXT', + 'value': 'escaped\; foo', + }) + + with self.assertRaises(Exception) as ctx: + Record.new(self.zone, 'txt', { + 'ttl': 44, + 'type': 'TXT', + 'value': 'un-escaped; foo', + }) + self.assertEquals('Invalid record txt.unit.tests., unescaped ;', + ctx.exception.message) + + def test_record_new(self): + txt = Record.new(self.zone, 'txt', { + 'ttl': 44, + 'type': 'TXT', + 'value': 'some text', + }) + self.assertIsInstance(txt, TxtRecord) + self.assertEquals('TXT', txt._type) + self.assertEquals(['some text'], txt.values) + + # Missing type + with self.assertRaises(Exception) as ctx: + Record.new(self.zone, 'unknown', {}) + self.assertTrue('missing type' in ctx.exception.message) + + # Unkown type + with self.assertRaises(Exception) as ctx: + Record.new(self.zone, 'unknown', { + 'type': 'XXX', + }) + self.assertTrue('Unknown record type' in ctx.exception.message) + + def test_change(self): + existing = Record.new(self.zone, 'txt', { + 'ttl': 44, + 'type': 'TXT', + 'value': 'some text', + }) + new = Record.new(self.zone, 'txt', { + 'ttl': 44, + 'type': 'TXT', + 'value': 'some change', + }) + create = Create(new) + self.assertEquals(new.values, create.record.values) + update = Update(existing, new) + self.assertEquals(new.values, update.record.values) + delete = Delete(existing) + self.assertEquals(existing.values, delete.record.values) + + def test_geo_value(self): + code = 'NA-US-CA' + values = ['1.2.3.4'] + geo = GeoValue(code, values) + self.assertEquals(code, geo.code) + self.assertEquals('NA', geo.continent_code) + self.assertEquals('US', geo.country_code) + self.assertEquals('CA', geo.subdivision_code) + self.assertEquals(values, geo.values) + self.assertEquals(['NA-US', 'NA'], list(geo.parents)) diff --git a/tests/test_octodns_source_tinydns.py b/tests/test_octodns_source_tinydns.py new file mode 100644 index 0000000..b4cea06 --- /dev/null +++ b/tests/test_octodns_source_tinydns.py @@ -0,0 +1,176 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from octodns.record import Record +from octodns.source.tinydns import TinyDnsFileSource +from octodns.zone import Zone + +from helpers import SimpleProvider + + +class TestTinyDnsFileSource(TestCase): + source = TinyDnsFileSource('test', './tests/zones') + + def test_populate_normal(self): + got = Zone('example.com.', []) + self.source.populate(got) + self.assertEquals(11, len(got.records)) + + expected = Zone('example.com.', []) + for name, data in ( + ('', { + 'type': 'A', + 'ttl': 30, + 'values': ['10.2.3.4', '10.2.3.5'], + }), + ('sub', { + 'type': 'NS', + 'ttl': 30, + 'values': ['ns1.ns.com.', 'ns2.ns.com.'], + }), + ('www', { + 'type': 'A', + 'ttl': 3600, + 'value': '10.2.3.6', + }), + ('cname', { + 'type': 'CNAME', + 'ttl': 3600, + 'value': 'www.example.com.', + }), + ('some-host-abc123', { + 'type': 'A', + 'ttl': 1800, + 'value': '10.2.3.7', + }), + ('has-dup-def123', { + 'type': 'A', + 'ttl': 3600, + 'value': '10.2.3.8', + }), + ('www.sub', { + 'type': 'A', + 'ttl': 3600, + 'value': '1.2.3.4', + }), + ('has-dup-def456', { + 'type': 'A', + 'ttl': 3600, + 'value': '10.2.3.8', + }), + ('', { + 'type': 'MX', + 'ttl': 3600, + 'values': [{ + 'priority': 10, + 'value': 'smtp-1-host.example.com.', + }, { + 'priority': 20, + 'value': 'smtp-2-host.example.com.', + }] + }), + ('smtp', { + 'type': 'MX', + 'ttl': 1800, + 'values': [{ + 'priority': 30, + 'value': 'smtp-1-host.example.com.', + }, { + 'priority': 40, + 'value': 'smtp-2-host.example.com.', + }] + }), + ): + record = Record.new(expected, name, data) + expected.add_record(record) + + changes = expected.changes(got, SimpleProvider()) + self.assertEquals([], changes) + + def test_populate_normal_sub1(self): + got = Zone('asdf.subtest.com.', []) + self.source.populate(got) + self.assertEquals(1, len(got.records)) + + expected = Zone('asdf.subtest.com.', []) + for name, data in ( + ('a3', { + 'type': 'A', + 'ttl': 3600, + 'values': ['10.2.3.7'], + }), + ): + record = Record.new(expected, name, data) + expected.add_record(record) + + changes = expected.changes(got, SimpleProvider()) + self.assertEquals([], changes) + + def test_populate_normal_sub2(self): + got = Zone('blah-asdf.subtest.com.', []) + self.source.populate(got) + self.assertEquals(2, len(got.records)) + + expected = Zone('sub-asdf.subtest.com.', []) + for name, data in ( + ('a1', { + 'type': 'A', + 'ttl': 3600, + 'values': ['10.2.3.5'], + }), + ('a2', { + 'type': 'A', + 'ttl': 3600, + 'values': ['10.2.3.6'], + }), + ): + record = Record.new(expected, name, data) + expected.add_record(record) + + changes = expected.changes(got, SimpleProvider()) + self.assertEquals([], changes) + + def test_populate_in_addr_arpa(self): + + got = Zone('3.2.10.in-addr.arpa.', []) + self.source.populate(got) + + expected = Zone('3.2.10.in-addr.arpa.', []) + for name, data in ( + ('10', { + 'type': 'PTR', + 'ttl': 3600, + 'value': 'a-ptr.example.com.' + }), + ('11', { + 'type': 'PTR', + 'ttl': 30, + 'value': 'a-ptr-2.example.com.' + }), + ('8', { + 'type': 'PTR', + 'ttl': 3600, + 'value': 'has-dup-def123.example.com.' + }), + ('7', { + 'type': 'PTR', + 'ttl': 1800, + 'value': 'some-host-abc123.example.com.' + }), + ): + record = Record.new(expected, name, data) + expected.add_record(record) + + changes = expected.changes(got, SimpleProvider()) + self.assertEquals([], changes) + + def test_ignores_subs(self): + got = Zone('example.com.', ['sub']) + self.source.populate(got) + self.assertEquals(10, len(got.records)) diff --git a/tests/test_octodns_yaml.py b/tests/test_octodns_yaml.py new file mode 100644 index 0000000..9c3cec5 --- /dev/null +++ b/tests/test_octodns_yaml.py @@ -0,0 +1,61 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from StringIO import StringIO +from unittest import TestCase +from yaml.constructor import ConstructorError + +from octodns.yaml import safe_dump, safe_load + + +class TestYaml(TestCase): + + def test_stuff(self): + self.assertEquals({ + 1: 'a', + 2: 'b', + '3': 'c', + 10: 'd', + '11': 'e', + }, safe_load(''' +1: a +2: b +'3': c +10: d +'11': e +''')) + + self.assertEquals({ + '*.1.2': 'a', + '*.2.2': 'b', + '*.10.1': 'c', + '*.11.2': 'd', + }, safe_load(''' +'*.1.2': 'a' +'*.2.2': 'b' +'*.10.1': 'c' +'*.11.2': 'd' +''')) + + with self.assertRaises(ConstructorError) as ctx: + safe_load(''' +'*.2.2': 'b' +'*.1.2': 'a' +'*.11.2': 'd' +'*.10.1': 'c' +''') + self.assertEquals('keys out of order: *.2.2, *.1.2, *.11.2, *.10.1', + ctx.exception.problem) + + buf = StringIO() + safe_dump({ + '*.1.1': 42, + '*.11.1': 43, + '*.2.1': 44, + }, buf) + self.assertEquals("---\n'*.1.1': 42\n'*.2.1': 44\n'*.11.1': 43\n", + buf.getvalue()) diff --git a/tests/test_octodns_zone.py b/tests/test_octodns_zone.py new file mode 100644 index 0000000..da83dfc --- /dev/null +++ b/tests/test_octodns_zone.py @@ -0,0 +1,174 @@ +# +# +# + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from octodns.record import ARecord, AaaaRecord, Create, Delete, Record, Update +from octodns.zone import DuplicateRecordException, SubzoneRecordException, Zone + +from helpers import SimpleProvider + + +class TestZone(TestCase): + + def test_lowering(self): + zone = Zone('UniT.TEsTs.', []) + self.assertEquals('unit.tests.', zone.name) + + def test_hostname_from_fqdn(self): + zone = Zone('unit.tests.', []) + for hostname, fqdn in ( + ('', 'unit.tests.'), + ('', 'unit.tests'), + ('foo', 'foo.unit.tests.'), + ('foo', 'foo.unit.tests'), + ('foo.bar', 'foo.bar.unit.tests.'), + ('foo.bar', 'foo.bar.unit.tests'), + ('foo.unit.tests', 'foo.unit.tests.unit.tests.'), + ('foo.unit.tests', 'foo.unit.tests.unit.tests'), + ): + self.assertEquals(hostname, zone.hostname_from_fqdn(fqdn)) + + def test_add_record(self): + zone = Zone('unit.tests.', []) + + a = ARecord(zone, 'a', {'ttl': 42, 'value': '1.1.1.1'}) + b = ARecord(zone, 'b', {'ttl': 42, 'value': '1.1.1.1'}) + + zone.add_record(a) + self.assertEquals(zone.records, set([a])) + # Can't add record with same name & type + with self.assertRaises(DuplicateRecordException) as ctx: + zone.add_record(a) + self.assertEquals('Duplicate record a.unit.tests., type A', + ctx.exception.message) + self.assertEquals(zone.records, set([a])) + # Can add dup name, with different type + zone.add_record(b) + self.assertEquals(zone.records, set([a, b])) + + def test_changes(self): + before = Zone('unit.tests.', []) + a = ARecord(before, 'a', {'ttl': 42, 'value': '1.1.1.1'}) + before.add_record(a) + b = AaaaRecord(before, 'b', {'ttl': 42, 'value': '1:1:1::1'}) + before.add_record(b) + + after = Zone('unit.tests.', []) + after.add_record(a) + after.add_record(b) + + target = SimpleProvider() + + # before == after -> no changes + self.assertFalse(before.changes(after, target)) + + # add a record, delete a record -> [Delete, Create] + c = ARecord(before, 'c', {'ttl': 42, 'value': '1.1.1.1'}) + after.add_record(c) + after.records.remove(b) + self.assertEquals(after.records, set([a, c])) + changes = before.changes(after, target) + self.assertEquals(2, len(changes)) + for change in changes: + if isinstance(change, Create): + create = change + elif isinstance(change, Delete): + delete = change + self.assertEquals(b, delete.existing) + self.assertFalse(delete.new) + self.assertEquals(c, create.new) + self.assertFalse(create.existing) + delete.__repr__() + create.__repr__() + + after = Zone('unit.tests.', []) + changed = ARecord(before, 'a', {'ttl': 42, 'value': '2.2.2.2'}) + after.add_record(changed) + after.add_record(b) + changes = before.changes(after, target) + self.assertEquals(1, len(changes)) + update = changes[0] + self.assertIsInstance(update, Update) + # Using changes here to get a full equality + self.assertFalse(a.changes(update.existing, target)) + self.assertFalse(changed.changes(update.new, target)) + update.__repr__() + + def test_unsupporting(self): + + class NoAaaaProvider(object): + id = 'no-aaaa' + SUPPORTS_GEO = False + + def supports(self, record): + return record._type != 'AAAA' + + current = Zone('unit.tests.', []) + + desired = Zone('unit.tests.', []) + a = ARecord(desired, 'a', {'ttl': 42, 'value': '1.1.1.1'}) + desired.add_record(a) + aaaa = AaaaRecord(desired, 'b', {'ttl': 42, 'value': '1:1:1::1'}) + desired.add_record(aaaa) + + # Only create the supported A, not the AAAA + changes = current.changes(desired, NoAaaaProvider()) + self.assertEquals(1, len(changes)) + self.assertIsInstance(changes[0], Create) + + # Only delete the supported A, not the AAAA + changes = desired.changes(current, NoAaaaProvider()) + self.assertEquals(1, len(changes)) + self.assertIsInstance(changes[0], Delete) + + def test_missing_dot(self): + with self.assertRaises(Exception) as ctx: + Zone('not.allowed', []) + self.assertTrue('missing ending dot' in ctx.exception.message) + + def test_sub_zones(self): + zone = Zone('unit.tests.', set(['sub', 'barred'])) + + # NS for exactly the sub is allowed + record = Record.new(zone, 'sub', { + 'ttl': 3600, + 'type': 'NS', + 'values': ['1.2.3.4.', '2.3.4.5.'], + }) + zone.add_record(record) + self.assertEquals(set([record]), zone.records) + + # non-NS for exactly the sub is rejected + record = Record.new(zone, 'sub', { + 'ttl': 3600, + 'type': 'A', + 'values': ['1.2.3.4', '2.3.4.5'], + }) + with self.assertRaises(SubzoneRecordException) as ctx: + zone.add_record(record) + self.assertTrue('not of type NS', ctx.exception.message) + + # NS for something below the sub is rejected + record = Record.new(zone, 'foo.sub', { + 'ttl': 3600, + 'type': 'NS', + 'values': ['1.2.3.4.', '2.3.4.5.'], + }) + with self.assertRaises(SubzoneRecordException) as ctx: + zone.add_record(record) + self.assertTrue('under a managed sub-zone', ctx.exception.message) + + # A for something below the sub is rejected + record = Record.new(zone, 'foo.bar.sub', { + 'ttl': 3600, + 'type': 'A', + 'values': ['1.2.3.4', '2.3.4.5'], + }) + with self.assertRaises(SubzoneRecordException) as ctx: + zone.add_record(record) + self.assertTrue('under a managed sub-zone', ctx.exception.message) diff --git a/tests/zones/.is-needed-for-tests b/tests/zones/.is-needed-for-tests new file mode 100644 index 0000000000000000000000000000000000000000..ca6ff49f6d4ce15cdac013fa86c5c940e91a1ec7 GIT binary patch literal 1024 zcmV+b1poWne(=(^(v2{1U$=b;gNCrIX`-0=mtn84E;c2_PXkj{-u{EP<^YXMi!8xR zL@5UQyCCBZ%q<*CXKR8QYdu+4TAKA#5JqY}2V&IluIa!!zI(cD0jAI60T{-ZZMvon ziZr|BBLJ;>>I24zTO!wHft1YfR0~xL6E{o2h(frUyc1ZkN_wcPKz&BfwQ)xMuUS91 zVK;6$#WV~HnlY4dOtD7r*OtknC*}F6PxRe4*3SF#5yuG}&guPZx6tr?vMS$Dcibe2 zlfCkR<8F44UB1VHIw|cC_y#pzljp2FJMf~d-ewg?Xw*}QwrFF^4&yXgjuazL;l?bg z23XLCe^V}Sv7z(*gTy-ST8$A%e(1vDMu06Y2}DdJg_7zZAP9PJXJ9!)eL2JlQdau> z8Bbu7&4M4GK0;x2CPoULB72&9ScRNy8SEKg3<6}I@54Xc-HXaL@7pm%QJuphJ4)Bj zb%7L|y_-#=Zp#^CaTP1RDMS2Mxe1a5%i(a0)(-#yIrf?H^~(p9+Nro`m=G}q`xy}6 zZv2IGIiUW6dj?+!=~|epm*yU5I2pkHt>AtV{$;Pj*6Z7~;_N6Tx*-f6Da0^yFi4ML z!`@&zuwarra~I>B$G5uX3s52cqna(KBVCNS)|in&&hiAq`l*Qs{>N4S{43g z3vj=xmPK7!e5Qy>9}5_N0fSk#(jf+XE6li?{>or)QUuQCej8e*d$!Sn2CRmXh%@xa z15`fwncID5I)`l+Y{llyBsAy=>b1vTmemkPUCv Date: Wed, 15 Mar 2017 15:47:46 -0700 Subject: [PATCH 2/6] Fix broken .github/CONTRIBUTING.md --- .github/CONTRIBUTING.md | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e69de29..bf128ef 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing + +Hi there! We're thrilled that you'd like to contribute to OctoDNS. Your help is essential for keeping it great. + +Please note that this project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#GitHub%20OctoDNS/opensource@github.com). By participating in this project you agree to abide by its terms. + +If you have questions, or you'd like to check with us before embarking on a major development effort, please [open an issue](https://github.com/github/octodns/issues/new). + +## How to contribute + +This project uses the [GitHub Flow](https://guides.github.com/introduction/flow/). That means that the `master` branch is stable and new development is done in feature branches. Feature branches are merged into the `master` branch via a Pull Request. + +0. Fork and clone the repository +0. Configure and install the dependencies: `script/bootstrap` +0. Make sure the tests pass on your machine: `script/test` +0. Create a new branch: `git checkout -b my-branch-name` +0. Make your change, add tests, and make sure the tests still pass +0. Make sure that `./script/lint` passes without any warnings +0. Make sure that coverage is at :100:% `script/coverage` and open `htmlcov/index.html` + * You can open PRs for :eyes: & discussion prior to this +0. Push to your fork and submit a pull request + +We will handle updating the version, tagging the release, and releasing the gem. Please don't bump the version or otherwise attempt to take on these administrative internal tasks as part of your pull request. + +Here are a few things you can do that will increase the likelihood of your pull request being accepted: + +* Follow [pep8](https://www.python.org/dev/peps/pep-0008/) + +- Write thorough tests. No PRs will be merged without :100:% code coverage. More than that tests should be very thorough and cover as many (edge) cases as possible. We're working with DNS here and bugs can have a major impact so we need to do as much as reasonably possible to ensure quality. While :100:% doesn't even begin to mean there are no bugs, getting there often requires close inspection & a relatively complete understanding of the code. More times than no the endevor will uncover at least minor problems. + +- Bug fixes require specific tests covering the addressed behavior. + +- Write or update documentation. If you have added a feature or changed an existing one, please make appropriate changes to the docs. Doc-only PRs are always welcome. + +- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. + +- We target Python 2.7, but have taken steps to make Python 3 support as easy as possible when someone decides it's needed. PR welcome. + +- Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). + +## License note + +We can only accept contributions that are compatible with the MIT license. + +It's OK to depend on gems licensed under either Apache 2.0 or MIT, but we cannot add dependencies on any gems that are licensed under GPL. + +Any contributions you make must be under the MIT license. + +## Resources + +- [Contributing to Open Source on GitHub](https://guides.github.com/activities/contributing-to-open-source/) +- [Using Pull Requests](https://help.github.com/articles/using-pull-requests/) +- [GitHub Help](https://help.github.com) From f18365f1f08eb12c6fc88a4bdbbc662af09d3684 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 15 Mar 2017 15:52:27 -0700 Subject: [PATCH 3/6] Add joewilliams to Authors section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 30c70c4..05ab741 100644 --- a/README.md +++ b/README.md @@ -216,4 +216,4 @@ OctoDNS is licensed under the [MIT license](LICENSE). ## Authors -OctoDNS was designed and authored by [Ross McFarland](https://github.com/ross) and is now maintained, reviewed, and tested by Ross and the rest of the Site Reliability Engineering team at GitHub. +OctoDNS was designed and authored by [Ross McFarland](https://github.com/ross) and [Joe Williams](https://github.com/joewilliams). It is now maintained, reviewed, and tested by Ross, Joe, and the rest of the Site Reliability Engineering team at GitHub. From e66e168554ff1d95dc35dec25cd8509b31c31cc2 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Wed, 15 Mar 2017 15:54:57 -0700 Subject: [PATCH 4/6] Untested .travis.yml --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b17ca01 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - 2.7 +script: ./script/cibuild +notifications: + email: + - ross@github.com From 6607271af61ffd8bde1b638af4a5d09322aa29ad Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 16 Mar 2017 08:19:56 -0700 Subject: [PATCH 5/6] Address README review feedback --- README.md | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 05ab741..1e16e13 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ -# OctoDns +OctoDNS# OctoDNS ## DNS as code - Tools for managing DNS across multiple providers In the vein of [infrastructure as code](https://en.wikipedia.org/wiki/Infrastructure_as_Code) OctoDNS provides a set of tools & patterns that make it easy to manage your DNS records across multiple providers. The resulting config can live in a repository and be [deployed](https://github.com/blog/1241-deploying-at-github) just like the rest of your code, maintaining a clear history and using your existing review & workflow. -The architecture is pluggable and tooling flexible intending to be applicable to a wide variety of use-cases. Effort has been made to make adding new providers as easy as possible. In the simple case that involves writing of a single `class` and a couple hundred lines of code, most of which is translating between the provider's schema and OctoDNS's. More on some of the ways we use it and how to go about extending it below and in the [/docs directory](/docs). +The architecture is pluggable and the tooling is flexible to make it applicable to a wide variety of use-cases. Effort has been made to make adding new providers as easy as possible. In the simple case that involves writing of a single `class` and a couple hundred lines of code, most of which is translating between the provider's schema and OctoDNS's. More on some of the ways we use it and how to go about extending it below and in the [/docs directory](/docs). It is similar to [Netflix/denominator](https://github.com/Netflix/denominator). @@ -27,7 +27,7 @@ $ mkdir config ### Config -So first we need to create the primary config file to tell OctoDns about our providers and the zone(s) we want it to manage. Below we're setting up a `YamlProvider` to source records from our config files and both a `Route53Provider` and `DynProvider` to serve as the targets for those records. You can have any number of zones set up and any number of sources of data and targets for records for each. You can also have multiple config files, that make use of separate accounts and each manage a distinct set of zones. A good example of this this might be `./config/staging.yaml` & `./config/production.yaml`. We'll focus on a `config/production.yaml`. +We start by creating a config file to tell OctoDNS about our providers and the zone(s) we want it to manage. Below we're setting up a `YamlProvider` to source records from our config files and both a `Route53Provider` and `DynProvider` to serve as the targets for those records. You can have any number of zones set up and any number of sources of data and targets for records for each. You can also have multiple config files, that make use of separate accounts and each manage a distinct set of zones. A good example of this this might be `./config/staging.yaml` & `./config/production.yaml`. We'll focus on a `config/production.yaml`. ```yaml --- @@ -46,7 +46,7 @@ providers: secret_access_key: env/AWS_SECRET_ACCESS_KEY zones: - github.com.: + example.com.: sources: - config targets: @@ -54,11 +54,11 @@ zones: - route53 ``` -`class` is a special key that tells OctoDNS what python class should be loaded. Any other keys will be passed as configuration values to that provider. In general any sensitive or frequently rotated values should come from environmental variables. When OctoDns sees a value that starts with `env/` it will look for that value in the process's environment and pass the result along. +`class` is a special key that tells OctoDNS what python class should be loaded. Any other keys will be passed as configuration values to that provider. In general any sensitive or frequently rotated values should come from environmental variables. When OctoDNS sees a value that starts with `env/` it will look for that value in the process's environment and pass the result along. -So now that we have something to tell OctoDns about our providers & zones we need to tell it about or records. We'll keep it simple for now and just create a single `A` record at the top-level of the domain. +Now that we have something to tell OctoDNS about our providers & zones we need to tell it about or records. We'll keep it simple for now and just create a single `A` record at the top-level of the domain. -`config/github.com.yaml` +`config/example.com.yaml` ```yaml --- @@ -72,31 +72,31 @@ So now that we have something to tell OctoDns about our providers & zones we nee ### Noop -So now we're ready to do a dry-run with our new setup to see what changes it would make. Since we're pretending here we'll act like there are no existing records for `github.com.` in our accounts on either provider. +We're ready to do a dry-run with our new setup to see what changes it would make. Since we're pretending here we'll act like there are no existing records for `example.com.` in our accounts on either provider. ``` $ octodns-sync --config-file=./config/production.yaml ... ******************************************************************************** -* github.com. +* example.com. ******************************************************************************** * route53 (Route53Provider) -* Create +* Create * Summary: Creates=1, Updates=0, Deletes=0, Existing Records=0 * dyn (DynProvider) -* Create +* Create * Summary: Creates=1, Updates=0, Deletes=0, Existing Records=0 ******************************************************************************** ... ``` -There will be other logging information presented on the screen, but successful runs of sync will always end with a summary like the above for any providers & zones with changes. If there are no changes a message saying so will be printed instead. Above we're creating a new zone in both providers so they show the same change, but that doesn't always have to be the case. If to start one of them had a different state you would see the changes OctoDns intends to make to sync them up. +There will be other logging information presented on the screen, but successful runs of sync will always end with a summary like the above for any providers & zones with changes. If there are no changes a message saying so will be printed instead. Above we're creating a new zone in both providers so they show the same change, but that doesn't always have to be the case. If to start one of them had a different state you would see the changes OctoDNS intends to make to sync them up. ### Making changes -**WARNING**: OctoDns assumes ownership of any domain you point it to. When you tell it to act it will do whatever is necessary to try and match up states including deleting any unexpected records. Be careful when playing around with OctoDNS. It's best to experiment with a fake zone or one without any data that matters until your comfortable with the system. +**WARNING**: OctoDNS assumes ownership of any domain you point it to. When you tell it to act it will do whatever is necessary to try and match up states including deleting any unexpected records. Be careful when playing around with OctoDNS. It's best to experiment with a fake zone or one without any data that matters until your comfortable with the system. -Now it's time to tell OctoDns to make things happen. We'll invoke it again with the same options and add a `--doit` on the end to tell it this time we actually want it to try and make the specified changes. +Now it's time to tell OctoDNS to make things happen. We'll invoke it again with the same options and add a `--doit` on the end to tell it this time we actually want it to try and make the specified changes. ``` $ octodns-sync --config-file=./config/production.yaml --doit @@ -107,13 +107,13 @@ The output here would be the same as before with a few more log lines at the end ### Workflow -In the above case we manually ran OctoDns from the command line. That works and it's a lot better than heading into the provider GUIs and making changes by clicking around, but OctoDns is designed to be run as part of a deploy process. The implementation of that is well beyond the scope of this README, but I will walk through the process we use. It works a lot like how we [branch deploy GitHub](https://githubengineering.com/deploying-branches-to-github-com/). +In the above case we manually ran OctoDNS from the command line. That works and it's better than heading into the provider GUIs and making changes by clicking around, but OctoDNS is designed to be run as part of a deploy process. The implementation details are well beyond the scope of this README, but here is an example of the workflow we use at GitHub. It follows the way [GitHub itself is branch deployed](https://githubengineering.com/deploying-branches-to-github-com/). The first step is to create a PR with your changes. ![](/docs/assets/pr.png) -Assuming the code tests and config validation statuses are green the next step is to do a noop deploy and verify that the changes OctoDns plans to make are the ones you expect. +Assuming the code tests and config validation statuses are green the next step is to do a noop deploy and verify that the changes OctoDNS plans to make are the ones you expect. ![](/docs/assets/noop.png) @@ -130,22 +130,22 @@ If that goes smoothly, you again see the expected changes, and verify them with Very few situations will involve starting with a blank slate which is why there's tooling built in to pull existing data out of providers into a matching config file. ``` -$ octodns-dump --config-file=config/production.yaml --output-dir=tmp/ github.com. route53 +$ octodns-dump --config-file=config/production.yaml --output-dir=tmp/ example.com. route53 2017-03-15T13:33:34 INFO Manager __init__: config_file=tmp/production.yaml -2017-03-15T13:33:34 INFO Manager dump: zone=github.com., sources=('route53',) +2017-03-15T13:33:34 INFO Manager dump: zone=example.com., sources=('route53',) 2017-03-15T13:33:36 INFO Route53Provider[route53] populate: found 64 records -2017-03-15T13:33:36 INFO YamlProvider[dump] plan: desired=github.com. +2017-03-15T13:33:36 INFO YamlProvider[dump] plan: desired=example.com. 2017-03-15T13:33:36 INFO YamlProvider[dump] plan: Creates=64, Updates=0, Deletes=0, Existing Records=0 2017-03-15T13:33:36 INFO YamlProvider[dump] apply: making changes ``` -The above command pulled the existing data out of Route53 and placed the results into `tmp/github.com.yaml`. That file can be inspected and moved into `config/` to become the new source. If things are working as designed a subsequent noop sync should show zero changes. +The above command pulled the existing data out of Route53 and placed the results into `tmp/example.com.yaml`. That file can be inspected and moved into `config/` to become the new source. If things are working as designed a subsequent noop sync should show zero changes. ## Custom Sources and Providers -You can check out the [source](/octodns/source/) and [provider](/octodns/provider/) directory to see what's currently supported. Sources as the name implies can only act as a source of record information. TinyDnsProvider is currently the only OSS source, though we have several others internally that are specific to our environment. These include something to pull host data from [gPanel](https://githubengineering.com/githubs-metal-cloud/) and a similar provider that sources information about our network gear to create both `A` & `PTR` records for their interfaces. Things that might make good OSS sources might include an `ElbSource` that pulls information about [AWS Elastic Load Balancers](https://aws.amazon.com/elasticloadbalancing/) and dynamically creates `CNAME`s for them, or `Ec2Source` that pulls instance information so that records can be created for hosts similar to our our `GPanelProvider` works. An `AxfrSource` could be really interesting as well. Another case where a source may make sense is if you'd like to export data from a legacy service that you have no plans to push changes back into. +You can check out the [source](/octodns/source/) and [provider](/octodns/provider/) directory to see what's currently supported. Sources act as a source of record information. TinyDnsProvider is currently the only OSS source, though we have several others internally that are specific to our environment. These include something to pull host data from [gPanel](https://githubengineering.com/githubs-metal-cloud/) and a similar provider that sources information about our network gear to create both `A` & `PTR` records for their interfaces. Things that might make good OSS sources might include an `ElbSource` that pulls information about [AWS Elastic Load Balancers](https://aws.amazon.com/elasticloadbalancing/) and dynamically creates `CNAME`s for them, or `Ec2Source` that pulls instance information so that records can be created for hosts similar to how our `GPanelProvider` works. An `AxfrSource` could be really interesting as well. Another case where a source may make sense is if you'd like to export data from a legacy service that you have no plans to push changes back into. -Most of the things included in OctoDNS are providers, the obvious difference being that they can serve as both sources and targets of data. We'd really like to see this list grow over time so if you use an unsupported provider PRs welcome. The existing providers should serve as reasonable examples. Those that have no GeoDNS support are relatively straightforward. Unfortunately most of the APIs involved to do GeoDNS style traffic management are complex and somewhat inconsistent so adding support for that function would be nice, but is optional and best done in a separate pass. +Most of the things included in OctoDNS are providers, the obvious difference being that they can serve as both sources and targets of data. We'd really like to see this list grow over time so if you use an unsupported provider then PRs are welcome. The existing providers should serve as reasonable examples. Those that have no GeoDNS support are relatively straightforward. Unfortunately most of the APIs involved to do GeoDNS style traffic management are complex and somewhat inconsistent so adding support for that function would be nice, but is optional and best done in a separate pass. The `class` key in the providers config section can be used to point to arbitrary classes in the python path so internal or 3rd party providers can easily be included with no coordiation beyond getting them into PYTHONPATH, most likely installed into the virtualenv with OctoDNS. @@ -153,7 +153,7 @@ The `class` key in the providers config section can be used to point to arbitrar ### Syncing between providers -While the primary use-case is to sync a set of yaml config files up to one or more DNS providers the piece of OctoDNS have been built in such a way that you can easily source and target things arbitrarily. As a quick example the config below would sync `githubtest.net.` from Route53 to Dyn. +While the primary use-case is to sync a set of yaml config files up to one or more DNS providers, OctoDNS has been built in such a way that you can easily source and target things arbitrarily. As a quick example the config below would sync `githubtest.net.` from Route53 to Dyn. ```yaml --- @@ -179,7 +179,7 @@ zones: ### Dynamic sources -Internally we use custom sources to create records based on dynamic data that changes frequently without direct human intervention. An example of that might look something like the following. For hosts this mechanisms is janitorial, run periodically, making sure the correct records exists so long as the host is alive and ensuring they go away after the host does. The host provisioning and destruction processes do the actual work to create and destroy the records. +Internally we use custom sources to create records based on dynamic data that changes frequently without direct human intervention. An example of that might look something like the following. For hosts this mechanism is janitorial, run periodically, making sure the correct records exist as long as the host is alive and ensuring they are removed after the host is destroyed. The host provisioning and destruction processes do the actual work to create and destroy the records. ```yaml --- From 23311c883bfd17375461df98053848ed1ae7379c Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 16 Mar 2017 10:44:30 -0700 Subject: [PATCH 6/6] Remove accidental paste from README header --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e16e13..e9bd770 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -OctoDNS# OctoDNS +# OctoDNS ## DNS as code - Tools for managing DNS across multiple providers