aboutsummaryrefslogtreecommitdiff
path: root/kolla_ansible/kolla_address.py
blob: 52d46e166f5fa0dc4bce4414b20676f812bae1d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# -*- coding: utf-8 -*-
#
# Copyright 2019 Radosław Piliszek (yoctozepto)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import ipaddress

from jinja2.filters import pass_context
from jinja2.runtime import Undefined

from kolla_ansible.exception import FilterError
from kolla_ansible.helpers import _call_bool_filter


def is_loopback(address):
    """Determines if the provided IP address is a loopback address.

    :param address: string representing an IP address (IPv4 or IPv6)
    :returns: bool indicating whether the address is a loopback address
    """

    ip = ipaddress.ip_address(address)
    return ip.is_loopback


@pass_context
def kolla_address(context, network_name, hostname=None, override_var=None):
    """returns IP address on the requested network

    The output is affected by '<network_name>_*' variables:
    '<network_name>_interface' sets the interface to obtain address for.
    '<network_name>_address_family' controls the address family (ipv4/ipv6).

    :param context: Jinja2 Context
    :param network_name: string denoting the name of the network to get IP
                         address for, e.g. 'api'
    :param hostname: to override host which address is retrieved for
    :param override_var: optional name of a host variable that can be used
                         to override the IP address
    :returns: string with IP address
    """

    # NOTE(yoctozepto): watch out as Jinja2 'context' behaves not exactly like
    # the python 'dict' (but mimics it most of the time)
    # for example it returns a special object of type 'Undefined' instead of
    # 'None' or value specified as default for 'get' method
    # 'HostVars' shares this behavior

    if hostname is None:
        hostname = context.get('inventory_hostname')
        if isinstance(hostname, Undefined):
            raise FilterError("'inventory_hostname' variable is unavailable")

    hostvars = context.get('hostvars')
    if hostvars is None or isinstance(hostvars, Undefined):
        raise FilterError("'hostvars' variable is unavailable")

    host = hostvars.get(hostname)
    if isinstance(host, Undefined):
        raise FilterError("'{hostname}' not in 'hostvars'"
                          .format(hostname=hostname))

    del hostvars  # remove for clarity (no need for other hosts)

    if override_var is not None:
        if override_var in host:
            try:
                # Use ipaddress to test IPv4/6 validity of
                # the string returned from override_var and
                # return the string-formatted, valid IP address
                ip = ipaddress.ip_address(host[override_var])
                return format(ip)
            except ValueError:
                # Catch ValueError from ipaddress and make the
                # output more useful for operators
                raise FilterError("variable '{override_var}' for "
                                  "host '{hostname}' is set to "
                                  "'{value}', which is not a valid "
                                  "IPv4 or IPv6 address"
                                  .format(override_var=override_var,
                                          hostname=hostname,
                                          value=host[override_var]))

    # NOTE(yoctozepto): variable "host" will *not* return Undefined
    # same applies to all its children (act like plain dictionary)

    interface_name = host.get(network_name + '_interface')
    if interface_name is None:
        raise FilterError("Interface name undefined "
                          "for network '{network_name}' "
                          "(set '{network_name}_interface')"
                          .format(network_name=network_name))

    address_family = host.get(network_name + '_address_family')
    if address_family is None:
        raise FilterError("Address family undefined "
                          "for network '{network_name}' "
                          "(set '{network_name}_address_family')"
                          .format(network_name=network_name))
    address_family = address_family.lower()
    if address_family not in ['ipv4', 'ipv6']:
        raise FilterError("Unknown address family '{address_family}' "
                          "for network '{network_name}'"
                          .format(address_family=address_family,
                                  network_name=network_name))

    ansible_interface_name = interface_name.replace('-', '_')
    interface = host['ansible_facts'].get(ansible_interface_name)
    if interface is None:
        raise FilterError("Interface '{interface_name}' "
                          "not present "
                          "on host '{hostname}'"
                          .format(interface_name=interface_name,
                                  hostname=hostname))

    af_interface = interface.get(address_family)
    if af_interface is None:
        raise FilterError("Address family '{address_family}' undefined "
                          "on interface '{interface_name}' "
                          "for host: '{hostname}'"
                          .format(address_family=address_family,
                                  interface_name=interface_name,
                                  hostname=hostname))

    if address_family == 'ipv4':
        address = af_interface.get('address')

        # NOTE(ednxzu): we do the following code in case the primary
        # address of the interface is a loopback address. This is
        # because sometimes, for example when doing BGP to the host,
        # the host's ip address will be mounted on the loopback interface,
        # as a secondary ip. This ensures that if there is a secondary
        # address on a loopback interface, this secondary address is used.

        non_loopback_addresses = []
        if address and is_loopback(address):
            ipv4_secondaries = interface.get("ipv4_secondaries", [])
            non_loopback_addresses = [
                sec["address"] for sec in ipv4_secondaries
                if not is_loopback(sec["address"])
            ]

        if non_loopback_addresses:
            address = non_loopback_addresses[0]

    elif address_family == 'ipv6':
        # ipv6 has no concept of a secondary address
        # explicitly exclude the vip addresses
        # to avoid excluding all /128

        haproxy_enabled = host.get('enable_haproxy')
        if haproxy_enabled is None:
            raise FilterError("'enable_haproxy' variable is unavailable")
        haproxy_enabled = _call_bool_filter(context, haproxy_enabled)

        if haproxy_enabled:
            vip_addresses = [
                host.get('kolla_internal_vip_address'),
                host.get('kolla_external_vip_address'),
            ]
        else:
            # no addresses are virtual (kolla-wise)
            vip_addresses = []

        global_ipv6_addresses = [x for x in af_interface if
                                 x['scope'] == 'global' and
                                 x['address'] not in vip_addresses]

        if global_ipv6_addresses:
            address = global_ipv6_addresses[0]['address']
        else:
            address = None

    if address is None:
        raise FilterError("{address_family} address missing "
                          "on interface '{interface_name}' "
                          "for host '{hostname}'"
                          .format(address_family=address_family,
                                  interface_name=interface_name,
                                  hostname=hostname))

    return address