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