oo_filters.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. #!/usr/bin/python
  2. # -*- coding: utf-8 -*-
  3. # vim: expandtab:tabstop=4:shiftwidth=4
  4. '''
  5. Custom filters for use in openshift-ansible
  6. '''
  7. from ansible import errors
  8. from operator import itemgetter
  9. import OpenSSL.crypto
  10. import os.path
  11. import pdb
  12. import re
  13. import json
  14. class FilterModule(object):
  15. ''' Custom ansible filters '''
  16. @staticmethod
  17. def oo_pdb(arg):
  18. ''' This pops you into a pdb instance where arg is the data passed in
  19. from the filter.
  20. Ex: "{{ hostvars | oo_pdb }}"
  21. '''
  22. pdb.set_trace()
  23. return arg
  24. @staticmethod
  25. def get_attr(data, attribute=None):
  26. ''' This looks up dictionary attributes of the form a.b.c and returns
  27. the value.
  28. Ex: data = {'a': {'b': {'c': 5}}}
  29. attribute = "a.b.c"
  30. returns 5
  31. '''
  32. if not attribute:
  33. raise errors.AnsibleFilterError("|failed expects attribute to be set")
  34. ptr = data
  35. for attr in attribute.split('.'):
  36. ptr = ptr[attr]
  37. return ptr
  38. @staticmethod
  39. def oo_flatten(data):
  40. ''' This filter plugin will flatten a list of lists
  41. '''
  42. if not issubclass(type(data), list):
  43. raise errors.AnsibleFilterError("|failed expects to flatten a List")
  44. return [item for sublist in data for item in sublist]
  45. @staticmethod
  46. def oo_collect(data, attribute=None, filters=None):
  47. ''' This takes a list of dict and collects all attributes specified into a
  48. list. If filter is specified then we will include all items that
  49. match _ALL_ of filters. If a dict entry is missing the key in a
  50. filter it will be excluded from the match.
  51. Ex: data = [ {'a':1, 'b':5, 'z': 'z'}, # True, return
  52. {'a':2, 'z': 'z'}, # True, return
  53. {'a':3, 'z': 'z'}, # True, return
  54. {'a':4, 'z': 'b'}, # FAILED, obj['z'] != obj['z']
  55. ]
  56. attribute = 'a'
  57. filters = {'z': 'z'}
  58. returns [1, 2, 3]
  59. '''
  60. if not issubclass(type(data), list):
  61. raise errors.AnsibleFilterError("|failed expects to filter on a List")
  62. if not attribute:
  63. raise errors.AnsibleFilterError("|failed expects attribute to be set")
  64. if filters is not None:
  65. if not issubclass(type(filters), dict):
  66. raise errors.AnsibleFilterError("|failed expects filter to be a"
  67. " dict")
  68. retval = [FilterModule.get_attr(d, attribute) for d in data if (
  69. all([d.get(key, None) == filters[key] for key in filters]))]
  70. else:
  71. retval = [FilterModule.get_attr(d, attribute) for d in data]
  72. return retval
  73. @staticmethod
  74. def oo_select_keys_from_list(data, keys):
  75. ''' This returns a list, which contains the value portions for the keys
  76. Ex: data = { 'a':1, 'b':2, 'c':3 }
  77. keys = ['a', 'c']
  78. returns [1, 3]
  79. '''
  80. if not issubclass(type(data), list):
  81. raise errors.AnsibleFilterError("|failed expects to filter on a list")
  82. if not issubclass(type(keys), list):
  83. raise errors.AnsibleFilterError("|failed expects first param is a list")
  84. # Gather up the values for the list of keys passed in
  85. retval = [FilterModule.oo_select_keys(item, keys) for item in data]
  86. return FilterModule.oo_flatten(retval)
  87. @staticmethod
  88. def oo_select_keys(data, keys):
  89. ''' This returns a list, which contains the value portions for the keys
  90. Ex: data = { 'a':1, 'b':2, 'c':3 }
  91. keys = ['a', 'c']
  92. returns [1, 3]
  93. '''
  94. if not issubclass(type(data), dict):
  95. raise errors.AnsibleFilterError("|failed expects to filter on a dict")
  96. if not issubclass(type(keys), list):
  97. raise errors.AnsibleFilterError("|failed expects first param is a list")
  98. # Gather up the values for the list of keys passed in
  99. retval = [data[key] for key in keys if data.has_key(key)]
  100. return retval
  101. @staticmethod
  102. def oo_prepend_strings_in_list(data, prepend):
  103. ''' This takes a list of strings and prepends a string to each item in the
  104. list
  105. Ex: data = ['cart', 'tree']
  106. prepend = 'apple-'
  107. returns ['apple-cart', 'apple-tree']
  108. '''
  109. if not issubclass(type(data), list):
  110. raise errors.AnsibleFilterError("|failed expects first param is a list")
  111. if not all(isinstance(x, basestring) for x in data):
  112. raise errors.AnsibleFilterError("|failed expects first param is a list"
  113. " of strings")
  114. retval = [prepend + s for s in data]
  115. return retval
  116. @staticmethod
  117. def oo_combine_key_value(data, joiner='='):
  118. '''Take a list of dict in the form of { 'key': 'value'} and
  119. arrange them as a list of strings ['key=value']
  120. '''
  121. if not issubclass(type(data), list):
  122. raise errors.AnsibleFilterError("|failed expects first param is a list")
  123. rval = []
  124. for item in data:
  125. rval.append("%s%s%s" % (item['key'], joiner, item['value']))
  126. return rval
  127. @staticmethod
  128. def oo_combine_dict(data, in_joiner='=', out_joiner=' '):
  129. '''Take a dict in the form of { 'key': 'value', 'key': 'value' } and
  130. arrange them as a string 'key=value key=value'
  131. '''
  132. if not issubclass(type(data), dict):
  133. raise errors.AnsibleFilterError("|failed expects first param is a dict")
  134. return out_joiner.join([in_joiner.join([k, v]) for k, v in data.items()])
  135. @staticmethod
  136. def oo_ami_selector(data, image_name):
  137. ''' This takes a list of amis and an image name and attempts to return
  138. the latest ami.
  139. '''
  140. if not issubclass(type(data), list):
  141. raise errors.AnsibleFilterError("|failed expects first param is a list")
  142. if not data:
  143. return None
  144. else:
  145. if image_name is None or not image_name.endswith('_*'):
  146. ami = sorted(data, key=itemgetter('name'), reverse=True)[0]
  147. return ami['ami_id']
  148. else:
  149. ami_info = [(ami, ami['name'].split('_')[-1]) for ami in data]
  150. ami = sorted(ami_info, key=itemgetter(1), reverse=True)[0][0]
  151. return ami['ami_id']
  152. @staticmethod
  153. def oo_ec2_volume_definition(data, host_type, docker_ephemeral=False):
  154. ''' This takes a dictionary of volume definitions and returns a valid ec2
  155. volume definition based on the host_type and the values in the
  156. dictionary.
  157. The dictionary should look similar to this:
  158. { 'master':
  159. { 'root':
  160. { 'volume_size': 10, 'device_type': 'gp2',
  161. 'iops': 500
  162. }
  163. },
  164. 'node':
  165. { 'root':
  166. { 'volume_size': 10, 'device_type': 'io1',
  167. 'iops': 1000
  168. },
  169. 'docker':
  170. { 'volume_size': 40, 'device_type': 'gp2',
  171. 'iops': 500, 'ephemeral': 'true'
  172. }
  173. }
  174. }
  175. '''
  176. if not issubclass(type(data), dict):
  177. raise errors.AnsibleFilterError("|failed expects first param is a dict")
  178. if host_type not in ['master', 'node', 'etcd']:
  179. raise errors.AnsibleFilterError("|failed expects etcd, master or node"
  180. " as the host type")
  181. root_vol = data[host_type]['root']
  182. root_vol['device_name'] = '/dev/sda1'
  183. root_vol['delete_on_termination'] = True
  184. if root_vol['device_type'] != 'io1':
  185. root_vol.pop('iops', None)
  186. if host_type == 'node':
  187. docker_vol = data[host_type]['docker']
  188. docker_vol['device_name'] = '/dev/xvdb'
  189. docker_vol['delete_on_termination'] = True
  190. if docker_vol['device_type'] != 'io1':
  191. docker_vol.pop('iops', None)
  192. if docker_ephemeral:
  193. docker_vol.pop('device_type', None)
  194. docker_vol.pop('delete_on_termination', None)
  195. docker_vol['ephemeral'] = 'ephemeral0'
  196. return [root_vol, docker_vol]
  197. elif host_type == 'etcd':
  198. etcd_vol = data[host_type]['etcd']
  199. etcd_vol['device_name'] = '/dev/xvdb'
  200. etcd_vol['delete_on_termination'] = True
  201. if etcd_vol['device_type'] != 'io1':
  202. etcd_vol.pop('iops', None)
  203. return [root_vol, etcd_vol]
  204. return [root_vol]
  205. @staticmethod
  206. def oo_split(string, separator=','):
  207. ''' This splits the input string into a list
  208. '''
  209. return string.split(separator)
  210. @staticmethod
  211. def oo_filter_list(data, filter_attr=None):
  212. ''' This returns a list, which contains all items where filter_attr
  213. evaluates to true
  214. Ex: data = [ { a: 1, b: True },
  215. { a: 3, b: False },
  216. { a: 5, b: True } ]
  217. filter_attr = 'b'
  218. returns [ { a: 1, b: True },
  219. { a: 5, b: True } ]
  220. '''
  221. if not issubclass(type(data), list):
  222. raise errors.AnsibleFilterError("|failed expects to filter on a list")
  223. if not issubclass(type(filter_attr), str):
  224. raise errors.AnsibleFilterError("|failed expects filter_attr is a str")
  225. # Gather up the values for the list of keys passed in
  226. return [x for x in data if x[filter_attr]]
  227. @staticmethod
  228. def oo_parse_heat_stack_outputs(data):
  229. ''' Formats the HEAT stack output into a usable form
  230. The goal is to transform something like this:
  231. +---------------+-------------------------------------------------+
  232. | Property | Value |
  233. +---------------+-------------------------------------------------+
  234. | capabilities | [] | |
  235. | creation_time | 2015-06-26T12:26:26Z | |
  236. | description | OpenShift cluster | |
  237. | … | … |
  238. | outputs | [ |
  239. | | { |
  240. | | "output_value": "value_A" |
  241. | | "description": "This is the value of Key_A" |
  242. | | "output_key": "Key_A" |
  243. | | }, |
  244. | | { |
  245. | | "output_value": [ |
  246. | | "value_B1", |
  247. | | "value_B2" |
  248. | | ], |
  249. | | "description": "This is the value of Key_B" |
  250. | | "output_key": "Key_B" |
  251. | | }, |
  252. | | ] |
  253. | parameters | { |
  254. | … | … |
  255. +---------------+-------------------------------------------------+
  256. into something like this:
  257. {
  258. "Key_A": "value_A",
  259. "Key_B": [
  260. "value_B1",
  261. "value_B2"
  262. ]
  263. }
  264. '''
  265. # Extract the “outputs” JSON snippet from the pretty-printed array
  266. in_outputs = False
  267. outputs = ''
  268. line_regex = re.compile(r'\|\s*(.*?)\s*\|\s*(.*?)\s*\|')
  269. for line in data['stdout_lines']:
  270. match = line_regex.match(line)
  271. if match:
  272. if match.group(1) == 'outputs':
  273. in_outputs = True
  274. elif match.group(1) != '':
  275. in_outputs = False
  276. if in_outputs:
  277. outputs += match.group(2)
  278. outputs = json.loads(outputs)
  279. # Revamp the “outputs” to put it in the form of a “Key: value” map
  280. revamped_outputs = {}
  281. for output in outputs:
  282. revamped_outputs[output['output_key']] = output['output_value']
  283. return revamped_outputs
  284. @staticmethod
  285. # pylint: disable=too-many-branches
  286. def oo_parse_certificate_names(certificates, data_dir, internal_hostnames):
  287. ''' Parses names from list of certificate hashes.
  288. Ex: certificates = [{ "certfile": "/etc/origin/master/custom1.crt",
  289. "keyfile": "/etc/origin/master/custom1.key" },
  290. { "certfile": "custom2.crt",
  291. "keyfile": "custom2.key" }]
  292. returns [{ "certfile": "/etc/origin/master/custom1.crt",
  293. "keyfile": "/etc/origin/master/custom1.key",
  294. "names": [ "public-master-host.com",
  295. "other-master-host.com" ] },
  296. { "certfile": "/etc/origin/master/custom2.crt",
  297. "keyfile": "/etc/origin/master/custom2.key",
  298. "names": [ "some-hostname.com" ] }]
  299. '''
  300. if not issubclass(type(certificates), list):
  301. raise errors.AnsibleFilterError("|failed expects certificates is a list")
  302. if not issubclass(type(data_dir), unicode):
  303. raise errors.AnsibleFilterError("|failed expects data_dir is unicode")
  304. if not issubclass(type(internal_hostnames), list):
  305. raise errors.AnsibleFilterError("|failed expects internal_hostnames is list")
  306. for certificate in certificates:
  307. if 'names' in certificate.keys():
  308. continue
  309. else:
  310. certificate['names'] = []
  311. if not os.path.isfile(certificate['certfile']) and not os.path.isfile(certificate['keyfile']):
  312. # Unable to find cert/key, try to prepend data_dir to paths
  313. certificate['certfile'] = os.path.join(data_dir, certificate['certfile'])
  314. certificate['keyfile'] = os.path.join(data_dir, certificate['keyfile'])
  315. if not os.path.isfile(certificate['certfile']) and not os.path.isfile(certificate['keyfile']):
  316. # Unable to find cert/key in data_dir
  317. raise errors.AnsibleFilterError("|certificate and/or key does not exist '%s', '%s'" %
  318. (certificate['certfile'], certificate['keyfile']))
  319. try:
  320. st_cert = open(certificate['certfile'], 'rt').read()
  321. cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, st_cert)
  322. certificate['names'].append(str(cert.get_subject().commonName.decode()))
  323. for i in range(cert.get_extension_count()):
  324. if cert.get_extension(i).get_short_name() == 'subjectAltName':
  325. for name in str(cert.get_extension(i)).replace('DNS:', '').split(', '):
  326. certificate['names'].append(name)
  327. except:
  328. raise errors.AnsibleFilterError(("|failed to parse certificate '%s', " % certificate['certfile'] +
  329. "please specify certificate names in host inventory"))
  330. certificate['names'] = [name for name in certificate['names'] if name not in internal_hostnames]
  331. certificate['names'] = list(set(certificate['names']))
  332. if not certificate['names']:
  333. raise errors.AnsibleFilterError(("|failed to parse certificate '%s' or " % certificate['certfile'] +
  334. "detected a collision with internal hostname, please specify " +
  335. "certificate names in host inventory"))
  336. return certificates
  337. def filters(self):
  338. ''' returns a mapping of filters to methods '''
  339. return {
  340. "oo_select_keys": self.oo_select_keys,
  341. "oo_select_keys_from_list": self.oo_select_keys_from_list,
  342. "oo_collect": self.oo_collect,
  343. "oo_flatten": self.oo_flatten,
  344. "oo_pdb": self.oo_pdb,
  345. "oo_prepend_strings_in_list": self.oo_prepend_strings_in_list,
  346. "oo_ami_selector": self.oo_ami_selector,
  347. "oo_ec2_volume_definition": self.oo_ec2_volume_definition,
  348. "oo_combine_key_value": self.oo_combine_key_value,
  349. "oo_combine_dict": self.oo_combine_dict,
  350. "oo_split": self.oo_split,
  351. "oo_filter_list": self.oo_filter_list,
  352. "oo_parse_heat_stack_outputs": self.oo_parse_heat_stack_outputs,
  353. "oo_parse_certificate_names": self.oo_parse_certificate_names
  354. }