PageRenderTime 58ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/maas/plugins/swift-recon.py

https://gitlab.com/d34dh0r53/rpc-openstack
Python | 389 lines | 318 code | 28 blank | 43 comment | 16 complexity | 9f5c9acfd9899810991a91386d1bca39 MD5 | raw file
  1. #!/usr/bin/env python
  2. # Copyright 2014, Rackspace US, Inc.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain 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,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. # Example usages
  16. # python swift-recon.py replication --ring-type account
  17. # python swift-recon.py replication --ring-type container
  18. # python swift-recon.py replication --ring-type object
  19. # python swift-recon.py async-pendings
  20. # python swift-recon.py md5
  21. # python swift-recon.py quarantine
  22. import argparse
  23. import re
  24. import subprocess
  25. import maas_common
  26. class ParseError(maas_common.MaaSException):
  27. pass
  28. class CommandNotRecognized(maas_common.MaaSException):
  29. pass
  30. def recon_output(for_ring, options=None):
  31. """Run swift-recon and filter out extraneous printed lines.
  32. ::
  33. >>> recon_output('account', '-r')
  34. ['[2014-11-21 00:25:16] Checking on replication',
  35. '[replication_failure] low: 0, high: 0, avg: 0.0, total: 0, \
  36. Failed: 0.0%, no_result: 0, reported: 2',
  37. '[replication_success] low: 2, high: 4, avg: 3.0, total: 6, \
  38. Failed: 0.0%, no_result: 0, reported: 2',
  39. '[replication_time] low: 0, high: 0, avg: 0.0, total: 0, \
  40. Failed: 0.0%, no_result: 0, reported: 2',
  41. '[replication_attempted] low: 1, high: 2, avg: 1.5, total: 3, \
  42. Failed: 0.0%, no_result: 0, reported: 2',
  43. 'Oldest completion was 2014-11-21 00:24:51 (25 seconds ago) by \
  44. 192.168.31.1:6002.',
  45. 'Most recent completion was 2014-11-21 00:24:56 (20 seconds ago) by \
  46. 192.168.31.2:6002.']
  47. :param str for_ring: Which ring to run swift-recon on
  48. :param list options: Command line options with which to run swift-recon
  49. :returns: Strings from output that are most important
  50. :rtype: list
  51. """
  52. command = ['swift-recon', for_ring]
  53. command.extend(options or [])
  54. out = subprocess.check_output(command)
  55. return filter(lambda s: s and not s.startswith(('==', '-')),
  56. out.split('\n'))
  57. def stat_regexp_generator(data):
  58. """Generate a regeular expression that will swift-recon stats.
  59. Lines printed by swift-recon look like::
  60. [data] low: 0, high: 0, avg: 0.0, total: 0, Failed: 0.0%, \
  61. no_result: 0, reported: 0
  62. Where data above is the value of the ``data`` parameter passed to the
  63. function.
  64. """
  65. expression = """\s+low:\s+(?P<low>\d+), # parse out the low result
  66. \s+high:\s+(?P<high>\d+), # parse out the high result
  67. \s+avg:\s+(?P<avg>\d+.\d+), # you get the idea now
  68. \s+total:\s+(?P<total>\d+),
  69. \s+Failed:\s+(?P<failed>\d+.\d+%),
  70. \s+no_result:\s+(?P<no_result>\d+),
  71. \s+reported:\s+(?P<reported>\d+)"""
  72. return re.compile('\[' + data + '\]' + expression, re.VERBOSE)
  73. def _parse_into_dict(line, parsed_by):
  74. match = parsed_by.match(line)
  75. if match:
  76. return match.groupdict()
  77. else:
  78. raise ParseError("Cannot parse '{0}' for statistics.".format(line))
  79. def recon_stats_dicts(for_ring, options, starting_with, parsed_by):
  80. """Return a list of dictionaries of parsed statistics.
  81. Swift-recon has a standard format for it's statistics:
  82. [name] low: 0, high: 0, avg: 0.0, total: 0, Failed: 0.0%, no_result: 0, \
  83. reported: 0
  84. Using the regular expression passed by the user in ``parsed_by``, we parse
  85. this out and return dictionaries of lines that start with
  86. ``starting_with``. This is after we get the recon output for the ring and
  87. options.
  88. :param str for_ring: Which ring to run swift-recon on
  89. :param list options: Command line options with which to run swift-recon
  90. :param str starting_with: String with which to filter lines
  91. :param parsed_by: Compiled regular expression to match and parse with
  92. :type parsed_by: _sre.SRE_Pattern (the result of calling re.compile)
  93. :returns: list of dictionaries of parse data
  94. """
  95. return map(lambda l: _parse_into_dict(l, parsed_by),
  96. filter(lambda s: s.startswith(starting_with),
  97. recon_output(for_ring, options)))
  98. def swift_replication(for_ring):
  99. """Parse swift-recon's replication statistics and return them.
  100. ::
  101. >>> swift_replication('account')
  102. {'attempted': {'avg': '1.5',
  103. 'failed': '0.0%',
  104. 'high': '2',
  105. 'low': '1',
  106. 'no_result': '0',
  107. 'reported": '5',
  108. 'total': '3'},
  109. 'failure': {'avg': '0.0',
  110. 'failed': '0.0%',
  111. 'high': '0',
  112. 'low': '0',
  113. 'no_result': '0',
  114. 'reported": '5',
  115. 'total': '0'},
  116. 'success': {'avg': '3.0',
  117. 'failed': '0.0%',
  118. 'high': '4',
  119. 'low': '2',
  120. 'no_result': '0',
  121. 'reported": '5',
  122. 'total': '6'},
  123. 'time': {'avg': '0.0',
  124. 'failed': '0.0%',
  125. 'high': '0',
  126. 'low': '0',
  127. 'no_result': '0',
  128. 'reported": '5',
  129. 'total': '0'}}
  130. :param str for_ring: Which ring to run swift-recon on
  131. :returns: Dictionary of attempted, failed, success, and time statistics
  132. :rtype: dict
  133. """
  134. regexp = stat_regexp_generator(r'replication_(?P<replication_type>\w+)')
  135. replication_dicts = recon_stats_dicts(for_ring, ['-r'], '[replication_',
  136. regexp)
  137. # reduce could work here but would require an enclosed function which is
  138. # less readable than this loop
  139. replication_statistics = {}
  140. for rep_dict in replication_dicts:
  141. replication_statistics[rep_dict.pop('replication_type')] = rep_dict
  142. return replication_statistics
  143. def swift_async():
  144. """Parse swift-recon's async pendings statistics and return them.
  145. ::
  146. >>> swift_async()
  147. {'avg': '0.0',
  148. 'failed': '0.0%',
  149. 'high': '0',
  150. 'low': '0',
  151. 'no_result': '0',
  152. 'reported': '2',
  153. 'total': '0'}
  154. :returns: Dictionary of average, failed, high, low, no_result, reported,
  155. and total statistics.
  156. """
  157. regexp = stat_regexp_generator('async_pending')
  158. async_dicts = recon_stats_dicts('object', ['-a'], '[async_pending]',
  159. regexp)
  160. stats = {}
  161. for async_dict in async_dicts:
  162. if async_dict:
  163. stats = async_dict
  164. # Break will skip the for-loop's else block
  165. break
  166. else:
  167. # If we didn't find a non-empty dict, error out
  168. maas_common.status_err(
  169. 'No data could be collected about pending async operations'
  170. )
  171. return {'async': stats}
  172. def swift_quarantine():
  173. """Parse swift-recon's quarantined objects and return them.
  174. ::
  175. >>> swift_quarantine()
  176. {'accounts': {'avg': '0.0',
  177. 'failed': '0.0%',
  178. 'high': '0',
  179. 'low': '0',
  180. 'no_result': '0',
  181. 'reported': '2',
  182. 'total': '0'},
  183. 'containers': {'avg': '0.0',
  184. 'failed': '0.0%',
  185. 'high': '0',
  186. 'low': '0',
  187. 'no_result': '0',
  188. 'reported': '2',
  189. 'total': '0'},
  190. 'objects': {'avg': '0.0',
  191. 'failed': '0.0%',
  192. 'high': '0',
  193. 'low': '0',
  194. 'no_result': '0',
  195. 'reported': '2',
  196. 'total': '0'}}
  197. :returns: Dictionary of objects, accounts, and containers.
  198. """
  199. regexp = stat_regexp_generator('quarantined_(?P<ring>\w+)')
  200. quarantined_dicts = recon_stats_dicts('-q', [], '[quarantined_',
  201. regexp)
  202. quarantined_statistics = {}
  203. for quar_dict in quarantined_dicts:
  204. quarantined_statistics[quar_dict.pop('ring')] = quar_dict
  205. return quarantined_statistics
  206. def swift_md5():
  207. """Parse swift-recon's md5 check output and return it.
  208. ::
  209. >>> swift_md5()
  210. {'ring': {'errors': '0', 'success': '2', 'total': '2'},
  211. 'swiftconf': {'errors': '0', 'success': '2', 'total': '2'}}
  212. :returns: Dictioanry
  213. """
  214. check_re = re.compile('Checking\s+(?P<check>[^\s]+)\s+md5sums?')
  215. error_re = re.compile('https?://(?P<address>[^:]+):\d+')
  216. result_re = re.compile(
  217. '(?P<success>\d+)/(?P<total>\d+)[^\d]+(?P<errors>\d+).*'
  218. )
  219. output = recon_output('--md5') # We need to pass --md5 as a string here
  220. md5_statistics = {}
  221. checking_dict = {}
  222. for line in output:
  223. check_match = check_re.search(line)
  224. if check_match and not checking_dict:
  225. checking_dict = check_match.groupdict()
  226. # First line of a grouping we care about, might as well skip all
  227. # other checks in the loop
  228. continue
  229. # If there was an error checking the md5sum, error out immediately
  230. if line.startswith('!!'):
  231. error_dict = error_re.search(line).groupdict()
  232. maas_common.status_err('md5 mismatch for {0} on host {1}'.format(
  233. checking_dict.get('check'), error_dict['address']
  234. ))
  235. results_match = result_re.match(line)
  236. if results_match:
  237. check_name = checking_dict['check'].replace('.', '_')
  238. md5_statistics[check_name] = results_match.groupdict()
  239. checking_dict = {}
  240. return md5_statistics
  241. def print_nested_stats(statistics):
  242. """Print out nested statistics.
  243. Nested statistics would be retrieved from ``swift_quarantine`` or
  244. ``swift_replication``.
  245. The following will show what you'll see
  246. ::
  247. >>> a = {'accounts': {'avg': '0.0',
  248. 'failed': '0.0%',
  249. 'high': '0',
  250. 'low': '0',
  251. 'no_result': '0',
  252. 'reported': '2',
  253. 'total': '0'}}
  254. >>> print_nested_stats(a)
  255. metric accounts_avg double 0.0
  256. metric accounts_failed double 0.0
  257. metric accounts_high uint64 0
  258. metric accounts_low uint64 0
  259. metric accounts_no_result uint64 0
  260. metric accounts_total uint64 0
  261. """
  262. for ring, stats in statistics.items():
  263. print_stats(ring, stats)
  264. metrics_per_stat = {
  265. 'avg': lambda name, val: maas_common.metric(name, 'double', val),
  266. 'failed': lambda name, val: maas_common.metric(name, 'double', val[:-1])
  267. }
  268. DEFAULT_METRIC = lambda name, val: maas_common.metric(name, 'uint64', val)
  269. def print_stats(prefix, statistics):
  270. """Print out statistics.
  271. """
  272. for name, value in statistics.items():
  273. metric = metrics_per_stat.get(name, DEFAULT_METRIC)
  274. metric('{0}_{1}'.format(prefix, name), value)
  275. def make_parser():
  276. parser = argparse.ArgumentParser(
  277. description='Process and print swift-recon statistics'
  278. )
  279. parser.add_argument('recon',
  280. help='Which statistics to collect. Acceptable recon: '
  281. '"async-pendings", "md5", "quarantine", '
  282. '"replication"')
  283. parser.add_argument('--ring-type', dest='ring',
  284. help='Which ring to run statistics for. Only used by '
  285. 'replication recon.')
  286. return parser
  287. def get_stats_from(args):
  288. stats = {}
  289. if args.recon == 'async-pendings':
  290. stats = swift_async()
  291. elif args.recon == 'md5':
  292. stats = swift_md5()
  293. elif args.recon == 'quarantine':
  294. stats = swift_quarantine()
  295. elif args.recon == 'replication':
  296. if args.ring not in {"account", "container", "object"}:
  297. maas_common.status_err('no ring provided to check')
  298. stats = swift_replication(args.ring)
  299. else:
  300. raise CommandNotRecognized('unrecognized command "{0}"'.format(
  301. args.recon))
  302. return stats
  303. def main():
  304. parser = make_parser()
  305. args = parser.parse_args()
  306. try:
  307. stats = get_stats_from(args)
  308. except (ParseError, CommandNotRecognized) as e:
  309. maas_common.status_err(str(e))
  310. if stats:
  311. maas_common.status_ok()
  312. print_nested_stats(stats)
  313. if __name__ == '__main__':
  314. with maas_common.print_output():
  315. main()