def test_resolve_hostname_to_ip_ips(self):
     ipv6_address = '2a01:348:2f4:0:dba7:dc58:659b:941f'
     ipv4_address = '10.10.10.2'
     self.assertEqual(percona_utils.resolve_hostname_to_ip(ipv6_address),
                      ipv6_address)
     self.assertEqual(percona_utils.resolve_hostname_to_ip(ipv4_address),
                      ipv4_address)
 def test_resolve_hostname_to_ip_ips(self, mock_is_ip, mock_is_ipv6):
     ipv6_address = '2a01:348:2f4:0:dba7:dc58:659b:941f'
     ipv4_address = '10.10.10.2'
     self.assertEqual(percona_utils.resolve_hostname_to_ip(ipv6_address),
                      ipv6_address)
     self.assertTrue(mock_is_ip.called)
     self.assertFalse(mock_is_ipv6.called)
     self.assertEqual(percona_utils.resolve_hostname_to_ip(ipv4_address),
                      ipv4_address)
     self.assertTrue(mock_is_ip.called)
     self.assertFalse(mock_is_ipv6.called)
 def test_resolve_hostname_to_ip_hostname_noanswer(self, dns_query):
     dns_query.return_value = []
     self.assertEqual(percona_utils.resolve_hostname_to_ip('myhostname'),
                      None)
     dns_query.assert_has_calls([
         mock.call('myhostname', 'A'),
     ])
 def test_resolve_hostname_to_ip_hostname_a(self, dns_query):
     mock_answer = mock.MagicMock()
     mock_answer.address = '10.10.10.20'
     dns_query.return_value = [mock_answer]
     self.assertEqual(percona_utils.resolve_hostname_to_ip('myhostname'),
                      '10.10.10.20')
     dns_query.assert_has_calls([
         mock.call('myhostname', 'A'),
     ])
 def test_resolve_hostname_to_ip_hostname_aaaa(self,
                                               dns_query):
     mock_answer = mock.MagicMock()
     mock_answer.address = '2a01:348:2f4:0:dba7:dc58:659b:941f'
     dns_query.return_value = [mock_answer]
     self.assertEqual(percona_utils.resolve_hostname_to_ip('myhostname',
                                                           ipv6=True),
                      '2a01:348:2f4:0:dba7:dc58:659b:941f')
     dns_query.assert_has_calls([
         mock.call('myhostname', 'AAAA'),
     ])
    def test_resolve_hostname_to_ip_hostname_aaaa(self, dns_query, mock_config,
                                                  mock_is_ip, mock_is_ipv6):
        def fake_config(key):
            return {'prefer-ipv6': True}.get(key)

        mock_config.side_effect = fake_config
        mock_answer = mock.MagicMock()
        mock_is_ipv6.return_value = False
        mock_answer.address = '2a01:348:2f4:0:dba7:dc58:659b:941f'
        dns_query.return_value = [mock_answer]
        self.assertEqual(percona_utils.resolve_hostname_to_ip('myhostname'),
                         '2a01:348:2f4:0:dba7:dc58:659b:941f')
        self.assertFalse(mock_is_ip.called)
        self.assertTrue(mock_is_ipv6.called)
        dns_query.assert_has_calls([
            mock.call('myhostname', 'AAAA'),
        ])
