satori/satori/shell.py

286 lines
9.0 KiB
Python

# Copyright 2012-2013 OpenStack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
"""Command-line interface to Configuration Discovery.
Accept a network location, run through the discovery process and report the
findings back to the user.
"""
from __future__ import print_function
import argparse
import json
import logging
import os
import sys
from satori.common import logging as common_logging
from satori.common import templating
from satori import discovery
from satori import errors
LOG = logging.getLogger(__name__)
def netloc_parser(data):
"""Parse the netloc parameter.
:returns: username, url.
"""
if data and '@' in data:
first_at = data.index('@')
return (data[0:first_at] or None), data[first_at + 1:] or None
else:
return None, data or None
def parse_args(argv):
"""Parse the command line arguments."""
parser = argparse.ArgumentParser(description='Configuration discovery.')
parser.add_argument(
'netloc',
help="Network location as a URL, address, or ssh-style user@address. "
"E.g. https://domain.com, sub.domain.com, 4.3.2.1, or root@web01. "
"Supplying a username before an @ without the `--system-info` "
" argument will default `--system-info` to 'ohai-solo'."
)
#
# Openstack Client Settings
#
openstack_group = parser.add_argument_group(
'OpenStack Settings',
"Cloud credentials, settings and endpoints. If a network location is "
"found to be hosted on the tenant additional information is provided."
)
openstack_group.add_argument(
'--os-username',
dest='username',
default=os.environ.get('OS_USERNAME'),
help="OpenStack Auth username. Defaults to env[OS_USERNAME]."
)
openstack_group.add_argument(
'--os-password',
dest='password',
default=os.environ.get('OS_PASSWORD'),
help="OpenStack Auth password. Defaults to env[OS_PASSWORD]."
)
openstack_group.add_argument(
'--os-region-name',
dest='region',
default=os.environ.get('OS_REGION_NAME'),
help="OpenStack region. Defaults to env[OS_REGION_NAME]."
)
openstack_group.add_argument(
'--os-auth-url',
dest='authurl',
default=os.environ.get('OS_AUTH_URL'),
help="OpenStack Auth endpoint. Defaults to env[OS_AUTH_URL]."
)
openstack_group.add_argument(
'--os-compute-api-version',
dest='compute_api_version',
default=os.environ.get('OS_COMPUTE_API_VERSION', '1.1'),
help="OpenStack Compute API version. Defaults to "
"env[OS_COMPUTE_API_VERSION] or 1.1."
)
# Tenant name or ID can be supplied
tenant_group = openstack_group.add_mutually_exclusive_group()
tenant_group.add_argument(
'--os-tenant-name',
dest='tenant_name',
default=os.environ.get('OS_TENANT_NAME'),
help="OpenStack Auth tenant name. Defaults to env[OS_TENANT_NAME]."
)
tenant_group.add_argument(
'--os-tenant-id',
dest='tenant_id',
default=os.environ.get('OS_TENANT_ID'),
help="OpenStack Auth tenant ID. Defaults to env[OS_TENANT_ID]."
)
#
# Plugins
#
parser.add_argument(
'--system-info',
help="Mechanism to use on a Nova resource to obtain system "
"information. E.g. ohai, facts, factor."
)
#
# Output formatting and logging
#
parser.add_argument(
'--format', '-F',
dest='format',
default='text',
help="Format for output (json or text)."
)
parser.add_argument(
"--logconfig",
help="Optional logging configuration file."
)
parser.add_argument(
"-d", "--debug",
action="store_true",
help="turn on additional debugging inspection and "
"output including full HTTP requests and responses. "
"Log output includes source file path and line "
"numbers."
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="turn up logging to DEBUG (default is INFO)."
)
parser.add_argument(
"-q", "--quiet",
action="store_true",
help="turn down logging to WARN (default is INFO)."
)
#
# SSH options
#
ssh_group = parser.add_argument_group(
'ssh-like Settings',
'To be used to access hosts.'
)
# ssh.py actualy handles the defaults. We're documenting it here so that
# the command-line help string is informative, but the default is set in
# ssh.py (by calling paramiko's load_system_host_keys).
ssh_group.add_argument(
"-i", "--host-key-path",
type=argparse.FileType('r'),
help="Selects a file from which the identity (private key) for public "
"key authentication is read. The default ~/.ssh/id_dsa, "
"~/.ssh/id_ecdsa and ~/.ssh/id_rsa. Supplying this without the "
"`--system-info` argument will default `--system-info` to 'ohai-solo'."
)
ssh_group.add_argument(
"-o",
metavar="ssh_options",
help="Mirrors the ssh -o option. See ssh_config(5)."
)
config = parser.parse_args(argv)
if config.host_key_path:
config.host_key = config.host_key_path.read()
else:
config.host_key = None
# argparse lacks a method to say "if this option is set, require these too"
required_to_access_cloud = [
config.username,
config.password,
config.authurl,
config.region,
config.tenant_name or config.tenant_id,
]
if any(required_to_access_cloud) and not all(required_to_access_cloud):
raise errors.SatoriShellException(
"To connect to an OpenStack cloud you must supply a username, "
"password, authentication endpoint, region and tenant. Either "
"provide all of these settings or none of them."
)
username, url = netloc_parser(config.netloc)
config.netloc = url
if (config.host_key or config.username) and not config.system_info:
config.system_info = 'ohai-solo'
if username:
config.host_username = username
else:
config.host_username = 'root'
return vars(config)
def main(argv=None):
"""Discover an existing configuration for a network location."""
config = parse_args(argv)
common_logging.init_logging(config)
if not (config['format'] == 'json' or
check_format(config['format'] or "text")):
sys.exit("Output format file (%s) not found or accessible. Try "
"specifying raw JSON format using `--format json`" %
get_template_path(config['format']))
try:
results, errors = discovery.run(config['netloc'], config,
interactive=True)
print(format_output(config['netloc'], results,
template_name=config['format']))
if errors:
sys.stderr.write(format_errors(errors, config))
except Exception as exc: # pylint: disable=W0703
if config['debug']:
LOG.exception(exc)
return str(exc)
sys.exit(0)
def get_template_path(name):
"""Get template path from name."""
root_dir = os.path.dirname(__file__)
return os.path.join(root_dir, "formats", "%s.jinja" % name)
def check_format(name):
"""Verify that we have the requested format template."""
template_path = get_template_path(name)
return os.path.exists(template_path)
def get_template(name):
"""Get template text from templates directory by name."""
root_dir = os.path.dirname(__file__)
template_path = os.path.join(root_dir, "formats", "%s.jinja" % name)
with open(template_path, 'r') as handle:
template = handle.read()
return template
def format_output(discovered_target, results, template_name="text"):
"""Format results in CLI format."""
if template_name == 'json':
return(json.dumps(results, indent=2))
else:
template = get_template(template_name)
env_vars = dict(lstrip_blocks=True, trim_blocks=True)
return templating.parse(template, target=discovered_target,
data=results, env_vars=env_vars).strip('\n')
def format_errors(errors, config):
"""Format errors for output to console."""
if config['debug']:
return str(errors)
else:
formatted = {}
for key, error in errors.items():
formatted[key] = error['message']
return str(formatted)
if __name__ == "__main__":
sys.exit(main())