Fix parser test bug with functest in real env.
[parser.git] / tosca2heat / heat-translator / translator / hot / tosca / tosca_compute.py
1 #
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
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
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
12 # under the License.
13
14 import json
15 import logging
16 import requests
17
18 from toscaparser.utils.gettextutils import _
19 import translator.common.utils
20 from translator.hot.syntax.hot_resource import HotResource
21
22 log = logging.getLogger('heat-translator')
23
24
25 # Name used to dynamically load appropriate map class.
26 TARGET_CLASS_NAME = 'ToscaCompute'
27
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}}
42
43 IMAGES = {'ubuntu-software-config-os-init': {'architecture': 'x86_64',
44                                              'type': 'Linux',
45                                              'distribution': 'Ubuntu',
46                                              'version': '14.04'},
47           'ubuntu-12.04-software-config-os-init': {'architecture': 'x86_64',
48                                                    'type': 'Linux',
49                                                    'distribution': 'Ubuntu',
50                                                    'version': '12.04'},
51           'fedora-amd64-heat-config': {'architecture': 'x86_64',
52                                        'type': 'Linux',
53                                        'distribution': 'Fedora',
54                                        'version': '18.0'},
55           'F18-x86_64-cfntools': {'architecture': 'x86_64',
56                                   'type': 'Linux',
57                                   'distribution': 'Fedora',
58                                   'version': '19'},
59           'Fedora-x86_64-20-20131211.1-sda': {'architecture': 'x86_64',
60                                               'type': 'Linux',
61                                               'distribution': 'Fedora',
62                                               'version': '20'},
63           'cirros-0.3.1-x86_64-uec': {'architecture': 'x86_64',
64                                       'type': 'Linux',
65                                       'distribution': 'CirrOS',
66                                       'version': '0.3.1'},
67           'cirros-0.3.2-x86_64-uec': {'architecture': 'x86_64',
68                                       'type': 'Linux',
69                                       'distribution': 'CirrOS',
70                                       'version': '0.3.2'},
71           'rhel-6.5-test-image': {'architecture': 'x86_64',
72                                   'type': 'Linux',
73                                   'distribution': 'RHEL',
74                                   'version': '6.5'}}
75
76
77 class ToscaCompute(HotResource):
78     '''Translate TOSCA node type tosca.nodes.Compute.'''
79
80     COMPUTE_HOST_PROP = (DISK_SIZE, MEM_SIZE, NUM_CPUS) = \
81                         ('disk_size', 'mem_size', 'num_cpus')
82
83     COMPUTE_OS_PROP = (ARCHITECTURE, DISTRIBUTION, TYPE, VERSION) = \
84                       ('architecture', 'distribution', 'type', 'version')
85     toscatype = 'tosca.nodes.Compute'
86
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')
94
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 = []
100         pass
101
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         tosca_props = self.get_tosca_props()
108         for key, value in tosca_props.items():
109             if key in self.ALLOWED_NOVA_SERVER_PROPS:
110                 self.properties[key] = value
111
112     # To be reorganized later based on new development in Glance and Graffiti
113     def translate_compute_flavor_and_image(self,
114                                            host_capability,
115                                            os_capability):
116         hot_properties = {}
117         host_cap_props = {}
118         os_cap_props = {}
119         image = None
120         flavor = None
121         if host_capability:
122             for prop in host_capability.get_properties_objects():
123                 host_cap_props[prop.name] = prop.value
124             # if HOST properties are not specified, we should not attempt to
125             # find best match of flavor
126             if host_cap_props:
127                 flavor = self._best_flavor(host_cap_props)
128         if os_capability:
129             for prop in os_capability.get_properties_objects():
130                 os_cap_props[prop.name] = prop.value
131             # if OS properties are not specified, we should not attempt to
132             # find best match of image
133             if os_cap_props:
134                 image = self._best_image(os_cap_props)
135         hot_properties['flavor'] = flavor
136         hot_properties['image'] = image
137         return hot_properties
138
139     def _create_nova_flavor_dict(self):
140         '''Populates and returns the flavors dict using Nova ReST API'''
141         try:
142             access_dict = translator.common.utils.get_ks_access_dict()
143             access_token = translator.common.utils.get_token_id(access_dict)
144             if access_token is None:
145                 return None
146             nova_url = translator.common.utils.get_url_for(access_dict,
147                                                            'compute')
148             if not nova_url:
149                 return None
150             nova_response = requests.get(nova_url + '/flavors/detail',
151                                          headers={'X-Auth-Token':
152                                                   access_token})
153             if nova_response.status_code != 200:
154                 return None
155             flavors = json.loads(nova_response.content)['flavors']
156             flavor_dict = dict()
157             for flavor in flavors:
158                 flavor_name = str(flavor['name'])
159                 flavor_dict[flavor_name] = {
160                     'mem_size': flavor['ram'],
161                     'disk_size': flavor['disk'],
162                     'num_cpus': flavor['vcpus'],
163                 }
164         except Exception as e:
165             # Handles any exception coming from openstack
166             log.warn(_('Choosing predefined flavors since received '
167                        'Openstack Exception: %s') % str(e))
168             return None
169         return flavor_dict
170
171     def _populate_image_dict(self):
172         '''Populates and returns the images dict using Glance ReST API'''
173         images_dict = {}
174         try:
175             access_dict = translator.common.utils.get_ks_access_dict()
176             access_token = translator.common.utils.get_token_id(access_dict)
177             if access_token is None:
178                 return None
179             glance_url = translator.common.utils.get_url_for(access_dict,
180                                                              'image')
181             if not glance_url:
182                 return None
183             glance_response = requests.get(glance_url + '/v2/images',
184                                            headers={'X-Auth-Token':
185                                                     access_token})
186             if glance_response.status_code != 200:
187                 return None
188             images = json.loads(glance_response.content)["images"]
189             for image in images:
190                 image_resp = requests.get(glance_url + '/v2/images/' +
191                                           image["id"],
192                                           headers={'X-Auth-Token':
193                                                    access_token})
194                 if image_resp.status_code != 200:
195                     continue
196                 metadata = ["architecture", "type", "distribution", "version"]
197                 image_data = json.loads(image_resp.content)
198                 if any(key in image_data.keys() for key in metadata):
199                     images_dict[image_data["name"]] = dict()
200                     for key in metadata:
201                         if key in image_data.keys():
202                             images_dict[image_data["name"]][key] = \
203                                 image_data[key]
204                 else:
205                     continue
206
207         except Exception as e:
208             # Handles any exception coming from openstack
209             log.warn(_('Choosing predefined flavors since received '
210                        'Openstack Exception: %s') % str(e))
211         return images_dict
212
213     def _best_flavor(self, properties):
214         log.info(_('Choosing the best flavor for given attributes.'))
215         # Check whether user exported all required environment variables.
216         flavors = FLAVORS
217         if translator.common.utils.check_for_env_variables():
218             resp = self._create_nova_flavor_dict()
219             if resp:
220                 flavors = resp
221
222         # start with all flavors
223         match_all = flavors.keys()
224
225         # TODO(anyone): Handle the case where the value contains something like
226         # get_input instead of a value.
227         # flavors that fit the CPU count
228         cpu = properties.get(self.NUM_CPUS)
229         if cpu is None:
230             self._log_compute_msg(self.NUM_CPUS, 'flavor')
231         match_cpu = self._match_flavors(match_all, flavors, self.NUM_CPUS, cpu)
232
233         # flavors that fit the mem size
234         mem = properties.get(self.MEM_SIZE)
235         if mem:
236             mem = translator.common.utils.MemoryUnit.convert_unit_size_to_num(
237                 mem, 'MB')
238         else:
239             self._log_compute_msg(self.MEM_SIZE, 'flavor')
240         match_cpu_mem = self._match_flavors(match_cpu, flavors,
241                                             self.MEM_SIZE, mem)
242         # flavors that fit the disk size
243         disk = properties.get(self.DISK_SIZE)
244         if disk:
245             disk = translator.common.utils.MemoryUnit.\
246                 convert_unit_size_to_num(disk, 'GB')
247         else:
248             self._log_compute_msg(self.DISK_SIZE, 'flavor')
249         match_cpu_mem_disk = self._match_flavors(match_cpu_mem, flavors,
250                                                  self.DISK_SIZE, disk)
251         # if multiple match, pick the flavor with the least memory
252         # the selection can be based on other heuristic, e.g. pick one with the
253         # least total resource
254         if len(match_cpu_mem_disk) > 1:
255             return self._least_flavor(match_cpu_mem_disk, flavors, 'mem_size')
256         elif len(match_cpu_mem_disk) == 1:
257             return match_cpu_mem_disk[0]
258         else:
259             return None
260
261     def _best_image(self, properties):
262         # Check whether user exported all required environment variables.
263         images = IMAGES
264         if translator.common.utils.check_for_env_variables():
265             resp = self._populate_image_dict()
266             if resp and len(resp.keys()) > 0:
267                 images = resp
268         match_all = images.keys()
269         architecture = properties.get(self.ARCHITECTURE)
270         if architecture is None:
271             self._log_compute_msg(self.ARCHITECTURE, 'image')
272         match_arch = self._match_images(match_all, images,
273                                         self.ARCHITECTURE, architecture)
274         type = properties.get(self.TYPE)
275         if type is None:
276             self._log_compute_msg(self.TYPE, 'image')
277         match_type = self._match_images(match_arch, images, self.TYPE, type)
278         distribution = properties.get(self.DISTRIBUTION)
279         if distribution is None:
280             self._log_compute_msg(self.DISTRIBUTION, 'image')
281         match_distribution = self._match_images(match_type, images,
282                                                 self.DISTRIBUTION,
283                                                 distribution)
284         version = properties.get(self.VERSION)
285         if version is None:
286             self._log_compute_msg(self.VERSION, 'image')
287         match_version = self._match_images(match_distribution, images,
288                                            self.VERSION, version)
289
290         if len(match_version):
291             return list(match_version)[0]
292
293     def _match_flavors(self, this_list, this_dict, attr, size):
294         '''Return from this list all flavors matching the attribute size.'''
295         if not size:
296             return list(this_list)
297         matching_flavors = []
298         for flavor in this_list:
299             if isinstance(size, int):
300                 if this_dict[flavor][attr] >= size:
301                     matching_flavors.append(flavor)
302         log.debug(_('Returning list of flavors matching the attribute size.'))
303         return matching_flavors
304
305     def _least_flavor(self, this_list, this_dict, attr):
306         '''Return from this list the flavor with the smallest attr.'''
307         least_flavor = this_list[0]
308         for flavor in this_list:
309             if this_dict[flavor][attr] < this_dict[least_flavor][attr]:
310                 least_flavor = flavor
311         return least_flavor
312
313     def _match_images(self, this_list, this_dict, attr, prop):
314         if not prop:
315             return this_list
316         matching_images = []
317         for image in this_list:
318             if this_dict[image][attr].lower() == str(prop).lower():
319                 matching_images.append(image)
320         return matching_images
321
322     def get_hot_attribute(self, attribute, args):
323         attr = {}
324         # Convert from a TOSCA attribute for a nodetemplate to a HOT
325         # attribute for the matching resource.  Unless there is additional
326         # runtime support, this should be a one to one mapping.
327
328         # Note: We treat private and public IP  addresses equally, but
329         # this will change in the future when TOSCA starts to support
330         # multiple private/public IP addresses.
331         log.debug(_('Converting TOSCA attribute for a nodetemplate to a HOT \
332                   attriute.'))
333         if attribute == 'private_address' or \
334            attribute == 'public_address':
335                 attr['get_attr'] = [self.name, 'networks', 'private', 0]
336
337         return attr
338
339     def _log_compute_msg(self, prop, what):
340         msg = _('No value is provided for Compute capability '
341                 'property "%(prop)s". This may set an undesired "%(what)s" '
342                 'in the template.') % {'prop': prop, 'what': what}
343         log.warn(msg)