# Copyright (c) 2013 Hortonworks, Inc. # # 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. from oslo_log import log as logging from oslo_serialization import jsonutils as json from sahara.i18n import _ from sahara.plugins import exceptions as ex from sahara.plugins.hdp.versions import versionhandlerfactory as vhf LOG = logging.getLogger(__name__) def validate_number_of_datanodes(cluster, scaled_groups, default_configs): dfs_replication = 0 for config in default_configs: if config.name == 'dfs.replication': dfs_replication = config.default_value conf = cluster.cluster_configs if 'HDFS' in conf and 'dfs.replication' in conf['HDFS']: dfs_replication = conf['HDFS']['dfs.replication'] if not scaled_groups: scaled_groups = {} dn_count = 0 for ng in cluster.node_groups: if 'DATANODE' in ng.node_processes: if ng.id in scaled_groups: dn_count += scaled_groups[ng.id] else: dn_count += ng.count if dn_count < int(dfs_replication): raise ex.InvalidComponentCountException( 'datanode', _('%s or more') % dfs_replication, dn_count, _('Number of %(dn)s instances should not be less ' 'than %(replication)s') % {'dn': 'DATANODE', 'replication': 'dfs.replication'}) class ClusterSpec(object): def __init__(self, config, version='2.0.6'): self._config_template = config self.services = [] self.configurations = {} self.node_groups = {} self.version = version self.user_input_handlers = {} cluster_template = json.loads(config) self._parse_services(cluster_template) self._parse_configurations(cluster_template) self._process_node_groups(template_json=cluster_template) def create_operational_config(self, cluster, user_inputs, scaled_groups=None): if scaled_groups is None: scaled_groups = {} self._determine_deployed_services(cluster) self._process_node_groups(cluster=cluster) for ng_id in scaled_groups: existing = next(group for group in self.node_groups.values() if group.id == ng_id) existing.count = scaled_groups[ng_id] self.validate_node_groups(cluster) self._finalize_ng_components() self._parse_configurations(json.loads(self._config_template)) self._process_user_inputs(user_inputs) self._replace_config_tokens() def scale(self, updated_groups): for ng_id in updated_groups: existing = next(group for group in self.node_groups.values() if group.id == ng_id) existing.count = updated_groups[ng_id] def validate_node_groups(self, cluster): for service in self.services: if service.deployed: service.validate(self, cluster) elif service.is_mandatory(): raise ex.RequiredServiceMissingException(service.name) def get_deployed_configurations(self): configs = set() for service in self.services: if service.deployed: configs |= service.configurations return configs def determine_component_hosts(self, component): hosts = set() for ng in self.node_groups.values(): if component in ng.components: hosts |= ng.instances return hosts def normalize(self): return NormalizedClusterConfig(self) def get_deployed_node_group_count(self, name): count = 0 for ng in self.get_node_groups_containing_component(name): count += ng.count return count def get_node_groups_containing_component(self, component): found_node_groups = [] for ng in self.node_groups.values(): if component in ng.components: found_node_groups.append(ng) return found_node_groups def get_components_for_type(self, type): components = set() for service in self.services: for component in service.components: if component.type == type: components.add(component.name) return components def is_hdfs_ha_enabled(self, cluster): if self.version == '2.0.6': if cluster.cluster_configs.get('HDFSHA', False): if cluster.cluster_configs.HDFSHA.get('hdfs.nnha', False) is True: return True return False def _parse_services(self, template_json): handler = (vhf.VersionHandlerFactory.get_instance(). get_version_handler(self.version)) sp = handler.get_services_processor() for s in template_json['services']: name = s['name'] service = sp.create_service(name) self.services.append(service) for c in s['components']: component = Component(c['name'], c['type'], c['cardinality']) service.add_component(component) if 'users' in s: for u in s['users']: user = User(u['name'], u['password'], u['groups']) service.add_user(user) configs = self._parse_configurations(s) for config in configs: service.add_configuration(config) def _parse_configurations(self, template_json): config_names = [] for config in template_json['configurations']: config_props = {} name = config['name'] config_names.append(name) if name in self.configurations: config_props = self.configurations[name] else: self.configurations[name] = config_props if 'properties' in config: for prop in config['properties']: config_props[prop['name']] = prop['value'] return config_names def _process_node_groups(self, template_json=None, cluster=None): # get node_groups from config if template_json and not cluster: for group in template_json['host_role_mappings']: node_group = NodeGroup(group['name']) for component in group['components']: node_group.add_component(component['name']) for host in group['hosts']: if 'predicate' in host: node_group.predicate = host['predicate'] if 'cardinality' in host: node_group.cardinality = host['cardinality'] if 'default_count' in host: node_group.count = host['default_count'] self.node_groups[node_group.name] = node_group if cluster: self.node_groups = {} node_groups = cluster.node_groups for ng in node_groups: node_group = NodeGroup(ng.name) node_group.count = ng.count node_group.id = ng.id node_group.components = ng.node_processes[:] for instance in ng.instances: node_group.instances.add(Instance(instance)) self.node_groups[node_group.name] = node_group def _determine_deployed_services(self, cluster): for ng in cluster.node_groups: for service in self.services: if service.deployed: continue for sc in service.components: if sc.name in ng.node_processes: service.deployed = True service.register_user_input_handlers( self.user_input_handlers) break def _process_user_inputs(self, user_inputs): for ui in user_inputs: # if it doesn't have a tag then it's not part of the # operational config that Ambari knows about if not hasattr(ui.config, 'tag'): continue user_input_handler = self.user_input_handlers.get( '{0}/{1}'.format(ui.config.tag, ui.config.name), self._default_user_input_handler) user_input_handler(ui, self.configurations) def _replace_config_tokens(self): for service in self.services: if service.deployed: service.finalize_configuration(self) def _finalize_ng_components(self): for service in self.services: if service.deployed: service.finalize_ng_components(self) def _default_user_input_handler(self, user_input, configurations): config_map = configurations[user_input.config.tag] config_map[user_input.config.name] = user_input.value class Component(object): def __init__(self, name, component_type, cardinality): self.name = name self.type = component_type self.cardinality = cardinality class NodeGroup(object): def __init__(self, name): self.id = None self.name = name self.components = [] self.predicate = None self.cardinality = None self.count = None self.instances = set() def add_component(self, component): self.components.append(component) class User(object): def __init__(self, name, password, groups): self.name = name self.password = password self.groups = groups class Instance(object): def __init__(self, sahara_instance): self.inst_fqdn = sahara_instance.fqdn() self.management_ip = sahara_instance.management_ip self.internal_ip = sahara_instance.internal_ip self.sahara_instance = sahara_instance def fqdn(self): return self.inst_fqdn def remote(self): return self.sahara_instance.remote() def __hash__(self): return hash(self.fqdn()) def __eq__(self, other): return self.fqdn() == other.fqdn() class NormalizedClusterConfig(object): def __init__(self, cluster_spec): self.hadoop_version = cluster_spec.version self.cluster_configs = [] self.node_groups = [] self.handler = (vhf.VersionHandlerFactory.get_instance(). get_version_handler(self.hadoop_version)) self._parse_configurations(cluster_spec.configurations) self._parse_node_groups(cluster_spec.node_groups) def _parse_configurations(self, configurations): for config_name, properties in configurations.items(): for prop, value in properties.items(): target = self._get_property_target(prop) if target: prop_type = self._get_property_type(prop, value) # TODO(sdpeidel): should we supply a scope? self.cluster_configs.append( NormalizedConfigEntry(NormalizedConfig( prop, prop_type, value, target, 'cluster'), value)) def _parse_node_groups(self, node_groups): for node_group in node_groups.values(): self.node_groups.append(NormalizedNodeGroup(node_group)) def _get_property_target(self, prop): return self.handler.get_applicable_target(prop) def _get_property_type(self, prop, value): # TODO(jspeidel): seems that all numeric prop values in default config # are encoded as strings. This may be incorrect. # TODO(jspeidel): should probably analyze string value to determine if # it is numeric # TODO(jspeidel): would then need to know whether Ambari expects a # string or a numeric value prop_type = type(value).__name__ # print 'Type: {0}'.format(prop_type) if prop_type == 'str' or prop_type == 'unicode' or value == '': return 'string' elif prop_type == 'int': return 'integer' elif prop_type == 'bool': return 'boolean' else: raise ValueError( _("Could not determine property type for property " "'%(property)s' with value: %(value)s") % {"property": prop, "value": value}) class NormalizedConfig(object): def __init__(self, name, config_type, default_value, target, scope): self.name = name self.description = None self.type = config_type self.default_value = default_value self.is_optional = False self.applicable_target = target self.scope = scope class NormalizedConfigEntry(object): def __init__(self, config, value): self.config = config self.value = value class NormalizedNodeGroup(object): def __init__(self, node_group): self.name = node_group.name self.node_processes = node_group.components self.node_configs = None # TODO(jpseidel): should not have to specify img/flavor self.img = None # TODO(jmaron) the flavor will be set via an ambari blueprint setting, # but that setting doesn't exist yet. It will be addressed by a bug # fix shortly self.flavor = 3 self.count = node_group.count self.id = node_group.id