def shared_db_changed(relation_id=None, unit=None):
    if not seeded():
        log(
            "Percona cluster not yet bootstrapped - deferring shared-db rel "
            "until bootstrapped", DEBUG)
        return

    if not is_leader() and client_node_is_ready():
        clear_and_populate_client_db_relations(relation_id, 'shared-db')
        return

    # Bail if leader is not ready
    if not leader_node_is_ready():
        return

    settings = relation_get(unit=unit, rid=relation_id)
    access_network = config('access-network')
    db_helper = get_db_helper()

    peer_store_and_set(relation_id=relation_id,
                       relation_settings={'access-network': access_network})

    singleset = set(['database', 'username', 'hostname'])
    if singleset.issubset(settings):
        # Process a single database configuration
        hostname = settings['hostname']
        database = settings['database']
        username = settings['username']

        normalized_address = resolve_hostname_to_ip(hostname)
        if access_network and not is_address_in_network(
                access_network, normalized_address):
            # NOTE: for configurations using access-network, only setup
            #       database access if remote unit has presented a
            #       hostname or ip address thats within the configured
            #       network cidr
            log("Host '%s' not in access-network '%s' - ignoring" %
                (normalized_address, access_network),
                level=INFO)
            return

        # NOTE: do this before querying access grants
        password = configure_db_for_hosts(hostname, database, username,
                                          db_helper)

        allowed_units = db_helper.get_allowed_units(database,
                                                    username,
                                                    relation_id=relation_id)
        allowed_units = unit_sorted(allowed_units)
        allowed_units = ' '.join(allowed_units)
        relation_set(relation_id=relation_id, allowed_units=allowed_units)

        db_host = get_db_host(hostname)
        peer_store_and_set(relation_id=relation_id,
                           db_host=db_host,
                           password=password,
                           allowed_units=allowed_units)
    else:
        # Process multiple database setup requests.
        # from incoming relation data:
        #  nova_database=xxx nova_username=xxx nova_hostname=xxx
        #  quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
        # create
        # {
        #   "nova": {
        #        "username": xxx,
        #        "database": xxx,
        #        "hostname": xxx
        #    },
        #    "quantum": {
        #        "username": xxx,
        #        "database": xxx,
        #        "hostname": xxx
        #    }
        # }
        #
        databases = {}
        for k, v in settings.iteritems():
            db = k.split('_')[0]
            x = '_'.join(k.split('_')[1:])
            if db not in databases:
                databases[db] = {}
            databases[db][x] = v

        allowed_units = {}
        return_data = {}
        for db in databases:
            if singleset.issubset(databases[db]):
                database = databases[db]['database']
                hostname = databases[db]['hostname']
                username = databases[db]['username']

                normalized_address = resolve_hostname_to_ip(hostname)
                if (access_network and not is_address_in_network(
                        access_network, normalized_address)):
                    # NOTE: for configurations using access-network,
                    #       only setup database access if remote unit
                    #       has presented a hostname or ip address
                    #       thats within the configured network cidr
                    return

                # NOTE: do this before querying access grants
                password = configure_db_for_hosts(hostname, database, username,
                                                  db_helper)

                a_units = db_helper.get_allowed_units(database,
                                                      username,
                                                      relation_id=relation_id)
                a_units = ' '.join(unit_sorted(a_units))
                allowed_units_key = '%s_allowed_units' % (db)
                allowed_units[allowed_units_key] = a_units

                return_data['%s_password' % (db)] = password
                return_data[allowed_units_key] = a_units
                db_host = get_db_host(hostname)

        if allowed_units:
            relation_set(relation_id=relation_id, **allowed_units)
        else:
            log("No allowed_units - not setting relation settings",
                level=DEBUG)

        if return_data:
            peer_store_and_set(relation_id=relation_id,
                               db_host=db_host,
                               **return_data)
        else:
            log("No return data - not setting relation settings", level=DEBUG)
