コード例 #1
0
ファイル: conman_etcd_test.py プロジェクト: tsupe/conman
    def setUp(self):
        self.conman = ConManEtcd()

        cli = self.conman.client
        delete_key(cli, 'good')
        delete_key(cli, 'refresh_test')
        set_key(cli, 'good', self.good_dict)
コード例 #2
0
 def __init__(self,
              key,
              filename,
              protocol='http',
              host='127.0.0.1',
              port=4001,
              username=None,
              password=None):
     self.conman = ConManEtcd(protocol=protocol,
                              host=host,
                              port=int(port),
                              username=username,
                              password=password,
                              on_change=self.on_configuration_change,
                              watch_timeout=5)
     self.filename = filename
     open(self.filename, 'w+')
     self.key = key
     self.last_change = None
     self.run()
コード例 #3
0
ファイル: dyn_conf_program.py プロジェクト: droc/conman
class Program(object):
    def __init__(self,
                 key,
                 filename,
                 protocol='http',
                 host='127.0.0.1',
                 port=4001,
                 username=None,
                 password=None):
        self.conman = ConManEtcd(protocol=protocol,
                                 host=host,
                                 port=int(port),
                                 username=username,
                                 password=password,
                                 on_change=self.on_configuration_change,
                                 watch_timeout=5)
        self.filename = filename
        open(self.filename, 'w+')
        self.key = key
        self.last_change = None
        self.run()

    def on_configuration_change(self, key, action, value):
        # Sometimes the same change is reported multiple times. Ignore repeats
        if self.last_change == (key, action, value):
            return

        self.last_change = (key, action, value)
        line = 'key: {}, action: {}, value: {}\n'.format(key,
                                                        action,
                                                        value)
        open(self.filename, 'a').write(line)
        self.conman.refresh(self.key)

    def run(self):
        self.conman.refresh(self.key)
        self.conman.watch(self.key)
        while True:
            if self.conman[self.key].get('stop') == '1':
                open(self.filename, 'a').write('Stopping...\n')
                self.conman.stop_watchers()
                return
            time.sleep(1)
コード例 #4
0
class Program(object):
    def __init__(self,
                 key,
                 filename,
                 protocol='http',
                 host='127.0.0.1',
                 port=4001,
                 username=None,
                 password=None):
        self.conman = ConManEtcd(protocol=protocol,
                                 host=host,
                                 port=int(port),
                                 username=username,
                                 password=password,
                                 on_change=self.on_configuration_change,
                                 watch_timeout=5)
        self.filename = filename
        open(self.filename, 'w+')
        self.key = key
        self.last_change = None
        self.run()

    def on_configuration_change(self, key, action, value):
        # Sometimes the same change is reported multiple times. Ignore repeats
        if self.last_change == (key, action, value):
            return

        self.last_change = (key, action, value)
        line = 'key: {}, action: {}, value: {}\n'.format(key, action, value)
        open(self.filename, 'a').write(line)
        self.conman.refresh(self.key)

    def run(self):
        self.conman.refresh(self.key)
        self.conman.watch(self.key)
        while True:
            if self.conman[self.key].get('stop') == '1':
                open(self.filename, 'a').write('Stopping...\n')
                self.conman.stop_watchers()
                return
            time.sleep(1)
コード例 #5
0
ファイル: dyn_conf_program.py プロジェクト: droc/conman
 def __init__(self,
              key,
              filename,
              protocol='http',
              host='127.0.0.1',
              port=4001,
              username=None,
              password=None):
     self.conman = ConManEtcd(protocol=protocol,
                              host=host,
                              port=int(port),
                              username=username,
                              password=password,
                              on_change=self.on_configuration_change,
                              watch_timeout=5)
     self.filename = filename
     open(self.filename, 'w+')
     self.key = key
     self.last_change = None
     self.run()
