summaryrefslogtreecommitdiff
path: root/ironic/api/controllers/v1/driver.py
blob: 5f5b1ec71ff5ebfd01885999fc52cbf3e0b1a581 (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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# Copyright 2013 Red Hat, Inc.
# 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.

from ironic_lib import metrics_utils
import pecan
from pecan import rest
from six.moves import http_client
import wsme
from wsme import types as wtypes

from ironic.api.controllers import base
from ironic.api.controllers import link
from ironic.api.controllers.v1 import types
from ironic.api.controllers.v1 import utils as api_utils
from ironic.api import expose
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import policy
from ironic.drivers import base as driver_base


METRICS = metrics_utils.get_metrics_logger(__name__)

# Property information for drivers:
#   key = driver name;
#   value = dictionary of properties of that driver:
#             key = property name.
#             value = description of the property.
# NOTE(rloo). This is cached for the lifetime of the API service. If one or
# more conductor services are restarted with new driver versions, the API
# service should be restarted.
_DRIVER_PROPERTIES = {}

# Vendor information for drivers:
#   key = driver name;
#   value = dictionary of vendor methods of that driver:
#             key = method name.
#             value = dictionary with the metadata of that method.
# NOTE(lucasagomes). This is cached for the lifetime of the API
# service. If one or more conductor services are restarted with new driver
# versions, the API service should be restarted.
_VENDOR_METHODS = {}

# RAID (logical disk) configuration information for drivers:
#   key = driver name;
#   value = dictionary of RAID configuration information of that driver:
#             key = property name.
#             value = description of the property
# NOTE(rloo). This is cached for the lifetime of the API service. If one or
# more conductor services are restarted with new driver versions, the API
# service should be restarted.
_RAID_PROPERTIES = {}


class Driver(base.APIBase):
    """API representation of a driver."""

    name = wtypes.text
    """The name of the driver"""

    hosts = [wtypes.text]
    """A list of active conductors that support this driver"""

    type = wtypes.text
    """Whether the driver is classic or dynamic (hardware type)"""

    links = wsme.wsattr([link.Link], readonly=True)
    """A list containing self and bookmark links"""

    properties = wsme.wsattr([link.Link], readonly=True)
    """A list containing links to driver properties"""

    """Default interface for a hardware type"""
    default_boot_interface = wtypes.text
    default_console_interface = wtypes.text
    default_deploy_interface = wtypes.text
    default_inspect_interface = wtypes.text
    default_management_interface = wtypes.text
    default_network_interface = wtypes.text
    default_power_interface = wtypes.text
    default_raid_interface = wtypes.text
    default_vendor_interface = wtypes.text

    """A list of enabled interfaces for a hardware type"""
    enabled_boot_interfaces = [wtypes.text]
    enabled_console_interfaces = [wtypes.text]
    enabled_deploy_interfaces = [wtypes.text]
    enabled_inspect_interfaces = [wtypes.text]
    enabled_management_interfaces = [wtypes.text]
    enabled_network_interfaces = [wtypes.text]
    enabled_power_interfaces = [wtypes.text]
    enabled_raid_interfaces = [wtypes.text]
    enabled_vendor_interfaces = [wtypes.text]

    @staticmethod
    def convert_with_links(name, hosts, driver_type, detail=False,
                           interface_info=None):
        """Convert driver/hardware type info to an API-serializable object.

        :param name: name of driver or hardware type.
        :param hosts: list of conductor hostnames driver is active on.
        :param driver_type: 'classic' for classic drivers, 'dynamic' for
                            hardware types.
        :param detail: boolean, whether to include detailed info, such as
                       the 'type' field and default/enabled interfaces fields.
        :param interface_info: optional list of dicts of hardware interface
                               info.
        :returns: API-serializable driver object.
        """
        driver = Driver()
        driver.name = name
        driver.hosts = hosts
        driver.links = [
            link.Link.make_link('self',
                                pecan.request.public_url,
                                'drivers', name),
            link.Link.make_link('bookmark',
                                pecan.request.public_url,
                                'drivers', name,
                                bookmark=True)
        ]
        if api_utils.allow_links_node_states_and_driver_properties():
            driver.properties = [
                link.Link.make_link('self',
                                    pecan.request.public_url,
                                    'drivers', name + "/properties"),
                link.Link.make_link('bookmark',
                                    pecan.request.public_url,
                                    'drivers', name + "/properties",
                                    bookmark=True)
            ]

        if api_utils.allow_dynamic_drivers():
            driver.type = driver_type
            if driver_type == 'dynamic' and detail:
                if interface_info is None:
                    # TODO(jroll) objectify this
                    interface_info = (pecan.request.dbapi
                                      .list_hardware_type_interfaces([name]))
                for iface_type in driver_base.ALL_INTERFACES:
                    default = None
                    enabled = set()
                    for iface in interface_info:
                        if iface['interface_type'] == iface_type:
                            iface_name = iface['interface_name']
                            enabled.add(iface_name)
                            # NOTE(jroll) this assumes the default is the same
                            # on all conductors
                            if iface['default']:
                                default = iface_name

                    default_key = 'default_%s_interface' % iface_type
                    enabled_key = 'enabled_%s_interfaces' % iface_type
                    setattr(driver, default_key, default)
                    setattr(driver, enabled_key, list(enabled))

            elif detail:
                for iface_type in driver_base.ALL_INTERFACES:
                    # always return None for classic drivers
                    setattr(driver, 'default_%s_interface' % iface_type, None)
                    setattr(driver, 'enabled_%s_interfaces' % iface_type, None)

        return driver

    @classmethod
    def sample(cls):
        attrs = {
            'name': 'sample-driver',
            'hosts': ['fake-host'],
            'type': 'classic',
        }
        for iface_type in driver_base.ALL_INTERFACES:
            attrs['default_%s_interface' % iface_type] = None
            attrs['enabled_%s_interfaces' % iface_type] = None

        sample = cls(**attrs)
        return sample


class DriverList(base.APIBase):
    """API representation of a list of drivers."""

    drivers = [Driver]
    """A list containing drivers objects"""

    @staticmethod
    def convert_with_links(drivers, hardware_types, detail=False):
        """Convert drivers and hardware types to an API-serializable object.

        :param drivers: dict mapping driver names to conductor hostnames.
        :param hardware_types: dict mapping hardware type names to conductor
                               hostnames.
        :param detail: boolean, whether to include detailed info, such as
                       the 'type' field and default/enabled interfaces fields.
        :returns: an API-serializable driver collection object.
        """
        collection = DriverList()
        collection.drivers = [
            Driver.convert_with_links(dname, list(drivers[dname]), 'classic',
                                      detail=detail)
            for dname in drivers]

        # NOTE(jroll) we return hardware types in all API versions,
        # but restrict type/default/enabled fields to 1.30.
        # This is checked in Driver.convert_with_links(), however also
        # checking here can save us a DB query.
        if api_utils.allow_dynamic_drivers() and detail:
            iface_info = pecan.request.dbapi.list_hardware_type_interfaces(
                list(hardware_types))
        else:
            iface_info = []

        for htname in hardware_types:
            interface_info = [i for i in iface_info
                              if i['hardware_type'] == htname]
            collection.drivers.append(
                Driver.convert_with_links(htname,
                                          list(hardware_types[htname]),
                                          'dynamic', detail=detail,
                                          interface_info=interface_info))
        return collection

    @classmethod
    def sample(cls):
        sample = cls()
        sample.drivers = [Driver.sample()]
        return sample


class DriverPassthruController(rest.RestController):
    """REST controller for driver passthru.

    This controller allow vendors to expose cross-node functionality in the
    Ironic API. Ironic will merely relay the message from here to the specified
    driver, no introspection will be made in the message body.
    """

    _custom_actions = {
        'methods': ['GET']
    }

    @METRICS.timer('DriverPassthruController.methods')
    @expose.expose(wtypes.text, wtypes.text)
    def methods(self, driver_name):
        """Retrieve information about vendor methods of the given driver.

        :param driver_name: name of the driver.
        :returns: dictionary with <vendor method name>:<method metadata>
                  entries.
        :raises: DriverNotFound if the driver name is invalid or the
                 driver cannot be loaded.
        """
        cdict = pecan.request.context.to_policy_values()
        policy.authorize('baremetal:driver:vendor_passthru', cdict, cdict)

        if driver_name not in _VENDOR_METHODS:
            topic = pecan.request.rpcapi.get_topic_for_driver(driver_name)
            ret = pecan.request.rpcapi.get_driver_vendor_passthru_methods(
                pecan.request.context, driver_name, topic=topic)
            _VENDOR_METHODS[driver_name] = ret

        return _VENDOR_METHODS[driver_name]

    @METRICS.timer('DriverPassthruController._default')
    @expose.expose(wtypes.text, wtypes.text, wtypes.text,
                   body=wtypes.text)
    def _default(self, driver_name, method, data=None):
        """Call a driver API extension.

        :param driver_name: name of the driver to call.
        :param method: name of the method, to be passed to the vendor
                       implementation.
        :param data: body of data to supply to the specified method.
        """
        cdict = pecan.request.context.to_policy_values()
        if method == "lookup":
            policy.authorize('baremetal:driver:ipa_lookup', cdict, cdict)
        else:
            policy.authorize('baremetal:driver:vendor_passthru', cdict, cdict)

        topic = pecan.request.rpcapi.get_topic_for_driver(driver_name)
        return api_utils.vendor_passthru(driver_name, method, topic, data=data,
                                         driver_passthru=True)


class DriverRaidController(rest.RestController):

    _custom_actions = {
        'logical_disk_properties': ['GET']
    }

    @METRICS.timer('DriverRaidController.logical_disk_properties')
    @expose.expose(types.jsontype, wtypes.text)
    def logical_disk_properties(self, driver_name):
        """Returns the logical disk properties for the driver.

        :param driver_name: Name of the driver.
        :returns: A dictionary containing the properties that can be mentioned
            for logical disks and a textual description for them.
        :raises: UnsupportedDriverExtension if the driver doesn't
            support RAID configuration.
        :raises: NotAcceptable, if requested version of the API is less than
            1.12.
        :raises: DriverNotFound, if driver is not loaded on any of the
            conductors.
        """
        cdict = pecan.request.context.to_policy_values()
        policy.authorize('baremetal:driver:get_raid_logical_disk_properties',
                         cdict, cdict)

        if not api_utils.allow_raid_config():
            raise exception.NotAcceptable()

        if driver_name not in _RAID_PROPERTIES:
            topic = pecan.request.rpcapi.get_topic_for_driver(driver_name)
            try:
                info = pecan.request.rpcapi.get_raid_logical_disk_properties(
                    pecan.request.context, driver_name, topic=topic)
            except exception.UnsupportedDriverExtension as e:
                # Change error code as 404 seems appropriate because RAID is a
                # standard interface and all drivers might not have it.
                e.code = http_client.NOT_FOUND
                raise

            _RAID_PROPERTIES[driver_name] = info
        return _RAID_PROPERTIES[driver_name]


class DriversController(rest.RestController):
    """REST controller for Drivers."""

    vendor_passthru = DriverPassthruController()

    raid = DriverRaidController()
    """Expose RAID as a sub-element of drivers"""

    _custom_actions = {
        'properties': ['GET'],
    }

    @METRICS.timer('DriversController.get_all')
    @expose.expose(DriverList, wtypes.text, types.boolean)
    def get_all(self, type=None, detail=None):
        """Retrieve a list of drivers."""
        # FIXME(deva): formatting of the auto-generated REST API docs
        #              will break from a single-line doc string.
        #              This is a result of a bug in sphinxcontrib-pecanwsme
        # https://github.com/dreamhost/sphinxcontrib-pecanwsme/issues/8
        cdict = pecan.request.context.to_policy_values()
        policy.authorize('baremetal:driver:get', cdict, cdict)

        api_utils.check_allow_driver_detail(detail)
        api_utils.check_allow_filter_driver_type(type)
        if type not in (None, 'classic', 'dynamic'):
            raise exception.Invalid(_(
                '"type" filter must be one of "classic" or "dynamic", '
                'if specified.'))

        driver_list = {}
        hw_type_dict = {}
        if type is None or type == 'classic':
            driver_list = pecan.request.dbapi.get_active_driver_dict()
        if type is None or type == 'dynamic':
            hw_type_dict = pecan.request.dbapi.get_active_hardware_type_dict()
        return DriverList.convert_with_links(driver_list, hw_type_dict,
                                             detail=detail)

    @METRICS.timer('DriversController.get_one')
    @expose.expose(Driver, wtypes.text)
    def get_one(self, driver_name):
        """Retrieve a single driver."""
        # NOTE(russell_h): There is no way to make this more efficient than
        # retrieving a list of drivers using the current sqlalchemy schema, but
        # this path must be exposed for Pecan to route any paths we might
        # choose to expose below it.
        cdict = pecan.request.context.to_policy_values()
        policy.authorize('baremetal:driver:get', cdict, cdict)

        def _find_driver(driver_dict, driver_type):
            for name, hosts in driver_dict.items():
                if name == driver_name:
                    return Driver.convert_with_links(name, list(hosts),
                                                     driver_type, detail=True)

        hw_type_dict = pecan.request.dbapi.get_active_hardware_type_dict()
        driver = _find_driver(hw_type_dict, 'dynamic')
        if driver:
            return driver
        driver_dict = pecan.request.dbapi.get_active_driver_dict()
        driver = _find_driver(driver_dict, 'classic')
        if driver:
            return driver

        raise exception.DriverNotFound(driver_name=driver_name)

    @METRICS.timer('DriversController.properties')
    @expose.expose(wtypes.text, wtypes.text)
    def properties(self, driver_name):
        """Retrieve property information of the given driver.

        :param driver_name: name of the driver.
        :returns: dictionary with <property name>:<property description>
                  entries.
        :raises: DriverNotFound (HTTP 404) if the driver name is invalid or
                 the driver cannot be loaded.
        """
        cdict = pecan.request.context.to_policy_values()
        policy.authorize('baremetal:driver:get_properties', cdict, cdict)

        if driver_name not in _DRIVER_PROPERTIES:
            topic = pecan.request.rpcapi.get_topic_for_driver(driver_name)
            properties = pecan.request.rpcapi.get_driver_properties(
                pecan.request.context, driver_name, topic=topic)
            _DRIVER_PROPERTIES[driver_name] = properties

        return _DRIVER_PROPERTIES[driver_name]