def get_db_host(client_hostname, interface='shared-db'):
    """Get address of local database host for use by db clients

    If an access-network has been configured, expect selected address to be
    on that network. If none can be found, revert to primary address.

    If network spaces are supported (Juju >= 2.0), use network-get to
    retrieve the network binding for the interface.

    If DNSHA is set pass os-access-hostname

    If vip(s) are configured, chooses first available.

    @param client_hostname: hostname of client side relation setting hostname.
                            Only used if access-network is configured
    @param interface: Network space binding to check.
                      Usually the relationship name.
    @returns IP for use with db clients
    """
    vips = config('vip').split() if config('vip') else []
    dns_ha = config('dns-ha')
    access_network = config('access-network')
    if is_clustered() and dns_ha:
        log("Using DNS HA hostname: {}".format(config('os-access-hostname')))
        return config('os-access-hostname')
    elif access_network:
        client_ip = resolve_hostname_to_ip(client_hostname)
        if is_address_in_network(access_network, client_ip):
            if is_clustered():
                for vip in vips:
                    if is_address_in_network(access_network, vip):
                        return vip

                log("Unable to identify a VIP in the access-network '%s'" %
                    (access_network),
                    level=WARNING)
            else:
                return get_address_in_network(access_network)
        else:
            log("Client address '%s' not in access-network '%s'" %
                (client_ip, access_network),
                level=WARNING)
    else:
        try:
            # NOTE(jamespage)
            # Try to use network spaces to resolve binding for
            # interface, and to resolve the VIP associated with
            # the binding if provided.
            interface_binding = network_get_primary_address(interface)
            if is_clustered() and vips:
                interface_cidr = resolve_network_cidr(interface_binding)
                for vip in vips:
                    if is_address_in_network(interface_cidr, vip):
                        return vip
            return interface_binding
        except NotImplementedError:
            # NOTE(jamespage): skip - fallback to previous behaviour
            pass

    if is_clustered() and vips:
        return vips[0]  # NOTE on private network

    if config('prefer-ipv6'):
        return get_ipv6_addr(exc_list=vips)[0]

    # Last resort
    return unit_get('private-address')
def shared_db_changed(relation_id=None, unit=None):
    if not seeded():
        log(
            "Percona cluster not yet bootstrapped - deferring shared-db rel "
            "until bootstrapped", DEBUG)
        return

    if not is_leader() and client_node_is_ready():
        # NOTE(jamespage): relation level data candidate
        log('Service is peered, clearing shared-db relation '
            'as this service unit is not the leader')
        relation_clear(relation_id)
        # Each unit needs to set the db information otherwise if the unit
        # with the info dies the settings die with it Bug# 1355848
        if is_relation_made('cluster'):
            for rel_id in relation_ids('shared-db'):
                peerdb_settings = \
                    peer_retrieve_by_prefix(rel_id, exc_list=['hostname'])

                passwords = [
                    key for key in peerdb_settings.keys()
                    if 'password' in key.lower()
                ]
                if len(passwords) > 0:
                    relation_set(relation_id=rel_id, **peerdb_settings)
        return

    # Bail if leader is not ready
    if not leader_node_is_ready():
        return

    settings = relation_get(unit=unit, rid=relation_id)
    access_network = config('access-network')
    db_helper = get_db_helper()

    peer_store_and_set(relation_id=relation_id,
                       relation_settings={'access-network': access_network})

    singleset = set(['database', 'username', 'hostname'])
    if singleset.issubset(settings):
        # Process a single database configuration
        hostname = settings['hostname']
        database = settings['database']
        username = settings['username']

        normalized_address = resolve_hostname_to_ip(hostname)
        if access_network and not is_address_in_network(
                access_network, normalized_address):
            # NOTE: for configurations using access-network, only setup
            #       database access if remote unit has presented a
            #       hostname or ip address thats within the configured
            #       network cidr
            log("Host '%s' not in access-network '%s' - ignoring" %
                (normalized_address, access_network),
                level=INFO)
            return

        # NOTE: do this before querying access grants
        password = configure_db_for_hosts(hostname, database, username,
                                          db_helper)

        allowed_units = db_helper.get_allowed_units(database,
                                                    username,
                                                    relation_id=relation_id)
        allowed_units = unit_sorted(allowed_units)
        allowed_units = ' '.join(allowed_units)
        relation_set(relation_id=relation_id, allowed_units=allowed_units)

        db_host = get_db_host(hostname)
        peer_store_and_set(relation_id=relation_id,
                           db_host=db_host,
                           password=password)
    else:
        # Process multiple database setup requests.
        # from incoming relation data:
        #  nova_database=xxx nova_username=xxx nova_hostname=xxx
        #  quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
        # create
        # {
        #   "nova": {
        #        "username": xxx,
        #        "database": xxx,
        #        "hostname": xxx
        #    },
        #    "quantum": {
        #        "username": xxx,
        #        "database": xxx,
        #        "hostname": xxx
        #    }
        # }
        #
        databases = {}
        for k, v in settings.iteritems():
            db = k.split('_')[0]
            x = '_'.join(k.split('_')[1:])
            if db not in databases:
                databases[db] = {}
            databases[db][x] = v

        allowed_units = {}
        return_data = {}
        for db in databases:
            if singleset.issubset(databases[db]):
                database = databases[db]['database']
                hostname = databases[db]['hostname']
                username = databases[db]['username']

                normalized_address = resolve_hostname_to_ip(hostname)
                if (access_network and not is_address_in_network(
                        access_network, normalized_address)):
                    # NOTE: for configurations using access-network,
                    #       only setup database access if remote unit
                    #       has presented a hostname or ip address
                    #       thats within the configured network cidr
                    return

                # NOTE: do this before querying access grants
                password = configure_db_for_hosts(hostname, database, username,
                                                  db_helper)

                a_units = db_helper.get_allowed_units(database,
                                                      username,
                                                      relation_id=relation_id)
                a_units = ' '.join(unit_sorted(a_units))
                allowed_units['%s_allowed_units' % (db)] = a_units

                return_data['%s_password' % (db)] = password
                db_host = get_db_host(hostname)

        if allowed_units:
            relation_set(relation_id=relation_id, **allowed_units)
        else:
            log("No allowed_units - not setting relation settings",
                level=DEBUG)

        if return_data:
            peer_store_and_set(relation_id=relation_id,
                               db_host=db_host,
                               **return_data)
        else:
            log("No return data - not setting relation settings", level=DEBUG)
