blob: 080de4212820db1f78bc00c07f556da45b875eb4 [file] [log] [blame]
Elena Ezhova1c2ebae2017-07-03 18:29:05 +04001# -*- coding: utf-8 -*-
2"""
3Module extending the salt.modules.glance modules.
4
5This module adds functionality for managing Glance V2 tasks by exposing the
6following functions:
7 - task_create
8 - task_show
9 - task_list
10
11:optdepends: - glanceclient Python adapter
12:configuration: This module is not usable until the following are specified
13 either in a pillar or in the minion's config file::
14
15 keystone.user: admin
16 keystone.password: verybadpass
17 keystone.tenant: admin
18 keystone.insecure: False #(optional)
19 keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'
20
21 If configuration for multiple openstack accounts is required, they can be
22 set up as different configuration profiles:
23 For example::
24
25 openstack1:
26 keystone.user: admin
27 keystone.password: verybadpass
28 keystone.tenant: admin
29 keystone.auth_url: 'http://127.0.0.1:5000/v2.0/'
30
31 openstack2:
32 keystone.user: admin
33 keystone.password: verybadpass
34 keystone.tenant: admin
35 keystone.auth_url: 'http://127.0.0.2:5000/v2.0/'
36
37 With this configuration in place, any of the glance functions can
38 make use of a configuration profile by declaring it explicitly.
39 For example::
40
41 salt '*' glance.image_list profile=openstack1
42"""
43
44# Import Python libs
45from __future__ import absolute_import
46import logging
47import pprint
48import re
49
50# Import salt libs
51from salt.exceptions import SaltInvocationError
52
53from salt.version import (
54 __version__,
55 SaltStackVersion
56 )
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +030057
58from salt.utils import warn_until
59
Elena Ezhova1c2ebae2017-07-03 18:29:05 +040060# pylint: disable=import-error
61HAS_GLANCE = False
62try:
63 from glanceclient import client
64 from glanceclient import exc
65 HAS_GLANCE = True
66except ImportError:
67 pass
68
Elena Ezhova1c2ebae2017-07-03 18:29:05 +040069
70logging.basicConfig(level=logging.DEBUG)
71log = logging.getLogger(__name__)
72
73
74def __virtual__():
75 '''
76 Only load this module if glance
77 is installed on this minion.
78 '''
79 if not HAS_GLANCE:
80 return False, ("The glance execution module cannot be loaded: "
81 "the glanceclient python library is not available.")
Elena Ezhova1c2ebae2017-07-03 18:29:05 +040082 return True
83
84
85__opts__ = {}
86
87
88def _auth(profile=None, api_version=2, **connection_args):
89 '''
90 Set up glance credentials, returns
91 `glanceclient.client.Client`. Optional parameter
92 "api_version" defaults to 2.
93
94 Only intended to be used within glance-enabled modules
95 '''
96
Oleg Iurchenko4afbbbb2018-02-20 15:50:47 +020097 kstone = __salt__['keystoneng.auth'](profile, **connection_args)
Vasyl Saienko0c896de2018-03-09 11:39:06 +020098 g_endpoint = __salt__['keystoneng.endpoint_get']('glance', profile=profile)
Oleg Iurchenko4afbbbb2018-02-20 15:50:47 +020099 glance_client = client.Client(api_version, session=kstone.session, endpoint=g_endpoint.get('url'))
100 return glance_client
Elena Ezhova1c2ebae2017-07-03 18:29:05 +0400101
102
103def _validate_image_params(visibility=None, container_format='bare',
104 disk_format='raw', tags=None, **kwargs):
105 # valid options for "visibility":
106 v_list = ['public', 'private', 'shared', 'community']
107 # valid options for "container_format":
108 cf_list = ['ami', 'ari', 'aki', 'bare', 'ovf']
109 # valid options for "disk_format":
110 df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
111 'raw', 'qcow2', 'vdi', 'iso']
112
113 if visibility is not None:
114 if visibility not in v_list:
115 raise SaltInvocationError('"visibility" needs to be one ' +
116 'of the following: {0}'.format(
117 ', '.join(v_list)))
118 if container_format not in cf_list:
119 raise SaltInvocationError('"container_format" needs to be ' +
120 'one of the following: {0}'.format(
121 ', '.join(cf_list)))
122 if disk_format not in df_list:
123 raise SaltInvocationError('"disk_format" needs to be one ' +
124 'of the following: {0}'.format(
125 ', '.join(df_list)))
126 if tags:
127 if not isinstance(tags, list):
128 raise SaltInvocationError('Incorrect input type for the {0} '
129 'parameter: expected: {1}, '
130 'got {2}'.format("tags", list,
131 type(tags)))
132
133
134def _validate_task_params(task_type, input_params):
135 # Only import tasks are currently supported
136 # TODO(eezhova): Add support for "export" and "clone" task types
137 valid_task_types = ["import", ]
138
139 import_required_params = {"import_from", "import_from_format",
140 "image_properties"}
141
142 if task_type not in valid_task_types:
143 raise SaltInvocationError("'task_type' must be one of the following: "
144 "{0}".format(', '.join(valid_task_types)))
145
146 if task_type == "import":
147 valid_import_from_formats = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
148 'raw', 'qcow2', 'vdi', 'iso']
149 missing_params = import_required_params - set(input_params.keys())
150 if missing_params:
151 raise SaltInvocationError(
152 "Missing the following task parameters for the 'import' task: "
153 "{0}".format(', '.join(missing_params)))
154
155 import_from = input_params['import_from']
156 import_from_format = input_params['import_from_format']
157 image_properties = input_params['image_properties']
158 if not import_from.startswith(('http://', 'https://')):
159 raise SaltInvocationError("Only non-local sources of image data "
160 "are supported.")
161 if import_from_format not in valid_import_from_formats:
162 raise SaltInvocationError(
163 "'import_from_format' needs to be one of the following: "
164 "{0}".format(', '.join(valid_import_from_formats)))
165 _validate_image_params(**image_properties)
166
167
168def task_create(task_type, profile=None, input_params=None):
169 """
170 Create a Glance V2 task of a given type
171
172 :param task_type: Task type
173 :param profile: Authentication profile
174 :param input_params: Dictionary with input parameters for a task
175 :return: Dictionary with created task's parameters
176 """
177 g_client = _auth(profile, api_version=2)
178 log.debug(
179 'Task type: {}\nInput params: {}'.format(task_type, input_params)
180 )
181 task = g_client.tasks.create(type=task_type, input=input_params)
182 log.debug("Created task: {}".format(dict(task)))
183 created_task = task_show(task.id, profile=profile)
184 return created_task
185
186
187def task_show(task_id, profile=None):
188 """
189 Show a Glance V2 task
190
191 :param task_id: ID of a task to show
192 :param profile: Authentication profile
193 :return: Dictionary with created task's parameters
194 """
195 g_client = _auth(profile)
196 ret = {}
197 try:
198 task = g_client.tasks.get(task_id)
199 except exc.HTTPNotFound:
200 return {
201 'result': False,
202 'comment': 'No task with ID {0}'.format(task_id)
203 }
204 pformat = pprint.PrettyPrinter(indent=4).pformat
205 log.debug('Properties of task {0}:\n{1}'.format(
206 task_id, pformat(task)))
207
208 schema = image_schema(schema_type='task', profile=profile)
209 if len(schema.keys()) == 1:
210 schema = schema['task']
211 for key in schema.keys():
212 if key in task:
213 ret[key] = task[key]
214 return ret
215
216
217def task_list(profile=None):
218 """
219 List Glance V2 tasks
220
221 :param profile: Authentication profile
222 :return: Dictionary with existing tasks
223 """
224 g_client = _auth(profile)
225 ret = {}
226 tasks = g_client.tasks.list()
227 schema = image_schema(schema_type='task', profile=profile)
228 if len(schema.keys()) == 1:
229 schema = schema['task']
230 for task in tasks:
231 task_dict = {}
232 for key in schema.keys():
233 if key in task:
234 task_dict[key] = task[key]
235 ret[task['id']] = task_dict
236 return ret
237
238
239def get_image_owner_id(name, profile=None):
240 """
241 Mine function to get image owner
242
243 :param name: Name of the image
244 :param profile: Authentication profile
245 :return: Image owner ID or [] if image is not found
246 """
247 g_client = _auth(profile)
248 image_id = None
249 for image in g_client.images.list():
250 if image.name == name:
251 image_id = image.id
252 continue
253 if not image_id:
254 return []
255 try:
256 image = g_client.images.get(image_id)
257 except exc.HTTPNotFound:
258 return []
259 return image['owner']
260
261
262def image_schema(schema_type='image', profile=None):
263 '''
264 Returns names and descriptions of the schema "image"'s
265 properties for this profile's instance of glance
266
267 CLI Example:
268
269 .. code-block:: bash
270
271 salt '*' glance.image_schema
272 '''
273 return schema_get(schema_type, profile)
274
275
276def schema_get(name, profile=None):
277 '''
278 Known valid names of schemas are:
279 - image
280 - images
281 - member
282 - members
283
284 CLI Example:
285
286 .. code-block:: bash
287
288 salt '*' glance.schema_get name=f16-jeos
289 '''
290 g_client = _auth(profile)
291 pformat = pprint.PrettyPrinter(indent=4).pformat
292 schema_props = {}
293 for prop in g_client.schemas.get(name).properties:
294 schema_props[prop.name] = prop.description
295 log.debug('Properties of schema {0}:\n{1}'.format(
296 name, pformat(schema_props)))
297 return {name: schema_props}
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300298
299def image_list(id=None, profile=None, name=None): # pylint: disable=C0103
300 '''
301 Return a list of available images (glance image-list)
302
303 CLI Example:
304
305 .. code-block:: bash
306
307 salt '*' glance.image_list
308 '''
Martin Polreichaeb1ea62018-06-04 16:13:03 +0200309
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300310 g_client = _auth(profile)
Martin Polreichaeb1ea62018-06-04 16:13:03 +0200311 ret = []
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300312 for image in g_client.images.list():
313 if id is None and name is None:
314 _add_image(ret, image)
315 else:
316 if id is not None and id == image.id:
317 _add_image(ret, image)
318 return ret
319 if name == image.name:
Martin Polreichaeb1ea62018-06-04 16:13:03 +0200320 if name in ret:
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300321 # Not really worth an exception
322 return {
323 'result': False,
324 'comment':
325 'More than one image with '
326 'name "{0}"'.format(name)
327 }
328 _add_image(ret, image)
329 log.debug('Returning images: {0}'.format(ret))
330 return ret
331
332def _add_image(collection, image):
333 '''
334 Add image to given dictionary
335 '''
336 image_prep = {
337 'id': image.id,
338 'name': image.name,
339 'created_at': image.created_at,
340 'file': image.file,
341 'min_disk': image.min_disk,
342 'min_ram': image.min_ram,
343 'owner': image.owner,
344 'protected': image.protected,
345 'status': image.status,
346 'tags': image.tags,
347 'updated_at': image.updated_at,
348 'visibility': image.visibility,
349 }
350 # Those cause AttributeErrors in Icehouse' glanceclient
351 for attr in ['container_format', 'disk_format', 'size']:
352 if attr in image:
353 image_prep[attr] = image[attr]
354 if type(collection) is dict:
355 collection[image.name] = image_prep
356 elif type(collection) is list:
357 collection.append(image_prep)
358 else:
359 msg = '"collection" is {0}'.format(type(collection)) +\
360 'instead of dict or list.'
361 log.error(msg)
362 raise TypeError(msg)
363 return collection
364
365def image_create(name, location=None, profile=None, visibility=None,
366 container_format='bare', disk_format='raw', protected=None,
367 copy_from=None, is_public=None):
368 '''
369 Create an image (glance image-create)
370
371 CLI Example, old format:
372
373 .. code-block:: bash
374
375 salt '*' glance.image_create name=f16-jeos is_public=true \\
376 disk_format=qcow2 container_format=ovf \\
377 copy_from=http://berrange.fedorapeople.org/\
378 images/2012-02-29/f16-x86_64-openstack-sda.qcow2
379
380 CLI Example, new format resembling Glance API v2:
381
382 .. code-block:: bash
383
384 salt '*' glance.image_create name=f16-jeos visibility=public \\
385 disk_format=qcow2 container_format=ovf \\
386 copy_from=http://berrange.fedorapeople.org/\
387 images/2012-02-29/f16-x86_64-openstack-sda.qcow2
388
389 The parameter 'visibility' defaults to 'public' if neither
390 'visibility' nor 'is_public' is specified.
391 '''
392 kwargs = {}
393 # valid options for "visibility":
394 v_list = ['public', 'private']
395 # valid options for "container_format":
396 cf_list = ['ami', 'ari', 'aki', 'bare', 'ovf']
397 # valid options for "disk_format":
398 df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
399 'raw', 'qcow2', 'vdi', 'iso']
400 # 'location' and 'visibility' are the parameters used in
401 # Glance API v2. For now we have to use v1 for now (see below)
402 # but this modules interface will change in Carbon.
403 if copy_from is not None or is_public is not None:
404 warn_until('Carbon', 'The parameters \'copy_from\' and '
405 '\'is_public\' are deprecated and will be removed. '
406 'Use \'location\' and \'visibility\' instead.')
407 if is_public is not None and visibility is not None:
408 raise SaltInvocationError('Must only specify one of '
409 '\'is_public\' and \'visibility\'')
410 if copy_from is not None and location is not None:
411 raise SaltInvocationError('Must only specify one of '
412 '\'copy_from\' and \'location\'')
413 if copy_from is not None:
414 kwargs['copy_from'] = copy_from
415 else:
416 kwargs['copy_from'] = location
417 if is_public is not None:
418 kwargs['is_public'] = is_public
419 elif visibility is not None:
420 if visibility not in v_list:
421 raise SaltInvocationError('"visibility" needs to be one ' +
422 'of the following: {0}'.format(', '.join(v_list)))
423 elif visibility == 'public':
424 kwargs['is_public'] = True
425 else:
426 kwargs['is_public'] = False
427 else:
428 kwargs['is_public'] = True
429 if container_format not in cf_list:
430 raise SaltInvocationError('"container_format" needs to be ' +
431 'one of the following: {0}'.format(', '.join(cf_list)))
432 else:
433 kwargs['container_format'] = container_format
434 if disk_format not in df_list:
435 raise SaltInvocationError('"disk_format" needs to be one ' +
436 'of the following: {0}'.format(', '.join(df_list)))
437 else:
438 kwargs['disk_format'] = disk_format
439 if protected is not None:
440 kwargs['protected'] = protected
441 # Icehouse's glanceclient doesn't have add_location() and
442 # glanceclient.v2 doesn't implement Client.images.create()
443 # in a usable fashion. Thus we have to use v1 for now.
444 g_client = _auth(profile, api_version=1)
445 image = g_client.images.create(name=name, **kwargs)
446 return image_show(image.id, profile=profile)
447
448def image_delete(id=None, name=None, profile=None): # pylint: disable=C0103
449 '''
450 Delete an image (glance image-delete)
451
452 CLI Examples:
453
454 .. code-block:: bash
455
456 salt '*' glance.image_delete c2eb2eb0-53e1-4a80-b990-8ec887eae7df
457 salt '*' glance.image_delete id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df
458 salt '*' glance.image_delete name=f16-jeos
459 '''
460 g_client = _auth(profile)
461 image = {'id': False, 'name': None}
462 if name:
463 for image in g_client.images.list():
464 if image.name == name:
465 id = image.id # pylint: disable=C0103
466 continue
467 if not id:
468 return {
469 'result': False,
470 'comment':
471 'Unable to resolve image id '
472 'for name {0}'.format(name)
473 }
474 elif not name:
475 name = image['name']
476 try:
477 g_client.images.delete(id)
478 except exc.HTTPNotFound:
479 return {
480 'result': False,
481 'comment': 'No image with ID {0}'.format(id)
482 }
483 except exc.HTTPForbidden as forbidden:
484 log.error(str(forbidden))
485 return {
486 'result': False,
487 'comment': str(forbidden)
488 }
489 return {
490 'result': True,
491 'comment': 'Deleted image \'{0}\' ({1}).'.format(name, id),
492 }
493
494def image_show(id=None, name=None, profile=None): # pylint: disable=C0103
495 '''
496 Return details about a specific image (glance image-show)
497
498 CLI Example:
499
500 .. code-block:: bash
501
502 salt '*' glance.image_show
503 '''
504 g_client = _auth(profile)
505 ret = {}
506 if name:
507 for image in g_client.images.list():
508 if image.name == name:
509 id = image.id # pylint: disable=C0103
510 continue
511 if not id:
512 return {
513 'result': False,
514 'comment':
515 'Unable to resolve image ID '
516 'for name \'{0}\''.format(name)
517 }
518 try:
519 image = g_client.images.get(id)
520 except exc.HTTPNotFound:
521 return {
522 'result': False,
523 'comment': 'No image with ID {0}'.format(id)
524 }
525 pformat = pprint.PrettyPrinter(indent=4).pformat
526 log.debug('Properties of image {0}:\n{1}'.format(
527 image.name, pformat(image)))
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300528 schema = image_schema(profile=profile)
529 if len(schema.keys()) == 1:
530 schema = schema['image']
531 for key in schema.keys():
532 if key in image:
Martin Polreichaeb1ea62018-06-04 16:13:03 +0200533 ret[key] = image[key]
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300534 return ret
535
536def image_update(id=None, name=None, profile=None, **kwargs): # pylint: disable=C0103
537 '''
538 Update properties of given image.
539 Known to work for:
540 - min_ram (in MB)
541 - protected (bool)
542 - visibility ('public' or 'private')
543
544 CLI Example:
545
546 .. code-block:: bash
547
548 salt '*' glance.image_update id=c2eb2eb0-53e1-4a80-b990-8ec887eae7df
549 salt '*' glance.image_update name=f16-jeos
550 '''
551 if id:
552 image = image_show(id=id, profile=profile)
553 if 'result' in image and not image['result']:
554 return image
555 elif len(image) == 1:
556 image = image.values()[0]
557 elif name:
558 img_list = image_list(name=name, profile=profile)
559 if img_list is dict and 'result' in img_list:
560 return img_list
561 elif len(img_list) == 0:
562 return {
563 'result': False,
564 'comment':
565 'No image with name \'{0}\' '
566 'found.'.format(name)
567 }
568 elif len(img_list) == 1:
569 try:
570 image = img_list[0]
571 except KeyError:
572 image = img_list[name]
573 else:
574 raise SaltInvocationError
575 log.debug('Found image:\n{0}'.format(image))
576 to_update = {}
577 for key, value in kwargs.items():
578 if key.startswith('_'):
579 continue
580 if key not in image or image[key] != value:
581 log.debug('add <{0}={1}> to to_update'.format(key, value))
582 to_update[key] = value
583 g_client = _auth(profile)
584 updated = g_client.images.update(image['id'], **to_update)
Oleh Hryhorov6c21bbb2018-03-28 17:06:09 +0300585 return updated
586
587def _item_list(profile=None):
588 '''
589 Template for writing list functions
590 Return a list of available items (glance items-list)
591
592 CLI Example:
593
594 .. code-block:: bash
595
596 salt '*' glance.item_list
597 '''
598 g_client = _auth(profile)
599 ret = []
600 for item in g_client.items.list():
601 ret.append(item.__dict__)
602 #ret[item.name] = {
603 # 'name': item.name,
604 # }
605 return ret