2 # Licensed under the Apache License, Version 2.0 (the "License"); you may
3 # not use this file except in compliance with the License. You may obtain
4 # a copy of the License at
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11 # License for the specific language governing permissions and limitations
18 from toscaparser.utils.gettextutils import _
19 import translator.common.utils
20 from translator.hot.syntax.hot_resource import HotResource
22 log = logging.getLogger('heat-translator')
25 # Name used to dynamically load appropriate map class.
26 TARGET_CLASS_NAME = 'ToscaCompute'
28 # A design issue to be resolved is how to translate the generic TOSCA server
29 # properties to OpenStack flavors and images. At the Atlanta design summit,
30 # there was discussion on using Glance to store metadata and Graffiti to
31 # describe artifacts. We will follow these projects to see if they can be
32 # leveraged for this TOSCA translation.
33 # For development purpose at this time, we temporarily hardcode a list of
34 # flavors and images here
35 FLAVORS = {'m1.xlarge': {'mem_size': 16384, 'disk_size': 160, 'num_cpus': 8},
36 'm1.large': {'mem_size': 8192, 'disk_size': 80, 'num_cpus': 4},
37 'm1.medium': {'mem_size': 4096, 'disk_size': 40, 'num_cpus': 2},
38 'm1.small': {'mem_size': 2048, 'disk_size': 20, 'num_cpus': 1},
39 'm1.tiny': {'mem_size': 512, 'disk_size': 1, 'num_cpus': 1},
40 'm1.micro': {'mem_size': 128, 'disk_size': 0, 'num_cpus': 1},
41 'm1.nano': {'mem_size': 64, 'disk_size': 0, 'num_cpus': 1}}
43 IMAGES = {'ubuntu-software-config-os-init': {'architecture': 'x86_64',
45 'distribution': 'Ubuntu',
47 'ubuntu-12.04-software-config-os-init': {'architecture': 'x86_64',
49 'distribution': 'Ubuntu',
51 'fedora-amd64-heat-config': {'architecture': 'x86_64',
53 'distribution': 'Fedora',
55 'F18-x86_64-cfntools': {'architecture': 'x86_64',
57 'distribution': 'Fedora',
59 'Fedora-x86_64-20-20131211.1-sda': {'architecture': 'x86_64',
61 'distribution': 'Fedora',
63 'cirros-0.3.1-x86_64-uec': {'architecture': 'x86_64',
65 'distribution': 'CirrOS',
67 'cirros-0.3.2-x86_64-uec': {'architecture': 'x86_64',
69 'distribution': 'CirrOS',
71 'rhel-6.5-test-image': {'architecture': 'x86_64',
73 'distribution': 'RHEL',
77 class ToscaCompute(HotResource):
78 '''Translate TOSCA node type tosca.nodes.Compute.'''
80 COMPUTE_HOST_PROP = (DISK_SIZE, MEM_SIZE, NUM_CPUS) = \
81 ('disk_size', 'mem_size', 'num_cpus')
83 COMPUTE_OS_PROP = (ARCHITECTURE, DISTRIBUTION, TYPE, VERSION) = \
84 ('architecture', 'distribution', 'type', 'version')
85 toscatype = 'tosca.nodes.Compute'
87 ALLOWED_NOVA_SERVER_PROPS = \
88 ('admin_pass', 'availability_zone', 'block_device_mapping',
89 'block_device_mapping_v2', 'config_drive', 'diskConfig', 'flavor',
90 'flavor_update_policy', 'image', 'image_update_policy', 'key_name',
91 'metadata', 'name', 'networks', 'personality', 'reservation_id',
92 'scheduler_hints', 'security_groups', 'software_config_transport',
93 'user_data', 'user_data_format', 'user_data_update_policy')
95 def __init__(self, nodetemplate):
96 super(ToscaCompute, self).__init__(nodetemplate,
97 type='OS::Nova::Server')
98 # List with associated hot port resources with this server
99 self.assoc_port_resources = []
102 def handle_properties(self):
103 self.properties = self.translate_compute_flavor_and_image(
104 self.nodetemplate.get_capability('host'),
105 self.nodetemplate.get_capability('os'))
106 self.properties['user_data_format'] = 'SOFTWARE_CONFIG'
107 self.properties['software_config_transport'] = 'POLL_SERVER_HEAT'
108 tosca_props = self.get_tosca_props()
109 for key, value in tosca_props.items():
110 if key in self.ALLOWED_NOVA_SERVER_PROPS:
111 self.properties[key] = value
113 # To be reorganized later based on new development in Glance and Graffiti
114 def translate_compute_flavor_and_image(self,
123 for prop in host_capability.get_properties_objects():
124 host_cap_props[prop.name] = prop.value
125 # if HOST properties are not specified, we should not attempt to
126 # find best match of flavor
128 flavor = self._best_flavor(host_cap_props)
130 for prop in os_capability.get_properties_objects():
131 os_cap_props[prop.name] = prop.value
132 # if OS properties are not specified, we should not attempt to
133 # find best match of image
135 image = self._best_image(os_cap_props)
136 hot_properties['flavor'] = flavor
137 hot_properties['image'] = image
138 return hot_properties
140 def _create_nova_flavor_dict(self):
141 '''Populates and returns the flavors dict using Nova ReST API'''
143 access_dict = translator.common.utils.get_ks_access_dict()
144 access_token = translator.common.utils.get_token_id(access_dict)
145 if access_token is None:
147 nova_url = translator.common.utils.get_url_for(access_dict,
151 nova_response = requests.get(nova_url + '/flavors/detail',
152 headers={'X-Auth-Token':
154 if nova_response.status_code != 200:
156 flavors = json.loads(nova_response.content)['flavors']
158 for flavor in flavors:
159 flavor_name = str(flavor['name'])
160 flavor_dict[flavor_name] = {
161 'mem_size': flavor['ram'],
162 'disk_size': flavor['disk'],
163 'num_cpus': flavor['vcpus'],
165 except Exception as e:
166 # Handles any exception coming from openstack
167 log.warn(_('Choosing predefined flavors since received '
168 'Openstack Exception: %s') % str(e))
172 def _populate_image_dict(self):
173 '''Populates and returns the images dict using Glance ReST API'''
176 access_dict = translator.common.utils.get_ks_access_dict()
177 access_token = translator.common.utils.get_token_id(access_dict)
178 if access_token is None:
180 glance_url = translator.common.utils.get_url_for(access_dict,
184 glance_response = requests.get(glance_url + '/v2/images',
185 headers={'X-Auth-Token':
187 if glance_response.status_code != 200:
189 images = json.loads(glance_response.content)["images"]
191 image_resp = requests.get(glance_url + '/v2/images/' +
193 headers={'X-Auth-Token':
195 if image_resp.status_code != 200:
197 metadata = ["architecture", "type", "distribution", "version"]
198 image_data = json.loads(image_resp.content)
199 if any(key in image_data.keys() for key in metadata):
200 images_dict[image_data["name"]] = dict()
202 if key in image_data.keys():
203 images_dict[image_data["name"]][key] = \
208 except Exception as e:
209 # Handles any exception coming from openstack
210 log.warn(_('Choosing predefined flavors since received '
211 'Openstack Exception: %s') % str(e))
214 def _best_flavor(self, properties):
215 log.info(_('Choosing the best flavor for given attributes.'))
216 # Check whether user exported all required environment variables.
218 if translator.common.utils.check_for_env_variables():
219 resp = self._create_nova_flavor_dict()
223 # start with all flavors
224 match_all = flavors.keys()
226 # TODO(anyone): Handle the case where the value contains something like
227 # get_input instead of a value.
228 # flavors that fit the CPU count
229 cpu = properties.get(self.NUM_CPUS)
231 self._log_compute_msg(self.NUM_CPUS, 'flavor')
232 match_cpu = self._match_flavors(match_all, flavors, self.NUM_CPUS, cpu)
234 # flavors that fit the mem size
235 mem = properties.get(self.MEM_SIZE)
237 mem = translator.common.utils.MemoryUnit.convert_unit_size_to_num(
240 self._log_compute_msg(self.MEM_SIZE, 'flavor')
241 match_cpu_mem = self._match_flavors(match_cpu, flavors,
243 # flavors that fit the disk size
244 disk = properties.get(self.DISK_SIZE)
246 disk = translator.common.utils.MemoryUnit.\
247 convert_unit_size_to_num(disk, 'GB')
249 self._log_compute_msg(self.DISK_SIZE, 'flavor')
250 match_cpu_mem_disk = self._match_flavors(match_cpu_mem, flavors,
251 self.DISK_SIZE, disk)
252 # if multiple match, pick the flavor with the least memory
253 # the selection can be based on other heuristic, e.g. pick one with the
254 # least total resource
255 if len(match_cpu_mem_disk) > 1:
256 return self._least_flavor(match_cpu_mem_disk, flavors, 'mem_size')
257 elif len(match_cpu_mem_disk) == 1:
258 return match_cpu_mem_disk[0]
262 def _best_image(self, properties):
263 # Check whether user exported all required environment variables.
265 if translator.common.utils.check_for_env_variables():
266 resp = self._populate_image_dict()
267 if resp and len(resp.keys()) > 0:
269 match_all = images.keys()
270 architecture = properties.get(self.ARCHITECTURE)
271 if architecture is None:
272 self._log_compute_msg(self.ARCHITECTURE, 'image')
273 match_arch = self._match_images(match_all, images,
274 self.ARCHITECTURE, architecture)
275 type = properties.get(self.TYPE)
277 self._log_compute_msg(self.TYPE, 'image')
278 match_type = self._match_images(match_arch, images, self.TYPE, type)
279 distribution = properties.get(self.DISTRIBUTION)
280 if distribution is None:
281 self._log_compute_msg(self.DISTRIBUTION, 'image')
282 match_distribution = self._match_images(match_type, images,
285 version = properties.get(self.VERSION)
287 self._log_compute_msg(self.VERSION, 'image')
288 match_version = self._match_images(match_distribution, images,
289 self.VERSION, version)
291 if len(match_version):
292 return list(match_version)[0]
294 def _match_flavors(self, this_list, this_dict, attr, size):
295 '''Return from this list all flavors matching the attribute size.'''
297 return list(this_list)
298 matching_flavors = []
299 for flavor in this_list:
300 if isinstance(size, int):
301 if this_dict[flavor][attr] >= size:
302 matching_flavors.append(flavor)
303 log.debug(_('Returning list of flavors matching the attribute size.'))
304 return matching_flavors
306 def _least_flavor(self, this_list, this_dict, attr):
307 '''Return from this list the flavor with the smallest attr.'''
308 least_flavor = this_list[0]
309 for flavor in this_list:
310 if this_dict[flavor][attr] < this_dict[least_flavor][attr]:
311 least_flavor = flavor
314 def _match_images(self, this_list, this_dict, attr, prop):
318 for image in this_list:
319 if this_dict[image][attr].lower() == str(prop).lower():
320 matching_images.append(image)
321 return matching_images
323 def get_hot_attribute(self, attribute, args):
325 # Convert from a TOSCA attribute for a nodetemplate to a HOT
326 # attribute for the matching resource. Unless there is additional
327 # runtime support, this should be a one to one mapping.
329 # Note: We treat private and public IP addresses equally, but
330 # this will change in the future when TOSCA starts to support
331 # multiple private/public IP addresses.
332 log.debug(_('Converting TOSCA attribute for a nodetemplate to a HOT \
334 if attribute == 'private_address' or \
335 attribute == 'public_address':
336 attr['get_attr'] = [self.name, 'networks']
340 def _log_compute_msg(self, prop, what):
341 msg = _('No value is provided for Compute capability '
342 'property "%(prop)s". This may set an undesired "%(what)s" '
343 'in the template.') % {'prop': prop, 'what': what}