#!/usr/bin/env python3
"""OSX-based script to watch for changes to network state and write out a
second resolv.conf file containing the DHCP provided nameservers, intended
for use with a local resolver such as dnsmasq. This is to workaround the
changes in Snow Leopard from Leopard with regards to DNS resolution.

ie: the inability to have both manually configured nameservers and
DHCP provided ones as well as the issues with split-DNS.

usage: python automasq.py /path/to/second/resolv.conf

original source: https://gist.github.com/edwardgeorge/270506

"""
import optparse
import sys

from SystemConfiguration import *

GLOBAL_KEY = 'State:/Network/Global/IPv4'


class Watcher(object):
    def __init__(self, filename, defaults_filename=None,
                append_defaults=False):
        self.filename = filename
        self.defaults = defaults_filename
        self.append = append_defaults

        store = self.store = SCDynamicStoreCreate(None, "automasq",
            self.dynamicStoreChanged, None)
        SCDynamicStoreSetNotificationKeys(store, None, [GLOBAL_KEY])
        source = self.source = SCDynamicStoreCreateRunLoopSource(None,
            store, 0)

        self.write_file(self.get_primary_dns(store))

        loop = self.loop = CFRunLoopGetCurrent()
        CFRunLoopAddSource(loop, source, kCFRunLoopCommonModes)
        CFRunLoopRun()

    def write_file(self, servers=[]):
        with open(self.filename, 'w+') as f:
            for server in servers:
                f.write('nameserver %s\n' % server)
            if (self.append or not servers) and self.defaults is not None:
                with open(self.defaults) as d:
                    f.write(d.read())

    def process_dns_for_service(self, store, service):
        key = 'State:/Network/Service/%s/DNS' % service
        val = SCDynamicStoreCopyValue(store, key)
        data = list(dict(val)['ServerAddresses'])
        return data

    def get_primary_dns(self, store=None):
        store = store or self.store
        val = SCDynamicStoreCopyValue(store, GLOBAL_KEY)
        if val:
            data = dict(val)
            svcid = data['PrimaryService']
            return self.process_dns_for_service(store, svcid)
        else:
            return []

    def dynamicStoreChanged(self, store, changedKeys, info):
        servers = []
        for key in list(changedKeys):
            #if key == GLOBAL_KEY:
            servers = self.get_primary_dns(store)
            self.write_file(servers)


def dummy_timer(*args):
    pass


def main(filename, options):
    # this gives us a callback into python every 1s for signal handling
    CFRunLoopAddTimer(CFRunLoopGetCurrent(),
        CFRunLoopTimerCreate(None, CFAbsoluteTimeGetCurrent(), 1.0, 0, 0,
            dummy_timer, None),
        kCFRunLoopCommonModes)
    try:
        watcher = Watcher(filename, defaults_filename=options.default,
            append_defaults=options.append_defaults)
    except KeyboardInterrupt as e:
        # exiting
        pass

if __name__ == '__main__':
    usage = "usage: %prog [options] output-file"
    parser = optparse.OptionParser(usage)
    parser.add_option('-d', '--default-conf', dest='default',
        help='default conf if no resolvers provided', metavar='RESOLVCONF')
    parser.add_option('-a', '--append', dest='append_defaults',
        action='store_true',
        help='always append defaults to generated resolv.conf')
    opts, args = parser.parse_args()
    if len(args) != 1:
        parser.error("specify a single output-file")
    if opts.append_defaults and not opts.default:
        parser.error("default conf must be specified to be able to append")
    main(args[0], opts)