1# Copyright 2013 IBM Corp.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15"""
16Handles all requests to Nova.
17"""
18
19from keystoneauth1 import identity
20from keystoneauth1 import loading as ks_loading
21from novaclient import api_versions
22from novaclient import client as nova_client
23from novaclient import exceptions as nova_exceptions
24from oslo_config import cfg
25from oslo_log import log as logging
26from requests import exceptions as request_exceptions
27
28from cinder.db import base
29from cinder import exception
30from cinder.message import api as message_api
31from cinder.message import message_field
32from cinder import service_auth
33
34nova_opts = [
35    cfg.StrOpt('region_name',
36               help='Name of nova region to use. Useful if keystone manages '
37                    'more than one region.',
38               deprecated_name="os_region_name",
39               deprecated_group="DEFAULT"),
40    cfg.StrOpt('interface',
41               default='public',
42               choices=['public', 'admin', 'internal'],
43               help='Type of the nova endpoint to use.  This endpoint will '
44                    'be looked up in the keystone catalog and should be '
45                    'one of public, internal or admin.'),
46    cfg.StrOpt('token_auth_url',
47               help='The authentication URL for the nova connection when '
48                    'using the current user''s token'),
49]
50
51
52NOVA_GROUP = 'nova'
53CONF = cfg.CONF
54
55nova_session_opts = ks_loading.get_session_conf_options()
56nova_auth_opts = ks_loading.get_auth_common_conf_options()
57
58CONF.register_opts(nova_opts, group=NOVA_GROUP)
59CONF.register_opts(nova_session_opts, group=NOVA_GROUP)
60CONF.register_opts(nova_auth_opts, group=NOVA_GROUP)
61
62LOG = logging.getLogger(__name__)
63
64NOVA_API_VERSION = "2.1"
65
66nova_extensions = [ext for ext in
67                   nova_client.discover_extensions(NOVA_API_VERSION)
68                   if ext.name in ("assisted_volume_snapshots",
69                                   "list_extensions",
70                                   "server_external_events")]
71
72
73def _get_identity_endpoint_from_sc(context):
74    # Search for the identity endpoint in the service catalog
75    for service in context.service_catalog:
76        if service.get('type') != 'identity':
77            continue
78        for endpoint in service['endpoints']:
79            if (not CONF[NOVA_GROUP].region_name or
80                    endpoint.get('region') == CONF[NOVA_GROUP].region_name):
81                return endpoint.get(CONF[NOVA_GROUP].interface + 'URL')
82    raise nova_exceptions.EndpointNotFound()
83
84
85def novaclient(context, privileged_user=False, timeout=None, api_version=None):
86    """Returns a Nova client
87
88    @param privileged_user:
89        If True, use the account from configuration
90        (requires 'auth_type' and the other usual Keystone authentication
91        options to be set in the [nova] section)
92    @param timeout:
93        Number of seconds to wait for an answer before raising a
94        Timeout exception (None to disable)
95    @param api_version:
96        api version of nova
97    """
98
99    if privileged_user and CONF[NOVA_GROUP].auth_type:
100        LOG.debug('Creating Keystone auth plugin from conf')
101        n_auth = ks_loading.load_auth_from_conf_options(CONF, NOVA_GROUP)
102    else:
103        if CONF[NOVA_GROUP].token_auth_url:
104            url = CONF[NOVA_GROUP].token_auth_url
105        else:
106            url = _get_identity_endpoint_from_sc(context)
107        LOG.debug('Creating Keystone token plugin using URL: %s', url)
108        n_auth = identity.Token(auth_url=url,
109                                token=context.auth_token,
110                                project_name=context.project_name,
111                                project_domain_id=context.project_domain_id)
112
113    if CONF.auth_strategy == 'keystone':
114        n_auth = service_auth.get_auth_plugin(context, auth=n_auth)
115
116    keystone_session = ks_loading.load_session_from_conf_options(
117        CONF,
118        NOVA_GROUP,
119        auth=n_auth)
120
121    c = nova_client.Client(
122        api_versions.APIVersion(api_version or NOVA_API_VERSION),
123        session=keystone_session,
124        insecure=CONF[NOVA_GROUP].insecure,
125        timeout=timeout,
126        region_name=CONF[NOVA_GROUP].region_name,
127        endpoint_type=CONF[NOVA_GROUP].interface,
128        cacert=CONF[NOVA_GROUP].cafile,
129        global_request_id=context.global_id,
130        extensions=nova_extensions)
131
132    return c
133
134
135class API(base.Base):
136    """API for interacting with novaclient."""
137
138    def __init__(self):
139        self.message_api = message_api.API()
140
141    def _get_volume_extended_event(self, server_id, volume_id):
142        return {'name': 'volume-extended',
143                'server_uuid': server_id,
144                'tag': volume_id}
145
146    def _send_events(self, context, events, api_version=None):
147        nova = novaclient(context, privileged_user=True,
148                          api_version=api_version)
149        try:
150            response = nova.server_external_events.create(events)
151        except nova_exceptions.NotFound:
152            LOG.warning('Nova returned NotFound for events: %s.', events)
153            return False
154        except Exception:
155            LOG.exception('Failed to notify nova on events: %s.', events)
156            return False
157        else:
158            if not isinstance(response, list):
159                LOG.error('Error response returned from nova: %s.', response)
160                return False
161            response_error = False
162            for event in response:
163                code = event.get('code')
164                if code is None:
165                    response_error = True
166                    continue
167                if code != 200:
168                    LOG.warning(
169                        'Nova event: %s returned with failed status.', event)
170                else:
171                    LOG.info('Nova event response: %s.', event)
172            if response_error:
173                LOG.error('Error response returned from nova: %s.', response)
174                return False
175        return True
176
177    def has_extension(self, context, extension, timeout=None):
178        try:
179            nova_exts = novaclient(context).list_extensions.show_all()
180        except request_exceptions.Timeout:
181            raise exception.APITimeout(service='Nova')
182        return extension in [e.name for e in nova_exts]
183
184    def update_server_volume(self, context, server_id, src_volid,
185                             new_volume_id):
186        nova = novaclient(context, privileged_user=True)
187        nova.volumes.update_server_volume(server_id,
188                                          src_volid,
189                                          new_volume_id)
190
191    def create_volume_snapshot(self, context, volume_id, create_info):
192        nova = novaclient(context, privileged_user=True)
193
194        # pylint: disable=E1101
195        nova.assisted_volume_snapshots.create(
196            volume_id,
197            create_info=create_info)
198
199    def delete_volume_snapshot(self, context, snapshot_id, delete_info):
200        nova = novaclient(context, privileged_user=True)
201
202        # pylint: disable=E1101
203        nova.assisted_volume_snapshots.delete(
204            snapshot_id,
205            delete_info=delete_info)
206
207    def get_server(self, context, server_id, privileged_user=False,
208                   timeout=None):
209        try:
210            return novaclient(context, privileged_user=privileged_user,
211                              timeout=timeout).servers.get(server_id)
212        except nova_exceptions.NotFound:
213            raise exception.ServerNotFound(uuid=server_id)
214        except request_exceptions.Timeout:
215            raise exception.APITimeout(service='Nova')
216
217    def extend_volume(self, context, server_ids, volume_id):
218        api_version = '2.51'
219        events = [self._get_volume_extended_event(server_id, volume_id)
220                  for server_id in server_ids]
221        result = self._send_events(context, events, api_version=api_version)
222        if not result:
223            self.message_api.create(
224                context,
225                message_field.Action.EXTEND_VOLUME,
226                resource_uuid=volume_id,
227                detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED)
228        return result
229