[CLI] Add os-faults discover command
`os-faults discover` discovers nodes and services and then adds them to output config file. * Added new CLI * Added commands: os-faults --version os-faults verify os-faults discover os-faults nodes os-faults drivers Change-Id: Id9f12c960fbba4d64ea181ca7245c89b3b4c655d
This commit is contained in:
parent
70ab8aa8be
commit
746fc45455
|
@ -10,6 +10,7 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
||||
import appdirs
|
||||
|
@ -119,15 +120,13 @@ CONFIG_SCHEMA = {
|
|||
}
|
||||
|
||||
|
||||
def _read_config(config_filename):
|
||||
os_faults_config = config_filename or os.environ.get('OS_FAULTS_CONFIG')
|
||||
if os_faults_config:
|
||||
CONFIG_FILES.insert(0, os_faults_config)
|
||||
def get_default_config_file():
|
||||
if 'OS_FAULTS_CONFIG' in os.environ:
|
||||
return os.environ['OS_FAULTS_CONFIG']
|
||||
|
||||
for config_file in CONFIG_FILES:
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file) as fd:
|
||||
return yaml.safe_load(fd.read())
|
||||
return config_file
|
||||
|
||||
msg = 'Config file is not found on any of paths: {}'.format(CONFIG_FILES)
|
||||
raise error.OSFError(msg)
|
||||
|
@ -144,10 +143,12 @@ def connect(cloud_config=None, config_filename=None):
|
|||
|
||||
:param cloud_config: dict with cloud and power management params
|
||||
:param config_filename: name of the file where to read config from
|
||||
:return: CloudManagement object
|
||||
:returns: CloudManagement object
|
||||
"""
|
||||
if cloud_config is None:
|
||||
cloud_config = _read_config(config_filename)
|
||||
config_filename = config_filename or get_default_config_file()
|
||||
with open(config_filename) as fd:
|
||||
cloud_config = yaml.safe_load(fd.read())
|
||||
|
||||
jsonschema.validate(cloud_config, CONFIG_SCHEMA)
|
||||
|
||||
|
@ -184,6 +185,42 @@ def connect(cloud_config=None, config_filename=None):
|
|||
return cloud_management
|
||||
|
||||
|
||||
def discover(cloud_config):
|
||||
"""Connect to the cloud and discover nodes and services
|
||||
|
||||
:param cloud_config: dict with cloud and power management params
|
||||
:returns: config dict with discovered nodes/services
|
||||
"""
|
||||
|
||||
cloud_config = copy.deepcopy(cloud_config)
|
||||
cloud_management = connect(cloud_config)
|
||||
|
||||
# discover nodes
|
||||
hosts = []
|
||||
for host in cloud_management.get_nodes().hosts:
|
||||
hosts.append({'ip': host.ip, 'mac': host.mac, 'fqdn': host.fqdn})
|
||||
LOG.info('Found node: %s' % str(host))
|
||||
cloud_config['node_discover'] = {'driver': 'node_list', 'args': hosts}
|
||||
|
||||
# discover services
|
||||
cloud_config['services'] = {}
|
||||
for service_name in cloud_management.list_supported_services():
|
||||
service = cloud_management.get_service(service_name)
|
||||
ips = service.get_nodes().get_ips()
|
||||
cloud_config['services'][service_name] = {
|
||||
'driver': service.NAME,
|
||||
'args': service.config
|
||||
}
|
||||
if ips:
|
||||
cloud_config['services'][service_name]['hosts'] = ips
|
||||
LOG.info('Found service "%s" on hosts: %s' % (
|
||||
service_name, str(ips)))
|
||||
else:
|
||||
LOG.warning('Service "%s" is not found' % service_name)
|
||||
|
||||
return cloud_config
|
||||
|
||||
|
||||
def human_api(distractor, command):
|
||||
"""Execute high-level text command with specified destructor
|
||||
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
# 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.
|
||||
|
||||
import logging
|
||||
|
||||
import click
|
||||
import yaml
|
||||
|
||||
import os_faults
|
||||
from os_faults import registry
|
||||
|
||||
|
||||
READABLE_FILE = click.Path(dir_okay=False, readable=True, exists=True,
|
||||
resolve_path=True)
|
||||
WRITABLE_FILE = click.Path(dir_okay=False, writable=True, resolve_path=True)
|
||||
|
||||
config_option = click.option('-c', '--config', type=READABLE_FILE,
|
||||
help='path to os-faults cloud connection config')
|
||||
|
||||
|
||||
def print_version(ctx, param, value):
|
||||
if not value or ctx.resilient_parsing:
|
||||
return
|
||||
click.echo('Version: %s' % os_faults.get_release())
|
||||
ctx.exit()
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.option('--debug', '-d', is_flag=True, help='Enable debug logs')
|
||||
@click.option('--version', is_flag=True, callback=print_version,
|
||||
expose_value=False, is_eager=True, help='Show version and exit.')
|
||||
def main(debug):
|
||||
logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
|
||||
level=logging.DEBUG if debug else logging.INFO)
|
||||
|
||||
|
||||
@main.command()
|
||||
@config_option
|
||||
def verify(config):
|
||||
"""Verify connection to the cloud"""
|
||||
config = config or os_faults.get_default_config_file()
|
||||
destructor = os_faults.connect(config_filename=config)
|
||||
destructor.verify()
|
||||
|
||||
|
||||
@main.command()
|
||||
@click.argument('output', type=WRITABLE_FILE)
|
||||
@config_option
|
||||
def discover(config, output):
|
||||
"""Discover services/nodes and save them to output config file"""
|
||||
config = config or os_faults.get_default_config_file()
|
||||
with open(config) as f:
|
||||
cloud_config = yaml.safe_load(f.read())
|
||||
discovered_config = os_faults.discover(cloud_config)
|
||||
with open(output, 'w') as f:
|
||||
f.write(yaml.safe_dump(discovered_config, default_flow_style=False))
|
||||
click.echo('Saved {}'.format(output))
|
||||
|
||||
|
||||
@main.command()
|
||||
@config_option
|
||||
def nodes(config):
|
||||
"""List cloud nodes"""
|
||||
config = config or os_faults.get_default_config_file()
|
||||
destructor = os_faults.connect(config_filename=config)
|
||||
hosts = [{'ip': host.ip, 'mac': host.mac, 'fqdn': host.fqdn}
|
||||
for host in destructor.get_nodes().hosts]
|
||||
click.echo(yaml.safe_dump(hosts, default_flow_style=False), nl=False)
|
||||
|
||||
|
||||
@main.command()
|
||||
def drivers():
|
||||
"""List os-faults drivers"""
|
||||
drivers = sorted(registry.get_drivers().keys())
|
||||
click.echo(yaml.safe_dump(drivers, default_flow_style=False), nl=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -0,0 +1,109 @@
|
|||
# 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.
|
||||
|
||||
import os
|
||||
|
||||
from click import testing
|
||||
import mock
|
||||
|
||||
from os_faults.api import cloud_management
|
||||
from os_faults.api import node_collection
|
||||
from os_faults.cmd import main
|
||||
from os_faults.tests.unit import test
|
||||
|
||||
|
||||
class MainTestCase(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(MainTestCase, self).setUp()
|
||||
self.runner = testing.CliRunner()
|
||||
|
||||
def test_version(self):
|
||||
result = self.runner.invoke(main.main, ['--version'])
|
||||
self.assertEqual(0, result.exit_code)
|
||||
self.assertIn('Version', result.output)
|
||||
|
||||
@mock.patch('os_faults.connect')
|
||||
def test_verify(self, mock_connect):
|
||||
with self.runner.isolated_filesystem():
|
||||
with open('my.yaml', 'w') as f:
|
||||
f.write('foo')
|
||||
result = self.runner.invoke(main.main, ['verify'],
|
||||
env={'OS_FAULTS_CONFIG': 'my.yaml'})
|
||||
self.assertEqual(0, result.exit_code)
|
||||
mock_connect.assert_called_once_with(config_filename='my.yaml')
|
||||
destructor = mock_connect.return_value
|
||||
destructor.verify.assert_called_once_with()
|
||||
|
||||
@mock.patch('os_faults.connect')
|
||||
def test_verify_with_config(self, mock_connect):
|
||||
with self.runner.isolated_filesystem():
|
||||
with open('my.yaml', 'w') as f:
|
||||
f.write('foo')
|
||||
myconf = os.path.abspath(f.name)
|
||||
result = self.runner.invoke(main.main, ['verify', '-c', myconf])
|
||||
self.assertEqual(0, result.exit_code)
|
||||
mock_connect.assert_called_once_with(config_filename=myconf)
|
||||
destructor = mock_connect.return_value
|
||||
destructor.verify.assert_called_once_with()
|
||||
|
||||
@mock.patch('os_faults.discover')
|
||||
def test_discover(self, mock_discover):
|
||||
mock_discover.return_value = {'foo': 'bar'}
|
||||
with self.runner.isolated_filesystem():
|
||||
with open('my.yaml', 'w') as f:
|
||||
f.write('foo')
|
||||
myconf = os.path.abspath(f.name)
|
||||
result = self.runner.invoke(main.main, ['discover', '-c', myconf,
|
||||
'my-new.yaml'])
|
||||
self.assertEqual(0, result.exit_code)
|
||||
mock_discover.assert_called_once_with('foo')
|
||||
|
||||
with open('my-new.yaml') as f:
|
||||
self.assertEqual('foo: bar\n', f.read())
|
||||
|
||||
@mock.patch('os_faults.connect')
|
||||
def test_nodes(self, mock_connect):
|
||||
cloud_management_mock = mock.create_autospec(
|
||||
cloud_management.CloudManagement)
|
||||
mock_connect.return_value = cloud_management_mock
|
||||
cloud_management_mock.get_nodes.return_value.hosts = [
|
||||
node_collection.Host(
|
||||
ip='10.0.0.2', mac='09:7b:74:90:63:c1', fqdn='node1.local'),
|
||||
node_collection.Host(
|
||||
ip='10.0.0.3', mac='09:7b:74:90:63:c2', fqdn='node2.local')]
|
||||
|
||||
with self.runner.isolated_filesystem():
|
||||
with open('my.yaml', 'w') as f:
|
||||
f.write('foo')
|
||||
myconf = os.path.abspath(f.name)
|
||||
result = self.runner.invoke(main.main, ['nodes', '-c', myconf])
|
||||
self.assertEqual(0, result.exit_code)
|
||||
self.assertEqual(
|
||||
'- fqdn: node1.local\n'
|
||||
' ip: 10.0.0.2\n'
|
||||
' mac: 09:7b:74:90:63:c1\n'
|
||||
'- fqdn: node2.local\n'
|
||||
' ip: 10.0.0.3\n'
|
||||
' mac: 09:7b:74:90:63:c2\n', result.output)
|
||||
|
||||
@mock.patch('os_faults.registry.get_drivers')
|
||||
def test_drivers(self, mock_get_drivers):
|
||||
mock_get_drivers.return_value = {'foo': 1, 'bar': 2}
|
||||
|
||||
with self.runner.isolated_filesystem():
|
||||
result = self.runner.invoke(main.main, ['drivers'])
|
||||
self.assertEqual(0, result.exit_code)
|
||||
self.assertEqual(
|
||||
'- bar\n'
|
||||
'- foo\n', result.output)
|
|
@ -18,7 +18,10 @@ import yaml
|
|||
|
||||
import os_faults
|
||||
from os_faults.ansible import executor
|
||||
from os_faults.api import cloud_management
|
||||
from os_faults.api import error
|
||||
from os_faults.api import node_collection
|
||||
from os_faults.api import service
|
||||
from os_faults.drivers import devstack
|
||||
from os_faults.drivers import fuel
|
||||
from os_faults.drivers import ipmi
|
||||
|
@ -219,3 +222,77 @@ class OSFaultsTestCase(test.TestCase):
|
|||
|
||||
self.assertRaises(error.OSFError, os_faults.register_ansible_modules,
|
||||
['/my/bad/path/'])
|
||||
|
||||
@mock.patch('os_faults.connect')
|
||||
def test_discover(self, mock_connect):
|
||||
cloud_config = {
|
||||
'cloud_management': {
|
||||
'driver': 'devstack',
|
||||
'args': {
|
||||
'address': 'devstack.local',
|
||||
'username': 'developer',
|
||||
'private_key_file': '/my/path/pk.key',
|
||||
}
|
||||
}
|
||||
}
|
||||
cloud_management_mock = mock.create_autospec(
|
||||
cloud_management.CloudManagement)
|
||||
mock_connect.return_value = cloud_management_mock
|
||||
cloud_management_mock.get_nodes.return_value.hosts = [
|
||||
node_collection.Host(
|
||||
ip='10.0.0.2', mac='09:7b:74:90:63:c1', fqdn='node1.local'),
|
||||
node_collection.Host(
|
||||
ip='10.0.0.3', mac='09:7b:74:90:63:c2', fqdn='node2.local')]
|
||||
cloud_management_mock.list_supported_services.return_value = [
|
||||
'srv1', 'srv2']
|
||||
|
||||
def mock_service(name, config, ips):
|
||||
m = mock.create_autospec(service.Service)
|
||||
m.NAME = name
|
||||
m.config = config
|
||||
m.get_nodes.return_value.get_ips.return_value = ips
|
||||
return m
|
||||
|
||||
srv1 = mock_service('process', {'grep': 'srv1'}, [])
|
||||
srv2 = mock_service('linux_service',
|
||||
{'grep': 'srv2', 'linux_service': 'srv2'},
|
||||
['10.0.0.2'])
|
||||
services = {'srv1': srv1, 'srv2': srv2}
|
||||
cloud_management_mock.get_service.side_effect = services.get
|
||||
|
||||
discovered_config = os_faults.discover(cloud_config)
|
||||
self.assertEqual({
|
||||
'cloud_management': {
|
||||
'driver': 'devstack',
|
||||
'args': {
|
||||
'address': 'devstack.local',
|
||||
'private_key_file': '/my/path/pk.key',
|
||||
'username': 'developer'
|
||||
}
|
||||
},
|
||||
'node_discover': {
|
||||
'driver': 'node_list',
|
||||
'args': [
|
||||
{
|
||||
'fqdn': 'node1.local',
|
||||
'ip': '10.0.0.2',
|
||||
'mac': '09:7b:74:90:63:c1'
|
||||
}, {
|
||||
'fqdn': 'node2.local',
|
||||
'ip': '10.0.0.3',
|
||||
'mac': '09:7b:74:90:63:c2'
|
||||
}
|
||||
]
|
||||
},
|
||||
'services': {
|
||||
'srv1': {
|
||||
'driver': 'process',
|
||||
'args': {'grep': 'srv1'},
|
||||
},
|
||||
'srv2': {
|
||||
'driver': 'linux_service',
|
||||
'args': {'grep': 'srv2', 'linux_service': 'srv2'},
|
||||
'hosts': ['10.0.0.2']
|
||||
}
|
||||
}
|
||||
}, discovered_config)
|
||||
|
|
|
@ -6,8 +6,9 @@ pbr>=2.0.0 # Apache-2.0
|
|||
|
||||
ansible>=2.2
|
||||
appdirs>=1.3.0 # MIT License
|
||||
jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
|
||||
click>=6.7 # BSD
|
||||
iso8601>=0.1.11 # MIT
|
||||
jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.serialization>=1.10.0 # Apache-2.0
|
||||
oslo.utils>=3.20.0 # Apache-2.0
|
||||
|
|
Loading…
Reference in New Issue