# # 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 from toscaparser.utils.gettextutils import _ from translator.common import flavors as nova_flavors from translator.common import images as glance_images import translator.common.utils from translator.hot.syntax.hot_resource import HotResource log = logging.getLogger('heat-translator') # Name used to dynamically load appropriate map class. TARGET_CLASS_NAME = 'ToscaCompute' class ToscaCompute(HotResource): '''Translate TOSCA node type tosca.nodes.Compute.''' COMPUTE_HOST_PROP = (DISK_SIZE, MEM_SIZE, NUM_CPUS) = \ ('disk_size', 'mem_size', 'num_cpus') COMPUTE_OS_PROP = (ARCHITECTURE, DISTRIBUTION, TYPE, VERSION) = \ ('architecture', 'distribution', 'type', 'version') toscatype = 'tosca.nodes.Compute' ALLOWED_NOVA_SERVER_PROPS = \ ('admin_pass', 'availability_zone', 'block_device_mapping', 'block_device_mapping_v2', 'config_drive', 'diskConfig', 'flavor', 'flavor_update_policy', 'image', 'image_update_policy', 'key_name', 'metadata', 'name', 'networks', 'personality', 'reservation_id', 'scheduler_hints', 'security_groups', 'software_config_transport', 'user_data', 'user_data_format', 'user_data_update_policy') def __init__(self, nodetemplate, csar_dir=None): super(ToscaCompute, self).__init__(nodetemplate, type='OS::Nova::Server', csar_dir=csar_dir) # List with associated hot port resources with this server self.assoc_port_resources = [] pass def handle_properties(self): self.properties = self.translate_compute_flavor_and_image( self.nodetemplate.get_capability('host'), self.nodetemplate.get_capability('os')) self.properties['user_data_format'] = 'SOFTWARE_CONFIG' self.properties['software_config_transport'] = 'POLL_SERVER_HEAT' tosca_props = self.get_tosca_props() for key, value in tosca_props.items(): if key in self.ALLOWED_NOVA_SERVER_PROPS: self.properties[key] = value # To be reorganized later based on new development in Glance and Graffiti def translate_compute_flavor_and_image(self, host_capability, os_capability): hot_properties = {} host_cap_props = {} os_cap_props = {} image = None flavor = None if host_capability: for prop in host_capability.get_properties_objects(): host_cap_props[prop.name] = prop.value # if HOST properties are not specified, we should not attempt to # find best match of flavor if host_cap_props: flavor = self._best_flavor(host_cap_props) if os_capability: for prop in os_capability.get_properties_objects(): os_cap_props[prop.name] = prop.value # if OS properties are not specified, we should not attempt to # find best match of image if os_cap_props: image = self._best_image(os_cap_props) hot_properties['flavor'] = flavor if image: hot_properties['image'] = image else: hot_properties.pop('image', None) return hot_properties def _best_flavor(self, properties): log.info(_('Choosing the best flavor for given attributes.')) # Check whether user exported all required environment variables. flavors = nova_flavors.get_flavors() # start with all flavors match_all = flavors.keys() # TODO(anyone): Handle the case where the value contains something like # get_input instead of a value. # flavors that fit the CPU count cpu = properties.get(self.NUM_CPUS) if cpu is None: self._log_compute_msg(self.NUM_CPUS, 'flavor') match_cpu = self._match_flavors(match_all, flavors, self.NUM_CPUS, cpu) # flavors that fit the mem size mem = properties.get(self.MEM_SIZE) if mem: mem = translator.common.utils.MemoryUnit.convert_unit_size_to_num( mem, 'MB') else: self._log_compute_msg(self.MEM_SIZE, 'flavor') match_cpu_mem = self._match_flavors(match_cpu, flavors, self.MEM_SIZE, mem) # flavors that fit the disk size disk = properties.get(self.DISK_SIZE) if disk: disk = translator.common.utils.MemoryUnit.\ convert_unit_size_to_num(disk, 'GB') else: self._log_compute_msg(self.DISK_SIZE, 'flavor') match_cpu_mem_disk = self._match_flavors(match_cpu_mem, flavors, self.DISK_SIZE, disk) # if multiple match, pick the flavor with the least memory # the selection can be based on other heuristic, e.g. pick one with the # least total resource if len(match_cpu_mem_disk) > 1: return self._least_flavor(match_cpu_mem_disk, flavors, 'mem_size') elif len(match_cpu_mem_disk) == 1: return match_cpu_mem_disk[0] else: return None def _best_image(self, properties): # Check whether user exported all required environment variables. images = glance_images.get_images() match_all = images.keys() architecture = properties.get(self.ARCHITECTURE) if architecture is None: self._log_compute_msg(self.ARCHITECTURE, 'image') match_arch = self._match_images(match_all, images, self.ARCHITECTURE, architecture) type = properties.get(self.TYPE) if type is None: self._log_compute_msg(self.TYPE, 'image') match_type = self._match_images(match_arch, images, self.TYPE, type) distribution = properties.get(self.DISTRIBUTION) if distribution is None: self._log_compute_msg(self.DISTRIBUTION, 'image') match_distribution = self._match_images(match_type, images, self.DISTRIBUTION, distribution) version = properties.get(self.VERSION) if version is None: self._log_compute_msg(self.VERSION, 'image') match_version = self._match_images(match_distribution, images, self.VERSION, version) if len(match_version): return list(match_version)[0] def _match_flavors(self, this_list, this_dict, attr, size): '''Return from this list all flavors matching the attribute size.''' if not size: return list(this_list) matching_flavors = [] for flavor in this_list: if isinstance(size, int): if this_dict[flavor][attr] >= size: matching_flavors.append(flavor) log.debug(_('Returning list of flavors matching the attribute size.')) return matching_flavors def _least_flavor(self, this_list, this_dict, attr): '''Return from this list the flavor with the smallest attr.''' least_flavor = this_list[0] for flavor in this_list: if this_dict[flavor][attr] < this_dict[least_flavor][attr]: least_flavor = flavor return least_flavor def _match_images(self, this_list, this_dict, attr, prop): if not prop: return this_list matching_images = [] for image in this_list: if attr in this_dict[image]: if this_dict[image][attr].lower() == str(prop).lower(): matching_images.insert(0, image) else: matching_images.append(image) return matching_images def get_hot_attribute(self, attribute, args): attr = {} # Convert from a TOSCA attribute for a nodetemplate to a HOT # attribute for the matching resource. Unless there is additional # runtime support, this should be a one to one mapping. # Note: We treat private and public IP addresses equally, but # this will change in the future when TOSCA starts to support # multiple private/public IP addresses. log.debug(_('Converting TOSCA attribute for a nodetemplate to a HOT \ attriute.')) if attribute == 'private_address' or \ attribute == 'public_address': attr['get_attr'] = [self.name, 'networks', 'private', 0] return attr def _log_compute_msg(self, prop, what): msg = _('No value is provided for Compute capability ' 'property "%(prop)s". This may set an undesired "%(what)s" ' 'in the template.') % {'prop': prop, 'what': what} log.warn(msg)