コード例 #6
0
ファイル: conman_etcd_test.py プロジェクト: tsupe/conman
class ConManEtcdTest(TestCase):
    @classmethod
    def setUpClass(cls):
        # Start local etcd server if not running
        start_local_etcd_server()

        # Add good key
        cls.good_dict = dict(a='1', b='Yeah, it works!!!')

    @classmethod
    def tearDownClass(cls):
        try:
            kill_local_etcd_server()
        except:  # noqa
            pass

    def setUp(self):
        self.conman = ConManEtcd()

        cli = self.conman.client
        delete_key(cli, 'good')
        delete_key(cli, 'refresh_test')
        set_key(cli, 'good', self.good_dict)

    def tearDown(self):
        delete_key(self.conman.client, 'good')
        delete_key(self.conman.client, 'refresh_test')
        delete_key(self.conman.client, 'watch_test')

    def test_initialization(self):
        cli = self.conman.client
        self.assertEqual('127.0.0.1:2379', cli._url)

    def test_add_good_key(self):
        self.conman.add_key('good')
        expected = self.good_dict
        actual = self.conman['good']
        self.assertEqual(expected, actual)

    def test_add_bad_key(self):
        self.assertRaises(Exception, self.conman.add_key, 'no such key')

    def test_refresh(self):
        self.assertFalse('refresh_test' in self.conman)

        # Insert a new key to etcd
        set_key(self.conman.client, 'refresh_test', dict(a='1'))

        # The new key should still not be visible by conman
        self.assertFalse('refresh_test' in self.conman)

        # Refresh to get the new key
        self.conman.refresh('refresh_test')

        # The new key should now be visible by conman
        self.assertEqual(dict(a='1'), self.conman['refresh_test'])

        # Change the key
        set_key(self.conman.client, 'refresh_test', dict(b='3'))

        # The previous value should still be visible by conman
        self.assertEqual(dict(a='1'), self.conman['refresh_test'])

        # Refresh again
        self.conman.refresh('refresh_test')

        # The new value should now be visible by conman
        self.assertEqual(dict(b='3'), self.conman['refresh_test'])

    def test_dictionary_access(self):
        self.conman.add_key('good')
        self.assertEqual(self.good_dict, self.conman['good'])

    def test_watch_existing_key(self):
        def on_change(change_dict, event):
            change_dict[event.key].append((type(event).__name__, event.value))

        change_dict = defaultdict(list)
        self.assertFalse('watch_test' in self.conman)

        # Insert a new key to etcd
        self.conman.client.put('watch_test/a', '1')

        # The new key should still not be visible by conman
        self.assertFalse('watch_test' in self.conman)

        # Refresh to get the new key
        self.conman.refresh('watch_test')

        # The new key should now be visible by conman
        self.assertEqual(dict(a='1'), self.conman['watch_test'])

        # Set the on_change() callback of conman (normally at construction)
        on_change = partial(on_change, change_dict)
        self.conman.on_change = on_change
        watch_id = None
        try:
            watch_id = self.conman.watch('watch_test/b')

            # Change the key
            self.conman.client.put('watch_test/b', '3')

            # The previous value should still be visible by conman
            self.assertEqual(dict(a='1'), self.conman['watch_test'])

            # Wait for the change callback to detect the change
            for i in range(3):
                if change_dict:
                    break
                time.sleep(1)

            expected = {b'watch_test/b': [('PutEvent', b'3')]}
            actual = dict(change_dict)
            self.assertEquals(expected, actual)

            # Refresh again
            self.conman.refresh('watch_test')

            # The new value should now be visible by conman
            self.assertEqual(dict(a='1', b='3'), self.conman['watch_test'])
        finally:
            if watch_id is not None:
                self.conman.cancel(watch_id)

    def test_watch_prefix(self):
        all_events = []

        def read_events_in_thread():
            events, cancel = self.conman.watch_prefix('watch_prefix_test')
            for event in events:
                k = event.key.decode()
                v = event.value.decode()
                s = f'{k}: {v}'
                all_events.append(s)
                if v == 'stop':
                    cancel()

            print('Done.')

        t = Thread(target=read_events_in_thread)
        t.start()

        # Insert a new key to etcd
        self.conman.client.put('watch_prefix_test/a', '1')
        self.conman.client.put('watch_prefix_test/b', '2')
        self.conman.client.put('watch_prefix_test', 'stop')

        t.join()

        expected = [
            'watch_prefix_test/a: 1',
            'watch_prefix_test/b: 2',
            'watch_prefix_test: stop'
        ]
        self.assertEquals(expected, all_events)
コード例 #7
0
 def setUp(self):
     self.conman = ConManEtcd()
     self.cli = self.conman.client
     self.key = 'dyn_conf'
     set_key(self.cli, self.key, dict(a='1', b='Yeah, it works!!!'))
