PageRenderTime 43ms CodeModel.GetById 13ms RepoModel.GetById 0ms app.codeStats 1ms

/v2/ansible/cli/__init__.py

https://gitlab.com/18runt88/ansible
Python | 447 lines | 384 code | 30 blank | 33 comment | 26 complexity | 73432a938e1a440c31bd54c2d636c56c MD5 | raw file
  1. # (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
  2. #
  3. # This file is part of Ansible
  4. #
  5. # Ansible is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # Ansible is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
  17. # Make coding more python3-ish
  18. from __future__ import (absolute_import, division, print_function)
  19. __metaclass__ = type
  20. import operator
  21. import optparse
  22. import os
  23. import sys
  24. import time
  25. import yaml
  26. import re
  27. import getpass
  28. import subprocess
  29. from ansible import __version__
  30. from ansible import constants as C
  31. from ansible.errors import AnsibleError
  32. from ansible.utils.unicode import to_bytes
  33. class SortedOptParser(optparse.OptionParser):
  34. '''Optparser which sorts the options by opt before outputting --help'''
  35. #FIXME: epilog parsing: OptionParser.format_epilog = lambda self, formatter: self.epilog
  36. def format_help(self, formatter=None, epilog=None):
  37. self.option_list.sort(key=operator.methodcaller('get_opt_string'))
  38. return optparse.OptionParser.format_help(self, formatter=None)
  39. class CLI(object):
  40. ''' code behind bin/ansible* programs '''
  41. VALID_ACTIONS = ['No Actions']
  42. _ITALIC = re.compile(r"I\(([^)]+)\)")
  43. _BOLD = re.compile(r"B\(([^)]+)\)")
  44. _MODULE = re.compile(r"M\(([^)]+)\)")
  45. _URL = re.compile(r"U\(([^)]+)\)")
  46. _CONST = re.compile(r"C\(([^)]+)\)")
  47. PAGER = 'less'
  48. LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars)
  49. # -S (chop long lines) -X (disable termcap init and de-init)
  50. def __init__(self, args, display=None):
  51. """
  52. Base init method for all command line programs
  53. """
  54. self.args = args
  55. self.options = None
  56. self.parser = None
  57. self.action = None
  58. if display is None:
  59. self.display = Display()
  60. else:
  61. self.display = display
  62. def set_action(self):
  63. """
  64. Get the action the user wants to execute from the sys argv list.
  65. """
  66. for i in range(0,len(self.args)):
  67. arg = self.args[i]
  68. if arg in self.VALID_ACTIONS:
  69. self.action = arg
  70. del self.args[i]
  71. break
  72. if not self.action:
  73. raise AnsibleOptionsError("Missing required action")
  74. def execute(self):
  75. """
  76. Actually runs a child defined method using the execute_<action> pattern
  77. """
  78. fn = getattr(self, "execute_%s" % self.action)
  79. fn()
  80. def parse(self):
  81. raise Exception("Need to implement!")
  82. def run(self):
  83. raise Exception("Need to implement!")
  84. @staticmethod
  85. def ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=False, confirm_vault=False, confirm_new=False):
  86. ''' prompt for vault password and/or password change '''
  87. vault_pass = None
  88. new_vault_pass = None
  89. if ask_vault_pass:
  90. vault_pass = getpass.getpass(prompt="Vault password: ")
  91. if ask_vault_pass and confirm_vault:
  92. vault_pass2 = getpass.getpass(prompt="Confirm Vault password: ")
  93. if vault_pass != vault_pass2:
  94. raise errors.AnsibleError("Passwords do not match")
  95. if ask_new_vault_pass:
  96. new_vault_pass = getpass.getpass(prompt="New Vault password: ")
  97. if ask_new_vault_pass and confirm_new:
  98. new_vault_pass2 = getpass.getpass(prompt="Confirm New Vault password: ")
  99. if new_vault_pass != new_vault_pass2:
  100. raise errors.AnsibleError("Passwords do not match")
  101. # enforce no newline chars at the end of passwords
  102. if vault_pass:
  103. vault_pass = to_bytes(vault_pass, errors='strict', nonstring='simplerepr').strip()
  104. if new_vault_pass:
  105. new_vault_pass = to_bytes(new_vault_pass, errors='strict', nonstring='simplerepr').strip()
  106. return vault_pass, new_vault_pass
  107. def ask_passwords(self):
  108. ''' prompt for connection and become passwords if needed '''
  109. op = self.options
  110. sshpass = None
  111. becomepass = None
  112. become_prompt = ''
  113. if op.ask_pass:
  114. sshpass = getpass.getpass(prompt="SSH password: ")
  115. become_prompt = "%s password[defaults to SSH password]: " % op.become_method.upper()
  116. if sshpass:
  117. sshpass = to_bytes(sshpass, errors='strict', nonstring='simplerepr')
  118. else:
  119. become_prompt = "%s password: " % op.become_method.upper()
  120. if op.become_ask_pass:
  121. becomepass = getpass.getpass(prompt=become_prompt)
  122. if op.ask_pass and becomepass == '':
  123. becomepass = sshpass
  124. if becomepass:
  125. becomepass = to_bytes(becomepass)
  126. return (sshpass, becomepass)
  127. def normalize_become_options(self):
  128. ''' this keeps backwards compatibility with sudo/su self.options '''
  129. self.options.become_ask_pass = self.options.become_ask_pass or self.options.ask_sudo_pass or self.options.ask_su_pass or C.DEFAULT_BECOME_ASK_PASS
  130. self.options.become_user = self.options.become_user or self.options.sudo_user or self.options.su_user or C.DEFAULT_BECOME_USER
  131. if self.options.become:
  132. pass
  133. elif self.options.sudo:
  134. self.options.become = True
  135. self.options.become_method = 'sudo'
  136. elif self.options.su:
  137. self.options.become = True
  138. options.become_method = 'su'
  139. def validate_conflicts(self):
  140. ''' check for conflicting options '''
  141. op = self.options
  142. # Check for vault related conflicts
  143. if (op.ask_vault_pass and op.vault_password_file):
  144. self.parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive")
  145. # Check for privilege escalation conflicts
  146. if (op.su or op.su_user or op.ask_su_pass) and \
  147. (op.sudo or op.sudo_user or op.ask_sudo_pass) or \
  148. (op.su or op.su_user or op.ask_su_pass) and \
  149. (op.become or op.become_user or op.become_ask_pass) or \
  150. (op.sudo or op.sudo_user or op.ask_sudo_pass) and \
  151. (op.become or op.become_user or op.become_ask_pass):
  152. self.parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') "
  153. "and su arguments ('-su', '--su-user', and '--ask-su-pass') "
  154. "and become arguments ('--become', '--become-user', and '--ask-become-pass')"
  155. " are exclusive of each other")
  156. @staticmethod
  157. def base_parser(usage="", output_opts=False, runas_opts=False, meta_opts=False, runtask_opts=False, vault_opts=False,
  158. async_opts=False, connect_opts=False, subset_opts=False, check_opts=False, diff_opts=False, epilog=None):
  159. ''' create an options parser for most ansible scripts '''
  160. #FIXME: implemente epilog parsing
  161. #OptionParser.format_epilog = lambda self, formatter: self.epilog
  162. # base opts
  163. parser = SortedOptParser(usage, version=CLI.version("%prog"))
  164. parser.add_option('-v','--verbose', dest='verbosity', default=0, action="count",
  165. help="verbose mode (-vvv for more, -vvvv to enable connection debugging)")
  166. if runtask_opts:
  167. parser.add_option('-f','--forks', dest='forks', default=C.DEFAULT_FORKS, type='int',
  168. help="specify number of parallel processes to use (default=%s)" % C.DEFAULT_FORKS)
  169. parser.add_option('-i', '--inventory-file', dest='inventory',
  170. help="specify inventory host file (default=%s)" % C.DEFAULT_HOST_LIST,
  171. default=C.DEFAULT_HOST_LIST)
  172. parser.add_option('--list-hosts', dest='listhosts', action='store_true',
  173. help='outputs a list of matching hosts; does not execute anything else')
  174. parser.add_option('-M', '--module-path', dest='module_path',
  175. help="specify path(s) to module library (default=%s)" % C.DEFAULT_MODULE_PATH, default=None)
  176. parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append",
  177. help="set additional variables as key=value or YAML/JSON", default=[])
  178. if vault_opts:
  179. parser.add_option('--ask-vault-pass', default=False, dest='ask_vault_pass', action='store_true',
  180. help='ask for vault password')
  181. parser.add_option('--vault-password-file', default=C.DEFAULT_VAULT_PASSWORD_FILE,
  182. dest='vault_password_file', help="vault password file")
  183. if subset_opts:
  184. parser.add_option('-l', '--limit', default=C.DEFAULT_SUBSET, dest='subset',
  185. help='further limit selected hosts to an additional pattern')
  186. parser.add_option('-t', '--tags', dest='tags', default='all',
  187. help="only run plays and tasks tagged with these values")
  188. parser.add_option('--skip-tags', dest='skip_tags',
  189. help="only run plays and tasks whose tags do not match these values")
  190. if output_opts:
  191. parser.add_option('-o', '--one-line', dest='one_line', action='store_true',
  192. help='condense output')
  193. parser.add_option('-t', '--tree', dest='tree', default=None,
  194. help='log output to this directory')
  195. if runas_opts:
  196. # priv user defaults to root later on to enable detecting when this option was given here
  197. parser.add_option('-K', '--ask-sudo-pass', default=C.DEFAULT_ASK_SUDO_PASS, dest='ask_sudo_pass', action='store_true',
  198. help='ask for sudo password (deprecated, use become)')
  199. parser.add_option('--ask-su-pass', default=C.DEFAULT_ASK_SU_PASS, dest='ask_su_pass', action='store_true',
  200. help='ask for su password (deprecated, use become)')
  201. parser.add_option("-s", "--sudo", default=C.DEFAULT_SUDO, action="store_true", dest='sudo',
  202. help="run operations with sudo (nopasswd) (deprecated, use become)")
  203. parser.add_option('-U', '--sudo-user', dest='sudo_user', default=None,
  204. help='desired sudo user (default=root) (deprecated, use become)')
  205. parser.add_option('-S', '--su', default=C.DEFAULT_SU, action='store_true',
  206. help='run operations with su (deprecated, use become)')
  207. parser.add_option('-R', '--su-user', default=None,
  208. help='run operations with su as this user (default=%s) (deprecated, use become)' % C.DEFAULT_SU_USER)
  209. # consolidated privilege escalation (become)
  210. parser.add_option("-b", "--become", default=C.DEFAULT_BECOME, action="store_true", dest='become',
  211. help="run operations with become (nopasswd implied)")
  212. parser.add_option('--become-method', dest='become_method', default=C.DEFAULT_BECOME_METHOD, type='string',
  213. help="privilege escalation method to use (default=%s), valid choices: [ %s ]" % (C.DEFAULT_BECOME_METHOD, ' | '.join(C.BECOME_METHODS)))
  214. parser.add_option('--become-user', default=None, dest='become_user', type='string',
  215. help='run operations as this user (default=%s)' % C.DEFAULT_BECOME_USER)
  216. parser.add_option('--ask-become-pass', default=False, dest='become_ask_pass', action='store_true',
  217. help='ask for privilege escalation password')
  218. if connect_opts:
  219. parser.add_option('-k', '--ask-pass', default=False, dest='ask_pass', action='store_true',
  220. help='ask for connection password')
  221. parser.add_option('--private-key', default=C.DEFAULT_PRIVATE_KEY_FILE, dest='private_key_file',
  222. help='use this file to authenticate the connection')
  223. parser.add_option('-u', '--user', default=C.DEFAULT_REMOTE_USER, dest='remote_user',
  224. help='connect as this user (default=%s)' % C.DEFAULT_REMOTE_USER)
  225. parser.add_option('-c', '--connection', dest='connection', default=C.DEFAULT_TRANSPORT,
  226. help="connection type to use (default=%s)" % C.DEFAULT_TRANSPORT)
  227. parser.add_option('-T', '--timeout', default=C.DEFAULT_TIMEOUT, type='int', dest='timeout',
  228. help="override the connection timeout in seconds (default=%s)" % C.DEFAULT_TIMEOUT)
  229. if async_opts:
  230. parser.add_option('-P', '--poll', default=C.DEFAULT_POLL_INTERVAL, type='int',
  231. dest='poll_interval',
  232. help="set the poll interval if using -B (default=%s)" % C.DEFAULT_POLL_INTERVAL)
  233. parser.add_option('-B', '--background', dest='seconds', type='int', default=0,
  234. help='run asynchronously, failing after X seconds (default=N/A)')
  235. if check_opts:
  236. parser.add_option("-C", "--check", default=False, dest='check', action='store_true',
  237. help="don't make any changes; instead, try to predict some of the changes that may occur")
  238. parser.add_option('--syntax-check', dest='syntax', action='store_true',
  239. help="perform a syntax check on the playbook, but do not execute it")
  240. if diff_opts:
  241. parser.add_option("-D", "--diff", default=False, dest='diff', action='store_true',
  242. help="when changing (small) files and templates, show the differences in those files; works great with --check"
  243. )
  244. if meta_opts:
  245. parser.add_option('--force-handlers', dest='force_handlers', action='store_true',
  246. help="run handlers even if a task fails")
  247. parser.add_option('--flush-cache', dest='flush_cache', action='store_true',
  248. help="clear the fact cache")
  249. return parser
  250. @staticmethod
  251. def version(prog):
  252. ''' return ansible version '''
  253. result = "{0} {1}".format(prog, __version__)
  254. gitinfo = CLI._gitinfo()
  255. if gitinfo:
  256. result = result + " {0}".format(gitinfo)
  257. result = result + "\n configured module search path = %s" % C.DEFAULT_MODULE_PATH
  258. return result
  259. @staticmethod
  260. def version_info(gitinfo=False):
  261. ''' return full ansible version info '''
  262. if gitinfo:
  263. # expensive call, user with care
  264. ansible_version_string = version('')
  265. else:
  266. ansible_version_string = __version__
  267. ansible_version = ansible_version_string.split()[0]
  268. ansible_versions = ansible_version.split('.')
  269. for counter in range(len(ansible_versions)):
  270. if ansible_versions[counter] == "":
  271. ansible_versions[counter] = 0
  272. try:
  273. ansible_versions[counter] = int(ansible_versions[counter])
  274. except:
  275. pass
  276. if len(ansible_versions) < 3:
  277. for counter in range(len(ansible_versions), 3):
  278. ansible_versions.append(0)
  279. return {'string': ansible_version_string.strip(),
  280. 'full': ansible_version,
  281. 'major': ansible_versions[0],
  282. 'minor': ansible_versions[1],
  283. 'revision': ansible_versions[2]}
  284. @staticmethod
  285. def _git_repo_info(repo_path):
  286. ''' returns a string containing git branch, commit id and commit date '''
  287. result = None
  288. if os.path.exists(repo_path):
  289. # Check if the .git is a file. If it is a file, it means that we are in a submodule structure.
  290. if os.path.isfile(repo_path):
  291. try:
  292. gitdir = yaml.safe_load(open(repo_path)).get('gitdir')
  293. # There is a possibility the .git file to have an absolute path.
  294. if os.path.isabs(gitdir):
  295. repo_path = gitdir
  296. else:
  297. repo_path = os.path.join(repo_path[:-4], gitdir)
  298. except (IOError, AttributeError):
  299. return ''
  300. f = open(os.path.join(repo_path, "HEAD"))
  301. branch = f.readline().split('/')[-1].rstrip("\n")
  302. f.close()
  303. branch_path = os.path.join(repo_path, "refs", "heads", branch)
  304. if os.path.exists(branch_path):
  305. f = open(branch_path)
  306. commit = f.readline()[:10]
  307. f.close()
  308. else:
  309. # detached HEAD
  310. commit = branch[:10]
  311. branch = 'detached HEAD'
  312. branch_path = os.path.join(repo_path, "HEAD")
  313. date = time.localtime(os.stat(branch_path).st_mtime)
  314. if time.daylight == 0:
  315. offset = time.timezone
  316. else:
  317. offset = time.altzone
  318. result = "({0} {1}) last updated {2} (GMT {3:+04d})".format(branch, commit,
  319. time.strftime("%Y/%m/%d %H:%M:%S", date), int(offset / -36))
  320. else:
  321. result = ''
  322. return result
  323. @staticmethod
  324. def _gitinfo():
  325. basedir = os.path.join(os.path.dirname(__file__), '..', '..', '..')
  326. repo_path = os.path.join(basedir, '.git')
  327. result = CLI._git_repo_info(repo_path)
  328. submodules = os.path.join(basedir, '.gitmodules')
  329. if not os.path.exists(submodules):
  330. return result
  331. f = open(submodules)
  332. for line in f:
  333. tokens = line.strip().split(' ')
  334. if tokens[0] == 'path':
  335. submodule_path = tokens[2]
  336. submodule_info = CLI._git_repo_info(os.path.join(basedir, submodule_path, '.git'))
  337. if not submodule_info:
  338. submodule_info = ' not found - use git submodule update --init ' + submodule_path
  339. result += "\n {0}: {1}".format(submodule_path, submodule_info)
  340. f.close()
  341. return result
  342. @staticmethod
  343. def pager(text):
  344. ''' find reasonable way to display text '''
  345. # this is a much simpler form of what is in pydoc.py
  346. if not sys.stdout.isatty():
  347. pager_print(text)
  348. elif 'PAGER' in os.environ:
  349. if sys.platform == 'win32':
  350. pager_print(text)
  351. else:
  352. CLI.pager_pipe(text, os.environ['PAGER'])
  353. elif subprocess.call('(less --version) 2> /dev/null', shell = True) == 0:
  354. CLI.pager_pipe(text, 'less')
  355. else:
  356. pager_print(text)
  357. @staticmethod
  358. def pager_pipe(text, cmd):
  359. ''' pipe text through a pager '''
  360. if 'LESS' not in os.environ:
  361. os.environ['LESS'] = LESS_OPTS
  362. try:
  363. cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout)
  364. cmd.communicate(input=text)
  365. except IOError:
  366. pass
  367. except KeyboardInterrupt:
  368. pass
  369. @classmethod
  370. def tty_ify(self, text):
  371. t = self._ITALIC.sub("`" + r"\1" + "'", text) # I(word) => `word'
  372. t = self._BOLD.sub("*" + r"\1" + "*", t) # B(word) => *word*
  373. t = self._MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word]
  374. t = self._URL.sub(r"\1", t) # U(word) => word
  375. t = self._CONST.sub("`" + r"\1" + "'", t) # C(word) => `word'
  376. return t