465 lines
16 KiB
Python
465 lines
16 KiB
Python
import os.path
|
|
import re
|
|
import sys
|
|
import tempfile
|
|
import logging
|
|
|
|
import spur
|
|
|
|
from ostack_validator.common import Issue, Mark, MarkedIssue, index, path_relative_to
|
|
from ostack_validator.model import *
|
|
|
|
|
|
class NodeClient(object):
|
|
|
|
def __init__(self, node_address, username, private_key_file, ssh_port=22):
|
|
super(NodeClient, self).__init__()
|
|
self.shell = spur.SshShell(
|
|
hostname=node_address,
|
|
port=ssh_port,
|
|
username=username,
|
|
private_key_file=private_key_file,
|
|
missing_host_key=spur.ssh.MissingHostKey.accept)
|
|
|
|
def run(self, command, *args, **kwargs):
|
|
return self.shell.run(command, allow_error=True, *args, **kwargs)
|
|
|
|
def open(self, path, mode='r'):
|
|
return self.shell.open(path, mode)
|
|
|
|
python_re = re.compile('(/?([^/]*/)*)python[0-9.]*')
|
|
host_port_re = re.compile('(\d+\.\d+\.\d+\.\d+):(\d+)')
|
|
|
|
|
|
class OpenstackDiscovery(object):
|
|
|
|
def discover(self, initial_nodes, username, private_key):
|
|
"Takes a list of node addresses and returns discovered openstack installation info"
|
|
openstack = Openstack()
|
|
|
|
private_key_file = None
|
|
if private_key:
|
|
private_key_file = tempfile.NamedTemporaryFile(suffix='.key')
|
|
private_key_file.write(private_key)
|
|
private_key_file.flush()
|
|
|
|
for address in initial_nodes:
|
|
try:
|
|
m = host_port_re.match(address)
|
|
if m:
|
|
host = m.group(1)
|
|
port = int(m.group(2))
|
|
else:
|
|
host = address
|
|
port = 22
|
|
client = NodeClient(
|
|
host,
|
|
ssh_port=port,
|
|
username=username,
|
|
private_key_file=private_key_file.name)
|
|
client.run(['echo', 'test'])
|
|
except:
|
|
openstack.report_issue(
|
|
Issue(
|
|
Issue.WARNING,
|
|
"Can't connect to node %s" %
|
|
address))
|
|
continue
|
|
|
|
host = self._discover_node(client)
|
|
|
|
if len(host.components) == 0:
|
|
continue
|
|
|
|
openstack.add_host(host)
|
|
|
|
if len(openstack.hosts) == 0:
|
|
openstack.report_issue(
|
|
Issue(Issue.FATAL, "No OpenStack nodes were discovered"))
|
|
|
|
if private_key_file:
|
|
private_key_file.close()
|
|
|
|
return openstack
|
|
|
|
def _discover_node(self, client):
|
|
hostname = client.run(['hostname']).output.strip()
|
|
|
|
host = Host(name=hostname)
|
|
host.id = self._collect_host_id(client)
|
|
host.network_addresses = self._collect_host_network_addresses(client)
|
|
|
|
host.add_component(self._collect_keystone_data(client))
|
|
host.add_component(self._collect_nova_api_data(client))
|
|
host.add_component(self._collect_nova_compute_data(client))
|
|
host.add_component(self._collect_nova_scheduler_data(client))
|
|
host.add_component(self._collect_glance_api_data(client))
|
|
host.add_component(self._collect_glance_registry_data(client))
|
|
host.add_component(self._collect_cinder_api_data(client))
|
|
host.add_component(self._collect_cinder_volume_data(client))
|
|
host.add_component(self._collect_cinder_scheduler_data(client))
|
|
host.add_component(self._collect_mysql_data(client))
|
|
host.add_component(self._collect_rabbitmq_data(client))
|
|
|
|
return host
|
|
|
|
def _find_process(self, client, name):
|
|
processes = self._get_processes(client)
|
|
for line in processes:
|
|
if len(line) > 0 and os.path.basename(line[0]) == name:
|
|
return line
|
|
|
|
return None
|
|
|
|
def _find_python_process(self, client, name):
|
|
processes = self._get_processes(client)
|
|
for line in processes:
|
|
if len(line) > 0 and (line[0] == name or line[0].endswith('/' + name)):
|
|
return line
|
|
if len(line) > 1 and python_re.match(line[0]) and (line[1] == name or line[1].endswith('/' + name)):
|
|
return line
|
|
|
|
return None
|
|
|
|
def _find_python_package_version(self, client, package):
|
|
result = client.run(
|
|
['python', '-c', 'import pkg_resources; version = pkg_resources.get_provider(pkg_resources.Requirement.parse("%s")).version; print(version)' %
|
|
package])
|
|
|
|
s = result.output.strip()
|
|
parts = []
|
|
for p in s.split('.'):
|
|
if not p[0].isdigit():
|
|
break
|
|
|
|
parts.append(p)
|
|
|
|
version = '.'.join(parts)
|
|
|
|
return version
|
|
|
|
def _get_processes(self, client):
|
|
return (
|
|
[line.split()
|
|
for line in client.run(['ps', '-Ao', 'cmd', '--no-headers']).output.split("\n")]
|
|
)
|
|
|
|
def _collect_host_id(self, client):
|
|
ether_re = re.compile('link/ether (([0-9a-f]{2}:){5}([0-9a-f]{2})) ')
|
|
result = client.run(['bash', '-c', 'ip link | grep "link/ether "'])
|
|
macs = []
|
|
for match in ether_re.finditer(result.output):
|
|
macs.append(match.group(1).replace(':', ''))
|
|
return ''.join(macs)
|
|
|
|
def _collect_host_network_addresses(self, client):
|
|
ipaddr_re = re.compile('inet (\d+\.\d+\.\d+\.\d+)/\d+')
|
|
addresses = []
|
|
result = client.run(['bash', '-c', 'ip address list | grep "inet "'])
|
|
for match in ipaddr_re.finditer(result.output):
|
|
addresses.append(match.group(1))
|
|
return addresses
|
|
|
|
def _permissions_string_to_number(self, s):
|
|
return 0
|
|
|
|
def _collect_file(self, client, path):
|
|
ls = client.run(['ls', '-l', '--time-style=full-iso', path])
|
|
if ls.return_code != 0:
|
|
return None
|
|
|
|
line = ls.output.split("\n")[0]
|
|
perm, links, owner, group, size, date, time, timezone, name = line.split(
|
|
)
|
|
permissions = self._permissions_string_to_number(perm)
|
|
|
|
with client.open(path) as f:
|
|
contents = f.read()
|
|
|
|
return FileResource(path, contents, owner, group, permissions)
|
|
|
|
def _get_keystone_db_data(self, client, command, env={}):
|
|
result = client.run(['keystone', command], update_env=env)
|
|
if result.return_code != 0:
|
|
return []
|
|
|
|
lines = result.output.strip().split("\n")
|
|
|
|
columns = []
|
|
last_pos = 0
|
|
l = lines[0]
|
|
while True:
|
|
pos = l.find('+', last_pos + 1)
|
|
if pos == -1:
|
|
break
|
|
|
|
columns.append({'start': last_pos + 1, 'end': pos - 1})
|
|
|
|
last_pos = pos
|
|
|
|
l = lines[1]
|
|
for c in columns:
|
|
c['name'] = l[c['start']:c['end']].strip()
|
|
|
|
data = []
|
|
for l in lines[3:-1]:
|
|
d = dict()
|
|
for c in columns:
|
|
d[c['name']] = l[c['start']:c['end']].strip()
|
|
|
|
data.append(d)
|
|
|
|
return data
|
|
|
|
def _collect_keystone_data(self, client):
|
|
keystone_process = self._find_python_process(client, 'keystone-all')
|
|
if not keystone_process:
|
|
return None
|
|
|
|
p = index(keystone_process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(keystone_process):
|
|
config_path = keystone_process[p + 1]
|
|
else:
|
|
config_path = '/etc/keystone/keystone.conf'
|
|
|
|
keystone = KeystoneComponent()
|
|
keystone.version = self._find_python_package_version(
|
|
client, 'keystone')
|
|
keystone.config_files = []
|
|
keystone.config_files.append(self._collect_file(client, config_path))
|
|
|
|
token = keystone.config['admin_token']
|
|
host = keystone.config['bind_host']
|
|
if host == '0.0.0.0':
|
|
host = '127.0.0.1'
|
|
port = int(keystone.config['admin_port'])
|
|
|
|
keystone_env = {
|
|
'OS_SERVICE_TOKEN': token,
|
|
'OS_SERVICE_ENDPOINT': 'http://%s:%d/v2.0' % (host, port)
|
|
}
|
|
|
|
keystone.db = dict()
|
|
keystone.db['tenants'] = self._get_keystone_db_data(
|
|
client, 'tenant-list', env=keystone_env)
|
|
keystone.db['users'] = self._get_keystone_db_data(
|
|
client, 'user-list', env=keystone_env)
|
|
keystone.db['services'] = self._get_keystone_db_data(
|
|
client, 'service-list', env=keystone_env)
|
|
keystone.db['endpoints'] = self._get_keystone_db_data(
|
|
client, 'endpoint-list', env=keystone_env)
|
|
|
|
return keystone
|
|
|
|
def _collect_nova_api_data(self, client):
|
|
process = self._find_python_process(client, 'nova-api')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/nova/nova.conf'
|
|
|
|
nova_api = NovaApiComponent()
|
|
nova_api.version = self._find_python_package_version(client, 'nova')
|
|
nova_api.config_files = []
|
|
nova_api.config_files.append(self._collect_file(client, config_path))
|
|
|
|
paste_config_path = path_relative_to(
|
|
nova_api.config['api_paste_config'],
|
|
os.path.dirname(config_path))
|
|
nova_api.paste_config_file = self._collect_file(
|
|
client, paste_config_path)
|
|
|
|
return nova_api
|
|
|
|
def _collect_nova_compute_data(self, client):
|
|
process = self._find_python_process(client, 'nova-compute')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/nova/nova.conf'
|
|
|
|
nova_compute = NovaComputeComponent()
|
|
nova_compute.version = self._find_python_package_version(
|
|
client, 'nova')
|
|
nova_compute.config_files = []
|
|
nova_compute.config_files.append(
|
|
self._collect_file(client, config_path))
|
|
|
|
return nova_compute
|
|
|
|
def _collect_nova_scheduler_data(self, client):
|
|
process = self._find_python_process(client, 'nova-scheduler')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/nova/nova.conf'
|
|
|
|
nova_scheduler = NovaSchedulerComponent()
|
|
nova_scheduler.version = self._find_python_package_version(
|
|
client, 'nova')
|
|
nova_scheduler.config_files = []
|
|
nova_scheduler.config_files.append(
|
|
self._collect_file(client, config_path))
|
|
|
|
return nova_scheduler
|
|
|
|
def _collect_glance_api_data(self, client):
|
|
process = self._find_python_process(client, 'glance-api')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/glance/glance-api.conf'
|
|
|
|
glance_api = GlanceApiComponent()
|
|
glance_api.version = self._find_python_package_version(
|
|
client, 'glance')
|
|
glance_api.config_files = []
|
|
glance_api.config_files.append(self._collect_file(client, config_path))
|
|
|
|
return glance_api
|
|
|
|
def _collect_glance_registry_data(self, client):
|
|
process = self._find_python_process(client, 'glance-registry')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/glance/glance-registry.conf'
|
|
|
|
glance_registry = GlanceRegistryComponent()
|
|
glance_registry.version = self._find_python_package_version(
|
|
client, 'glance')
|
|
glance_registry.config_files = []
|
|
glance_registry.config_files.append(
|
|
self._collect_file(client, config_path))
|
|
|
|
return glance_registry
|
|
|
|
def _collect_cinder_api_data(self, client):
|
|
process = self._find_python_process(client, 'cinder-api')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/cinder/cinder.conf'
|
|
|
|
cinder_api = CinderApiComponent()
|
|
cinder_api.version = self._find_python_package_version(
|
|
client, 'cinder')
|
|
cinder_api.config_files = []
|
|
cinder_api.config_files.append(self._collect_file(client, config_path))
|
|
|
|
paste_config_path = path_relative_to(
|
|
cinder_api.config['api_paste_config'],
|
|
os.path.dirname(config_path))
|
|
cinder_api.paste_config_file = self._collect_file(
|
|
client, paste_config_path)
|
|
|
|
return cinder_api
|
|
|
|
def _collect_cinder_volume_data(self, client):
|
|
process = self._find_python_process(client, 'cinder-volume')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/cinder/cinder.conf'
|
|
|
|
cinder_volume = CinderVolumeComponent()
|
|
cinder_volume.version = self._find_python_package_version(
|
|
client, 'cinder')
|
|
cinder_volume.config_files = []
|
|
cinder_volume.config_files.append(
|
|
self._collect_file(client, config_path))
|
|
|
|
rootwrap_config_path = path_relative_to(
|
|
cinder_volume.config['rootwrap_config'],
|
|
os.path.dirname(config_path))
|
|
cinder_volume.rootwrap_config = self._collect_file(
|
|
client, rootwrap_config_path)
|
|
|
|
return cinder_volume
|
|
|
|
def _collect_cinder_scheduler_data(self, client):
|
|
process = self._find_python_process(client, 'cinder-scheduler')
|
|
if not process:
|
|
return None
|
|
|
|
p = index(process, lambda s: s == '--config-file')
|
|
if p != -1 and p + 1 < len(process):
|
|
config_path = process[p + 1]
|
|
else:
|
|
config_path = '/etc/cinder/cinder.conf'
|
|
|
|
cinder_scheduler = CinderSchedulerComponent()
|
|
cinder_scheduler.version = self._find_python_package_version(
|
|
client, 'cinder')
|
|
cinder_scheduler.config_files = []
|
|
cinder_scheduler.config_files.append(
|
|
self._collect_file(client, config_path))
|
|
|
|
return cinder_scheduler
|
|
|
|
def _collect_mysql_data(self, client):
|
|
process = self._find_process(client, 'mysqld')
|
|
if not process:
|
|
return None
|
|
|
|
mysqld_version_re = re.compile('mysqld\s+Ver\s(\S+)\s')
|
|
|
|
mysql = MysqlComponent()
|
|
|
|
version_result = client.run(['mysqld', '--version'])
|
|
m = mysqld_version_re.match(version_result.output)
|
|
mysql.version = m.group(1) if m else 'unknown'
|
|
|
|
mysql.config_files = []
|
|
config_locations_result = client.run(
|
|
['bash', '-c', 'mysqld --help --verbose | grep "Default options are read from the following files in the given order" -A 1'])
|
|
config_locations = config_locations_result.output.strip().split(
|
|
"\n")[-1].split()
|
|
for path in config_locations:
|
|
f = self._collect_file(client, path)
|
|
if f:
|
|
mysql.config_files.append(f)
|
|
|
|
return mysql
|
|
|
|
def _collect_rabbitmq_data(self, client):
|
|
process = self._find_process(client, 'beam.smp')
|
|
if not process:
|
|
return None
|
|
|
|
if ' '.join(process).find('rabbit') == -1:
|
|
return None
|
|
|
|
rabbitmq = RabbitMqComponent()
|
|
rabbitmq.version = 'unknown'
|
|
|
|
return rabbitmq
|