コード例 #8
0
class DynamicConfigurationTest(TestCase):
    @classmethod
    def setUpClass(cls):
        os.system('rm dyn_conf*.txt')
        kill_local_etcd_server()
        # Start local etcd server if not running
        start_local_etcd_server()

    @classmethod
    def tearDownClass(cls):
        try:
            kill_local_etcd_server()
        except:  # noqa
            pass

    def setUp(self):
        self.conman = ConManEtcd()
        self.cli = self.conman.client
        self.key = 'dyn_conf'
        set_key(self.cli, self.key, dict(a='1', b='Yeah, it works!!!'))

    def tearDown(self):
        self.conman.stop_watchers()
        delete_key(self.conman.client, self.key)

    def test_dynamic_configuration(self):
        # Launch 3 programs
        count = 3
        programs = []
        filenames = [os.path.abspath('dyn_conf_{}.txt'.format(i)) for i
                     in range(count)]
        for i in range(count):
            p = Process(target=Program, kwargs=dict(key='dyn_conf',
                                                    filename=filenames[i]))
            programs.append(p)
            p.start()

        cli = self.conman.client

        # Let the programs start and connect to etcd
        time.sleep(3)

        # Update the configuration
        cli.write(self.key + '/c', value=dict(d=6))

        # Get the output from all the programs
        output = [open(f).read() for f in filenames]
        for i in range(5):
            if output[0] == '':
                time.sleep(1)
            output = [open(f).read() for f in filenames]

        expected = \
            ["key: /dyn_conf/c, action: set, value: {'d': 6}\n"] * count
        self.assertEquals(expected, output)

        # Tell all programs to stop via dynamic configuration
        cli.write(self.key + '/stop', value=1)

        for p in programs:
            p.join()

        print 'Done.'
コード例 #9
0
ファイル: conman_etcd_test.py プロジェクト: droc/conman
class ConManEtcdTest(TestCase):
    @classmethod
    def setUpClass(cls):
        # Start local etcd server if not running
        start_local_etcd_server()

        # Add good key
        cls.good_dict = dict(a='1', b='Yeah, it works!!!')

    @classmethod
    def tearDownClass(cls):
        try:
            kill_local_etcd_server()
        except:  # noqa
            pass

    def setUp(self):
        self.conman = ConManEtcd()

        cli = self.conman.client
        delete_key(cli, 'good')
        delete_key(cli, 'refresh_test')
        set_key(cli, 'good', self.good_dict)

    def tearDown(self):
        delete_key(self.conman.client, 'good')
        delete_key(self.conman.client, 'refresh_test')
        delete_key(self.conman.client, 'watch_test')
        self.conman.stop_watchers()

    def test_initialization(self):
        cli = self.conman.client
        self.assertEqual('http://127.0.0.1:4001', cli.base_uri)
        self.assertEqual('127.0.0.1', cli.host)
        self.assertEqual(4001, cli.port)

    def test_add_good_key(self):
        self.conman.add_key('good')
        expected = self.good_dict
        actual = self.conman['good']
        self.assertEqual(expected, actual)

    def test_add_bad_key(self):
        self.assertRaises(Exception, self.conman.add_key, 'no such key')

    def test_refresh(self):
        self.assertFalse('refresh_test' in self.conman)

        # Insert a new key to etcd
        set_key(self.conman.client, 'refresh_test', dict(a='1'))

        # The new key should still not be visible by conman
        self.assertFalse('refresh_test' in self.conman)

        # Refresh to get the new key
        self.conman.refresh('refresh_test')

        # The new key should now be visible by conman
        self.assertEqual(dict(a='1'), self.conman['refresh_test'])

        # Change the key
        set_key(self.conman.client, 'refresh_test', dict(b='3'))

        # The previous value should still be visible by conman
        self.assertEqual(dict(a='1'), self.conman['refresh_test'])

        # Refresh again
        self.conman.refresh('refresh_test')

        # The new value should now be visible by conman
        self.assertEqual(dict(b='3'), self.conman['refresh_test'])

    def test_dictionary_access(self):
        self.conman.add_key('good')
        self.assertEqual(self.good_dict, self.conman['good'])

    def test_watch_existing_key(self):
        def on_change(change_dict, key, action, value):
            change_dict[key].append((action, value))

        change_dict = defaultdict(list)
        self.assertFalse('watch_test' in self.conman)

        # Insert a new key to etcd
        self.conman.client.write('watch_test/a', 1)

        # The new key should still not be visible by conman
        self.assertFalse('watch_test' in self.conman)

        # Refresh to get the new key
        self.conman.refresh('watch_test')

        # The new key should now be visible by conman
        self.assertEqual(dict(a='1'), self.conman['watch_test'])

        # Set the on_change() callback of conman (normally at construction)
        on_change = partial(on_change, change_dict)
        self.conman.on_change = on_change
        self.conman.watch('watch_test')

        # Change the key
        self.conman.client.write('watch_test/b', '3')

        # The previous value should still be visible by conman
        self.assertEqual(dict(a='1'), self.conman['watch_test'])

        # Wait for the change callback to detect the change
        for i in range(3):
            if change_dict:
                break
            time.sleep(1)

        expected = {'/watch_test/b':[('set', '3')]}
        actual = dict(change_dict)
        self.assertEquals(expected, actual)

        # Refresh again
        self.conman.refresh('watch_test')

        # The new value should now be visible by conman
        self.assertEqual(dict(a='1', b='3'), self.conman['watch_test'])