def shared_db_changed(relation_id=None, unit=None):
    if not seeded():
        log("Percona cluster not yet bootstrapped - deferring shared-db rel "
            "until bootstrapped", DEBUG)
        return

    if not is_leader() and client_node_is_ready():
        clear_and_populate_client_db_relations(relation_id, 'shared-db')
        return

    # Bail if leader is not ready
    if not leader_node_is_ready():
        return

    settings = relation_get(unit=unit, rid=relation_id)
    access_network = config('access-network')
    db_helper = get_db_helper()

    peer_store_and_set(relation_id=relation_id,
                       relation_settings={'access-network': access_network})

    singleset = set(['database', 'username', 'hostname'])
    if singleset.issubset(settings):
        # Process a single database configuration
        hostname = settings['hostname']
        database = settings['database']
        username = settings['username']

        normalized_address = resolve_hostname_to_ip(hostname)
        if access_network and not is_address_in_network(access_network,
                                                        normalized_address):
            # NOTE: for configurations using access-network, only setup
            #       database access if remote unit has presented a
            #       hostname or ip address thats within the configured
            #       network cidr
            log("Host '%s' not in access-network '%s' - ignoring" %
                (normalized_address, access_network), level=INFO)
            return

        # NOTE: do this before querying access grants
        password = configure_db_for_hosts(hostname, database, username,
                                          db_helper)

        allowed_units = db_helper.get_allowed_units(database, username,
                                                    relation_id=relation_id)
        allowed_units = unit_sorted(allowed_units)
        allowed_units = ' '.join(allowed_units)
        relation_set(relation_id=relation_id, allowed_units=allowed_units)

        db_host = get_db_host(hostname)
        peer_store_and_set(relation_id=relation_id,
                           db_host=db_host,
                           password=password,
                           allowed_units=allowed_units)
    else:
        # Process multiple database setup requests.
        # from incoming relation data:
        #  nova_database=xxx nova_username=xxx nova_hostname=xxx
        #  quantum_database=xxx quantum_username=xxx quantum_hostname=xxx
        # create
        # {
        #   "nova": {
        #        "username": xxx,
        #        "database": xxx,
        #        "hostname": xxx
        #    },
        #    "quantum": {
        #        "username": xxx,
        #        "database": xxx,
        #        "hostname": xxx
        #    }
        # }
        #
        databases = {}
        for k, v in settings.iteritems():
            db = k.split('_')[0]
            x = '_'.join(k.split('_')[1:])
            if db not in databases:
                databases[db] = {}
            databases[db][x] = v

        allowed_units = {}
        return_data = {}
        for db in databases:
            if singleset.issubset(databases[db]):
                database = databases[db]['database']
                hostname = databases[db]['hostname']
                username = databases[db]['username']

                normalized_address = resolve_hostname_to_ip(hostname)
                if (access_network and
                        not is_address_in_network(access_network,
                                                  normalized_address)):
                    # NOTE: for configurations using access-network,
                    #       only setup database access if remote unit
                    #       has presented a hostname or ip address
                    #       thats within the configured network cidr
                    return

                # NOTE: do this before querying access grants
                password = configure_db_for_hosts(hostname, database, username,
                                                  db_helper)

                a_units = db_helper.get_allowed_units(database, username,
                                                      relation_id=relation_id)
                a_units = ' '.join(unit_sorted(a_units))
                allowed_units_key = '%s_allowed_units' % (db)
                allowed_units[allowed_units_key] = a_units

                return_data['%s_password' % (db)] = password
                return_data[allowed_units_key] = a_units
                db_host = get_db_host(hostname)

        if allowed_units:
            relation_set(relation_id=relation_id, **allowed_units)
        else:
            log("No allowed_units - not setting relation settings",
                level=DEBUG)

        if return_data:
            peer_store_and_set(relation_id=relation_id, db_host=db_host,
                               **return_data)
        else:
            log("No return data - not setting relation settings", level=DEBUG)
