286 lines
9.0 KiB
Python
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())
|