A Python library for code common to TripleO CLI and TripleO UI.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

700 lines
27KB

  1. # Copyright 2017 Red Hat, Inc.
  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. from datetime import datetime
  16. import json
  17. import logging
  18. import os
  19. import shutil
  20. import six
  21. from six.moves import configparser
  22. from six.moves import cStringIO as StringIO
  23. import sys
  24. import tempfile
  25. import time
  26. import yaml
  27. from mistral_lib import actions
  28. from oslo_concurrency import processutils
  29. from oslo_rootwrap import subprocess
  30. from tripleo_common.actions import base
  31. from tripleo_common import constants
  32. from tripleo_common import inventory
  33. LOG = logging.getLogger(__name__)
  34. def write_default_ansible_cfg(work_dir,
  35. remote_user,
  36. ssh_private_key=None,
  37. transport=None,
  38. base_ansible_cfg='/etc/ansible/ansible.cfg',
  39. override_ansible_cfg=None):
  40. ansible_config_path = os.path.join(work_dir, 'ansible.cfg')
  41. shutil.copy(base_ansible_cfg, ansible_config_path)
  42. modules_path = ('/root/.ansible/plugins/modules:'
  43. '/usr/share/ansible/plugins/modules:'
  44. '%s/library' % constants.DEFAULT_VALIDATIONS_BASEDIR)
  45. lookups_path = (
  46. 'root/.ansible/plugins/lookup:'
  47. '/usr/share/ansible/plugins/lookup:'
  48. '%s/lookup_plugins' % constants.DEFAULT_VALIDATIONS_BASEDIR)
  49. callbacks_path = (
  50. '~/.ansible/plugins/callback:'
  51. '/usr/share/ansible/plugins/callback:'
  52. '%s/callback_plugins' % constants.DEFAULT_VALIDATIONS_BASEDIR)
  53. callbacks_whitelist = ','.join(['profile_tasks'])
  54. roles_path = ('%(work_dir)s/roles:'
  55. '/root/.ansible/roles:'
  56. '/usr/share/ansible/roles:'
  57. '/etc/ansible/roles:'
  58. '%(ooo_val_path)s/roles:'
  59. '%(work_dir)s' % {
  60. 'work_dir': work_dir,
  61. 'ooo_val_path': constants.DEFAULT_VALIDATIONS_BASEDIR
  62. })
  63. config = configparser.ConfigParser()
  64. config.read(ansible_config_path)
  65. config.set('defaults', 'retry_files_enabled', 'False')
  66. config.set('defaults', 'roles_path', roles_path)
  67. config.set('defaults', 'library', modules_path)
  68. config.set('defaults', 'callback_plugins', callbacks_path)
  69. config.set('defaults', 'callback_whitelist', callbacks_whitelist)
  70. config.set('defaults', 'lookup_plugins', lookups_path)
  71. # suppress tasks if all hosts skip, was previously full_skip callback
  72. config.set('defaults', 'display_skipped_hosts', 'no')
  73. log_path = os.path.join(work_dir, 'ansible.log')
  74. config.set('defaults', 'log_path', log_path)
  75. if os.path.exists(log_path):
  76. new_path = (log_path + '-' +
  77. datetime.now().strftime("%Y-%m-%dT%H:%M:%S"))
  78. os.rename(log_path, new_path)
  79. config.set('defaults', 'forks', '25')
  80. config.set('defaults', 'timeout', '30')
  81. config.set('defaults', 'gather_timeout', '30')
  82. # Setup fact cache to improve playbook execution speed
  83. config.set('defaults', 'gathering', 'smart')
  84. config.set('defaults', 'fact_caching', 'jsonfile')
  85. config.set('defaults', 'fact_caching_connection',
  86. '/var/lib/mistral/ansible_fact_cache')
  87. # Set the interpreter discovery to auto mode.
  88. config.set('defaults', 'interpreter_python', 'auto')
  89. # Expire facts in the fact cache after 7200s (2h)
  90. config.set('defaults', 'fact_caching_timeout', '7200')
  91. # mistral user has no home dir set, so no place to save a known hosts file
  92. config.set('ssh_connection', 'ssh_args',
  93. '-o UserKnownHostsFile=/dev/null '
  94. '-o StrictHostKeyChecking=no '
  95. '-o ControlMaster=auto '
  96. '-o ControlPersist=30m '
  97. '-o ServerAliveInterval=5 '
  98. '-o ServerAliveCountMax=5')
  99. config.set('ssh_connection', 'control_path_dir',
  100. os.path.join(work_dir, 'ansible-ssh'))
  101. config.set('ssh_connection', 'retries', '8')
  102. config.set('ssh_connection', 'pipelining', 'True')
  103. # Set connection info in config file so that subsequent/nested ansible
  104. # calls can re-use it
  105. if remote_user:
  106. config.set('defaults', 'remote_user', remote_user)
  107. if ssh_private_key:
  108. config.set('defaults', 'private_key_file', ssh_private_key)
  109. if transport:
  110. config.set('defaults', 'transport', transport)
  111. if override_ansible_cfg:
  112. sio_cfg = StringIO()
  113. sio_cfg.write(override_ansible_cfg)
  114. sio_cfg.seek(0)
  115. config.readfp(sio_cfg)
  116. sio_cfg.close()
  117. with open(ansible_config_path, 'w') as configfile:
  118. config.write(configfile)
  119. return ansible_config_path
  120. class AnsibleAction(actions.Action):
  121. """Executes ansible module"""
  122. def __init__(self, **kwargs):
  123. self._kwargs_for_run = kwargs
  124. self.hosts = self._kwargs_for_run.pop('hosts', None)
  125. self.module = self._kwargs_for_run.pop('module', None)
  126. self.module_args = self._kwargs_for_run.pop('module_args', None)
  127. if self.module_args and isinstance(self.module_args, dict):
  128. self.module_args = json.dumps(self.module_args)
  129. self.limit_hosts = self._kwargs_for_run.pop('limit_hosts', None)
  130. self.remote_user = self._kwargs_for_run.pop('remote_user', None)
  131. self.become = self._kwargs_for_run.pop('become', None)
  132. self.become_user = self._kwargs_for_run.pop('become_user', None)
  133. self.extra_vars = self._kwargs_for_run.pop('extra_vars', None)
  134. if self.extra_vars:
  135. self.extra_vars = json.dumps(self.extra_vars)
  136. self._inventory = self._kwargs_for_run.pop('inventory', None)
  137. self.verbosity = self._kwargs_for_run.pop('verbosity', 5)
  138. self._ssh_private_key = self._kwargs_for_run.pop(
  139. 'ssh_private_key', None)
  140. self.forks = self._kwargs_for_run.pop('forks', None)
  141. self.timeout = self._kwargs_for_run.pop('timeout', None)
  142. self.ssh_extra_args = self._kwargs_for_run.pop('ssh_extra_args', None)
  143. if self.ssh_extra_args:
  144. self.ssh_extra_args = json.dumps(self.ssh_extra_args)
  145. self.ssh_common_args = self._kwargs_for_run.pop(
  146. 'ssh_common_args', None)
  147. if self.ssh_common_args:
  148. self.ssh_common_args = json.dumps(self.ssh_common_args)
  149. self.use_openstack_credentials = self._kwargs_for_run.pop(
  150. 'use_openstack_credentials', False)
  151. self.extra_env_variables = self._kwargs_for_run.pop(
  152. 'extra_env_variables', None)
  153. self.gather_facts = self._kwargs_for_run.pop('gather_facts', False)
  154. self._work_dir = None
  155. @property
  156. def work_dir(self):
  157. if self._work_dir:
  158. return self._work_dir
  159. self._work_dir = tempfile.mkdtemp(prefix='ansible-mistral-action')
  160. return self._work_dir
  161. @property
  162. def inventory(self):
  163. if not self._inventory:
  164. return None
  165. # NOTE(flaper87): if it's a path, use it
  166. if (isinstance(self._inventory, six.string_types) and
  167. os.path.exists(self._inventory)):
  168. return self._inventory
  169. elif not isinstance(self._inventory, six.string_types):
  170. self._inventory = yaml.safe_dump(self._inventory)
  171. path = os.path.join(self.work_dir, 'inventory.yaml')
  172. # NOTE(flaper87):
  173. # We could probably catch parse errors here
  174. # but if we do, they won't be propagated and
  175. # we should not move forward with the action
  176. # if the inventory generation failed
  177. with open(path, 'w') as inventory:
  178. inventory.write(self._inventory)
  179. self._inventory = path
  180. return path
  181. @property
  182. def ssh_private_key(self):
  183. if not self._ssh_private_key:
  184. return None
  185. # NOTE(flaper87): if it's a path, use it
  186. if (isinstance(self._ssh_private_key, six.string_types) and
  187. os.path.exists(self._ssh_private_key)):
  188. return self._ssh_private_key
  189. path = os.path.join(self.work_dir, 'ssh_private_key')
  190. # NOTE(flaper87):
  191. # We could probably catch parse errors here
  192. # but if we do, they won't be propagated and
  193. # we should not move forward with the action
  194. # if the inventory generation failed
  195. with open(path, 'w') as ssh_key:
  196. ssh_key.write(self._ssh_private_key)
  197. os.chmod(path, 0o600)
  198. self._ssh_private_key = path
  199. return path
  200. def run(self, context):
  201. if 0 < self.verbosity < 6:
  202. verbosity_option = '-' + ('v' * self.verbosity)
  203. command = ['ansible', self.hosts, verbosity_option, ]
  204. else:
  205. command = ['ansible', self.hosts, ]
  206. if self.module:
  207. command.extend(['--module-name', self.module])
  208. if self.module_args:
  209. command.extend(['--args', self.module_args])
  210. if self.limit_hosts:
  211. command.extend(['--limit', self.limit_hosts])
  212. if self.become:
  213. command.extend(['--become'])
  214. if self.become_user:
  215. command.extend(['--become-user', self.become_user])
  216. if self.extra_vars:
  217. command.extend(['--extra-vars', self.extra_vars])
  218. if self.forks:
  219. command.extend(['--forks', self.forks])
  220. if self.ssh_common_args:
  221. command.extend(['--ssh-common-args', self.ssh_common_args])
  222. if self.ssh_extra_args:
  223. command.extend(['--ssh-extra-args', self.ssh_extra_args])
  224. if self.timeout:
  225. command.extend(['--timeout', self.timeout])
  226. if self.inventory:
  227. command.extend(['--inventory-file', self.inventory])
  228. if self.extra_env_variables:
  229. if not isinstance(self.extra_env_variables, dict):
  230. msg = "extra_env_variables must be a dict"
  231. return actions.Result(error=msg)
  232. if self.gather_facts:
  233. command.extend(['--gather-facts', self.gather_facts])
  234. try:
  235. ansible_config_path = write_default_ansible_cfg(
  236. self.work_dir,
  237. self.remote_user,
  238. ssh_private_key=self.ssh_private_key)
  239. env_variables = {
  240. 'HOME': self.work_dir,
  241. 'ANSIBLE_LOCAL_TEMP': self.work_dir,
  242. 'ANSIBLE_CONFIG': ansible_config_path
  243. }
  244. if self.extra_env_variables:
  245. env_variables.update(self.extra_env_variables)
  246. if self.use_openstack_credentials:
  247. env_variables.update({
  248. 'OS_AUTH_URL': context.security.auth_uri,
  249. 'OS_USERNAME': context.security.user_name,
  250. 'OS_AUTH_TOKEN': context.security.auth_token,
  251. 'OS_PROJECT_NAME': context.security.project_name})
  252. LOG.info('Running ansible command: %s', command)
  253. stderr, stdout = processutils.execute(
  254. *command, cwd=self.work_dir,
  255. env_variables=env_variables,
  256. log_errors=processutils.LogErrors.ALL)
  257. return {"stderr": stderr, "stdout": stdout,
  258. "log_path": os.path.join(self.work_dir, 'ansible.log')}
  259. finally:
  260. # NOTE(flaper87): clean the mess if debug is disabled.
  261. if not self.verbosity:
  262. shutil.rmtree(self.work_dir)
  263. class AnsiblePlaybookAction(base.TripleOAction):
  264. """Executes ansible playbook"""
  265. def __init__(self, **kwargs):
  266. self._kwargs_for_run = kwargs
  267. self._playbook = self._kwargs_for_run.pop('playbook', None)
  268. self.playbook_name = self._kwargs_for_run.pop('playbook_name',
  269. 'playbook.yaml')
  270. self.plan_name = self._kwargs_for_run.pop('plan_name', None)
  271. self.limit_hosts = self._kwargs_for_run.pop('limit_hosts', None)
  272. self.module_path = self._kwargs_for_run.pop('module_path', None)
  273. self.remote_user = self._kwargs_for_run.pop('remote_user', None)
  274. self.become = self._kwargs_for_run.pop('become', None)
  275. self.become_user = self._kwargs_for_run.pop('become_user', None)
  276. self.extra_vars = self._kwargs_for_run.pop('extra_vars', None)
  277. if self.extra_vars:
  278. self.extra_vars = json.dumps(self.extra_vars)
  279. self._inventory = self._kwargs_for_run.pop('inventory', None)
  280. self.verbosity = self._kwargs_for_run.pop('verbosity', 5)
  281. self._ssh_private_key = self._kwargs_for_run.pop(
  282. 'ssh_private_key', None)
  283. self.flush_cache = self._kwargs_for_run.pop('flush_cache', None)
  284. self.forks = self._kwargs_for_run.pop('forks', None)
  285. self.timeout = self._kwargs_for_run.pop('timeout', None)
  286. self.ssh_extra_args = self._kwargs_for_run.pop('ssh_extra_args', None)
  287. if self.ssh_extra_args:
  288. self.ssh_extra_args = json.dumps(self.ssh_extra_args)
  289. self.ssh_common_args = self._kwargs_for_run.pop(
  290. 'ssh_common_args', None)
  291. if self.ssh_common_args:
  292. self.ssh_common_args = json.dumps(self.ssh_common_args)
  293. self.use_openstack_credentials = self._kwargs_for_run.pop(
  294. 'use_openstack_credentials', False)
  295. self.tags = self._kwargs_for_run.pop('tags', None)
  296. self.skip_tags = self._kwargs_for_run.pop('skip_tags', None)
  297. self.extra_env_variables = self._kwargs_for_run.pop(
  298. 'extra_env_variables', None)
  299. self.queue_name = self._kwargs_for_run.pop('queue_name', None)
  300. self.reproduce_command = self._kwargs_for_run.pop(
  301. 'reproduce_command', True)
  302. self.execution_id = self._kwargs_for_run.pop('execution_id', None)
  303. self._work_dir = self._kwargs_for_run.pop(
  304. 'work_dir', None)
  305. self.max_message_size = self._kwargs_for_run.pop(
  306. 'max_message_size', 1048576)
  307. self.gather_facts = self._kwargs_for_run.pop('gather_facts', False)
  308. self.trash_output = self._kwargs_for_run.pop('trash_output', False)
  309. self.profile_tasks = self._kwargs_for_run.pop('profile_tasks', True)
  310. self.profile_tasks_limit = self._kwargs_for_run.pop(
  311. 'profile_tasks_limit', 0)
  312. self.blacklisted_hostnames = self._kwargs_for_run.pop(
  313. 'blacklisted_hostnames', [])
  314. self.override_ansible_cfg = self._kwargs_for_run.pop(
  315. 'override_ansible_cfg', None)
  316. self.command_timeout = self._kwargs_for_run.pop(
  317. 'command_timeout', None)
  318. @property
  319. def work_dir(self):
  320. if self._work_dir:
  321. return self._work_dir
  322. self._work_dir = tempfile.mkdtemp(prefix='ansible-mistral-action')
  323. return self._work_dir
  324. @property
  325. def inventory(self):
  326. if not self._inventory:
  327. return None
  328. # NOTE(flaper87): if it's a path, use it
  329. if (isinstance(self._inventory, six.string_types) and
  330. os.path.exists(self._inventory)):
  331. return self._inventory
  332. elif not isinstance(self._inventory, six.string_types):
  333. self._inventory = yaml.safe_dump(self._inventory)
  334. path = os.path.join(self.work_dir, 'inventory.yaml')
  335. # NOTE(flaper87):
  336. # We could probably catch parse errors here
  337. # but if we do, they won't be propagated and
  338. # we should not move forward with the action
  339. # if the inventory generation failed
  340. with open(path, 'w') as inventory:
  341. inventory.write(self._inventory)
  342. self._inventory = path
  343. return path
  344. @property
  345. def playbook(self):
  346. if not self._playbook:
  347. return None
  348. # NOTE(flaper87): if it's a path, use it
  349. if (isinstance(self._playbook, six.string_types) and
  350. os.path.exists(self._playbook)):
  351. return self._playbook
  352. elif not isinstance(self._playbook, six.string_types):
  353. self._playbook = yaml.safe_dump(self._playbook)
  354. path = os.path.join(self.work_dir, self.playbook_name)
  355. # NOTE(flaper87):
  356. # We could probably catch parse errors here
  357. # but if we do, they won't be propagated and
  358. # we should not move forward with the action
  359. # if the inventory generation failed
  360. with open(path, 'w') as playbook:
  361. playbook.write(self._playbook)
  362. self._playbook = path
  363. return path
  364. @property
  365. def ssh_private_key(self):
  366. if not self._ssh_private_key:
  367. return None
  368. # NOTE(flaper87): if it's a path, use it
  369. if (isinstance(self._ssh_private_key, six.string_types) and
  370. os.path.exists(self._ssh_private_key)):
  371. return self._ssh_private_key
  372. path = os.path.join(self.work_dir, 'ssh_private_key')
  373. # NOTE(flaper87):
  374. # We could probably catch parse errors here
  375. # but if we do, they won't be propagated and
  376. # we should not move forward with the action
  377. # if the inventory generation failed
  378. with open(path, 'w') as ssh_key:
  379. ssh_key.write(self._ssh_private_key)
  380. os.chmod(path, 0o600)
  381. self._ssh_private_key = path
  382. return path
  383. def format_message(self, message):
  384. type_ = 'tripleo.ansible-playbook.{}'.format(self.playbook_name)
  385. return {
  386. 'body': {
  387. 'type': type_,
  388. 'payload': {
  389. 'message': message,
  390. 'plan_name': self.plan_name,
  391. 'status': 'RUNNING',
  392. 'execution': {'id': self.execution_id}}}}
  393. def post_message(self, queue, message):
  394. """Posts message to queue
  395. Breaks the message up by maximum message size if needed.
  396. """
  397. start = 0
  398. # We use 50% of the max message size to account for any overhead
  399. # due to JSON encoding plus the wrapped dict structure from
  400. # format_message.
  401. message_size = int(self.max_message_size * 0.5)
  402. while True:
  403. end = start + message_size
  404. message_part = message[start:end]
  405. start = end
  406. if not message_part:
  407. return
  408. queue.post(self.format_message(message_part))
  409. def run(self, context):
  410. python_version = sys.version_info.major
  411. ansible_playbook_cmd = "ansible-playbook-{}".format(python_version)
  412. if 0 < self.verbosity < 6:
  413. verbosity_option = '-' + ('v' * self.verbosity)
  414. command = [ansible_playbook_cmd, verbosity_option,
  415. self.playbook]
  416. else:
  417. command = [ansible_playbook_cmd, self.playbook]
  418. if self.limit_hosts:
  419. command.extend(['--limit', self.limit_hosts])
  420. if self.module_path:
  421. command.extend(['--module-path', self.module_path])
  422. if self.become:
  423. command.extend(['--become'])
  424. if self.become_user:
  425. command.extend(['--become-user', self.become_user])
  426. if self.extra_vars:
  427. command.extend(['--extra-vars', self.extra_vars])
  428. if self.flush_cache:
  429. command.extend(['--flush-cache'])
  430. if self.forks:
  431. command.extend(['--forks', self.forks])
  432. if self.ssh_common_args:
  433. command.extend(['--ssh-common-args', self.ssh_common_args])
  434. if self.ssh_extra_args:
  435. command.extend(['--ssh-extra-args', self.ssh_extra_args])
  436. if self.timeout:
  437. command.extend(['--timeout', self.timeout])
  438. if self.inventory:
  439. command.extend(['--inventory-file', self.inventory])
  440. if self.blacklisted_hostnames:
  441. host_pattern = ':'.join(
  442. ['!%s' % h for h in self.blacklisted_hostnames])
  443. command.extend(['--limit', host_pattern])
  444. if self.tags:
  445. command.extend(['--tags', self.tags])
  446. if self.skip_tags:
  447. command.extend(['--skip-tags', self.skip_tags])
  448. if self.extra_env_variables:
  449. if not isinstance(self.extra_env_variables, dict):
  450. msg = "extra_env_variables must be a dict"
  451. return actions.Result(error=msg)
  452. else:
  453. for key, value in self.extra_env_variables.items():
  454. self.extra_env_variables[key] = six.text_type(value)
  455. if self.gather_facts:
  456. command.extend(['--gather-facts', self.gather_facts])
  457. try:
  458. ansible_config_path = write_default_ansible_cfg(
  459. self.work_dir,
  460. self.remote_user,
  461. ssh_private_key=self.ssh_private_key,
  462. override_ansible_cfg=self.override_ansible_cfg)
  463. env_variables = {
  464. 'HOME': self.work_dir,
  465. 'ANSIBLE_LOCAL_TEMP': self.work_dir,
  466. 'ANSIBLE_CONFIG': ansible_config_path,
  467. }
  468. if self.profile_tasks:
  469. env_variables.update({
  470. # the whitelist could be collected from multiple
  471. # arguments if we find a use case for it
  472. 'ANSIBLE_CALLBACK_WHITELIST': 'profile_tasks',
  473. 'PROFILE_TASKS_TASK_OUTPUT_LIMIT':
  474. six.text_type(self.profile_tasks_limit),
  475. })
  476. if self.extra_env_variables:
  477. env_variables.update(self.extra_env_variables)
  478. if self.use_openstack_credentials:
  479. security_ctx = context.security
  480. env_variables.update({
  481. 'OS_AUTH_URL': security_ctx.auth_uri,
  482. 'OS_USERNAME': security_ctx.user_name,
  483. 'OS_AUTH_TOKEN': security_ctx.auth_token,
  484. 'OS_PROJECT_NAME': security_ctx.project_name})
  485. command = [str(c) for c in command]
  486. if self.reproduce_command:
  487. command_path = os.path.join(self.work_dir,
  488. "ansible-playbook-command.sh")
  489. with open(command_path, 'w') as f:
  490. f.write('#!/bin/bash\n')
  491. f.write('\n')
  492. for var in env_variables:
  493. f.write('%s="%s"\n' % (var, env_variables[var]))
  494. f.write('\n')
  495. f.write(' '.join(command))
  496. f.write(' "$@"')
  497. f.write('\n')
  498. os.chmod(command_path, 0o750)
  499. if self.command_timeout:
  500. command = ['timeout', str(self.command_timeout)] + command
  501. if self.queue_name:
  502. zaqar = self.get_messaging_client(context)
  503. queue = zaqar.queue(self.queue_name)
  504. # TODO(d0ugal): We don't have the log errors functionality
  505. # that processutils has, do we need to replicate that somehow?
  506. process = subprocess.Popen(command, stdout=subprocess.PIPE,
  507. stderr=subprocess.STDOUT,
  508. shell=False, bufsize=1,
  509. cwd=self.work_dir,
  510. env=env_variables,
  511. universal_newlines=True)
  512. start = time.time()
  513. stdout = []
  514. lines = []
  515. for line in iter(process.stdout.readline, ''):
  516. lines.append(line)
  517. if not self.trash_output:
  518. stdout.append(line)
  519. if time.time() - start > 30:
  520. self.post_message(queue, ''.join(lines))
  521. lines = []
  522. start = time.time()
  523. self.post_message(queue, ''.join(lines))
  524. process.stdout.close()
  525. returncode = process.wait()
  526. # TODO(d0ugal): This bit isn't ideal - as we redirect stderr to
  527. # stdout we don't know the difference. To keep the return dict
  528. # similar there is an empty stderr. We can use the return code
  529. # to determine if there was an error.
  530. return {"stdout": "".join(stdout), "returncode": returncode,
  531. "stderr": ""}
  532. LOG.info('Running ansible-playbook command: %s', command)
  533. stderr, stdout = processutils.execute(
  534. *command, cwd=self.work_dir,
  535. env_variables=env_variables,
  536. log_errors=processutils.LogErrors.ALL)
  537. if self.trash_output:
  538. stdout = ""
  539. stderr = ""
  540. return {"stderr": stderr, "stdout": stdout,
  541. "log_path": os.path.join(self.work_dir, 'ansible.log')}
  542. finally:
  543. # NOTE(flaper87): clean the mess if debug is disabled.
  544. if not self.verbosity:
  545. shutil.rmtree(self.work_dir)
  546. class AnsibleGenerateInventoryAction(base.TripleOAction):
  547. """Executes tripleo-ansible-inventory to generate an inventory"""
  548. def __init__(self, **kwargs):
  549. self._kwargs_for_run = kwargs
  550. self.ansible_ssh_user = self._kwargs_for_run.pop(
  551. 'ansible_ssh_user', 'tripleo-admin')
  552. self.undercloud_key_file = self._kwargs_for_run.pop(
  553. 'undercloud_key_file', None)
  554. self.ansible_python_interpreter = self._kwargs_for_run.pop(
  555. 'ansible_python_interpreter', None)
  556. self._work_dir = self._kwargs_for_run.pop(
  557. 'work_dir', None)
  558. self.plan_name = self._kwargs_for_run.pop(
  559. 'plan_name', 'overcloud')
  560. self.ssh_network = self._kwargs_for_run.pop(
  561. 'ssh_network', 'ctlplane')
  562. @property
  563. def work_dir(self):
  564. if self._work_dir:
  565. return self._work_dir
  566. self._work_dir = tempfile.mkdtemp(prefix='ansible-mistral-action')
  567. return self._work_dir
  568. def run(self, context):
  569. inventory_path = os.path.join(
  570. self.work_dir, 'tripleo-ansible-inventory.yaml')
  571. inv = inventory.TripleoInventory(
  572. session=self.get_session(context, 'heat'),
  573. hclient=self.get_orchestration_client(context),
  574. auth_url=context.security.auth_uri,
  575. cacert=context.security.auth_cacert,
  576. project_name=context.security.project_name,
  577. username=context.security.user_name,
  578. ansible_ssh_user=self.ansible_ssh_user,
  579. undercloud_key_file=self.undercloud_key_file,
  580. undercloud_connection=inventory.UNDERCLOUD_CONNECTION_SSH,
  581. ansible_python_interpreter=self.ansible_python_interpreter,
  582. plan_name=self.plan_name,
  583. host_network=self.ssh_network)
  584. inv.write_static_inventory(inventory_path)
  585. return inventory_path