Merge "Replace test file from test_tosca_nfv_sample with vRNC"
[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         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
112
113     # To be reorganized later based on new development in Glance and Graffiti
114     def translate_compute_flavor_and_image(self,
115                                            host_capability,
116                                            os_capability):
117         hot_properties = {}
118         host_cap_props = {}
119         os_cap_props = {}
120         image = None
121         flavor = None
122         if host_capability:
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
127             if host_cap_props:
128                 flavor = self._best_flavor(host_cap_props)
129         if os_capability:
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
134             if os_cap_props:
135                 image = self._best_image(os_cap_props)
136         hot_properties['flavor'] = flavor
137         hot_properties['image'] = image
138         return hot_properties
139
140     def _create_nova_flavor_dict(self):
141         '''Populates and returns the flavors dict using Nova ReST API'''
142         try:
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:
146                 return None
147             nova_url = translator.common.utils.get_url_for(access_dict,
148                                                            'compute')
149             if not nova_url:
150                 return None
151             nova_response = requests.get(nova_url + '/flavors/detail',
152                                          headers={'X-Auth-Token':
153                                                   access_token})
154             if nova_response.status_code != 200:
155                 return None
156             flavors = json.loads(nova_response.content)['flavors']
157             flavor_dict = dict()
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'],
164                 }
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))
169             return None
170         return flavor_dict
171
172     def _populate_image_dict(self):
173         '''Populates and returns the images dict using Glance ReST API'''
174         images_dict = {}
175         try:
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:
179                 return None
180             glance_url = translator.common.utils.get_url_for(access_dict,
181                                                              'image')
182             if not glance_url:
183                 return None
184             glance_response = requests.get(glance_url + '/v2/images',
185                                            headers={'X-Auth-Token':
186                                                     access_token})
187             if glance_response.status_code != 200:
188                 return None
189             images = json.loads(glance_response.content)["images"]
190             for image in images:
191                 image_resp = requests.get(glance_url + '/v2/images/' +
192                                           image["id"],
193                                           headers={'X-Auth-Token':
194                                                    access_token})
195                 if image_resp.status_code != 200:
196                     continue
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()
201                     for key in metadata:
202                         if key in image_data.keys():
203                             images_dict[image_data["name"]][key] = \
204                                 image_data[key]
205                 else:
206                     continue
207
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))
212         return images_dict
213
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.
217         flavors = FLAVORS
218         if translator.common.utils.check_for_env_variables():
219             resp = self._create_nova_flavor_dict()
220             if resp:
221                 flavors = resp
222
223         # start with all flavors
224         match_all = flavors.keys()
225
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)
230         if cpu is None:
231             self._log_compute_msg(self.NUM_CPUS, 'flavor')
232         match_cpu = self._match_flavors(match_all, flavors, self.NUM_CPUS, cpu)
233
234         # flavors that fit the mem size
235         mem = properties.get(self.MEM_SIZE)
236         if mem:
237             mem = translator.common.utils.MemoryUnit.convert_unit_size_to_num(
238                 mem, 'MB')
239         else:
240             self._log_compute_msg(self.MEM_SIZE, 'flavor')
241         match_cpu_mem = self._match_flavors(match_cpu, flavors,
242                                             self.MEM_SIZE, mem)
243         # flavors that fit the disk size
244         disk = properties.get(self.DISK_SIZE)
245         if disk:
246             disk = translator.common.utils.MemoryUnit.\
247                 convert_unit_size_to_num(disk, 'GB')
248         else:
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]
259         else:
260             return None
261
262     def _best_image(self, properties):
263         # Check whether user exported all required environment variables.
264         images = IMAGES
265         if translator.common.utils.check_for_env_variables():
266             resp = self._populate_image_dict()
267             if resp and len(resp.keys()) > 0:
268                 images = resp
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)
276         if type is None:
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,
283                                                 self.DISTRIBUTION,
284                                                 distribution)
285         version = properties.get(self.VERSION)
286         if version is None:
287             self._log_compute_msg(self.VERSION, 'image')
288         match_version = self._match_images(match_distribution, images,
289                                            self.VERSION, version)
290
291         if len(match_version):
292             return list(match_version)[0]
293
294     def _match_flavors(self, this_list, this_dict, attr, size):
295         '''Return from this list all flavors matching the attribute size.'''
296         if not 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
305
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
312         return least_flavor
313
314     def _match_images(self, this_list, this_dict, attr, prop):
315         if not prop:
316             return this_list
317         matching_images = []
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
322
323     def get_hot_attribute(self, attribute, args):
324         attr = {}
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.
328
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 \
333                   attriute.'))
334         if attribute == 'private_address' or \
335            attribute == 'public_address':
336                 attr['get_attr'] = [self.name, 'networks']
337
338         return attr
339
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}
344         log.warn(msg)