blob: 0de0a857966e3a79da9b2b12f9d0be5c29fc05f6 [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 )
57# is there not SaltStackVersion.current() to get
58# the version of the salt running this code??
59_version_ary = __version__.split('.')
60CUR_VER = SaltStackVersion(_version_ary[0], _version_ary[1])
61BORON = SaltStackVersion.from_name('Boron')
62
63# pylint: disable=import-error
64HAS_GLANCE = False
65try:
66 from glanceclient import client
67 from glanceclient import exc
68 HAS_GLANCE = True
69except ImportError:
70 pass
71
72# Workaround, as the Glance API v2 requires you to
73# already have a keystone session token
74HAS_KEYSTONE = False
75try:
76 from keystoneclient.v2_0 import client as kstone
77 HAS_KEYSTONE = True
78except ImportError:
79 pass
80
81
82logging.basicConfig(level=logging.DEBUG)
83log = logging.getLogger(__name__)
84
85
86def __virtual__():
87 '''
88 Only load this module if glance
89 is installed on this minion.
90 '''
91 if not HAS_GLANCE:
92 return False, ("The glance execution module cannot be loaded: "
93 "the glanceclient python library is not available.")
94 if not HAS_KEYSTONE:
95 return False, ("The keystone execution module cannot be loaded: "
96 "the keystoneclient python library is not available.")
97 return True
98
99
100__opts__ = {}
101
102
103def _auth(profile=None, api_version=2, **connection_args):
104 '''
105 Set up glance credentials, returns
106 `glanceclient.client.Client`. Optional parameter
107 "api_version" defaults to 2.
108
109 Only intended to be used within glance-enabled modules
110 '''
111
112 if profile:
113 prefix = profile + ":keystone."
114 else:
115 prefix = "keystone."
116
117 def get(key, default=None):
118 '''
119 Checks connection_args, then salt-minion config,
120 falls back to specified default value.
121 '''
122 return connection_args.get('connection_' + key,
123 __salt__['config.get'](prefix + key, default))
124
125 user = get('user', 'admin')
126 password = get('password', None)
127 tenant = get('tenant', 'admin')
128 tenant_id = get('tenant_id')
129 auth_url = get('auth_url', 'http://127.0.0.1:35357/v2.0')
130 insecure = get('insecure', False)
131 admin_token = get('token')
132 region = get('region')
133 ks_endpoint = get('endpoint', 'http://127.0.0.1:9292/')
134 g_endpoint_url = __salt__['keystone.endpoint_get']('glance', profile)
135 # The trailing 'v2' causes URLs like thise one:
136 # http://127.0.0.1:9292/v2/v1/images
137 g_endpoint_url = re.sub('/v2', '', g_endpoint_url['internalurl'])
138
139 if admin_token and api_version != 1 and not password:
140 # If we had a password we could just
141 # ignore the admin-token and move on...
142 raise SaltInvocationError('Only can use keystone admin token '
143 'with Glance API v1')
144 elif password:
145 # Can't use the admin-token anyway
146 kwargs = {'username': user,
147 'password': password,
148 'tenant_id': tenant_id,
149 'auth_url': auth_url,
150 'endpoint_url': g_endpoint_url,
151 'region_name': region,
152 'tenant_name': tenant}
153 # 'insecure' keyword not supported by all v2.0 keystone clients
154 # this ensures it's only passed in when defined
155 if insecure:
156 kwargs['insecure'] = True
157 elif api_version == 1 and admin_token:
158 kwargs = {'token': admin_token,
159 'auth_url': auth_url,
160 'endpoint_url': g_endpoint_url}
161 else:
162 raise SaltInvocationError('No credentials to authenticate with.')
163
164 if HAS_KEYSTONE:
165 log.debug('Calling keystoneclient.v2_0.client.Client(' +
166 '{0}, **{1})'.format(ks_endpoint, kwargs))
167 keystone = kstone.Client(**kwargs)
168 kwargs['token'] = keystone.get_token(keystone.session)
169 # This doesn't realy prevent the password to show up
170 # in the minion log as keystoneclient.session is
171 # logging it anyway when in debug-mode
172 kwargs.pop('password')
173 log.debug('Calling glanceclient.client.Client(' +
174 '{0}, {1}, **{2})'.format(api_version,
175 g_endpoint_url, kwargs))
176 # may raise exc.HTTPUnauthorized, exc.HTTPNotFound
177 # but we deal with those elsewhere
178 return client.Client(api_version, g_endpoint_url, **kwargs)
179 else:
180 raise NotImplementedError(
181 "Can't retrieve a auth_token without keystone")
182
183
184def _validate_image_params(visibility=None, container_format='bare',
185 disk_format='raw', tags=None, **kwargs):
186 # valid options for "visibility":
187 v_list = ['public', 'private', 'shared', 'community']
188 # valid options for "container_format":
189 cf_list = ['ami', 'ari', 'aki', 'bare', 'ovf']
190 # valid options for "disk_format":
191 df_list = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
192 'raw', 'qcow2', 'vdi', 'iso']
193
194 if visibility is not None:
195 if visibility not in v_list:
196 raise SaltInvocationError('"visibility" needs to be one ' +
197 'of the following: {0}'.format(
198 ', '.join(v_list)))
199 if container_format not in cf_list:
200 raise SaltInvocationError('"container_format" needs to be ' +
201 'one of the following: {0}'.format(
202 ', '.join(cf_list)))
203 if disk_format not in df_list:
204 raise SaltInvocationError('"disk_format" needs to be one ' +
205 'of the following: {0}'.format(
206 ', '.join(df_list)))
207 if tags:
208 if not isinstance(tags, list):
209 raise SaltInvocationError('Incorrect input type for the {0} '
210 'parameter: expected: {1}, '
211 'got {2}'.format("tags", list,
212 type(tags)))
213
214
215def _validate_task_params(task_type, input_params):
216 # Only import tasks are currently supported
217 # TODO(eezhova): Add support for "export" and "clone" task types
218 valid_task_types = ["import", ]
219
220 import_required_params = {"import_from", "import_from_format",
221 "image_properties"}
222
223 if task_type not in valid_task_types:
224 raise SaltInvocationError("'task_type' must be one of the following: "
225 "{0}".format(', '.join(valid_task_types)))
226
227 if task_type == "import":
228 valid_import_from_formats = ['ami', 'ari', 'aki', 'vhd', 'vmdk',
229 'raw', 'qcow2', 'vdi', 'iso']
230 missing_params = import_required_params - set(input_params.keys())
231 if missing_params:
232 raise SaltInvocationError(
233 "Missing the following task parameters for the 'import' task: "
234 "{0}".format(', '.join(missing_params)))
235
236 import_from = input_params['import_from']
237 import_from_format = input_params['import_from_format']
238 image_properties = input_params['image_properties']
239 if not import_from.startswith(('http://', 'https://')):
240 raise SaltInvocationError("Only non-local sources of image data "
241 "are supported.")
242 if import_from_format not in valid_import_from_formats:
243 raise SaltInvocationError(
244 "'import_from_format' needs to be one of the following: "
245 "{0}".format(', '.join(valid_import_from_formats)))
246 _validate_image_params(**image_properties)
247
248
249def task_create(task_type, profile=None, input_params=None):
250 """
251 Create a Glance V2 task of a given type
252
253 :param task_type: Task type
254 :param profile: Authentication profile
255 :param input_params: Dictionary with input parameters for a task
256 :return: Dictionary with created task's parameters
257 """
258 g_client = _auth(profile, api_version=2)
259 log.debug(
260 'Task type: {}\nInput params: {}'.format(task_type, input_params)
261 )
262 task = g_client.tasks.create(type=task_type, input=input_params)
263 log.debug("Created task: {}".format(dict(task)))
264 created_task = task_show(task.id, profile=profile)
265 return created_task
266
267
268def task_show(task_id, profile=None):
269 """
270 Show a Glance V2 task
271
272 :param task_id: ID of a task to show
273 :param profile: Authentication profile
274 :return: Dictionary with created task's parameters
275 """
276 g_client = _auth(profile)
277 ret = {}
278 try:
279 task = g_client.tasks.get(task_id)
280 except exc.HTTPNotFound:
281 return {
282 'result': False,
283 'comment': 'No task with ID {0}'.format(task_id)
284 }
285 pformat = pprint.PrettyPrinter(indent=4).pformat
286 log.debug('Properties of task {0}:\n{1}'.format(
287 task_id, pformat(task)))
288
289 schema = image_schema(schema_type='task', profile=profile)
290 if len(schema.keys()) == 1:
291 schema = schema['task']
292 for key in schema.keys():
293 if key in task:
294 ret[key] = task[key]
295 return ret
296
297
298def task_list(profile=None):
299 """
300 List Glance V2 tasks
301
302 :param profile: Authentication profile
303 :return: Dictionary with existing tasks
304 """
305 g_client = _auth(profile)
306 ret = {}
307 tasks = g_client.tasks.list()
308 schema = image_schema(schema_type='task', profile=profile)
309 if len(schema.keys()) == 1:
310 schema = schema['task']
311 for task in tasks:
312 task_dict = {}
313 for key in schema.keys():
314 if key in task:
315 task_dict[key] = task[key]
316 ret[task['id']] = task_dict
317 return ret
318
319
320def get_image_owner_id(name, profile=None):
321 """
322 Mine function to get image owner
323
324 :param name: Name of the image
325 :param profile: Authentication profile
326 :return: Image owner ID or [] if image is not found
327 """
328 g_client = _auth(profile)
329 image_id = None
330 for image in g_client.images.list():
331 if image.name == name:
332 image_id = image.id
333 continue
334 if not image_id:
335 return []
336 try:
337 image = g_client.images.get(image_id)
338 except exc.HTTPNotFound:
339 return []
340 return image['owner']
341
342
343def image_schema(schema_type='image', profile=None):
344 '''
345 Returns names and descriptions of the schema "image"'s
346 properties for this profile's instance of glance
347
348 CLI Example:
349
350 .. code-block:: bash
351
352 salt '*' glance.image_schema
353 '''
354 return schema_get(schema_type, profile)
355
356
357def schema_get(name, profile=None):
358 '''
359 Known valid names of schemas are:
360 - image
361 - images
362 - member
363 - members
364
365 CLI Example:
366
367 .. code-block:: bash
368
369 salt '*' glance.schema_get name=f16-jeos
370 '''
371 g_client = _auth(profile)
372 pformat = pprint.PrettyPrinter(indent=4).pformat
373 schema_props = {}
374 for prop in g_client.schemas.get(name).properties:
375 schema_props[prop.name] = prop.description
376 log.debug('Properties of schema {0}:\n{1}'.format(
377 name, pformat(schema_props)))
378 return {name: schema_props}