コード例 #10
0
    def __init__(self,
                 key,
                 vpp_uplink_interface_index,
                 uplink_ip,
                 uplink_subnet,
                 protocol='http',
                 host='127.0.0.1',
                 port=4001,
                 username=None,
                 password=None):

        # logging
        logging.basicConfig(filename='vpp-calico-routeagent.log',level=logging.DEBUG)

        # ConMan setup.
        self.conman = ConManEtcd(protocol=protocol,
                                 host=host,
                                 port=int(port),
                                 username=username,
                                 password=password,
                                 on_change=self.on_configuration_change,
                                 watch_timeout=200)

        # Need to know our hostname
        self.hostname = socket.gethostname()

        # This is the interface and IP this host's VPP uses for the outside world.
        # We'll publish this in ETCD to allow other VPP's to form adjacencys.
        self.vpp_uplink_interface_index = int(vpp_uplink_interface_index)
        self.uplink_ip = uplink_ip
        self.uplink_subnet = uplink_subnet

        # Publish our VPP uplink IP to /vpp-calico/hosts/<hostname>/peerip/ipv4/1
        self.etcd_cli = self.conman.client
        self.host_uplink_info_key = '/vpp-calico/hosts/' + self.hostname + '/peerip/ipv4/1'
        self.etcd_cli.write(self.host_uplink_info_key, value=uplink_ip)

        # Connect to VPP API
        self.r = vpp_papi.connect("vpp-calico")
        if self.r != 0:
            logging.critical("vppapi: could not connect to vpp")
            return
        logging.debug('Connected to VPP API %s', type(self.r))

        # Set VPP uplink interface to 'up'
        logging.debug('Configuring VPP Uplink interface.')
        flags_r = vpp_papi.sw_interface_set_flags(self.vpp_uplink_interface_index,1,1,0)
        if type(flags_r) == list or flags_r.retval != 0:
            logging.critical("Failed to bring up our UPLINK VPP interface. Failing.")
            return
        logging.debug("vppapi: VPP Uplink interface UP!")

        # Configure Uplink IP address based on agent configuration (uplink_ip, uplink_subnet)
        uplink_ip = uplink_ip.encode('utf-8', 'ignore')
        uplink_ip = socket.inet_pton(socket.AF_INET, uplink_ip)
        uplinkip_r = vpp_papi.sw_interface_add_del_address(self.vpp_uplink_interface_index,True,False,False,int(uplink_subnet),uplink_ip)
        if type(uplinkip_r) == list or uplinkip_r.retval != 0:
            logging.critical("Failed to add IPv4 address to uplink")
            return
        logging.debug("vppapi: VPP Uplink IPv4 Configured!")

        #ConMan Vars for watching IP blocks.
        self.key = key
        self.last_change = None
        self.run()