def get_db_host(client_hostname, interface='shared-db'):
    """Get address of local database host for use by db clients

    If an access-network has been configured, expect selected address to be
    on that network. If none can be found, revert to primary address.

    If network spaces are supported (Juju >= 2.0), use network-get to
    retrieve the network binding for the interface.

    If DNSHA is set pass os-access-hostname

    If vip(s) are configured, chooses first available.

    @param client_hostname: hostname of client side relation setting hostname.
                            Only used if access-network is configured
    @param interface: Network space binding to check.
                      Usually the relationship name.
    @returns IP for use with db clients
    """
    vips = config('vip').split() if config('vip') else []
    dns_ha = config('dns-ha')
    access_network = config('access-network')
    if is_clustered() and dns_ha:
        log("Using DNS HA hostname: {}".format(config('os-access-hostname')))
        return config('os-access-hostname')
    elif access_network:
        client_ip = resolve_hostname_to_ip(client_hostname)
        if is_address_in_network(access_network, client_ip):
            if is_clustered():
                for vip in vips:
                    if is_address_in_network(access_network, vip):
                        return vip

                log("Unable to identify a VIP in the access-network '%s'" %
                    (access_network), level=WARNING)
            else:
                return get_address_in_network(access_network)
        else:
            log("Client address '%s' not in access-network '%s'" %
                (client_ip, access_network), level=WARNING)
    else:
        try:
            # NOTE(jamespage)
            # Try to use network spaces to resolve binding for
            # interface, and to resolve the VIP associated with
            # the binding if provided.
            interface_binding = network_get_primary_address(interface)
            if is_clustered() and vips:
                interface_cidr = resolve_network_cidr(interface_binding)
                for vip in vips:
                    if is_address_in_network(interface_cidr, vip):
                        return vip
            return interface_binding
        except NotImplementedError:
            # NOTE(jamespage): skip - fallback to previous behaviour
            pass

    if is_clustered() and vips:
        return vips[0]  # NOTE on private network

    if config('prefer-ipv6'):
        return get_ipv6_addr(exc_list=vips)[0]

    # Last resort
    return unit_get('private-address')