Skip to content

Commit

Permalink
Add dynect aRecord support
Browse files Browse the repository at this point in the history
  • Loading branch information
Sumit Vij committed Aug 10, 2015
1 parent cefcd9b commit c9de328
Show file tree
Hide file tree
Showing 7 changed files with 327 additions and 3 deletions.
55 changes: 55 additions & 0 deletions nix/dynect-record.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{ config, pkgs, lib, uuid, name, ...}:
with lib;
with (import ./lib.nix lib);
let
aRecordSubModule =
{ options, ... }:
{ options = {
address = mkOption {
type = types.str;
description = "IPv4 Address.";
example = "127.0.0.1";
};
};
};
in
{
options = {
name = mkOption {
default = "nixops-${uuid}-${name}";
example = "";
type = types.str;
description = "Name of the record";
};

fqdn = mkOption {
type = types.str;
description = "Name of node where the record will be added.";
example = "www.example.com";
};

zone = mkOption {
type = types.str;
description = "Name of zone where the record will be added.";
example = "example.com";
};

ttl = mkOption {
default = 0;
example = 3600;
type = types.int;
description = "TTL for the record in seconds. Set to 0 to use zone default.";
};

aRecord = mkOption {
default = null;
type = types.nullOr ( types.submodule ( aRecordSubModule ) );
example = {
address = "127.0.0.1";
};
description = "aRecord type";
};
};

config._type = "dynect-record";
}
4 changes: 3 additions & 1 deletion nix/eval-machine-info.nix
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ with import <nixpkgs/nixos/lib/testing.nix> { inherit system; };
with pkgs;
with lib;


