3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
18 from datetime import datetime
19 from dateutil import tz
20 from cliff.show import ShowOne
21 from cliff.lister import Lister
22 from cliff.command import Command
31 CHOICES = 'choices' # allowed values for an argument (coming from argparse 'choices' facility)
32 VALUES = 'values' # allowed values for an argument, even if the argument supports adding multiple instances
33 FIELDS = 'fields' # filtered list of expected columns in the response (translates to argparse's 'columns' of)
34 COLUMNS = 'columns' # same as fields
35 DETAILED = 'detailed' # Makes the command to show all accessible details of the requsted objects.
36 # Should not be positional (so should not be the first in the arguments list)
41 class HelperBase(object):
42 """Helper base class validating arguments and doing the business logic (send query and receive and process table in response)"""
44 self.operation = 'get'
51 self.no_positional = False
52 self.mandatory_positional = False
54 self.resource_prefix = ''
55 self.default_sort = None
56 self.positional_count = 1 # how many mandatory arguments are
58 def get_parser_with_arguments(self, parser):
59 args = self.arguments[:]
60 if self.no_positional is False:
61 for i in range (0, self.positional_count):
63 parser.add_argument(first,
64 metavar=first.upper(),
65 nargs=None if self.mandatory_positional else '?',
66 default=self.fieldmap[first].get(DEFAULT, ALL),
67 help=self.fieldmap[first][HELP])
69 # This is very similar to 'choices' facility of argparse, however it allows multiple choices combined...
71 default = self.fieldmap[e].get(DEFAULT, ALL)
72 if e in [DETAILED, UTC]:
73 parser.add_argument('--%s' % e, dest=e, action='store_true', help=self.fieldmap[e][HELP])
76 # ...and is needed here to list the allowed arguments in the help
77 multichoices = ' [%s]' % ','.join([self.fieldmap[i][DISPLAY] for i in self.columns])
79 default = '%s:%s' % (self.fieldmap[self.default_sort[0]][DISPLAY], self.default_sort[1])
80 elif VALUES in self.fieldmap[e]:
81 multichoices = ' [%s]' % ','.join(self.fieldmap[e][VALUES])
82 parser.add_argument('--%s' % e,
88 choices=self.fieldmap[e].get(CHOICES, None),
89 help=self.fieldmap[e][HELP] + multichoices)
92 def send_receive(self, app, parsed_args):
93 parsed_args = self.validate_parameters(parsed_args)
94 if parsed_args.fields:
95 self.arguments.append(FIELDS)
96 arguments = {k: v for k, v in sorted(vars(parsed_args).items()) if k in self.arguments and
102 req = app.client_manager.resthandler
103 response = req._operation(self.operation,
104 '%s%s' %(self.resource_prefix, self.endpoint),
105 arguments if self.usebody else None,
106 None if self.usebody else arguments,
109 raise Exception('Request response is not OK (%s)' % response.reason)
110 result = response.json()
111 if 0 != result['code']:
112 raise Exception(result['description'])
115 def validate_parameters(self, args):
116 if 'starttime' in self.arguments:
117 args.starttime = HelperBase.convert_timezone_to_utc(args.starttime)
118 if 'endtime' in self.arguments:
119 args.endtime = HelperBase.convert_timezone_to_utc(args.endtime)
121 if hasattr(args, COLUMNS):
122 args.columns = list(set(j for i in args.columns for j in i.split(',')))
124 args.fields = ','.join([self.get_key_by_value(k) for k in sorted(args.columns)])
125 for a in self.arguments:
126 argval = getattr(args, a)
127 if isinstance(argval, str):
128 for p in argval.split(','):
129 if argval != ALL and VALUES in self.fieldmap[a] and p not in self.fieldmap[a][VALUES]:
130 raise Exception('%s is not supported by %s argument' % (p, a))
134 def validate_datetime(dt):
137 formats = ['%Y-%m-%dT%H:%M:%S.%fZ',
138 '%Y-%m-%dT%H:%M:%SZ',
144 if 'Z' != retval[-1]:
146 retval = datetime.strptime(retval, f).__str__()
147 retval = '%s.000' % retval if len(retval) <= 19 else retval[:-3]
148 return retval.replace(' ', 'T') + 'Z'
151 raise Exception('Datetime format (%s) is not supported' % dt)
154 def convert_utc_to_timezone(timestr):
155 timestr = timestr.replace('Z', '')
156 # max resolution for strptime is microsec
157 if len(timestr) > 26:
158 timestr = timestr[:26]
159 from_zone = tz.tzutc()
160 to_zone = tz.tzlocal()
161 utc = datetime.strptime(HelperBase.validate_datetime(timestr + 'Z'), '%Y-%m-%dT%H:%M:%S.%fZ')
162 utc = utc.replace(tzinfo=from_zone)
163 ret = str(utc.astimezone(to_zone))
164 return ret[:23] if '.' in ret else ret[:19] + '.000'
167 def convert_timezone_to_utc(timestr):
168 if timestr[-1] == 'Z' or timestr == ALL:
169 # we assume that UTC will always have a Z character at the end
171 formats = ['%Y-%m-%dT%H:%M:%S.%f',
176 timestr = timestr.replace(' ', 'T')
177 if len(timestr) > 26:
178 timestr = timestr[:26]
179 from_zone = tz.tzlocal()
183 localtime = datetime.strptime(timestr, f)
184 localtime = localtime.replace(tzinfo=from_zone)
185 ret = str(localtime.astimezone(to_zone))
186 retval = ret[:23] if '.' in ret else ret[:19] + '.000'
187 return retval.replace(' ', 'T') + 'Z'
190 raise Exception('Datetime format (%s) is not supported' % origstr)
192 def filter_columns(self, args):
193 if getattr(args, DETAILED, False) is True:
194 self.columns.extend(self.detailed)
195 if ALL != args.fields:
196 for i in range(len(self.columns) - 1, -1, -1):
197 if self.columns[i] not in args.fields:
199 return [self.fieldmap[f][DISPLAY] for f in self.columns]
201 def get_key_by_value(self, val):
202 for k, v in self.fieldmap.items():
203 if DISPLAY in v and val == v[DISPLAY]:
205 raise Exception('No column named %s' % val)
207 def get_sorted_keys(self, parsed_args, data):
208 keylist = data.keys()
209 if hasattr(parsed_args, SORT):
210 sortexp = parsed_args.sort
212 # The next one generates two lists, one with the field names (to be sorted),
213 # and another with the directions. True if reversed, false otherwise
214 # also if no direction is added for a field, then it adds an ':asc' by default.
215 skeys, sdir = zip(*[(self.get_key_by_value(x[0]), False if 'asc' in x[1].lower() else True)
216 for x in (('%s:asc' % x).split(":") for x in reversed(sortexp.split(',')))])
217 for k, d in zip(skeys, sdir):
218 keylist.sort(key=lambda x: data[x][k], reverse=d)
222 def construct_message(text, result):
223 p = re.compile('\#\#(\w+)')
228 text = p.sub(result[DATA][m.group(1)], text, 1)
232 class ListerHelper(Lister, HelperBase):
233 """Helper class for Lister"""
234 def __init__(self, app, app_args, cmd_name=None):
235 Lister.__init__(self, app, app_args, cmd_name)
236 HelperBase.__init__(self)
238 def get_parser(self, prog_name):
239 parser = super(ListerHelper, self).get_parser(prog_name)
240 return self.get_parser_with_arguments(parser)
242 def take_action(self, parsed_args):
244 result = self.send_receive(self.app, parsed_args)
245 header = self.filter_columns(parsed_args)
247 for k in self.get_sorted_keys(parsed_args, result[DATA]):
248 row = [HelperBase.convert_utc_to_timezone(result[DATA][k][i])
249 if not getattr(parsed_args, UTC, False) and i == TIME
250 else result[DATA][k][i] for i in self.columns]
253 self.app.stdout.write(self.message + '\n')
255 except Exception as exp:
256 self.app.stderr.write('Failed with error:\n%s\n' % str(exp))
260 class ShowOneHelper(ShowOne, HelperBase):
261 """Helper class for ShowOne"""
262 def __init__(self, app, app_args, cmd_name=None):
263 ShowOne.__init__(self, app, app_args, cmd_name)
264 HelperBase.__init__(self)
266 def get_parser(self, prog_name):
267 parser = super(ShowOneHelper, self).get_parser(prog_name)
268 return self.get_parser_with_arguments(parser)
270 def take_action(self, parsed_args):
272 result = self.send_receive(self.app, parsed_args)
273 header = self.filter_columns(parsed_args)
274 sorted_keys = self.get_sorted_keys(parsed_args, result[DATA])
276 self.app.stdout.write(self.message + '\n')
277 for k in sorted_keys:
278 data = [HelperBase.convert_utc_to_timezone(result[DATA][k][i])
279 if not getattr(parsed_args, UTC, False) and i == TIME
280 else result[DATA][k][i] for i in self.columns]
281 if k != sorted_keys[-1]:
282 self.formatter.emit_one(header, data, self.app.stdout, parsed_args)
283 self.app.stdout.write('\n')
285 except Exception as exp:
286 self.app.stderr.write('Failed with error:\n%s\n' % str(exp))
290 class CommandHelper(Command, HelperBase):
291 """Helper class for Command"""
292 def __init__(self, app, app_args, cmd_name=None):
293 Command.__init__(self, app, app_args, cmd_name)
294 HelperBase.__init__(self)
296 def get_parser(self, prog_name):
297 parser = super(CommandHelper, self).get_parser(prog_name)
298 return self.get_parser_with_arguments(parser)
300 def take_action(self, parsed_args):
302 result = self.send_receive(self.app, parsed_args)
304 self.app.stdout.write(HelperBase.construct_message(self.message, result))
305 except Exception as exp:
306 self.app.stderr.write('Failed with error:\n%s\n' % str(exp))