コード例 #11
0
class Program(object):
    def __init__(self,
                 key,
                 vpp_uplink_interface_index,
                 uplink_ip,
                 uplink_subnet,
                 protocol='http',
                 host='127.0.0.1',
                 port=4001,
                 username=None,
                 password=None):

        # logging
        logging.basicConfig(filename='vpp-calico-routeagent.log',level=logging.DEBUG)

        # ConMan setup.
        self.conman = ConManEtcd(protocol=protocol,
                                 host=host,
                                 port=int(port),
                                 username=username,
                                 password=password,
                                 on_change=self.on_configuration_change,
                                 watch_timeout=200)

        # Need to know our hostname
        self.hostname = socket.gethostname()

        # This is the interface and IP this host's VPP uses for the outside world.
        # We'll publish this in ETCD to allow other VPP's to form adjacencys.
        self.vpp_uplink_interface_index = int(vpp_uplink_interface_index)
        self.uplink_ip = uplink_ip
        self.uplink_subnet = uplink_subnet

        # Publish our VPP uplink IP to /vpp-calico/hosts/<hostname>/peerip/ipv4/1
        self.etcd_cli = self.conman.client
        self.host_uplink_info_key = '/vpp-calico/hosts/' + self.hostname + '/peerip/ipv4/1'
        self.etcd_cli.write(self.host_uplink_info_key, value=uplink_ip)

        # Connect to VPP API
        self.r = vpp_papi.connect("vpp-calico")
        if self.r != 0:
            logging.critical("vppapi: could not connect to vpp")
            return
        logging.debug('Connected to VPP API %s', type(self.r))

        # Set VPP uplink interface to 'up'
        logging.debug('Configuring VPP Uplink interface.')
        flags_r = vpp_papi.sw_interface_set_flags(self.vpp_uplink_interface_index,1,1,0)
        if type(flags_r) == list or flags_r.retval != 0:
            logging.critical("Failed to bring up our UPLINK VPP interface. Failing.")
            return
        logging.debug("vppapi: VPP Uplink interface UP!")

        # Configure Uplink IP address based on agent configuration (uplink_ip, uplink_subnet)
        uplink_ip = uplink_ip.encode('utf-8', 'ignore')
        uplink_ip = socket.inet_pton(socket.AF_INET, uplink_ip)
        uplinkip_r = vpp_papi.sw_interface_add_del_address(self.vpp_uplink_interface_index,True,False,False,int(uplink_subnet),uplink_ip)
        if type(uplinkip_r) == list or uplinkip_r.retval != 0:
            logging.critical("Failed to add IPv4 address to uplink")
            return
        logging.debug("vppapi: VPP Uplink IPv4 Configured!")

        #ConMan Vars for watching IP blocks.
        self.key = key
        self.last_change = None
        self.run()

    def on_configuration_change(self, key, action, value):
        # Sometimes the same change is reported multiple times. Ignore repeats.
        if self.last_change == (key, action, value):
            logging.debug('Duplicate Update, Ingore! Key: %s Action: %s',key,action)
            return
        # Calico regularly read+updates key contents to track IPAM. We just want new routes.
        if action != 'create':
            logging.debug('Ignoring all actions apart from create. Key: %s Action: %s', key, action)
            return

        logging.debug('Valid Update, Key: %s Action: %s Value: %s',key,action,value)
        self.last_change = (key, action, value)
        self.conman.refresh(self.key)

        # Convert our value data (json) into a dict.
        update_dict = json.loads(value)
        ourhost='host:'+ socket.gethostname()

        # Check if the route update is for us
        if update_dict['affinity'] == str(ourhost):
           logging.debug('Block is on our host, ignoring update. Key: %s', key)
           return
        else:
           logging.debug('Update IS for us, processing route: %s', key)

           # Update VPP Routing Table
           # Which host is our next hop? Translate hostname to reachable IP via ETCD.
           # Strip 'host:' from 'affinity' record, leave us with hostname.
           # Lookup hostname <> IP mapping in ETCD /vpp-calico Tree

           regex_host = re.compile(ur'(?:host:)(.*)')
           re_result = re.search(regex_host, update_dict['affinity'])
           host_path = "/vpp-calico/hosts/" + re_result.group(1) + "/peerip/ipv4/1"
           route_via_ip = self.etcd_cli.read(host_path).value

           if route_via_ip == "":
             logging.debug('We failed to resolve the remote host via etcd /vpp-calico tree')
             return

           #Split CIDR into network and subnet components
           route_components = update_dict['cidr'].split("/")
           cidr = int(route_components[1])
           network = str(route_components[0])
           #Route-via destination in Binary format
           via_address = route_via_ip.encode('utf-8', 'ignore')
           via_address = socket.inet_pton(socket.AF_INET, via_address)
           #Subnet CIDR and Network in binary format.
           dst_address = network.encode('utf-8', 'ignore')
           dst_address = socket.inet_pton(socket.AF_INET, dst_address)
           #Other VPP API vars
           vpp_vrf_id = 0
           is_add = True
           is_ipv6 = False
           is_static = False
           print('calling vpp_papi')
           route_r = vpp_papi.ip_add_del_route(self.vpp_uplink_interface_index,
                                             vpp_vrf_id,
                                             False, 9, 0,
                                             False, True,
                                             is_add, False,
                                             is_ipv6, False,
                                             False, False,
                                             False, 1,
                                             cidr, dst_address,
                                             via_address)
           if type(route_r) != list and route_r.retval == 0:
               logging.debug("vpp-route-agent: added static route for %s/%s via %s", network, cidr, route_via_ip)
           else:
               logging.critical("vpp-route-agent: Could not add route to %s/%s via %s", network, cidr, route_via_ip)
               return
    def run(self):
        self.conman.refresh(self.key)
        print 'Refreshed Tree: %s', self.key
        self.conman.watch(self.key)
        print 'Watching Tree: %s', self.key
        while True:
            if self.conman[self.key].get('vppagentstop') == '1':
                open(self.filename, 'a').write('Stopping...\n')
                self.conman.stop_watchers()
                return
            time.sleep(1)