rec {

networks =
Expand Down Expand Up @@ -105,6 +104,9 @@ rec {
resources.gseBuckets = evalResources ./gse-bucket.nix (zipAttrs resourcesByType.gseBuckets or []);
resources.gceImages = evalResources ./gce-image.nix (gce_default_bootstrap_images // ( zipAttrs resourcesByType.gceImages or []) );

# Dynect resources
resources.dynectRecords = evalResources ./dynect-record.nix (zipAttrs resourcesByType.dynectRecords or []);

gce_deployments = flip filterAttrs nodes
( n: v: let dc = (scrubOptionValue v).config.deployment; in dc.targetEnv == "gce" );

Expand Down
2 changes: 1 addition & 1 deletion nix/lib.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ with lib;

resource = type: mkOptionType {
name = "resource of type ‘${type}’";
check = x: x._type or "" == type;
check = x: (x._type or "") == type;
merge = mergeOneOption;
};

Expand Down
5 changes: 4 additions & 1 deletion nixops/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ def __init__(self):
import nixops.resources.gce_target_pool
import nixops.resources.gce_forwarding_rule
import nixops.resources.gse_bucket
import nixops.resources.dynect_record

def create_definition(xml):
"""Create a machine definition object from the given XML representation of the machine's attributes."""
Expand Down Expand Up @@ -453,8 +454,10 @@ def create_state(depl, type, name, id):
nixops.resources.gce_http_health_check.GCEHTTPHealthCheckState,
nixops.resources.gce_target_pool.GCETargetPoolState,
nixops.resources.gce_forwarding_rule.GCEForwardingRuleState,
nixops.resources.gse_bucket.GSEBucketState
nixops.resources.gse_bucket.GSEBucketState,
nixops.resources.dynect_record.DynectRecordState
]:
if type == i.get_type():
return i(depl, name, id)

raise nixops.deployment.UnknownBackend("unknown resource type ‘{0}’".format(type))
4 changes: 4 additions & 0 deletions nixops/deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,10 @@ def evaluate(self):
defn = nixops.resources.gse_bucket.GSEBucketDefinition(x)
self.definitions[defn.name] = defn

for x in res.find("attr[@name='dynectRecords']/attrs").findall("attr"):
defn = nixops.resources.dynect_record.DynectRecordDefination(x)
self.definitions[defn.name] = defn


def evaluate_option_value(self, machine_name, option_name, xml=False, include_physical=False):
"""Evaluate a single option of a single machine in the deployment specification."""
Expand Down
241 changes: 241 additions & 0 deletions nixops/resources/dynect_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
# -*- coding: utf-8 -*-

import re
import nixops.util
import threading
import traceback
import os

from nixops.resources import ResourceDefinition
from nixops.resources import ResourceState
from dyn.tm.session import DynectSession
from dyn.tm.zones import Zone
from dyn.tm.records import ARecord
from dyn.tm.errors import DynectGetError


class DynectRecordDefination(ResourceDefinition):
"""TODO: Documentation"""
supported_records = ['aRecord']

@classmethod
def get_type(cls):
return "dynect-record"

def __init__(self, xml):
ResourceDefinition.__init__(self, xml)
defination = self._get_value(xml)
records = filter(lambda x: x is not None,
map(lambda x: x if x in defination and defination[x] != None else None,
DynectRecordDefination.supported_records))
if not len(records) == 1:
raise Exception(
"None or more than one records are present {0}".format(records))
self.record_type = records[0]
try:
self.record_defn = self.parse_record(self.record_type, defination)
except KeyError as e:
raise Exception(
"{0} is missing in the nix expression".format(
e.args))

def parse_record(self, record_type, defi):
record = {
"ttl": defi["ttl"],
"fqdn": defi["fqdn"],
"zone": defi["zone"]
}
if (record_type == "aRecord"):
record["address"] = defi["aRecord"]["address"]
else:
raise Exception("Unsupported type {0}".format(self.record_type))
return record

# TODO: Move this code into the base class
def _get_value(self, attr):
value = None
_type = attr.tag
if _type == "list":
value = []
for x in attr:
value.append(self._get_value(x))
elif _type == "attrs":
value = {}
for x in attr:
value[x.attrib['name']] = self._get_value(x)
elif _type == "attr":
if len(attr) > 1:
raise Exception("More than 1 child elements")
value = self._get_value(attr[0])
elif _type == "string":
value = attr.get("value")
elif _type == "int":
value = int(attr.get("value"))
elif _type == "bool":
value = bool(attr.get("value"))
elif _type == "null":
value = None
else:
raise Exception("Unknown {0} type".format(_type))
return value

def show_type(self):
return "{0} [{1}]".format(self.get_type(), self.record_type)


class DynectRecordState(ResourceState):

nix_name = "dynectRecords"
record_type = nixops.util.attr_property("dynect.record_type", None)
record = nixops.util.attr_property("dynect.record", {}, 'json')
dyn_record_id = nixops.util.attr_property("dynect.dyn_record_id", None)
rlock = threading.RLock()

@classmethod
def get_type(cls):
return "dynect-record"

def __init__(self, depl, name, id):
ResourceState.__init__(self, depl, name, id)

def show_type(self):
"""A short description of the type of resource this is"""
return "{0} [{1}]".format(self.get_type(), self.record_type)

@property
def resource_id(self):
return self.name

def create(self, defn, check, allow_reboot, allow_recreate):
# Ignore the check flag
self.logger.log("DynectDNS resource state is {0}".format(self.state))
record_type = defn.record_type
self.name = defn.name
record_defn = defn.record_defn
if self.state == self.UNKNOWN:
self.logger.log("Resource state is Unknown. Creating..")
create = self._get_def(record_type, "create")
record = create(record_type, record_defn)
else:
get = self._get_def(record_type, "get")
record = get(
self.record,
self.dyn_record_id) if self.dyn_record_id is not None else None
if record is None:
self.logger.log("Resource not found on Dynect. Creating..")
create = self._get_def(record_type, "create")
record = create(record_type, record_defn)
elif self._changes(self.record, self.record_type, record_defn, record_type):
self.logger.log(
"Resource defination has been change. Updating..")
update = self._get_def(record_type, "update")
update(record, record_defn)
else:
self.logger.log("Resource is up to date")

self.dyn_record_id = record.record_id
self.record_type = record_type
self.record = record_defn
self.state = self.UP

def destroy(self, wipe=False):
if self.state == self.UP:
self.logger.log("Destroying..")
get = self._get_def(self.record_type, "get")
record = get(self.record, self.dyn_record_id)

def delete_record(record): record.delete()
self._make_changes(self.record['zone'], delete_record, record)
return True

def _changes(self, record, record_type, record_defn, record_defn_type):
changes = False
if record_type != record_defn_type:
raise Exception(
"Can't change the record type from {0} to {1}".format(
record_type, record_defn_type))
if record != record_defn:
if record['fqdn'] != record_defn['fqdn']:
raise Exception("Can't change fqdn of the record")
elif self.record['zone'] != record_defn['zone']:
raise Exception("Can't change fqdn of the record")
else:
changes = True
return changes

def _make_changes(self, zone_name, change_def, *change_params):
self._build_session()
val = change_def(*change_params)
zone = Zone(zone_name)
zone.publish()
return val

def _build_session(self):
def get_env_var(var):
value = os.environ.get(var)
if value is None:
raise Exception("Env var {0} is not set".format(var))
return value

user_name = get_env_var('DYN_USER_NAME')
customer_name = get_env_var('DYN_CUSTOMER_NAME')
password = get_env_var('DYN_PASSWORD')
# So ghetto, but race causes a NullPointer error
with DynectRecordState.rlock:
DynectSession(customer_name, user_name, password)

# TODO: Instead have a class hierachy
# DynectDyn api has side effects in constructor which makes it hard
# to use
def _get_def(self, record_type, op):
if record_type == "aRecord":
if op == "create":
return self._create_arecord
elif op == "get":
return self._get_arecord
elif op == "update":
return self._update_arecord
else:
raise Exception("Unsupported {0} for aRecord".format(op))
else:
raise Exception("Unsupported type {0}".format(record_type))

def _create_arecord(self, record_type, record_defn):
zone_name = record_defn['zone']
fqdn = record_defn['fqdn']
ttl = record_defn['ttl']
address = record_defn['address']
params = {'address': address, 'ttl': ttl}

def create_record(
zone_name,
fqdn,
params): return ARecord(
zone_name,
fqdn,
**params)
return self._make_changes(
zone_name, create_record, zone_name, fqdn, params)

def _update_arecord(self, dyn_record, record_defn):
zone_name = record_defn['zone']

def update_record(dyn_record, record_defn):
dyn_record.address = record_defn['address']
dyn_record.ttl = record_defn['ttl']
return self._make_changes(
zone_name, update_record, dyn_record, record_defn)

def _get_arecord(self, record, record_id):
self.logger.log("Fetching record {0}".format(record_id))
self._build_session()
fqdn = record['fqdn']
zone = record['zone']
try:
record = ARecord(zone, fqdn, record_id=record_id)
except DynectGetError as e:
# Hope that it is RecordNotFound (aka 404)
self.logger.warn(
traceback.format_exc()) if nixops.deployment.debug else None
return None
return record
19 changes: 19 additions & 0 deletions release.nix
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ let
preConfigure = "cp libcloud/test/secrets.py-dist libcloud/test/secrets.py";
});

# TODO: move this into the nixpkgs
dyn = pkgs.pythonPackages.buildPythonPackage rec {
version = "1.4.0";
name = "dyn-${version}";

src = pkgs.fetchurl {
url = "https://pypi.python.org/packages/source/d/dyn/${name}.tar.gz";
sha256 = "19shdfm7g51qrry84zr7kvrxpi6p2x0jvsr98640848dmw1l55jr";
};

buildInputs = with pkgs.pythonPackages; [ pytest pytestcov mock pytestpep8 pytest_xdist covCore ];

meta = {
description = "Dynect dns lib";
homepage = "http://dyn.readthedocs.org/en/latest/intro.html";
};
};

in

rec {
Expand Down Expand Up @@ -85,6 +103,7 @@ rec {
pythonPackages.boto
pythonPackages.hetzner
libcloud
dyn
pythonPackages.sqlite3
];

Expand Down

0 comments on commit c9de328

Please sign in to comment.