summaryrefslogtreecommitdiff
path: root/neutron/services/provider_configuration.py
blob: af26a252007e337dc07bd4b1c886566acfe75f2c (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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# Copyright 2013 OpenStack Foundation.
# All Rights Reserved.
#
#    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 importlib
import itertools
import os

from neutron_lib.db import constants as db_const
from neutron_lib.db import utils as db_utils
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from oslo_log import log as logging
from oslo_log import versionutils
import stevedore

from neutron._i18n import _
from neutron.conf.services import provider_configuration as prov_config

LOG = logging.getLogger(__name__)

SERVICE_PROVIDERS = 'neutron.service_providers'

# TODO(HenryG): use MovedGlobals to deprecate this.
serviceprovider_opts = prov_config.serviceprovider_opts

prov_config.register_service_provider_opts()


class NeutronModule(object):
    """A Neutron extension module."""

    def __init__(self, service_module):
        self.module_name = service_module
        self.repo = {
            'mod': self._import_or_none(),
            'ini': None
        }

    def _import_or_none(self):
        try:
            return importlib.import_module(self.module_name)
        except ImportError:
            return None

    def installed(self):
        LOG.debug("NeutronModule installed = %s", self.module_name)
        return self.module_name

    def module(self):
        return self.repo['mod']

    # Return an INI parser for the child module
    def ini(self, neutron_dir=None):
        if self.repo['ini'] is None:
            ini_file = cfg.ConfigOpts()
            prov_config.register_service_provider_opts(ini_file)

            if neutron_dir is not None:
                neutron_dirs = [neutron_dir]
            else:
                try:
                    neutron_dirs = cfg.CONF.config_dirs
                except cfg.NoSuchOptError:
                    neutron_dirs = None
                if not neutron_dirs:
                    neutron_dirs = ['/etc/neutron']

            # load configuration from all matching files to reflect oslo.config
            # behaviour
            config_files = []
            for neutron_dir in neutron_dirs:
                ini_path = os.path.join(neutron_dir,
                                        '%s.conf' % self.module_name)
                if os.path.exists(ini_path):
                    config_files.append(ini_path)

            # NOTE(ihrachys): we could pass project=self.module_name instead to
            # rely on oslo.config to find configuration files for us, but:
            # 1. that would render neutron_dir argument ineffective;
            # 2. that would break loading configuration file from under
            # /etc/neutron in case no --config-dir is passed.
            # That's why we need to explicitly construct CLI here.
            ini_file(args=list(itertools.chain.from_iterable(
                ['--config-file', file_] for file_ in config_files
            )))
            self.repo['ini'] = ini_file

        return self.repo['ini']

    def service_providers(self):
        """Return the service providers for the extension module."""
        providers = []
        # Attempt to read the config from cfg.CONF first; when passing
        # --config-dir, the option is merged from all the definitions
        # made across all the imported config files
        try:
            providers = cfg.CONF.service_providers.service_provider
        except cfg.NoSuchOptError:
            pass

        # Alternatively, if the option is not available, try to load
        # it from the provider module's config file; this may be
        # necessary, if modules are loaded on the fly (DevStack may
        # be an example)
        if not providers:
            providers = self.ini().service_providers.service_provider

            if providers:
                versionutils.report_deprecated_feature(
                    LOG,
                    'Implicit loading of service providers from '
                    'neutron_*.conf files is deprecated and will be '
                    'removed in Ocata release.')

        return providers


# global scope function that should be used in service APIs
def normalize_provider_name(name):
    return name.lower()


def get_provider_driver_class(driver, namespace=SERVICE_PROVIDERS):
    """Return path to provider driver class

    In order to keep backward compatibility with configs < Kilo, we need to
    translate driver class paths after advanced services split. This is done by
    defining old class path as entry point in neutron package.
    """
    try:
        driver_manager = stevedore.driver.DriverManager(
            namespace, driver).driver
    except ImportError:
        return driver
    except RuntimeError:
        return driver
    new_driver = "%s.%s" % (driver_manager.__module__,
                            driver_manager.__name__)
    LOG.warning(
        "The configured driver %(driver)s has been moved, automatically "
        "using %(new_driver)s instead. Please update your config files, "
        "as this automatic fixup will be removed in a future release.",
        {'driver': driver, 'new_driver': new_driver})
    return new_driver


def parse_service_provider_opt(service_module='neutron', service_type=None):

    """Parse service definition opts and returns result."""
    def validate_name(name):
        if len(name) > db_const.NAME_FIELD_SIZE:
            raise n_exc.Invalid(
                _("Provider name %(name)s is limited by %(len)s characters")
                % {'name': name, 'len': db_const.NAME_FIELD_SIZE})

    neutron_mod = NeutronModule(service_module)
    svc_providers_opt = neutron_mod.service_providers()

    LOG.debug("Service providers = %s", svc_providers_opt)

    res = []
    for prov_def in svc_providers_opt:
        split = prov_def.split(':')
        try:
            svc_type, name, driver = split[:3]
            if service_type and service_type != svc_type:
                continue
        except ValueError:
            raise n_exc.Invalid(_("Invalid service provider format"))
        validate_name(name)
        name = normalize_provider_name(name)
        default = False
        if len(split) == 4 and split[3]:
            if split[3] == 'default':
                default = True
            else:
                msg = (_("Invalid provider format. "
                         "Last part should be 'default' or empty: %s") %
                       prov_def)
                LOG.error(msg)
                raise n_exc.Invalid(msg)

        driver = get_provider_driver_class(driver)
        res.append({'service_type': svc_type,
                    'name': name,
                    'driver': driver,
                    'default': default})
    return res


class ServiceProviderNotFound(n_exc.InvalidInput):
    message = _("Service provider '%(provider)s' could not be found "
                "for service type %(service_type)s")


class DefaultServiceProviderNotFound(n_exc.InvalidInput):
    message = _("Service type %(service_type)s does not have a default "
                "service provider")


class ServiceProviderAlreadyAssociated(n_exc.Conflict):
    message = _("Resource '%(resource_id)s' is already associated with "
                "provider '%(provider)s' for service type '%(service_type)s'")


class ProviderConfiguration(object):

    def __init__(self, svc_module='neutron', svc_type=None):
        self.providers = {}
        for prov in parse_service_provider_opt(svc_module, svc_type):
            self.add_provider(prov)

    def _ensure_driver_unique(self, driver):
        for v in self.providers.values():
            if v['driver'] == driver:
                msg = (_("Driver %s is not unique across providers") %
                       driver)
                LOG.error(msg)
                raise n_exc.Invalid(msg)

    def _ensure_default_unique(self, type, default):
        if not default:
            return
        for k, v in self.providers.items():
            if k[0] == type and v['default']:
                msg = _("Multiple default providers "
                        "for service %s") % type
                LOG.error(msg)
                raise n_exc.Invalid(msg)

    def add_provider(self, provider):
        self._ensure_driver_unique(provider['driver'])
        self._ensure_default_unique(provider['service_type'],
                                    provider['default'])
        provider_type = (provider['service_type'], provider['name'])
        if provider_type in self.providers:
            msg = (_("Multiple providers specified for service "
                     "%s") % provider['service_type'])
            LOG.error(msg)
            raise n_exc.Invalid(msg)
        self.providers[provider_type] = {'driver': provider['driver'],
                                         'default': provider['default']}

    def _check_entry(self, k, v, filters):
        # small helper to deal with query filters
        if not filters:
            return True
        for index, key in enumerate(['service_type', 'name']):
            if key in filters:
                if k[index] not in filters[key]:
                    return False

        for key in ['driver', 'default']:
            if key in filters:
                if v[key] not in filters[key]:
                    return False
        return True

    def get_service_providers(self, filters=None, fields=None):
        return [db_utils.resource_fields({'service_type': k[0],
                                          'name': k[1],
                                          'driver': v['driver'],
                                          'default': v['default']},
                                         fields)
                for k, v in self.providers.items()
                if self._check_entry(k, v, filters)]