コード例 #12
0
ファイル: conman_etcd_test.py プロジェクト: yaron2/conman
class ConManEtcdTest(TestCase):
    @classmethod
    def setUpClass(cls):
        # Start local etcd server if not running
        start_local_etcd_server()

        # Add good key
        cls.good_dict = dict(a='1', b='Yeah, it works!!!')

    @classmethod
    def tearDownClass(cls):
        try:
            kill_local_etcd_server()
        except:  # noqa
            pass

    def setUp(self):
        self.conman = ConManEtcd()

        cli = self.conman.client
        delete_key(cli, 'good')
        delete_key(cli, 'refresh_test')
        set_key(cli, 'good', self.good_dict)

    def tearDown(self):
        delete_key(self.conman.client, 'good')
        delete_key(self.conman.client, 'refresh_test')
        delete_key(self.conman.client, 'watch_test')
        self.conman.stop_watchers()

    def test_initialization(self):
        cli = self.conman.client
        self.assertEqual('http://127.0.0.1:4001', cli.base_uri)
        self.assertEqual('127.0.0.1', cli.host)
        self.assertEqual(4001, cli.port)

    def test_add_good_key(self):
        self.conman.add_key('good')
        expected = self.good_dict
        actual = self.conman['good']
        self.assertEqual(expected, actual)

    def test_add_bad_key(self):
        self.assertRaises(Exception, self.conman.add_key, 'no such key')

    def test_refresh(self):
        self.assertFalse('refresh_test' in self.conman)

        # Insert a new key to etcd
        set_key(self.conman.client, 'refresh_test', dict(a='1'))

        # The new key should still not be visible by conman
        self.assertFalse('refresh_test' in self.conman)

        # Refresh to get the new key
        self.conman.refresh('refresh_test')

        # The new key should now be visible by conman
        self.assertEqual(dict(a='1'), self.conman['refresh_test'])

        # Change the key
        set_key(self.conman.client, 'refresh_test', dict(b='3'))

        # The previous value should still be visible by conman
        self.assertEqual(dict(a='1'), self.conman['refresh_test'])

        # Refresh again
        self.conman.refresh('refresh_test')

        # The new value should now be visible by conman
        self.assertEqual(dict(b='3'), self.conman['refresh_test'])

    def test_dictionary_access(self):
        self.conman.add_key('good')
        self.assertEqual(self.good_dict, self.conman['good'])

    def test_watch_existing_key(self):
        def on_change(change_dict, key, action, value):
            change_dict[key].append((action, value))

        change_dict = defaultdict(list)
        self.assertFalse('watch_test' in self.conman)

        # Insert a new key to etcd
        self.conman.client.write('watch_test/a', 1)

        # The new key should still not be visible by conman
        self.assertFalse('watch_test' in self.conman)

        # Refresh to get the new key
        self.conman.refresh('watch_test')

        # The new key should now be visible by conman
        self.assertEqual(dict(a='1'), self.conman['watch_test'])

        # Set the on_change() callback of conman (normally at construction)
        on_change = partial(on_change, change_dict)
        self.conman.on_change = on_change
        self.conman.watch('watch_test')

        # Change the key
        self.conman.client.write('watch_test/b', '3')

        # The previous value should still be visible by conman
        self.assertEqual(dict(a='1'), self.conman['watch_test'])

        # Wait for the change callback to detect the change
        for i in range(3):
            if change_dict:
                break
            time.sleep(1)

        expected = {'/watch_test/b': [('set', '3')]}
        actual = dict(change_dict)
        self.assertEquals(expected, actual)

        # Refresh again
        self.conman.refresh('watch_test')

        # The new value should now be visible by conman
        self.assertEqual(dict(a='1', b='3'), self.conman['watch_test'])