def func(*args, **kwargs): # Echo warning if this method is deprecated. if getattr(method, 'deprecated', False): debug.log('This method is deprecated in Tower 3.0.', header='warning') result = method(*args, **kwargs) # If this was a request that could result in a modification # of data, print it in Ansible coloring. color_info = {} if isinstance(result, dict) and 'changed' in result: if result['changed']: color_info['fg'] = 'yellow' else: color_info['fg'] = 'green' # Piece together the result into the proper format. format = getattr( self, '_format_%s' % (getattr(method, 'format_freezer', None) or settings.format)) output = format(result) # Perform the echo. secho(output, **color_info)
def login(username, password, scope, client_id, client_secret, verbose): """ Retrieves and stores an OAuth2 personal auth token. """ if not supports_oauth(): raise exc.TowerCLIError( 'This version of Tower does not support OAuth2.0. Set credentials using tower-cli config.' ) # Explicitly set a basic auth header for PAT acquisition (so that we don't # try to auth w/ an existing user+pass or oauth2 token in a config file) req = collections.namedtuple('req', 'headers')({}) if client_id and client_secret: HTTPBasicAuth(client_id, client_secret)(req) req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post( '/o/token/', data={ "grant_type": "password", "username": username, "password": password, "scope": scope }, headers=req.headers ) elif client_id: req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post( '/o/token/', data={ "grant_type": "password", "username": username, "password": password, "client_id": client_id, "scope": scope }, headers=req.headers ) else: HTTPBasicAuth(username, password)(req) r = client.post( '/users/{}/personal_tokens/'.format(username), data={"description": "Tower CLI", "application": None, "scope": scope}, headers=req.headers ) if r.ok: result = r.json() result.pop('summary_fields', None) result.pop('related', None) if client_id: token = result.pop('access_token', None) else: token = result.pop('token', None) if settings.verbose: # only print the actual token if -v result['token'] = token secho(json.dumps(result, indent=1), fg='blue', bold=True) config.main(['oauth_token', token, '--scope=user'])
def test_color_false(self): """Establish that when the color setting is false, that color data is stripped. """ with settings.runtime_values(color=False): with mock.patch.object(click, 'secho') as click_secho: secho('foo bar baz', fg='green') click_secho.assert_called_once_with('foo bar baz')
def echo_setting(key): """Echo a setting to the CLI.""" value = getattr(settings, key) secho('%s: ' % key, fg='magenta', bold=True, nl=False) secho(six.text_type(value), bold=True, fg='white' if isinstance(value, six.text_type) else 'cyan', )
def login(username, password, scope, client_id, client_secret, verbose): """ Retrieves and stores an OAuth2 personal auth token. """ if not supports_oauth(): raise exc.TowerCLIError( 'This version of Tower does not support OAuth2.0. Set credentials using tower-cli config.' ) # Explicitly set a basic auth header for PAT acquisition (so that we don't # try to auth w/ an existing user+pass or oauth2 token in a config file) req = collections.namedtuple('req', 'headers')({}) if client_id and client_secret: HTTPBasicAuth(client_id, client_secret)(req) req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post('/o/token/', data={ "grant_type": "password", "username": username, "password": password, "scope": scope }, headers=req.headers) elif client_id: req.headers['Content-Type'] = 'application/x-www-form-urlencoded' r = client.post('/o/token/', data={ "grant_type": "password", "username": username, "password": password, "client_id": client_id, "scope": scope }, headers=req.headers) else: HTTPBasicAuth(username, password)(req) r = client.post('/users/{}/personal_tokens/'.format(username), data={ "description": "Tower CLI", "application": None, "scope": scope }, headers=req.headers) if r.ok: result = r.json() result.pop('summary_fields', None) result.pop('related', None) if client_id: token = result.pop('access_token', None) else: token = result.pop('token', None) if settings.verbose: # only print the actual token if -v result['token'] = token secho(json.dumps(result, indent=1), fg='blue', bold=True) config.main(['oauth_token', token, '--scope=user'])
def _make_request(self, method, url, args, kwargs): # Decide whether to require SSL verification verify_ssl = True if (settings.verify_ssl is False) or hasattr(settings, 'insecure'): verify_ssl = False elif settings.certificate is not None: verify_ssl = settings.certificate # Call the superclass method. try: with warnings.catch_warnings(): warnings.simplefilter( "ignore", urllib3.exceptions.InsecureRequestWarning) return super(Client, self).request(method, url, *args, verify=verify_ssl, **kwargs) except SSLError as ex: # Throw error if verify_ssl not set to false and server # is not using verified certificate. if settings.verbose: debug.log('SSL connection failed:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) if not settings.host.startswith('http'): secho( 'Suggestion: add the correct http:// or ' 'https:// prefix to the host configuration.', fg='blue', bold=True) raise exc.ConnectionError( 'Could not establish a secure connection. ' 'Please add the server to your certificate ' 'authority.\nYou can run this command without verifying SSL ' 'with the --insecure flag, or permanently disable ' 'verification by the config setting:\n\n ' 'tower-cli config verify_ssl false') except ConnectionError as ex: # Throw error if server can not be reached. if settings.verbose: debug.log('Cannot connect to Tower:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) raise exc.ConnectionError( 'There was a network error of some kind trying to connect ' 'to Tower.\n\nThe most common reason for this is a settings ' 'issue; is your "host" value in `tower-cli config` correct?\n' 'Right now it is: "%s".' % settings.host)
def func(*args, **kwargs): result = method(*args, **kwargs) # If this was a request that could result in a modification # of data, print it in Ansible coloring. color_info = {} if 'changed' in result: if result['changed']: color_info['fg'] = 'yellow' else: color_info['fg'] = 'green' # Piece together the result into the proper format. format = getattr(self, '_format_%s' % settings.format) output = format(result) # Perform the echo. secho(output, **color_info)
def func(*args, **kwargs): result = method(*args, **kwargs) # If this was a request that could result in a modification # of data, print it in Ansible coloring. color_info = {} if "changed" in result: if result["changed"]: color_info["fg"] = "yellow" else: color_info["fg"] = "green" # Piece together the result into the proper format. format = getattr(self, "_format_%s" % settings.format) output = format(result) # Perform the echo. secho(output, **color_info)
def _make_request(self, method, url, args, kwargs): # Decide whether to require SSL verification verify_ssl = True if (settings.verify_ssl is False) or hasattr(settings, 'insecure'): verify_ssl = False elif settings.certificate: verify_ssl = settings.certificate # Call the superclass method. try: with warnings.catch_warnings(): warnings.simplefilter( "ignore", urllib3.exceptions.InsecureRequestWarning) return super(Client, self).request( method, url, *args, verify=verify_ssl, **kwargs) except SSLError as ex: # Throw error if verify_ssl not set to false and server # is not using verified certificate. if settings.verbose: debug.log('SSL connection failed:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) if not settings.host.startswith('http'): secho('Suggestion: add the correct http:// or ' 'https:// prefix to the host configuration.', fg='blue', bold=True) raise exc.ConnectionError( 'Could not establish a secure connection. ' 'Please add the server to your certificate ' 'authority.\nYou can run this command without verifying SSL ' 'with the --insecure flag, or permanently disable ' 'verification by the config setting:\n\n ' 'tower-cli config verify_ssl false' ) except ConnectionError as ex: # Throw error if server can not be reached. if settings.verbose: debug.log('Cannot connect to Tower:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) raise exc.ConnectionError( 'There was a network error of some kind trying to connect ' 'to Tower.\n\nThe most common reason for this is a settings ' 'issue; is your "host" value in `tower-cli config` correct?\n' 'Right now it is: "%s".' % settings.host )
def get_command(self, ctx, name): """Given a command identified by its name, import the appropriate module and return the decorated command. Resources are automatically commands, but if both a resource and a command are defined, the command takes precedence. """ # First, attempt to get a basic command from `tower_cli.api.misc`. if name in misc.__all__: return getattr(misc, name) # No command was found; try to get a resource. try: resource = tower_cli.get_resource(name) return ResSubcommand(resource) except ImportError: pass # Okay, we weren't able to find a command. secho('No such command: %s.' % name, fg='red', bold=True) sys.exit(2)
def func(*args, **kwargs): # Echo warning if this method is deprecated. if getattr(method, 'deprecated', False): debug.log('This method is deprecated in Tower 3.0.', header='warning') result = method(*args, **kwargs) # If this was a request that could result in a modification # of data, print it in Ansible coloring. color_info = {} if isinstance(result, dict) and 'changed' in result: if result['changed']: color_info['fg'] = 'yellow' else: color_info['fg'] = 'green' # Piece together the result into the proper format. format = getattr(self, '_format_%s' % (getattr(method, 'format_freezer', None) or settings.format)) output = format(result) # Perform the echo. secho(output, **color_info)
def log(s, header='', file=sys.stderr, nl=1, **kwargs): """Log the given output to stderr if and only if we are in verbose mode. If we are not in verbose mode, this is a no-op. """ # Sanity check: If we are not in verbose mode, this is a no-op. if not settings.verbose: return # Construct multi-line string to stderr if header is provided. if header: word_arr = s.split(' ') multi = [] word_arr.insert(0, '%s:' % header.upper()) i = 0 while i < len(word_arr): to_add = ['***'] count = 3 while count <= 79: count += len(word_arr[i]) + 1 if count <= 79: to_add.append(word_arr[i]) i += 1 if i == len(word_arr): break # Handle corner case of extra-long word longer than 75 characters. if len(to_add) == 1: to_add.append(word_arr[i]) i += 1 if i != len(word_arr): count -= len(word_arr[i]) + 1 to_add.append('*' * (78 - count)) multi.append(' '.join(to_add)) s = '\n'.join(multi) lines = len(multi) else: lines = 1 # If `nl` is an int greater than the number of rows of a message, # add the appropriate newlines to the output. if isinstance(nl, int) and nl > lines: s += '\n' * (nl - lines) # Output to stderr. return secho(s, file=file, **kwargs)
def log(s, header='', file=sys.stderr, nl=1, **kwargs): """Log the given output to stderr if and only if we are in verbose mode. If we are not in verbose mode, this is a no-op. """ # Sanity check: If we are not in verbose mode, this is a no-op. if not settings.verbose: return # Construct multi-line string to stderr if header is provided. if header: word_arr = s.split(' ') multi = [] word_arr.insert(0, '%s:' % header.upper()) i = 0 while i < len(word_arr): to_add = ['***'] count = 3 while count <= 79: count += len(word_arr[i]) + 1 if count <= 79: to_add.append(word_arr[i]) i += 1 if i == len(word_arr): break if i != len(word_arr): count -= len(word_arr[i]) + 1 to_add.append('*' * (78 - count)) multi.append(' '.join(to_add)) s = '\n'.join(multi) lines = len(multi) else: lines = 1 # If `nl` is an int greater than the number of rows of a message, # add the appropriate newlines to the output. if isinstance(nl, int) and nl > lines: s += '\n' * (nl - lines) # Output to stderr. return secho(s, file=file, **kwargs)
def log(s, header='', file=sys.stderr, nl=1, **kwargs): """Log the given output to stderr if and only if we are in verbose mode. If we are not in verbose mode, this is a no-op. """ # Sanity check: If we are not in verbose mode, this is a no-op. if not settings.verbose: return # If this is a "header" line, make it a header. if header: s = '*** %s: %s %s' % \ (header.upper(), s, '*' * (72 - len(header) - len(s))) # If `nl` is an int greater than 1, add the appropriate newlines # to the output. if isinstance(nl, int) and nl > 1: s += '\n' * (nl - 1) # Output to stderr. return secho(s, file=file, **kwargs)
def config(key=None, value=None, scope='user', global_=False, unset=False): """Read or write tower-cli configuration. `tower config` saves the given setting to the appropriate Tower CLI; either the user's ~/.tower_cli.cfg file, or the /etc/tower/tower_cli.cfg file if --global is used. Writing to /etc/tower/tower_cli.cfg is likely to require heightened permissions (in other words, sudo). """ # If the old-style `global_` option is set, issue a deprecation notice. if global_: scope = 'global' warnings.warn( 'The `--global` option is deprecated and will be ' 'removed. Use `--scope=global` to get the same effect.', DeprecationWarning) # If no key was provided, print out the current configuration # in play. if not key: seen = set() parser_desc = { 'runtime': 'Runtime options.', 'environment': 'Options from environment variables.', 'local': 'Local options (set with `tower-cli config ' '--scope=local`; stored in .tower_cli.cfg of this ' 'directory or a parent)', 'user': '******' '~/.tower_cli.cfg).', 'global': 'Global options (set with `tower-cli config ' '--scope=global`, stored in /etc/tower/tower_cli.cfg).', 'defaults': 'Defaults.', } # Iterate over each parser (English: location we can get settings from) # and print any settings that we haven't already seen. # # We iterate over settings from highest precedence to lowest, so any # seen settings are overridden by the version we iterated over already. click.echo('') for name, parser in zip(settings._parser_names, settings._parsers): # Determine if we're going to see any options in this # parser that get echoed. will_echo = False for option in parser.options('general'): if option in seen: continue will_echo = True # Print a segment header if will_echo: secho('# %s' % parser_desc[name], fg='green', bold=True) # Iterate over each option in the parser and, if we haven't # already seen an option at higher precedence, print it. for option in parser.options('general'): if option in seen: continue _echo_setting(option) seen.add(option) # Print a nice newline, for formatting. if will_echo: click.echo('') return # Sanity check: Is this a valid configuration option? If it's not # a key we recognize, abort. if not hasattr(settings, key): raise exc.TowerCLIError('Invalid configuration option "%s".' % key) # Sanity check: The combination of a value and --unset makes no # sense. if value and unset: raise exc.UsageError('Cannot provide both a value and --unset.') # If a key was provided but no value was provided, then just # print the current value for that key. if key and not value and not unset: _echo_setting(key) return # Okay, so we're *writing* a key. Let's do this. # First, we need the appropriate file. filename = os.path.expanduser('~/.tower_cli.cfg') if scope == 'global': if not os.path.isdir('/etc/tower/'): raise exc.TowerCLIError('/etc/tower/ does not exist, and this ' 'command cowardly declines to create it.') filename = '/etc/tower/tower_cli.cfg' elif scope == 'local': filename = '.tower_cli.cfg' # Read in the appropriate config file, write this value, and save # the result back to the file. parser = Parser() parser.add_section('general') parser.read(filename) if unset: parser.remove_option('general', key) else: parser.set('general', key, value) with open(filename, 'w') as config_file: parser.write(config_file) # Give rw permissions to user only fix for issue number 48 try: os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR) except Exception as e: warnings.warn( 'Unable to set permissions on {0} - {1} '.format(filename, e), UserWarning) click.echo('Configuration updated successfully.')
def config(key=None, value=None, scope='user', global_=False, unset=False): """Read or write tower-cli configuration. `tower config` saves the given setting to the appropriate Tower CLI; either the user's ~/.tower_cli.cfg file, or the /etc/tower/tower_cli.cfg file if --global is used. Writing to /etc/tower/tower_cli.cfg is likely to require heightened permissions (in other words, sudo). """ # If the old-style `global_` option is set, issue a deprecation notice. if global_: scope = 'global' warnings.warn('The `--global` option is deprecated and will be ' 'removed. Use `--scope=global` to get the same effect.', DeprecationWarning) # If no key was provided, print out the current configuration # in play. if not key: seen = set() parser_desc = { 'runtime': 'Runtime options.', 'local': 'Local options (set with `tower-cli config ' '--scope=local`; stored in .tower_cli.cfg of this ' 'directory or a parent)', 'user': '******' '~/.tower_cli.cfg).', 'global': 'Global options (set with `tower-cli config ' '--scope=global`, stored in /etc/tower/tower_cli.cfg).', 'defaults': 'Defaults.', } # Iterate over each parser (English: location we can get settings from) # and print any settings that we haven't already seen. # # We iterate over settings from highest precedence to lowest, so any # seen settings are overridden by the version we iterated over already. click.echo('') for name, parser in zip(settings._parser_names, settings._parsers): # Determine if we're going to see any options in this # parser that get echoed. will_echo = False for option in parser.options('general'): if option in seen: continue will_echo = True # Print a segment header if will_echo: secho('# %s' % parser_desc[name], fg='green', bold=True) # Iterate over each option in the parser and, if we haven't # already seen an option at higher precedence, print it. for option in parser.options('general'): if option in seen: continue echo_setting(option) seen.add(option) # Print a nice newline, for formatting. if will_echo: click.echo('') return # Sanity check: Is this a valid configuration option? If it's not # a key we recognize, abort. if not hasattr(settings, key): raise exc.TowerCLIError('Invalid configuration option "%s".' % key) # Sanity check: The combination of a value and --unset makes no # sense. if value and unset: raise exc.UsageError('Cannot provide both a value and --unset.') # If a key was provided but no value was provided, then just # print the current value for that key. if key and not value and not unset: echo_setting(key) return # Okay, so we're *writing* a key. Let's do this. # First, we need the appropriate file. filename = os.path.expanduser('~/.tower_cli.cfg') if scope == 'global': if not os.path.isdir('/etc/tower/'): raise exc.TowerCLIError('/etc/tower/ does not exist, and this ' 'command cowardly declines to create it.') filename = '/etc/tower/tower_cli.cfg' elif scope == 'local': filename = '.tower_cli.cfg' # Read in the appropriate config file, write this value, and save # the result back to the file. parser = Parser() parser.add_section('general') parser.read(filename) if unset: parser.remove_option('general', key) else: parser.set('general', key, value) with open(filename, 'w') as config_file: parser.write(config_file) try: os.chmod(filename, stat.S_IRUSR | stat.S_IWUSR) # give rw permissions to user only # fix for issue number 48 except Exception as e: warnings.warn('Unable to set permissions on {0} - {1} '.format(filename,e), UserWarning) click.echo('Configuration updated successfully.')
def request(self, method, url, *args, **kwargs): """Make a request to the Ansible Tower API, and return the response. """ # Piece together the full URL. url = "%s%s" % (self.prefix, url.lstrip("/")) # Ansible Tower expects authenticated requests; add the authentication # from settings if it's provided. kwargs.setdefault("auth", (settings.username, settings.password)) # POST and PUT requests will send JSON by default; make this # the content_type by default. This makes it such that we don't have # to constantly write that in our code, which gets repetitive. headers = kwargs.get("headers", {}) if method.upper() in ("PATCH", "POST", "PUT"): headers.setdefault("Content-Type", "application/json") kwargs["headers"] = headers # If debugging is on, print the URL and data being sent. debug.log("%s %s" % (method, url), fg="blue", bold=True) if method in ("POST", "PUT", "PATCH"): debug.log("Data: %s" % kwargs.get("data", {}), fg="blue", bold=True) if method == "GET" or kwargs.get("params", None): debug.log("Params: %s" % kwargs.get("params", {}), fg="blue", bold=True) debug.log("") # If this is a JSON request, encode the data value. if headers.get("Content-Type", "") == "application/json": kwargs["data"] = json.dumps(kwargs.get("data", {})) # Decide whether to require SSL verification verify_ssl = True if (settings.verify_ssl is False) or hasattr(settings, "insecure"): verify_ssl = False elif settings.certificate is not None: verify_ssl = settings.certificate # Call the superclass method. try: with warnings.catch_warnings(): warnings.simplefilter("ignore", urllib3.exceptions.InsecureRequestWarning) r = super(Client, self).request(method, url, *args, verify=verify_ssl, **kwargs) except SSLError as ex: # Throw error if verify_ssl not set to false and server # is not using verified certificate. if settings.verbose: debug.log("SSL connection failed:", fg="yellow", bold=True) debug.log(str(ex), fg="yellow", bold=True, nl=2) if not settings.host.startswith("http"): secho( "Suggestion: add the correct http:// or " "https:// prefix to the host configuration.", fg="blue", bold=True, ) raise exc.ConnectionError( "Could not establish a secure connection. " "Please add the server to your certificate " "authority.\nYou can run this command without verifying SSL " "with the --insecure flag, or permanently disable " "verification by the config setting:\n\n " "tower-cli config verify_ssl false" ) except ConnectionError as ex: # Throw error if server can not be reached. if settings.verbose: debug.log("Cannot connect to Tower:", fg="yellow", bold=True) debug.log(str(ex), fg="yellow", bold=True, nl=2) raise exc.ConnectionError( "There was a network error of some kind trying to connect " "to Tower.\n\nThe most common reason for this is a settings " 'issue; is your "host" value in `tower-cli config` correct?\n' 'Right now it is: "%s".' % settings.host ) # Sanity check: Did the server send back some kind of internal error? # If so, bubble this up. if r.status_code >= 500: raise exc.ServerError("The Tower server sent back a server error. " "Please try again later.") # Sanity check: Did we fail to authenticate properly? # If so, fail out now; this is always a failure. if r.status_code == 401: raise exc.AuthError("Invalid Tower authentication credentials.") # Sanity check: Did we get a forbidden response, which means that # the user isn't allowed to do this? Report that. if r.status_code == 403: raise exc.Forbidden("You don't have permission to do that.") # Sanity check: Did we get a 404 response? # Requests with primary keys will return a 404 if there is no response, # and we want to consistently trap these. if r.status_code == 404: raise exc.NotFound("The requested object could not be found.") # Sanity check: Did we get a 405 response? # A 405 means we used a method that isn't allowed. Usually this # is a bad request, but it requires special treatment because the # API sends it as a logic error in a few situations (e.g. trying to # cancel a job that isn't running). if r.status_code == 405: raise exc.MethodNotAllowed( "The Tower server says you can't make a request with the " "%s method to that URL (%s)." % (method, url) ) # Sanity check: Did we get some other kind of error? # If so, write an appropriate error message. if r.status_code >= 400: raise exc.BadRequest( "The Tower server claims it was sent a bad request.\n\n" "%s %s\nParams: %s\nData: %s\n\nResponse: %s" % (method, url, kwargs.get("params", None), kwargs.get("data", None), r.content.decode("utf8")) ) # Django REST Framework intelligently prints API keys in the # order that they are defined in the models and serializer. # # We want to preserve this behavior when it is possible to do so # with minimal effort, because while the order has no explicit meaning, # we make some effort to order keys in a convenient manner. # # To this end, make this response into an APIResponse subclass # (defined below), which has a `json` method that doesn't lose key # order. r.__class__ = APIResponse # Return the response object. return r
def monitor(self, pk, min_interval=1, max_interval=30, timeout=None, outfile=sys.stdout, **kwargs): """Monitor a running job. Blocks further input until the job completes (whether successfully or unsuccessfully) and a final status can be given. """ dots = itertools.cycle([0, 1, 2, 3]) longest_string = 0 interval = min_interval start = time.time() # Poll the Ansible Tower instance for status, and print the status # to the outfile (usually standard out). # # Note that this is one of the few places where we use `secho` # even though we're in a function that might theoretically be imported # and run in Python. This seems fine; outfile can be set to /dev/null # and very much the normal use for this method should be CLI # monitoring. result = self.status(pk, detail=True) last_poll = time.time() timeout_check = 0 while result['status'] != 'successful': # If the job has failed, we want to raise an Exception for that # so we get a non-zero response. if result['failed']: if is_tty(outfile) and not settings.verbose: secho('\r' + ' ' * longest_string + '\n', file=outfile) raise exc.JobFailure('Job failed.') # Sanity check: Have we officially timed out? # The timeout check is incremented below, so this is checking # to see if we were timed out as of the previous iteration. # If we are timed out, abort. if timeout and timeout_check - start > timeout: raise exc.Timeout('Monitoring aborted due to timeout.') # If the outfile is a TTY, print the current status. output = '\rCurrent status: %s%s' % (result['status'], '.' * next(dots)) if longest_string > len(output): output += ' ' * (longest_string - len(output)) else: longest_string = len(output) if is_tty(outfile) and not settings.verbose: secho(output, nl=False, file=outfile) # Put the process to sleep briefly. time.sleep(0.2) # Sanity check: Have we reached our timeout? # If we're about to time out, then we need to ensure that we # do one last check. # # Note that the actual timeout will be performed at the start # of the **next** iteration, so there's a chance for the job's # completion to be noted first. timeout_check = time.time() if timeout and timeout_check - start > timeout: last_poll -= interval # If enough time has elapsed, ask the server for a new status. # # Note that this doesn't actually do a status check every single # time; we want the "spinner" to spin even if we're not actively # doing a check. # # So, what happens is that we are "counting down" (actually up) # to the next time that we intend to do a check, and once that # time hits, we do the status check as part of the normal cycle. if time.time() - last_poll > interval: result = self.status(pk, detail=True) last_poll = time.time() interval = min(interval * 1.5, max_interval) # If the outfile is *not* a TTY, print a status update # when and only when we make an actual check to job status. if not is_tty(outfile) or settings.verbose: click.echo('Current status: %s' % result['status'], file=outfile) # Wipe out the previous output if is_tty(outfile) and not settings.verbose: secho('\r' + ' ' * longest_string, file=outfile, nl=False) secho('\r', file=outfile, nl=False) # Return the job ID and other response data answer = OrderedDict(( ('changed', True), ('id', pk), )) answer.update(result) # Make sure to return ID of resource and not update number # relevant for project creation and update answer['id'] = pk return answer
def my_print(self, message=None, fg='', bold=False, nl=True): if self.no_color: secho(message, fg='', bold=False, nl=nl) else: secho(message, fg=fg, bold=bold, nl=nl)
def request(self, method, url, *args, **kwargs): """Make a request to the Ansible Tower API, and return the response. """ # Piece together the full URL. url = '%s%s' % (self.prefix, url.lstrip('/')) # Ansible Tower expects authenticated requests; add the authentication # from settings if it's provided. kwargs.setdefault('auth', (settings.username, settings.password)) # POST and PUT requests will send JSON by default; make this # the content_type by default. This makes it such that we don't have # to constantly write that in our code, which gets repetitive. headers = kwargs.get('headers', {}) if method.upper() in ('PATCH', 'POST', 'PUT'): headers.setdefault('Content-Type', 'application/json') kwargs['headers'] = headers # If debugging is on, print the URL and data being sent. debug.log('%s %s' % (method, url), fg='blue', bold=True) if method in ('POST', 'PUT', 'PATCH'): debug.log('Data: %s' % kwargs.get('data', {}), fg='blue', bold=True) if method == 'GET' or kwargs.get('params', None): debug.log('Params: %s' % kwargs.get('params', {}), fg='blue', bold=True) debug.log('') # If this is a JSON request, encode the data value. if headers.get('Content-Type', '') == 'application/json': kwargs['data'] = json.dumps(kwargs.get('data', {})) # Decide whether to require SSL verification verify_ssl = True if (settings.verify_ssl is False) or hasattr(settings, 'insecure'): verify_ssl = False # Call the superclass method. try: with warnings.catch_warnings(): warnings.simplefilter( "ignore", urllib3.exceptions.InsecureRequestWarning) r = super(Client, self).request(method, url, *args, verify=verify_ssl, **kwargs) except SSLError as ex: # Throw error if verify_ssl not set to false and server # is not using verified certificate. if settings.verbose: debug.log('SSL connection failed:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) if not settings.host.startswith('http'): secho( 'Suggestion: add the correct http:// or ' 'https:// prefix to the host configuration.', fg='blue', bold=True) raise exc.ConnectionError( 'Could not establish a secure connection. ' 'Please add the server to your certificate ' 'authority.\nYou can run this command without verifying SSL ' 'with the --insecure flag, or permanently disable ' 'verification by the config setting:\n\n ' 'tower-cli config verify_ssl false') except ConnectionError as ex: # Throw error if server can not be reached. if settings.verbose: debug.log('Cannot connect to Tower:', fg='yellow', bold=True) debug.log(str(ex), fg='yellow', bold=True, nl=2) raise exc.ConnectionError( 'There was a network error of some kind trying to connect ' 'to Tower.\n\nThe most common reason for this is a settings ' 'issue; is your "host" value in `tower-cli config` correct?\n' 'Right now it is: "%s".' % settings.host) # Sanity check: Did the server send back some kind of internal error? # If so, bubble this up. if r.status_code >= 500: raise exc.ServerError('The Tower server sent back a server error. ' 'Please try again later.') # Sanity check: Did we fail to authenticate properly? # If so, fail out now; this is always a failure. if r.status_code == 401: raise exc.AuthError('Invalid Tower authentication credentials.') # Sanity check: Did we get a forbidden response, which means that # the user isn't allowed to do this? Report that. if r.status_code == 403: raise exc.Forbidden("You don't have permission to do that.") # Sanity check: Did we get a 404 response? # Requests with primary keys will return a 404 if there is no response, # and we want to consistently trap these. if r.status_code == 404: raise exc.NotFound('The requested object could not be found.') # Sanity check: Did we get a 405 response? # A 405 means we used a method that isn't allowed. Usually this # is a bad request, but it requires special treatment because the # API sends it as a logic error in a few situations (e.g. trying to # cancel a job that isn't running). if r.status_code == 405: raise exc.MethodNotAllowed( "The Tower server says you can't make a request with the " "%s method to that URL (%s)." % (method, url), ) # Sanity check: Did we get some other kind of error? # If so, write an appropriate error message. if r.status_code >= 400: raise exc.BadRequest( 'The Tower server claims it was sent a bad request.\n\n' '%s %s\nParams: %s\nData: %s\n\nResponse: %s' % (method, url, kwargs.get('params', None), kwargs.get('data', None), r.content.decode('utf8'))) # Django REST Framework intelligently prints API keys in the # order that they are defined in the models and serializer. # # We want to preserve this behavior when it is possible to do so # with minimal effort, because while the order has no explicit meaning, # we make some effort to order keys in a convenient manner. # # To this end, make this response into an APIResponse subclass # (defined below), which has a `json` method that doesn't lose key # order. r.__class__ = APIResponse # Return the response object. return r