escalator should use oslo.xxx instead of oslo-xxx
[escalator.git] / client / escalatorclient / common / utils.py
1 # Copyright 2012 OpenStack Foundation
2 # All Rights Reserved.
3 #
4 #    Licensed under the Apache License, Version 2.0 (the "License"); you may
5 #    not use this file except in compliance with the License. You may obtain
6 #    a copy of the License at
7 #
8 #         http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #    Unless required by applicable law or agreed to in writing, software
11 #    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 #    License for the specific language governing permissions and limitations
14 #    under the License.
15
16 from __future__ import print_function
17
18 import errno
19 import hashlib
20 import json
21 import os
22 import re
23 import sys
24 import threading
25 import uuid
26 from oslo.utils import encodeutils
27 from oslo.utils import strutils
28 import prettytable
29 import six
30
31 from escalatorclient import exc
32 from oslo.utils import importutils
33
34 if os.name == 'nt':
35     import msvcrt
36 else:
37     msvcrt = None
38
39
40 _memoized_property_lock = threading.Lock()
41
42 SENSITIVE_HEADERS = ('X-Auth-Token', )
43
44
45 # Decorator for cli-args
46 def arg(*args, **kwargs):
47     def _decorator(func):
48         # Because of the sematics of decorator composition if we just append
49         # to the options list positional options will appear to be backwards.
50         func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
51         return func
52     return _decorator
53
54
55 def schema_args(schema_getter, omit=None):
56     omit = omit or []
57     typemap = {
58         'string': str,
59         'integer': int,
60         'boolean': strutils.bool_from_string,
61         'array': list
62     }
63
64     def _decorator(func):
65         schema = schema_getter()
66         if schema is None:
67             param = '<unavailable>'
68             kwargs = {
69                 'help': ("Please run with connection parameters set to "
70                          "retrieve the schema for generating help for this "
71                          "command")
72             }
73             func.__dict__.setdefault('arguments', []).insert(0, ((param, ),
74                                                                  kwargs))
75         else:
76             properties = schema.get('properties', {})
77             for name, property in six.iteritems(properties):
78                 if name in omit:
79                     continue
80                 param = '--' + name.replace('_', '-')
81                 kwargs = {}
82
83                 type_str = property.get('type', 'string')
84
85                 if isinstance(type_str, list):
86                     # NOTE(flaper87): This means the server has
87                     # returned something like `['null', 'string']`,
88                     # therfore we use the first non-`null` type as
89                     # the valid type.
90                     for t in type_str:
91                         if t != 'null':
92                             type_str = t
93                             break
94
95                 if type_str == 'array':
96                     items = property.get('items')
97                     kwargs['type'] = typemap.get(items.get('type'))
98                     kwargs['nargs'] = '+'
99                 else:
100                     kwargs['type'] = typemap.get(type_str)
101
102                 if type_str == 'boolean':
103                     kwargs['metavar'] = '[True|False]'
104                 else:
105                     kwargs['metavar'] = '<%s>' % name.upper()
106
107                 description = property.get('description', "")
108                 if 'enum' in property:
109                     if len(description):
110                         description += " "
111
112                     # NOTE(flaper87): Make sure all values are `str/unicode`
113                     # for the `join` to succeed. Enum types can also be `None`
114                     # therfore, join's call would fail without the following
115                     # list comprehension
116                     vals = [six.text_type(val) for val in property.get('enum')]
117                     description += ('Valid values: ' + ', '.join(vals))
118                 kwargs['help'] = description
119
120                 func.__dict__.setdefault('arguments',
121                                          []).insert(0, ((param, ), kwargs))
122         return func
123
124     return _decorator
125
126
127 def pretty_choice_list(l):
128     return ', '.join("'%s'" % i for i in l)
129
130
131 def print_list(objs, fields, formatters=None, field_settings=None,
132                conver_field=True):
133     formatters = formatters or {}
134     field_settings = field_settings or {}
135     pt = prettytable.PrettyTable([f for f in fields], caching=False)
136     pt.align = 'l'
137
138     for o in objs:
139         row = []
140         for field in fields:
141             if field in field_settings:
142                 for setting, value in six.iteritems(field_settings[field]):
143                     setting_dict = getattr(pt, setting)
144                     setting_dict[field] = value
145
146             if field in formatters:
147                 row.append(formatters[field](o))
148             else:
149                 if conver_field:
150                     field_name = field.lower().replace(' ', '_')
151                 else:
152                     field_name = field.replace(' ', '_')
153                 data = getattr(o, field_name, None)
154                 row.append(data)
155         pt.add_row(row)
156
157     print(encodeutils.safe_decode(pt.get_string()))
158
159
160 def print_dict(d, max_column_width=80):
161     pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
162     pt.align = 'l'
163     pt.max_width = max_column_width
164     for k, v in six.iteritems(d):
165         if isinstance(v, (dict, list)):
166             v = json.dumps(v)
167         pt.add_row([k, v])
168     print(encodeutils.safe_decode(pt.get_string(sortby='Property')))
169
170
171 def find_resource(manager, id):
172     """Helper for the _find_* methods."""
173     # first try to get entity as integer id
174     try:
175         if isinstance(id, int) or id.isdigit():
176             return manager.get(int(id))
177     except exc.NotFound:
178         pass
179
180     # now try to get entity as uuid
181     try:
182         # This must be unicode for Python 3 compatibility.
183         # If you pass a bytestring to uuid.UUID, you will get a TypeError
184         uuid.UUID(encodeutils.safe_decode(id))
185         return manager.get(id)
186     except (ValueError, exc.NotFound):
187         msg = ("id %s is error " % id)
188         raise exc.CommandError(msg)
189
190     # finally try to find entity by name
191     matches = list(manager.list(filters={'name': id}))
192     num_matches = len(matches)
193     if num_matches == 0:
194         msg = "No %s with a name or ID of '%s' exists." % \
195               (manager.resource_class.__name__.lower(), id)
196         raise exc.CommandError(msg)
197     elif num_matches > 1:
198         msg = ("Multiple %s matches found for '%s', use an ID to be more"
199                " specific." % (manager.resource_class.__name__.lower(),
200                                id))
201         raise exc.CommandError(msg)
202     else:
203         return matches[0]
204
205
206 def skip_authentication(f):
207     """Function decorator used to indicate a caller may be unauthenticated."""
208     f.require_authentication = False
209     return f
210
211
212 def is_authentication_required(f):
213     """Checks to see if the function requires authentication.
214
215     Use the skip_authentication decorator to indicate a caller may
216     skip the authentication step.
217     """
218     return getattr(f, 'require_authentication', True)
219
220
221 def env(*vars, **kwargs):
222     """Search for the first defined of possibly many env vars
223
224     Returns the first environment variable defined in vars, or
225     returns the default defined in kwargs.
226     """
227     for v in vars:
228         value = os.environ.get(v, None)
229         if value:
230             return value
231     return kwargs.get('default', '')
232
233
234 def import_versioned_module(version, submodule=None):
235     module = 'escalatorclient.v%s' % version
236     if submodule:
237         module = '.'.join((module, submodule))
238     return importutils.import_module(module)
239
240
241 def exit(msg='', exit_code=1):
242     if msg:
243         print(encodeutils.safe_decode(msg), file=sys.stderr)
244     sys.exit(exit_code)
245
246
247 def save_image(data, path):
248     """
249     Save an image to the specified path.
250
251     :param data: binary data of the image
252     :param path: path to save the image to
253     """
254     if path is None:
255         image = sys.stdout
256     else:
257         image = open(path, 'wb')
258     try:
259         for chunk in data:
260             image.write(chunk)
261     finally:
262         if path is not None:
263             image.close()
264
265
266 def make_size_human_readable(size):
267     suffix = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']
268     base = 1024.0
269
270     index = 0
271     while size >= base:
272         index = index + 1
273         size = size / base
274
275     padded = '%.1f' % size
276     stripped = padded.rstrip('0').rstrip('.')
277
278     return '%s%s' % (stripped, suffix[index])
279
280
281 def getsockopt(self, *args, **kwargs):
282     """
283     A function which allows us to monkey patch eventlet's
284     GreenSocket, adding a required 'getsockopt' method.
285     TODO: (mclaren) we can remove this once the eventlet fix
286     (https://bitbucket.org/eventlet/eventlet/commits/609f230)
287     lands in mainstream packages.
288     """
289     return self.fd.getsockopt(*args, **kwargs)
290
291
292 def exception_to_str(exc):
293     try:
294         error = six.text_type(exc)
295     except UnicodeError:
296         try:
297             error = str(exc)
298         except UnicodeError:
299             error = ("Caught '%(exception)s' exception." %
300                      {"exception": exc.__class__.__name__})
301     return encodeutils.safe_decode(error, errors='ignore')
302
303
304 def get_file_size(file_obj):
305     """
306     Analyze file-like object and attempt to determine its size.
307
308     :param file_obj: file-like object.
309     :retval The file's size or None if it cannot be determined.
310     """
311     if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and
312             (six.PY2 or six.PY3 and file_obj.seekable())):
313         try:
314             curr = file_obj.tell()
315             file_obj.seek(0, os.SEEK_END)
316             size = file_obj.tell()
317             file_obj.seek(curr)
318             return size
319         except IOError as e:
320             if e.errno == errno.ESPIPE:
321                 # Illegal seek. This means the file object
322                 # is a pipe (e.g. the user is trying
323                 # to pipe image data to the client,
324                 # echo testdata | bin/escalator add blah...), or
325                 # that file object is empty, or that a file-like
326                 # object which doesn't support 'seek/tell' has
327                 # been supplied.
328                 return
329             else:
330                 raise
331
332
333 def get_data_file(args):
334     if args.file:
335         return open(args.file, 'rb')
336     else:
337         # distinguish cases where:
338         # (1) stdin is not valid (as in cron jobs):
339         #     escalator ... <&-
340         # (2) image data is provided through standard input:
341         #     escalator ... < /tmp/file or cat /tmp/file | escalator ...
342         # (3) no image data provided:
343         #     escalator ...
344         try:
345             os.fstat(0)
346         except OSError:
347             # (1) stdin is not valid (closed...)
348             return None
349         if not sys.stdin.isatty():
350             # (2) image data is provided through standard input
351             if msvcrt:
352                 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
353             return sys.stdin
354         else:
355             # (3) no image data provided
356             return None
357
358
359 def strip_version(endpoint):
360     """Strip version from the last component of endpoint if present."""
361     # NOTE(flaper87): This shouldn't be necessary if
362     # we make endpoint the first argument. However, we
363     # can't do that just yet because we need to keep
364     # backwards compatibility.
365     if not isinstance(endpoint, six.string_types):
366         raise ValueError("Expected endpoint")
367
368     version = None
369     # Get rid of trailing '/' if present
370     endpoint = endpoint.rstrip('/')
371     url_bits = endpoint.split('/')
372     # regex to match 'v1' or 'v2.0' etc
373     if re.match('v\d+\.?\d*', url_bits[-1]):
374         version = float(url_bits[-1].lstrip('v'))
375         endpoint = '/'.join(url_bits[:-1])
376     return endpoint, version
377
378
379 def print_image(image_obj, max_col_width=None):
380     ignore = ['self', 'access', 'file', 'schema']
381     image = dict([item for item in six.iteritems(image_obj)
382                   if item[0] not in ignore])
383     if str(max_col_width).isdigit():
384         print_dict(image, max_column_width=max_col_width)
385     else:
386         print_dict(image)
387
388
389 def integrity_iter(iter, checksum):
390     """
391     Check image data integrity.
392
393     :raises: IOError
394     """
395     md5sum = hashlib.md5()
396     for chunk in iter:
397         yield chunk
398         if isinstance(chunk, six.string_types):
399             chunk = six.b(chunk)
400         md5sum.update(chunk)
401     md5sum = md5sum.hexdigest()
402     if md5sum != checksum:
403         raise IOError(errno.EPIPE,
404                       'Corrupt image download. Checksum was %s expected %s' %
405                       (md5sum, checksum))
406
407
408 def memoized_property(fn):
409     attr_name = '_lazy_once_' + fn.__name__
410
411     @property
412     def _memoized_property(self):
413         if hasattr(self, attr_name):
414             return getattr(self, attr_name)
415         else:
416             with _memoized_property_lock:
417                 if not hasattr(self, attr_name):
418                     setattr(self, attr_name, fn(self))
419             return getattr(self, attr_name)
420     return _memoized_property
421
422
423 def safe_header(name, value):
424     if name in SENSITIVE_HEADERS:
425         v = value.encode('utf-8')
426         h = hashlib.sha1(v)
427         d = h.hexdigest()
428         return name, "{SHA1}%s" % d
429     else:
430         return name, value
431
432
433 def to_str(value):
434     if value is None:
435         return value
436     if not isinstance(value, six.string_types):
437         return str(value)
438     return value
439
440
441 def get_host_min_mac(host_interfaces):
442     mac_list = [interface['mac'] for interface in
443                 host_interfaces if interface.get('mac')]
444     if mac_list:
445         return min(mac_list)
446     else:
447         return None
448
449
450 class IterableWithLength(object):
451     def __init__(self, iterable, length):
452         self.iterable = iterable
453         self.length = length
454
455     def __iter__(self):
456         return self.iterable
457
458     def next(self):
459         return next(self.iterable)
460
461     def __len__(self):
462         return self.length