Implemented type args in config schema

Added 'enum' and 'regex' types
Update ConfigSchema to support iteration, len and getitem
Added ProcessResource to model
Implemented permission saving for FileResources
Added new base class for all model resources
Renamed Host model to HostResource
Added DirectoryResource; implemented discovery for files and directories
Needed for blueprint:
https://blueprints.launchpad.net/rubick/+spec/python33-support

Change-Id: I41f67db38ee3894aa8cf3d6e51991b027dc20485
This commit is contained in:
Maxim Kulkin 2013-11-14 10:48:25 +04:00 committed by Peter Lomakin
parent aae261e036
commit 46c5ad5406
7 changed files with 341 additions and 82 deletions

View File

@ -11,6 +11,7 @@ assert rubick.inspections
import rubick.schemas
assert rubick.schemas
from rubick.json import openstack_for_json
from rubick.model import DirectoryResource
def indent_prefix(indent=0):
@ -50,8 +51,16 @@ def print_issues(issues, indent=0):
def print_service(service):
print(' ' + str(service))
print_issues(service.all_issues, indent=2)
print(' ' + service.name)
print_issues(service.all_issues, indent=3)
def print_path(path):
if isinstance(path, DirectoryResource):
print(' ' + path.path + '/')
else:
print(' ' + path.path)
print_issues(path.all_issues, indent=3)
def print_host(host):
@ -59,9 +68,16 @@ def print_host(host):
print_issues(host.issues, indent=1)
print(' Services:')
for service in host.components:
print_service(service)
print(' Filesystem:')
for path in sorted(host.filesystem.values(), key=lambda f: f.path):
print_path(path)
def print_openstack(openstack):
print_issues(openstack.issues)
@ -90,8 +106,8 @@ def main():
x = inspection()
x.inspect(openstack)
# print_openstack(openstack)
print(json.dumps(openstack_for_json(openstack)))
print_openstack(openstack)
# print(json.dumps(openstack_for_json(openstack)))
if __name__ == '__main__':
main()

View File

@ -132,7 +132,7 @@ class Configuration(object):
if param_schema:
type_validator = TypeValidatorRegistry.get_validator(
param_schema.type)
type_validation_result = type_validator.validate(value)
type_validation_result = type_validator.validate(value, **param_schema.type_args)
if not isinstance(type_validation_result, InvalidValueError):
value = type_validation_result
@ -159,7 +159,7 @@ class Configuration(object):
type_validator = TypeValidatorRegistry.get_validator(
param_schema.type)
type_validation_result = type_validator.validate(value)
type_validation_result = type_validator.validate(value, **param_schema.type_args)
if not isinstance(type_validation_result, InvalidValueError):
return None

View File

