oo_filters.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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
  11. import pdb
  12. import re
  13. import json
  14. import yaml
  15. from ansible.utils.unicode import to_unicode
  16. class FilterModule(object):
  17. ''' Custom ansible filters '''
  18. @staticmethod
  19. def oo_pdb(arg):
  20. ''' This pops you into a pdb instance where arg is the data passed in
  21. from the filter.
  22. Ex: "{{ hostvars | oo_pdb }}"
  23. '''
  24. pdb.set_trace()
  25. return arg
  26. @staticmethod
  27. def get_attr(data, attribute=None):
  28. ''' This looks up dictionary attributes of the form a.b.c and returns
  29. the value.
  30. Ex: data = {'a': {'b': {'c': 5}}}
  31. attribute = "a.b.c"
  32. returns 5
  33. '''
  34. if not attribute:
  35. raise errors.AnsibleFilterError("|failed expects attribute to be set")
  36. ptr = data
  37. for attr in attribute.split('.'):
  38. ptr = ptr[attr]
  39. return ptr
  40. @staticmethod
  41. def oo_flatten(data):
  42. ''' This filter plugin will flatten a list of lists
  43. '''
  44. if not issubclass(type(data), list):
  45. raise errors.AnsibleFilterError("|failed expects to flatten a List")
  46. return [item for sublist in data for item in sublist]
  47. @staticmethod
  48. def oo_collect(data, attribute=None, filters=None):
  49. ''' This takes a list of dict and collects all attributes specified into a
  50. list. If filter is specified then we will include all items that
  51. match _ALL_ of filters. If a dict entry is missing the key in a
  52. filter it will be excluded from the match.
  53. Ex: data = [ {'a':1, 'b':5, 'z': 'z'}, # True, return
  54. {'a':2, 'z': 'z'}, # True, return
  55. {'a':3, 'z': 'z'}, # True, return
  56. {'a':4, 'z': 'b'}, # FAILED, obj['z'] != obj['z']
  57. ]
  58. attribute = 'a'
  59. filters = {'z': 'z'}
  60. returns [1, 2, 3]
  61. '''
  62. if not issubclass(type(data), list):
  63. raise errors.AnsibleFilterError("|failed expects to filter on a List")
  64. if not attribute:
  65. raise errors.AnsibleFilterError("|failed expects attribute to be set")
  66. if filters is not None:
  67. if not issubclass(type(filters), dict):
  68. raise errors.AnsibleFilterError("|failed expects filter to be a"
  69. " dict")
  70. retval = [FilterModule.get_attr(d, attribute) for d in data if (
  71. all([d.get(key, None) == filters[key] for key in filters]))]
  72. else:
  73. retval = [FilterModule.get_attr(d, attribute) for d in data]
  74. return retval
  75. @staticmethod
  76. def oo_select_keys_from_list(data, keys):
  77. ''' This returns a list, which contains the value portions for the keys
  78. Ex: data = { 'a':1, 'b':2, 'c':3 }
  79. keys = ['a', 'c']
  80. returns [1, 3]
  81. '''
  82. if not issubclass(type(data), list):
  83. raise errors.AnsibleFilterError("|failed expects to filter on a list")
  84. if not issubclass(type(keys), list):
  85. raise errors.AnsibleFilterError("|failed expects first param is a list")
  86. # Gather up the values for the list of keys passed in
  87. retval = [FilterModule.oo_select_keys(item, keys) for item in data]
  88. return FilterModule.oo_flatten(retval)
  89. @staticmethod
  90. def oo_select_keys(data, keys):
  91. ''' This returns a list, which contains the value portions for the keys
  92. Ex: data = { 'a':1, 'b':2, 'c':3 }
  93. keys = ['a', 'c']
  94. returns [1, 3]
  95. '''
  96. if not issubclass(type(data), dict):
  97. raise errors.AnsibleFilterError("|failed expects to filter on a dict")
  98. if not issubclass(type(keys), list):
  99. raise errors.AnsibleFilterError("|failed expects first param is a list")
  100. # Gather up the values for the list of keys passed in
  101. retval = [data[key] for key in keys if data.has_key(key)]
  102. return retval
  103. @staticmethod
  104. def oo_prepend_strings_in_list(data, prepend):
  105. ''' This takes a list of strings and prepends a string to each item in the
  106. list
  107. Ex: data = ['cart', 'tree']
  108. prepend = 'apple-'
  109. returns ['apple-cart', 'apple-tree']
  110. '''
  111. if not issubclass(type(data), list):
  112. raise errors.AnsibleFilterError("|failed expects first param is a list")
  113. if not all(isinstance(x, basestring) for x in data):
  114. raise errors.AnsibleFilterError("|failed expects first param is a list"
  115. " of strings")
  116. retval = [prepend + s for s in data]
  117. return retval
  118. @staticmethod
  119. def oo_combine_key_value(data, joiner='='):
  120. '''Take a list of dict in the form of { 'key': 'value'} and
  121. arrange them as a list of strings ['key=value']
  122. '''
  123. if not issubclass(type(data), list):
  124. raise errors.AnsibleFilterError("|failed expects first param is a list")
  125. rval = []
  126. for item in data:
  127. rval.append("%s%s%s" % (item['key'], joiner, item['value']))
  128. return rval
  129. @staticmethod
  130. def oo_combine_dict(data, in_joiner='=', out_joiner=' '):
  131. '''Take a dict in the form of { 'key': 'value', 'key': 'value' } and
  132. arrange them as a string 'key=value key=value'
  133. '''
  134. if not issubclass(type(data), dict):
  135. raise errors.AnsibleFilterError("|failed expects first param is a dict")
  136. return out_joiner.join([in_joiner.join([k, v]) for k, v in data.items()])
  137. @staticmethod
  138. def oo_ami_selector(data, image_name):
  139. ''' This takes a list of amis and an image name and attempts to return
  140. the latest ami.
  141. '''
  142. if not issubclass(type(data), list):
  143. raise errors.AnsibleFilterError("|failed expects first param is a list")
  144. if not data:
  145. return None
  146. else:
  147. if image_name is None or not image_name.endswith('_*'):
  148. ami = sorted(data, key=itemgetter('name'), reverse=True)[0]
  149. return ami['ami_id']
  150. else:
  151. ami_info = [(ami, ami['name'].split('_')[-1]) for ami in data]
  152. ami = sorted(ami_info, key=itemgetter(1), reverse=True)[0][0]
  153. return ami['ami_id']
  154. @staticmethod
  155. def oo_ec2_volume_definition(data, host_type, docker_ephemeral=False):
  156. ''' This takes a dictionary of volume definitions and returns a valid ec2
  157. volume definition based on the host_type and the values in the
  158. dictionary.
  159. The dictionary should look similar to this:
  160. { 'master':
  161. { 'root':
  162. { 'volume_size': 10, 'device_type': 'gp2',
  163. 'iops': 500
  164. },
  165. 'docker':
  166. { 'volume_size': 40, 'device_type': 'gp2',
  167. 'iops': 500, 'ephemeral': 'true'
  168. }
  169. },
  170. 'node':
  171. { 'root':
  172. { 'volume_size': 10, 'device_type': 'io1',
  173. 'iops': 1000
  174. },
  175. 'docker':
  176. { 'volume_size': 40, 'device_type': 'gp2',
  177. 'iops': 500, 'ephemeral': 'true'
  178. }
  179. }
  180. }
  181. '''
  182. if not issubclass(type(data), dict):
  183. raise errors.AnsibleFilterError("|failed expects first param is a dict")
  184. if host_type not in ['master', 'node', 'etcd']:
  185. raise errors.AnsibleFilterError("|failed expects etcd, master or node"
  186. " as the host type")
  187. root_vol = data[host_type]['root']
  188. root_vol['device_name'] = '/dev/sda1'
  189. root_vol['delete_on_termination'] = True
  190. if root_vol['device_type'] != 'io1':
  191. root_vol.pop('iops', None)
  192. if host_type in ['master', 'node'] and 'docker' in data[host_type]:
  193. docker_vol = data[host_type]['docker']
  194. docker_vol['device_name'] = '/dev/xvdb'
  195. docker_vol['delete_on_termination'] = True
  196. if docker_vol['device_type'] != 'io1':
  197. docker_vol.pop('iops', None)
  198. if docker_ephemeral:
  199. docker_vol.pop('device_type', None)
  200. docker_vol.pop('delete_on_termination', None)
  201. docker_vol['ephemeral'] = 'ephemeral0'
  202. return [root_vol, docker_vol]
  203. elif host_type == 'etcd' and 'etcd' in data[host_type]:
  204. etcd_vol = data[host_type]['etcd']
  205. etcd_vol['device_name'] = '/dev/xvdb'
  206. etcd_vol['delete_on_termination'] = True
  207. if etcd_vol['device_type'] != 'io1':
  208. etcd_vol.pop('iops', None)
  209. return [root_vol, etcd_vol]
  210. return [root_vol]
  211. @staticmethod
  212. def oo_split(string, separator=','):
  213. ''' This splits the input string into a list
  214. '''
  215. return string.split(separator)
  216. @staticmethod
  217. def oo_haproxy_backend_masters(hosts):
  218. ''' This takes an array of dicts and returns an array of dicts
  219. to be used as a backend for the haproxy role
  220. '''
  221. servers = []
  222. for idx, host_info in enumerate(hosts):
  223. server = dict(name="master%s" % idx)
  224. server_ip = host_info['openshift']['common']['ip']
  225. server_port = host_info['openshift']['master']['api_port']
  226. server['address'] = "%s:%s" % (server_ip, server_port)
  227. server['opts'] = 'check'
  228. servers.append(server)
  229. return servers
  230. @staticmethod
  231. def oo_filter_list(data, filter_attr=None):
  232. ''' This returns a list, which contains all items where filter_attr
  233. evaluates to true
  234. Ex: data = [ { a: 1, b: True },
  235. { a: 3, b: False },
  236. { a: 5, b: True } ]
  237. filter_attr = 'b'
  238. returns [ { a: 1, b: True },
  239. { a: 5, b: True } ]
  240. '''
  241. if not issubclass(type(data), list):
  242. raise errors.AnsibleFilterError("|failed expects to filter on a list")
  243. if not issubclass(type(filter_attr), str):
  244. raise errors.AnsibleFilterError("|failed expects filter_attr is a str")
  245. # Gather up the values for the list of keys passed in
  246. return [x for x in data if x.has_key(filter_attr) and x[filter_attr]]
  247. @staticmethod
  248. def oo_parse_heat_stack_outputs(data):
  249. ''' Formats the HEAT stack output into a usable form
  250. The goal is to transform something like this:
  251. +---------------+-------------------------------------------------+
  252. | Property | Value |
  253. +---------------+-------------------------------------------------+
  254. | capabilities | [] | |
  255. | creation_time | 2015-06-26T12:26:26Z | |
  256. | description | OpenShift cluster | |
  257. | … | … |
  258. | outputs | [ |
  259. | | { |
  260. | | "output_value": "value_A" |
  261. | | "description": "This is the value of Key_A" |
  262. | | "output_key": "Key_A" |
  263. | | }, |
  264. | | { |
  265. | | "output_value": [ |
  266. | | "value_B1", |
  267. | | "value_B2" |
  268. | | ], |
  269. | | "description": "This is the value of Key_B" |
  270. | | "output_key": "Key_B" |
  271. | | }, |
  272. | | ] |
  273. | parameters | { |
  274. | … | … |
  275. +---------------+-------------------------------------------------+
  276. into something like this:
  277. {
  278. "Key_A": "value_A",
  279. "Key_B": [
  280. "value_B1",
  281. "value_B2"
  282. ]
  283. }
  284. '''
  285. # Extract the “outputs” JSON snippet from the pretty-printed array
  286. in_outputs = False
  287. outputs = ''
  288. line_regex = re.compile(r'\|\s*(.*?)\s*\|\s*(.*?)\s*\|')
  289. for line in data['stdout_lines']:
  290. match = line_regex.match(line)
  291. if match:
  292. if match.group(1) == 'outputs':
  293. in_outputs = True
  294. elif match.group(1) != '':
  295. in_outputs = False
  296. if in_outputs:
  297. outputs += match.group(2)
  298. outputs = json.loads(outputs)
  299. # Revamp the “outputs” to put it in the form of a “Key: value” map
  300. revamped_outputs = {}
  301. for output in outputs:
  302. revamped_outputs[output['output_key']] = output['output_value']
  303. return revamped_outputs
  304. @staticmethod
  305. # pylint: disable=too-many-branches
  306. def oo_parse_named_certificates(certificates, named_certs_dir, internal_hostnames):
  307. ''' Parses names from list of certificate hashes.
  308. Ex: certificates = [{ "certfile": "/root/custom1.crt",
  309. "keyfile": "/root/custom1.key" },
  310. { "certfile": "custom2.crt",
  311. "keyfile": "custom2.key" }]
  312. returns [{ "certfile": "/etc/origin/master/named_certificates/custom1.crt",
  313. "keyfile": "/etc/origin/master/named_certificates/custom1.key",
  314. "names": [ "public-master-host.com",
  315. "other-master-host.com" ] },
  316. { "certfile": "/etc/origin/master/named_certificates/custom2.crt",
  317. "keyfile": "/etc/origin/master/named_certificates/custom2.key",
  318. "names": [ "some-hostname.com" ] }]
  319. '''
  320. if not issubclass(type(named_certs_dir), unicode):
  321. raise errors.AnsibleFilterError("|failed expects named_certs_dir is unicode")
  322. if not issubclass(type(internal_hostnames), list):
  323. raise errors.AnsibleFilterError("|failed expects internal_hostnames is list")
  324. for certificate in certificates:
  325. if 'names' in certificate.keys():
  326. continue
  327. else:
  328. certificate['names'] = []
  329. if not os.path.isfile(certificate['certfile']) or not os.path.isfile(certificate['keyfile']):
  330. raise errors.AnsibleFilterError("|certificate and/or key does not exist '%s', '%s'" %
  331. (certificate['certfile'], certificate['keyfile']))
  332. try:
  333. st_cert = open(certificate['certfile'], 'rt').read()
  334. cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, st_cert)
  335. certificate['names'].append(str(cert.get_subject().commonName.decode()))
  336. for i in range(cert.get_extension_count()):
  337. if cert.get_extension(i).get_short_name() == 'subjectAltName':
  338. for name in str(cert.get_extension(i)).replace('DNS:', '').split(', '):
  339. certificate['names'].append(name)
  340. except:
  341. raise errors.AnsibleFilterError(("|failed to parse certificate '%s', " % certificate['certfile'] +
  342. "please specify certificate names in host inventory"))
  343. certificate['names'] = [name for name in certificate['names'] if name not in internal_hostnames]
  344. certificate['names'] = list(set(certificate['names']))
  345. if not certificate['names']:
  346. raise errors.AnsibleFilterError(("|failed to parse certificate '%s' or " % certificate['certfile'] +
  347. "detected a collision with internal hostname, please specify " +
  348. "certificate names in host inventory"))
  349. for certificate in certificates:
  350. # Update paths for configuration
  351. certificate['certfile'] = os.path.join(named_certs_dir, os.path.basename(certificate['certfile']))
  352. certificate['keyfile'] = os.path.join(named_certs_dir, os.path.basename(certificate['keyfile']))
  353. return certificates
  354. @staticmethod
  355. def oo_pretty_print_cluster(data):
  356. ''' Read a subset of hostvars and build a summary of the cluster
  357. in the following layout:
  358. "c_id": {
  359. "master": [
  360. { "name": "c_id-master-12345", "public IP": "172.16.0.1", "private IP": "192.168.0.1", "subtype": "default" }]
  361. "node": [
  362. { "name": "c_id-node-infra-23456", "public IP": "172.16.0.2", "private IP": "192.168.0.2", "subtype": "infra" },
  363. { "name": "c_id-node-compute-23456", "public IP": "172.16.0.3", "private IP": "192.168.0.3", "subtype": "compute" },
  364. ...
  365. ]}
  366. '''
  367. def _get_tag_value(tags, key):
  368. ''' Extract values of a map implemented as a set.
  369. Ex: tags = { 'tag_foo_value1', 'tag_bar_value2', 'tag_baz_value3' }
  370. key = 'bar'
  371. returns 'value2'
  372. '''
  373. for tag in tags:
  374. # Skip tag_env-host-type to avoid ambiguity with tag_env
  375. # Removing env-host-type tag but leaving this here
  376. if tag[:17] == 'tag_env-host-type':
  377. continue
  378. if tag[:len(key)+4] == 'tag_' + key:
  379. return tag[len(key)+5:]
  380. raise KeyError(key)
  381. def _add_host(clusters,
  382. env,
  383. host_type,
  384. sub_host_type,
  385. host):
  386. ''' Add a new host in the clusters data structure '''
  387. if env not in clusters:
  388. clusters[env] = {}
  389. if host_type not in clusters[env]:
  390. clusters[env][host_type] = {}
  391. if sub_host_type not in clusters[env][host_type]:
  392. clusters[env][host_type][sub_host_type] = []
  393. clusters[env][host_type][sub_host_type].append(host)
  394. clusters = {}
  395. for host in data:
  396. try:
  397. _add_host(clusters=clusters,
  398. env=_get_tag_value(host['group_names'], 'env'),
  399. host_type=_get_tag_value(host['group_names'], 'host-type'),
  400. sub_host_type=_get_tag_value(host['group_names'], 'sub-host-type'),
  401. host={'name': host['inventory_hostname'],
  402. 'public IP': host['ansible_ssh_host'],
  403. 'private IP': host['ansible_default_ipv4']['address']})
  404. except KeyError:
  405. pass
  406. return clusters
  407. @staticmethod
  408. def oo_generate_secret(num_bytes):
  409. ''' generate a session secret '''
  410. if not issubclass(type(num_bytes), int):
  411. raise errors.AnsibleFilterError("|failed expects num_bytes is int")
  412. secret = os.urandom(num_bytes)
  413. return secret.encode('base-64').strip()
  414. @staticmethod
  415. def to_padded_yaml(data, level=0, indent=2, **kw):
  416. ''' returns a yaml snippet padded to match the indent level you specify '''
  417. if data in [None, ""]:
  418. return ""
  419. try:
  420. transformed = yaml.safe_dump(data, indent=indent, allow_unicode=True, default_flow_style=False, **kw)
  421. padded = "\n".join([" " * level * indent + line for line in transformed.splitlines()])
  422. return to_unicode("\n{0}".format(padded))
  423. except Exception as my_e:
  424. raise errors.AnsibleFilterError('Failed to convert: %s', my_e)
  425. def filters(self):
  426. ''' returns a mapping of filters to methods '''
  427. return {
  428. "oo_select_keys": self.oo_select_keys,
  429. "oo_select_keys_from_list": self.oo_select_keys_from_list,
  430. "oo_collect": self.oo_collect,
  431. "oo_flatten": self.oo_flatten,
  432. "oo_pdb": self.oo_pdb,
  433. "oo_prepend_strings_in_list": self.oo_prepend_strings_in_list,
  434. "oo_ami_selector": self.oo_ami_selector,
  435. "oo_ec2_volume_definition": self.oo_ec2_volume_definition,
  436. "oo_combine_key_value": self.oo_combine_key_value,
  437. "oo_combine_dict": self.oo_combine_dict,
  438. "oo_split": self.oo_split,
  439. "oo_filter_list": self.oo_filter_list,
  440. "oo_parse_heat_stack_outputs": self.oo_parse_heat_stack_outputs,
  441. "oo_parse_named_certificates": self.oo_parse_named_certificates,
  442. "oo_haproxy_backend_masters": self.oo_haproxy_backend_masters,
  443. "oo_pretty_print_cluster": self.oo_pretty_print_cluster,
  444. "oo_generate_secret": self.oo_generate_secret,
  445. "to_padded_yaml": self.to_padded_yaml,
  446. }