/
nova.py
489 lines (395 loc) · 17.9 KB
/
nova.py
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
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
# Copyright (c) 2013 Mirantis Inc.
#
# 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 uuid as uuidgen
from keystoneauth1 import session
from keystoneauth1 import token_endpoint
from novaclient import client as nova_client
from novaclient import exceptions as nova_exception
from novaclient.v2 import servers
from oslo_config import cfg
from oslo_log import log as logging
from blazar import context
from blazar.manager import exceptions as manager_exceptions
from blazar.plugins import oshosts
from blazar.utils.openstack import base
nova_opts = [
cfg.StrOpt('nova_client_version',
default='2',
deprecated_group='DEFAULT',
help='Novaclient version'),
cfg.StrOpt('compute_service',
default='compute',
deprecated_group='DEFAULT',
help='Nova name in keystone'),
cfg.StrOpt('image_prefix',
default='reserved_',
deprecated_group='DEFAULT',
help='Prefix for VM images if you want to create snapshots'),
cfg.StrOpt('aggregate_freepool_name',
default='freepool',
deprecated_group=oshosts.RESOURCE_TYPE,
help='Name of the special aggregate where all hosts '
'are candidate for physical host reservation'),
cfg.StrOpt('project_id_key',
default='blazar:project',
deprecated_group=oshosts.RESOURCE_TYPE,
help='Aggregate metadata value for key matching project_id'),
cfg.StrOpt('blazar_owner',
default='blazar:owner',
deprecated_group=oshosts.RESOURCE_TYPE,
help='Aggregate metadata key for knowing owner project_id')
]
CONF = cfg.CONF
CONF.register_opts(nova_opts, group='nova')
CONF.import_opt('identity_service', 'blazar.utils.openstack.keystone')
LOG = logging.getLogger(__name__)
class BlazarNovaClient(object):
def __init__(self, **kwargs):
"""Description
BlazarNovaClient can be used in two ways: from context or kwargs.
:param version: service client version which we will use
:type version: str
:param ctx: request context
:type ctx: context object
:param auth_token: keystone auth token
:type auth_token: str
:param endpoint_override: endpoint url which we will use
:type endpoint_override: str
:param username: username to use with nova client
:type username: str
:param password: password to use with nova client
:type password: str
:param user_domain_name: domain name of the user
:type user_domain_name: str
:param project_name: project name to use with nova client
:type project_name: str
:param project_domain_name: domain name of the project
:type project_domain_name: str
:param auth_url: keystone url to authenticate against
:type auth_url: str
"""
ctx = kwargs.pop('ctx', None)
auth_token = kwargs.pop('auth_token', None)
endpoint_override = kwargs.pop('endpoint_override', None)
version = kwargs.pop('version', CONF.nova.nova_client_version)
username = kwargs.pop('username', None)
password = kwargs.pop('password', None)
user_domain_name = kwargs.pop('user_domain_name', None)
project_name = kwargs.pop('project_name', None)
project_domain_name = kwargs.pop('project_domain_name', None)
auth_url = kwargs.pop('auth_url', None)
if ctx is None:
try:
ctx = context.current()
except RuntimeError:
pass
if ctx is not None:
auth_token = auth_token or ctx.auth_token
endpoint_override = endpoint_override or \
base.url_for(ctx.service_catalog,
CONF.nova.compute_service)
auth_url = base.url_for(ctx.service_catalog, CONF.identity_service)
if auth_url is None:
auth_url = "%s://%s:%s/v3" % (CONF.os_auth_protocol,
CONF.os_auth_host,
CONF.os_auth_port)
if username:
kwargs.setdefault('username', username)
kwargs.setdefault('password', password)
kwargs.setdefault('project_name', project_name)
kwargs.setdefault('auth_url', auth_url)
if "v2.0" not in auth_url:
kwargs.setdefault('project_domain_name', project_domain_name)
kwargs.setdefault('user_domain_name', user_domain_name)
else:
auth = token_endpoint.Token(endpoint_override,
auth_token)
sess = session.Session(auth=auth)
kwargs.setdefault('session', sess)
kwargs.setdefault('endpoint_override', endpoint_override)
kwargs.setdefault('version', version)
self.nova = nova_client.Client(**kwargs)
self.nova.servers = ServerManager(self.nova)
self.exceptions = nova_exception
def __getattr__(self, name):
return getattr(self.nova, name)
# TODO(dbelova): remove these lines after novaclient 2.16.0 will be released
class BlazarServer(servers.Server):
def unshelve(self):
"""Unshelve -- Unshelve the server."""
self.manager.unshelve(self)
class ServerManager(servers.ServerManager):
resource_class = BlazarServer
def unshelve(self, server):
"""Unshelve the server."""
self._action('unshelve', server, None)
def create_image(self, server, image_name=None, metadata=None):
"""Snapshot a server."""
if image_name is None:
image_name = CONF.nova.image_prefix + server.name
return super(ServerManager, self).create_image(server,
image_name=image_name,
metadata=metadata)
class NovaClientWrapper(object):
def __init__(self, username=None, password=None, user_domain_name=None,
project_name=None, project_domain_name=None):
self.username = username
self.password = password
self.user_domain_name = user_domain_name
self.project_name = project_name
self.project_domain_name = project_domain_name
@property
def nova(self):
ctx = context.current()
nova = BlazarNovaClient(ctx=ctx,
username=self.username,
password=self.password,
user_domain_name=self.user_domain_name,
project_name=self.project_name,
project_domain_name=self.project_domain_name)
return nova
class ReservationPool(NovaClientWrapper):
def __init__(self):
super(ReservationPool, self).__init__(
username=CONF.os_admin_username,
password=CONF.os_admin_password,
user_domain_name=CONF.os_admin_user_domain_name,
project_name=CONF.os_admin_project_name,
project_domain_name=CONF.os_admin_user_domain_name)
self.config = CONF.nova
self.freepool_name = self.config.aggregate_freepool_name
def get_aggregate_from_name_or_id(self, aggregate_obj):
"""Return an aggregate by name or an id."""
aggregate = None
agg_id = None
try:
agg_id = int(aggregate_obj)
except (ValueError, TypeError):
if hasattr(aggregate_obj, 'id') and aggregate_obj.id:
# pool is an aggregate
agg_id = aggregate_obj.id
if agg_id is not None:
try:
aggregate = self.nova.aggregates.get(agg_id)
except nova_exception.NotFound:
aggregate = None
else:
# FIXME(scroiset): can't get an aggregate by name
# so iter over all aggregate and check for the good one
all_aggregates = self.nova.aggregates.list()
for agg in all_aggregates:
if aggregate_obj == agg.name:
aggregate = agg
if aggregate:
return aggregate
else:
raise manager_exceptions.AggregateNotFound(pool=aggregate_obj)
@staticmethod
def _generate_aggregate_name():
return str(uuidgen.uuid4())
def create(self, name=None, az=None, metadata=None):
"""Create a Pool (an Aggregate) with or without Availability Zone.
By default expose to user the aggregate with an Availability Zone.
Return an aggregate or raise a nova exception.
"""
name = name or self._generate_aggregate_name()
LOG.debug('Creating pool aggregate: %(name)s with Availability Zone '
'%(az)s', {'name': name, 'az': az})
agg = self.nova.aggregates.create(name, az)
try:
ctx = context.current()
project_id = ctx.project_id
except RuntimeError:
e = manager_exceptions.ProjectIdNotFound()
LOG.error(e.message)
raise e
if metadata:
metadata[self.config.blazar_owner] = project_id
else:
metadata = {self.config.blazar_owner: project_id}
self.nova.aggregates.set_metadata(agg, metadata)
return agg
def delete(self, pool, force=True):
"""Delete an aggregate.
pool can be an aggregate name or an aggregate id.
Remove all hosts before delete aggregate (default).
If force is False, raise exception if at least one
host is attached to.
"""
agg = self.get_aggregate_from_name_or_id(pool)
hosts = agg.hosts
if len(hosts) > 0 and not force:
raise manager_exceptions.AggregateHaveHost(name=agg.name,
hosts=agg.hosts)
try:
freepool_agg = self.get(self.freepool_name)
except manager_exceptions.AggregateNotFound:
raise manager_exceptions.NoFreePool()
for host in hosts:
LOG.debug("Removing host '%(host)s' from aggregate '%(id)s')",
{'host': host, 'id': agg.id})
self.nova.aggregates.remove_host(agg.id, host)
if freepool_agg.id != agg.id and host not in freepool_agg.hosts:
self.nova.aggregates.add_host(freepool_agg.id, host)
self.nova.aggregates.delete(agg.id)
def get_all(self):
"""Return all aggregate."""
return self.nova.aggregates.list()
def get(self, pool):
"""return details for aggregate pool or raise AggregateNotFound."""
return self.get_aggregate_from_name_or_id(pool)
def get_computehosts(self, pool):
"""Return a list of compute host names for an aggregate."""
try:
agg = self.get_aggregate_from_name_or_id(pool)
return agg.hosts
except manager_exceptions.AggregateNotFound:
return []
def add_computehost(self, pool, host, stay_in=False):
"""Add a compute host to an aggregate.
The `host` must exist otherwise raise an error
and the `host` must be in the freepool.
:param pool: Name or UUID of the pool to rattach the host
:param host: Name (not UUID) of the host to associate
:type host: str
Return the related aggregate.
Raise an aggregate exception if something wrong.
"""
agg = self.get_aggregate_from_name_or_id(pool)
try:
freepool_agg = self.get(self.freepool_name)
except manager_exceptions.AggregateNotFound:
raise manager_exceptions.NoFreePool()
if freepool_agg.id != agg.id and not stay_in:
if host not in freepool_agg.hosts:
raise manager_exceptions.HostNotInFreePool(
host=host, freepool_name=freepool_agg.name)
LOG.info("removing host '%(host)s' from aggregate freepool "
"%(name)s", {'host': host, 'name': freepool_agg.name})
try:
self.remove_computehost(freepool_agg.id, host)
except nova_exception.NotFound:
raise manager_exceptions.HostNotFound(host=host)
LOG.info("adding host '%(host)s' to aggregate %(id)s",
{'host': host, 'id': agg.id})
try:
return self.nova.aggregates.add_host(agg.id, host)
except nova_exception.NotFound:
raise manager_exceptions.HostNotFound(host=host)
except nova_exception.Conflict:
raise manager_exceptions.AggregateAlreadyHasHost(pool=pool,
host=host)
def remove_all_computehosts(self, pool):
"""Remove all compute hosts attached to an aggregate."""
hosts = self.get_computehosts(pool)
self.remove_computehost(pool, hosts)
def remove_computehost(self, pool, hosts):
"""Remove compute host(s) from an aggregate."""
if not isinstance(hosts, list):
hosts = [hosts]
agg = self.get_aggregate_from_name_or_id(pool)
try:
freepool_agg = self.get(self.freepool_name)
except manager_exceptions.AggregateNotFound:
raise manager_exceptions.NoFreePool()
hosts_failing_to_remove = []
hosts_failing_to_add = []
hosts_not_in_freepool = []
for host in hosts:
if freepool_agg.id == agg.id:
if host not in freepool_agg.hosts:
hosts_not_in_freepool.append(host)
continue
try:
self.nova.aggregates.remove_host(agg.id, host)
except nova_exception.ClientException:
hosts_failing_to_remove.append(host)
if freepool_agg.id != agg.id:
# NOTE(sbauza) : We don't want to put again the host in
# freepool if the requested pool is the freepool...
try:
self.nova.aggregates.add_host(freepool_agg.id, host)
except nova_exception.ClientException:
hosts_failing_to_add.append(host)
if hosts_failing_to_remove:
raise manager_exceptions.CantRemoveHost(
host=hosts_failing_to_remove, pool=agg)
if hosts_failing_to_add:
raise manager_exceptions.CantAddHost(host=hosts_failing_to_add,
pool=freepool_agg)
if hosts_not_in_freepool:
raise manager_exceptions.HostNotInFreePool(
host=hosts_not_in_freepool, freepool_name=freepool_agg.name)
def add_project(self, pool, project_id):
"""Add a project to an aggregate."""
metadata = {project_id: self.config.project_id_key}
agg = self.get_aggregate_from_name_or_id(pool)
return self.nova.aggregates.set_metadata(agg.id, metadata)
def remove_project(self, pool, project_id):
"""Remove a project from an aggregate."""
agg = self.get_aggregate_from_name_or_id(pool)
metadata = {project_id: None}
return self.nova.aggregates.set_metadata(agg.id, metadata)
class NovaInventory(NovaClientWrapper):
def get_host_details(self, host):
"""Get Nova capabilities of a single host
:param host: UUID or name of nova-compute host
:return: Dict of capabilities or raise HostNotFound
"""
try:
hypervisor = self.nova.hypervisors.get(host)
except nova_exception.NotFound:
try:
hypervisors_list = self.nova.hypervisors.search(host)
except nova_exception.NotFound:
raise manager_exceptions.HostNotFound(host=host)
if len(hypervisors_list) > 1:
raise manager_exceptions.MultipleHostsFound(host)
else:
hypervisor_id = hypervisors_list[0].id
# NOTE(sbauza): No need to catch the exception as we're sure
# that the hypervisor exists
hypervisor = self.nova.hypervisors.get(hypervisor_id)
try:
return {'id': hypervisor.id,
'hypervisor_hostname': hypervisor.hypervisor_hostname,
'service_name': hypervisor.service['host'],
'vcpus': hypervisor.vcpus,
'cpu_info': hypervisor.cpu_info,
'hypervisor_type': hypervisor.hypervisor_type,
'hypervisor_version': hypervisor.hypervisor_version,
'memory_mb': hypervisor.memory_mb,
'local_gb': hypervisor.local_gb}
except AttributeError:
raise manager_exceptions.InvalidHost(host=host)
def get_servers_per_host(self, host):
"""List all servers of a nova-compute host
:param host: Name (not UUID) of nova-compute host
:return: Dict of servers or None
"""
try:
hypervisors_list = self.nova.hypervisors.search(host, servers=True)
except nova_exception.NotFound:
raise manager_exceptions.HostNotFound(host=host)
if len(hypervisors_list) > 1:
raise manager_exceptions.MultipleHostsFound(host)
else:
try:
return hypervisors_list[0].servers
except AttributeError:
# NOTE(sbauza): nova.hypervisors.search(servers=True) returns
# a list of hosts without 'servers' attribute if no servers
# are running on that host
return None