@ -319,20 +319,62 @@ def get_host_network_addresses(client):
return addresses
def permissions_string_to_number(s):
# TODO(someone): implement it
return 0
def permissions_string_to_mode(s):
mode = 0
if s[0] == 'd':
mode |= stat.S_IFDIR
elif s[0] == 's':
mode |= stat.S_IFSOCK
elif s[0] == 'l':
mode |= stat.S_IFLNK
else:
mode |= stat.S_IFREG
if s[1] == 'r':
mode |= stat.S_IRUSR
if s[2] == 'w':
mode |= stat.S_IWUSR
if s[3] == 'x':
mode |= stat.S_IXUSR
if s[4] == 'r':
mode |= stat.S_IRGRP
if s[5] == 'w':
mode |= stat.S_IWGRP
if s[6] == 'x':
mode |= stat.S_IXGRP
if s[7] == 'r':
mode |= stat.S_IROTH
if s[8] == 'w':
mode |= stat.S_IWOTH
if s[9] == 'x':
mode |= stat.S_IXOTH
return mode
def collect_process(client, process_info):
result = client.run(['readlink', '/proc/%d/cwd' % process_info.pid])
cwd = result.output.strip()
process = ProcessResource(
pid=process_info.pid,
cmdline=process_info.command,
cwd=cwd)
process.listen_sockets = get_process_listen_sockets(client, process.pid)
return process
def collect_file(client, path):
ls = client.run(['ls', '-l', '--time-style=full-iso', path])
ls = client.run(['ls', '-ld', '--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 = permissions_string_to_number(perm)
permissions = permissions_string_to_mode(perm)
with client.open(path) as f:
contents = f.read()
@ -340,6 +382,19 @@ def collect_file(client, path):
return FileResource(path, contents, owner, group, permissions)
def collect_directory(client, path):
ls = client.run(['ls', '-ld', '--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 = permissions_string_to_mode(perm)
return DirectoryResource(path, owner, group, permissions)
def collect_component_configs(client, component,
command, default_config=None):
config_files = []
@ -399,7 +454,7 @@ class HostDiscovery(BaseDiscovery):
hostname = client.run(['hostname']).output.strip()
item = Host(name=hostname)
item = HostResource(name=hostname)
item.id = get_host_id(client)
item.network_addresses = get_host_network_addresses(client)
@ -447,15 +502,70 @@ class HostDiscovery(BaseDiscovery):
return item is not None
class FileSystemDiscovery(BaseDiscovery):
def seen(self, driver, host, path=None, **data):
if not path:
return True
client = driver.client(host)
host_id = get_host_id(client)
item = find(self._seen_items,
lambda f: f.path == path and f.host_id == host_id)
return item is not None
class FileDiscovery(FileSystemDiscovery):
item_type = 'file'
def discover(self, driver, host, path=None, **data):
client = driver.client(host)
if not path:
return None
item = collect_file(client, path)
if not item:
return None
item.host_id = get_host_id(client)
self._seen_items.append(item)
return item
class DirectoryDiscovery(FileSystemDiscovery):
item_type = 'directory'
def discover(self, driver, host, path=None, **data):
client = driver.client(host)
if not path:
return None
item = collect_directory(client, path)
if not item:
return None
item.host_id = get_host_id(client)
self._seen_items.append(item)
return item
class ServiceDiscovery(BaseDiscovery):
def seen(self, driver, host, **data):
if 'sockets' in data:
item = find(self._seen_items,
lambda s: data['sockets'] == s.listen_sockets)
lambda s: data['sockets'] == s.process.listen_sockets)
elif 'port' in data:
item = find(self._seen_items,
lambda s: (host, data['port']) in s.listen_sockets)
lambda s: (host, data['port']) in s.process.listen_sockets)
else:
client = driver.client(host)
host_id = client.get_host_id()
@ -476,8 +586,9 @@ class KeystoneDiscovery(ServiceDiscovery):
keystone = KeystoneComponent()
keystone.host_id = get_host_id(client)
keystone.listen_sockets = get_process_listen_sockets(client,
process.pid)
keystone.process = collect_process(client, process)
keystone.version = find_python_package_version(client, 'keystone')
keystone.config_files = []
@ -556,8 +667,9 @@ class NovaApiDiscovery(ServiceDiscovery):
nova_api = NovaApiComponent()
nova_api.host_id = get_host_id(client)
nova_api.listen_sockets = get_process_listen_sockets(client,
process.pid)
nova_api.process = collect_process(client, process)
nova_api.version = find_python_package_version(client, 'nova')
collect_component_configs(
@ -568,6 +680,14 @@ class NovaApiDiscovery(ServiceDiscovery):
if len(nova_api.config_files) > 0:
config_dir = os.path.dirname(nova_api.config_files[0].path)
for param in nova_api.config.schema:
if param.type == 'file':
path = nova_api.config[param.name]
driver.enqueue('file', host=host, path=path)
elif param.type == 'directory':
path = nova_api.config[param.name]
driver.enqueue('directory', host=host, path=path)
paste_config_path = path_relative_to(
nova_api.config['api_paste_config'], config_dir)
nova_api.paste_config_file = collect_file(
@ -590,8 +710,9 @@ class NovaComputeDiscovery(ServiceDiscovery):
nova_compute = NovaComputeComponent()
nova_compute.host_id = get_host_id(client)
nova_compute.listen_sockets = get_process_listen_sockets(client,
process.pid)
nova_compute.process = collect_process(client, process)
nova_compute.version = find_python_package_version(client, 'nova')
collect_component_configs(
@ -615,8 +736,9 @@ class NovaSchedulerDiscovery(ServiceDiscovery):
nova_scheduler = NovaSchedulerComponent()
nova_scheduler.host_id = get_host_id(client)
nova_scheduler.listen_sockets = get_process_listen_sockets(client,
process.pid)
nova_scheduler.process = collect_process(client, process)
nova_scheduler.version = find_python_package_version(client, 'nova')
collect_component_configs(
@ -640,8 +762,9 @@ class GlanceApiDiscovery(ServiceDiscovery):
glance_api = GlanceApiComponent()
glance_api.host_id = get_host_id(client)
glance_api.listen_sockets = get_process_listen_sockets(client,
process.pid)
glance_api.process = collect_process(client, process)
glance_api.version = find_python_package_version(client, 'glance')
collect_component_configs(
@ -665,8 +788,9 @@ class GlanceRegistryDiscovery(ServiceDiscovery):
glance_registry = GlanceRegistryComponent()
glance_registry.host_id = get_host_id(client)
glance_registry.listen_sockets = get_process_listen_sockets(
client, process.pid)
glance_registry.process = collect_process(client, process)
glance_registry.version = find_python_package_version(client, 'glance')
collect_component_configs(
@ -690,8 +814,9 @@ class CinderApiDiscovery(ServiceDiscovery):
cinder_api = CinderApiComponent()
cinder_api.host_id = get_host_id(client)
cinder_api.listen_sockets = get_process_listen_sockets(client,
process.pid)
cinder_api.process = collect_process(client, process)
cinder_api.version = find_python_package_version(client, 'cinder')
collect_component_configs(
@ -724,8 +849,9 @@ class CinderVolumeDiscovery(ServiceDiscovery):
cinder_volume = CinderVolumeComponent()
cinder_volume.host_id = get_host_id(client)
cinder_volume.listen_sockets = get_process_listen_sockets(client,
process.pid)
cinder_volume.process = collect_process(client, process)
cinder_volume.version = find_python_package_version(client, 'cinder')
collect_component_configs(
@ -758,8 +884,9 @@ class CinderSchedulerDiscovery(ServiceDiscovery):
cinder_scheduler = CinderSchedulerComponent()
cinder_scheduler.host_id = get_host_id(client)
cinder_scheduler.listen_sockets = get_process_listen_sockets(
client, process.pid)
cinder_scheduler.process = collect_process(client, process)
cinder_scheduler.version = find_python_package_version(client,
'cinder')
@ -786,7 +913,8 @@ class MysqlDiscovery(ServiceDiscovery):
mysql = MysqlComponent()
mysql.host_id = get_host_id(client)
mysql.listen_sockets = get_process_listen_sockets(client, process.pid)
mysql.process = collect_process(client, process)
version_result = client.run(['mysqld', '--version'])
m = mysqld_version_re.match(version_result.output)
@ -826,8 +954,9 @@ class RabbitmqDiscovery(ServiceDiscovery):
rabbitmq = RabbitMqComponent()
rabbitmq.host_id = get_host_id(client)
rabbitmq.listen_sockets = get_process_listen_sockets(client,
process.pid)
rabbitmq.process = collect_process(client, process)
rabbitmq.version = 'unknown'
env_file = '/etc/rabbitmq/rabbitmq-env.conf'
@ -866,8 +995,9 @@ class SwiftProxyServerDiscovery(ServiceDiscovery):
swift_proxy_server = SwiftProxyServerComponent()
swift_proxy_server.host_id = get_host_id(client)
swift_proxy_server.listen_sockets = get_process_listen_sockets(
client, process.pid)
swift_proxy_server.process = collect_process(client, process)
swift_proxy_server.version = find_python_package_version(client,
'swift')
@ -892,8 +1022,9 @@ class SwiftContainerServerDiscovery(ServiceDiscovery):
swift_container_server = SwiftContainerServerComponent()
swift_container_server.host_id = get_host_id(client)
swift_container_server.listen_sockets = get_process_listen_sockets(
client, process.pid)
swift_container_server.process = collect_process(client, process)
swift_container_server.version = find_python_package_version(client,
'swift')
@ -918,8 +1049,9 @@ class SwiftAccountServerDiscovery(ServiceDiscovery):
swift_account_server = SwiftAccountServerComponent()
swift_account_server.host_id = get_host_id(client)
swift_account_server.listen_sockets = get_process_listen_sockets(
client, process.pid)
swift_account_server.process = collect_process(client, process)
swift_account_server.version = find_python_package_version(client,
'swift')
@ -944,8 +1076,9 @@ class SwiftObjectServerDiscovery(ServiceDiscovery):
swift_object_server = SwiftObjectServerComponent()
swift_object_server.host_id = get_host_id(client)
swift_object_server.listen_sockets = get_process_listen_sockets(
client, process.pid)
swift_object_server.process = collect_process(client, process)
swift_object_server.version = find_python_package_version(client,
'swift')
@ -970,8 +1103,9 @@ class NeutronServerDiscovery(ServiceDiscovery):
neutron_server = NeutronServerComponent()
neutron_server.host_id = get_host_id(client)
neutron_server.listen_sockets = get_process_listen_sockets(client,
process.pid)
neutron_server.process = collect_process(client, process)
neutron_server.version = find_python_package_version(client, 'neutron')
collect_component_configs(
@ -995,8 +1129,9 @@ class NeutronDhcpAgentDiscovery(ServiceDiscovery):
neutron_dhcp_agent = NeutronDhcpAgentComponent()
neutron_dhcp_agent.host_id = get_host_id(client)
neutron_dhcp_agent.listen_sockets = get_process_listen_sockets(
client, process.pid)
neutron_dhcp_agent.process = collect_process(client, process)
neutron_dhcp_agent.version = find_python_package_version(client,
'neutron')
@ -1021,8 +1156,9 @@ class NeutronL3AgentDiscovery(ServiceDiscovery):
neutron_l3_agent = NeutronL3AgentComponent()
neutron_l3_agent.host_id = get_host_id(client)
neutron_l3_agent.listen_sockets = get_process_listen_sockets(
client, process.pid)
neutron_l3_agent.process = collect_process(client, process)
neutron_l3_agent.version = find_python_package_version(client,
'neutron')
@ -1047,8 +1183,9 @@ class NeutronMetadataAgentDiscovery(ServiceDiscovery):
neutron_metadata_agent = NeutronMetadataAgentComponent()
neutron_metadata_agent.host_id = get_host_id(client)
neutron_metadata_agent.listen_sockets = get_process_listen_sockets(
client, process.pid)
neutron_metadata_agent.process = collect_process(client, process)
neutron_metadata_agent.version = find_python_package_version(client,
'neutron')
@ -1073,8 +1210,9 @@ class NeutronOpenvswitchAgentDiscovery(ServiceDiscovery):
neutron_openvswitch_agent = NeutronOpenvswitchAgentComponent()
neutron_openvswitch_agent.host_id = get_host_id(client)
neutron_openvswitch_agent.listen_sockets = get_process_listen_sockets(
client, process.pid)
neutron_openvswitch_agent.process = collect_process(client, process)
neutron_openvswitch_agent.version = find_python_package_version(
client, 'neutron')
@ -1164,14 +1302,25 @@ class OpenstackDiscovery(object):
# Rebuild model tree
openstack = Openstack()
for host in filter(lambda i: isinstance(i, Host), items):
for host in filter(lambda i: isinstance(i, HostResource), items):
openstack.add_host(host)
for service in filter(lambda i: isinstance(i, Service), items):
host = find(openstack.hosts, lambda h: h.id == service.host_id)
if not host:
logger.error('Got resource "%s" '
'that belong to non-existing host' % service)
continue
host.add_component(service)
for fs_resource in filter(lambda f: isinstance(f, FileSystemResource), items):
host = find(openstack.hosts, lambda h: h.id == fs_resource.host_id)
if not host:
logger.error('Got resource "%s" '
'that belong to non-existing host' % fs_resource)
continue
host.add_fs_resource(fs_resource)
return openstack

View File

@ -23,7 +23,11 @@ class IssueReporter(object):
return list(self.issues)
class Openstack(IssueReporter):
class Resource(IssueReporter):
pass
class Openstack(Resource):
def __init__(self):
super(Openstack, self).__init__()
@ -54,12 +58,13 @@ class Openstack(IssueReporter):
return components
class Host(IssueReporter):
class HostResource(Resource):
def __init__(self, name):
super(Host, self).__init__()
super(HostResource, self).__init__()
self.name = name
self.components = []
self.filesystem = {}
def __str__(self):
return 'Host "%s"' % self.name
@ -71,13 +76,20 @@ class Host(IssueReporter):
self.components.append(component)
component.parent = self
def add_fs_resource(self, resource):
if not resource:
return
self.filesystem[resource.path] = resource
resource.parent = self
@property
def openstack(self):
return self.parent
@property
def all_issues(self):
result = super(Host, self).all_issues
result = super(HostResource, self).all_issues
for component in self.components:
result.extend(component.all_issues)
@ -85,7 +97,16 @@ class Host(IssueReporter):
return result
class Service(IssueReporter):
class ProcessResource(Resource):
def __init__(self, pid, cmdline, cwd):
super(ProcessResource, self).__init__()
self.pid = pid
self.cmdline = cmdline
self.cwd = cwd
class Service(Resource):
def __init__(self):
super(Service, self).__init__()
@ -383,15 +404,34 @@ class SwiftObjectServerComponent(OpenstackComponent):
name = 'swift-object-server'
class FileResource(IssueReporter):
def __init__(self, path, contents, owner, group, permissions):
super(FileResource, self).__init__()
class FileSystemResource(Resource):
def __init__(self, path, owner, group, permissions):
super(FileSystemResource, self).__init__()
self.path = path
self.contents = contents
self.owner = owner
self.group = group
self.permissions = permissions
def __str__(self):
return 'File "%s"' % self.path
return '%s "%s"' % (
self.__class__.__name__.split('.')[-1].replace('Resource', ''),
self.path)
def __repr__(self):
return (
'%s(path=%s, owner=%s, group=%s, permissions=%s)' %
(self.__class__.__name__.split('.')[-1], repr(self.path),
repr(self.owner), repr(self.group), repr(self.permissions))
)
class FileResource(FileSystemResource):
def __init__(self, path, contents, owner, group, permissions):
super(FileResource, self).__init__(
path, owner, group, permissions)
self.contents = contents
class DirectoryResource(FileSystemResource):
pass

View File

@ -65,6 +65,7 @@ class ConfigSchemaRegistry:
param = ConfigParameterSchema(
name, param_data['type'], section=section,
type_args=param_data.get('type_args', {}),
default=param_data.get('default', None),
description=param_data.get('help', None),
required=param_data.get('required', False),
@ -120,6 +121,19 @@ class ConfigSchema:
return self._parameterByName.get(fullname, None)
def __len__(self):
return len(self.parameters)
def __iter__(self):
for param in self.parameters:
yield param
def __getitem__(self, key):
return self.get_parameter(key)
def __contains__(self, item):
return item in self._parameterByName
def __repr__(self):
return ('<ConfigSchema name=%s version=%s format=%s parameters=%s>' %
(self.name, self.version, self.format, self.parameters))
@ -127,11 +141,12 @@ class ConfigSchema:
class ConfigParameterSchema:
def __init__(self, name, type, section=None, description=None,
def __init__(self, name, type, type_args={}, section=None, description=None,
default=None, required=False, deprecation_message=None):
self.section = section
self.section = section or 'DEFAULT'
self.name = name
self.type = type
self.type_args = type_args
self.fullname = param_fullname(name, section)
self.description = description
self.default = default
@ -183,10 +198,10 @@ class TypeValidator(object):
self.base_type = base_type
self.f = f
def validate(self, value):
def validate(self, value, **kwargs):
if value is None:
return value
return getattr(self, 'f')(value)
return getattr(self, 'f')(value, **kwargs)
def type_validator(name, base_type=None, default=False, **kwargs):
@ -194,8 +209,8 @@ def type_validator(name, base_type=None, default=False, **kwargs):
base_type = name
def wrap(fn):
def wrapped(s):
return fn(s, **kwargs)
def wrapped(s, **immediate_kwargs):
return fn(s, **dict(kwargs, **immediate_kwargs))
o = TypeValidator(base_type, wrapped)
TypeValidatorRegistry.register_validator(name, o, default=default)
return fn
@ -221,16 +236,19 @@ def validate_boolean(s):
return InvalidValueError('Value should be "true" or "false"')
@type_validator('enum')
def validate_enum(s, values=[]):
if s in values:
return None
if len(values) == 0:
message = 'There should be no value'
message = 'There should be no value, but found %s' % repr(s)
elif len(values) == 1:
message = 'The only valid value is %s' % values[0]
message = 'The only valid value is "%s", but found "%s"' % (
repr(values[0]), repr(s))
else:
message = 'Valid values are %s and %s' % (
', '.join(values[:-1]), values[-1])
message = 'Valid values are %s and %s, but found %s' % (
', '.join([repr(v) for v in values[:-1]]),
repr(values[-1]), repr(s))
return InvalidValueError('%s' % message)
@ -390,12 +408,22 @@ def validate_host_and_port(s, default_port=None):
@type_validator('multi', base_type='multi')
@type_validator('file', base_type='string')
@type_validator('directory', base_type='string')
@type_validator('regex', base_type='string')
@type_validator('host_v6', base_type='string')
def validate_string(s):
return s
@type_validator('regex', base_type='string')
@type_validator('regexp', base_type='string')
def validate_regex(s):
try:
re.compile(s)
except re.error as e:
return InvalidValueError(str(e))
return s
@type_validator('integer')
def validate_integer(s, min=None, max=None):
if isinstance(s, int):

View File

@ -118,7 +118,8 @@ def generate_project_schema(project):
param['type'] = prev_param['type']
if param.get('default', None) is not None:
value = validator.validate(param['default'])
type_args = param.get('type_args', {})
value = validator.validate(param['default'], **type_args)
if not isinstance(value, Issue):
param['default'] = value
else:
@ -217,7 +218,8 @@ def generate_project_schema(project):
param['default'] = old_param['default']
if param.get('default', None) is not None:
value = validator.validate(old_param['default'])
type_args = old_param.get('type_args', {})
value = validator.validate(old_param['default'], **type_args)
if not isinstance(value, Issue):
param['default'] = value
else:

View File

@ -9,12 +9,13 @@ class TypeValidatorTestHelper(object):
super(TypeValidatorTestHelper, self).setUp()
self.validator = TypeValidatorRegistry.get_validator(self.type_name)
def assertValid(self, value):
self.assertNotIsInstance(self.validator.validate(value), Issue)
def assertValid(self, value, type_args={}):
self.assertNotIsInstance(
self.validator.validate(value, **type_args), Issue)
def assertInvalid(self, value):
def assertInvalid(self, value, type_args={}):
self.assertIsInstance(
self.validator.validate(value), Issue)
self.validator.validate(value, **type_args), Issue)
class StringTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
@ -31,6 +32,19 @@ class StringTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
self.assertEqual(s, self.validator.validate(s))
class EnumTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
type_name = 'enum'
def test_listed_value(self):
self.assertValid('foo', type_args={'values': ['foo', 'bar']})
def test_unlisted_value(self):
self.assertInvalid('baz', type_args={'values': ['foo', 'bar']})
def test_with_no_values_returns_error(self):
self.assertInvalid('foo')
class BooleanTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
type_name = 'boolean'
@ -250,6 +264,16 @@ class HostAndPortTypeValidatorTests(TypeValidatorTestHelper,
self.assertInvalid('10.0.0.1:65536')
class RegexTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
type_name = 'regex'
def test_valid_regex(self):
self.assertValid('\d+\.\d+\.\d+\.\d+')
def test_invalid_regex(self):
self.assertInvalid('(\d+')
class StringListTypeValidatorTests(TypeValidatorTestHelper, unittest.TestCase):
type_name = 'string_list'