Merge "Update tosca lib to version 0.5"
[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     def __init__(self, nodetemplate):
88         super(ToscaCompute, self).__init__(nodetemplate,
89                                            type='OS::Nova::Server')
90         # List with associated hot port resources with this server
91         self.assoc_port_resources = []
92         pass
93
94     def handle_properties(self):
95         self.properties = self.translate_compute_flavor_and_image(
96             self.nodetemplate.get_capability('host'),
97             self.nodetemplate.get_capability('os'))
98         self.properties['user_data_format'] = 'SOFTWARE_CONFIG'
99         tosca_props = self.get_tosca_props()
100         for key, value in tosca_props.items():
101             self.properties[key] = value
102
103     # To be reorganized later based on new development in Glance and Graffiti
104     def translate_compute_flavor_and_image(self,
105                                            host_capability,
106                                            os_capability):
107         hot_properties = {}
108         host_cap_props = {}
109         os_cap_props = {}
110         image = None
111         flavor = None
112         if host_capability:
113             for prop in host_capability.get_properties_objects():
114                 host_cap_props[prop.name] = prop.value
115             flavor = self._best_flavor(host_cap_props)
116         if os_capability:
117             for prop in os_capability.get_properties_objects():
118                 os_cap_props[prop.name] = prop.value
119             image = self._best_image(os_cap_props)
120         hot_properties['flavor'] = flavor
121         hot_properties['image'] = image
122         return hot_properties
123
124     def _create_nova_flavor_dict(self):
125         '''Populates and returns the flavors dict using Nova ReST API'''
126         try:
127             access_dict = translator.common.utils.get_ks_access_dict()
128             access_token = translator.common.utils.get_token_id(access_dict)
129             if access_token is None:
130                 return None
131             nova_url = translator.common.utils.get_url_for(access_dict,
132                                                            'compute')
133             if not nova_url:
134                 return None
135             nova_response = requests.get(nova_url + '/flavors/detail',
136                                          headers={'X-Auth-Token':
137                                                   access_token})
138             if nova_response.status_code != 200:
139                 return None
140             flavors = json.loads(nova_response.content)['flavors']
141             flavor_dict = dict()
142             for flavor in flavors:
143                 flavor_name = str(flavor['name'])
144                 flavor_dict[flavor_name] = {
145                     'mem_size': flavor['ram'],
146                     'disk_size': flavor['disk'],
147                     'num_cpus': flavor['vcpus'],
148                 }
149         except Exception as e:
150             # Handles any exception coming from openstack
151             log.warn(_('Choosing predefined flavors since received '
152                        'Openstack Exception: %s') % str(e))
153             return None
154         return flavor_dict
155
156     def _best_flavor(self, properties):
157         log.info(_('Choosing the best flavor for given attributes.'))
158         # Check whether user exported all required environment variables.
159         flavors = FLAVORS
160         if translator.common.utils.check_for_env_variables():
161             resp = self._create_nova_flavor_dict()
162             if resp:
163                 flavors = resp
164
165         # start with all flavors
166         match_all = flavors.keys()
167
168         # TODO(anyone): Handle the case where the value contains something like
169         # get_input instead of a value.
170         # flavors that fit the CPU count
171         cpu = properties.get(self.NUM_CPUS)
172         if cpu is None:
173             self._log_compute_msg(self.NUM_CPUS, 'flavor')
174         match_cpu = self._match_flavors(match_all, flavors, self.NUM_CPUS, cpu)
175
176         # flavors that fit the mem size
177         mem = properties.get(self.MEM_SIZE)
178         if mem:
179             mem = translator.common.utils.MemoryUnit.convert_unit_size_to_num(
180                 mem, 'MB')
181         else:
182             self._log_compute_msg(self.MEM_SIZE, 'flavor')
183         match_cpu_mem = self._match_flavors(match_cpu, flavors,
184                                             self.MEM_SIZE, mem)
185         # flavors that fit the disk size
186         disk = properties.get(self.DISK_SIZE)
187         if disk:
188             disk = translator.common.utils.MemoryUnit.\
189                 convert_unit_size_to_num(disk, 'GB')
190         else:
191             self._log_compute_msg(self.DISK_SIZE, 'flavor')
192         match_cpu_mem_disk = self._match_flavors(match_cpu_mem, flavors,
193                                                  self.DISK_SIZE, disk)
194         # if multiple match, pick the flavor with the least memory
195         # the selection can be based on other heuristic, e.g. pick one with the
196         # least total resource
197         if len(match_cpu_mem_disk) > 1:
198             return self._least_flavor(match_cpu_mem_disk, flavors, 'mem_size')
199         elif len(match_cpu_mem_disk) == 1:
200             return match_cpu_mem_disk[0]
201         else:
202             return None
203
204     def _best_image(self, properties):
205         match_all = IMAGES.keys()
206         architecture = properties.get(self.ARCHITECTURE)
207         if architecture is None:
208             self._log_compute_msg(self.ARCHITECTURE, 'image')
209         match_arch = self._match_images(match_all, IMAGES,
210                                         self.ARCHITECTURE, architecture)
211         type = properties.get(self.TYPE)
212         if type is None:
213             self._log_compute_msg(self.TYPE, 'image')
214         match_type = self._match_images(match_arch, IMAGES, self.TYPE, type)
215         distribution = properties.get(self.DISTRIBUTION)
216         if distribution is None:
217             self._log_compute_msg(self.DISTRIBUTION, 'image')
218         match_distribution = self._match_images(match_type, IMAGES,
219                                                 self.DISTRIBUTION,
220                                                 distribution)
221         version = properties.get(self.VERSION)
222         if version is None:
223             self._log_compute_msg(self.VERSION, 'image')
224         match_version = self._match_images(match_distribution, IMAGES,
225                                            self.VERSION, version)
226
227         if len(match_version):
228             return list(match_version)[0]
229
230     def _match_flavors(self, this_list, this_dict, attr, size):
231         '''Return from this list all flavors matching the attribute size.'''
232         if not size:
233             return list(this_list)
234         matching_flavors = []
235         for flavor in this_list:
236             if isinstance(size, int):
237                 if this_dict[flavor][attr] >= size:
238                     matching_flavors.append(flavor)
239         log.debug(_('Returning list of flavors matching the attribute size.'))
240         return matching_flavors
241
242     def _least_flavor(self, this_list, this_dict, attr):
243         '''Return from this list the flavor with the smallest attr.'''
244         least_flavor = this_list[0]
245         for flavor in this_list:
246             if this_dict[flavor][attr] < this_dict[least_flavor][attr]:
247                 least_flavor = flavor
248         return least_flavor
249
250     def _match_images(self, this_list, this_dict, attr, prop):
251         if not prop:
252             return this_list
253         matching_images = []
254         for image in this_list:
255             if this_dict[image][attr].lower() == str(prop).lower():
256                 matching_images.append(image)
257         return matching_images
258
259     def get_hot_attribute(self, attribute, args):
260         attr = {}
261         # Convert from a TOSCA attribute for a nodetemplate to a HOT
262         # attribute for the matching resource.  Unless there is additional
263         # runtime support, this should be a one to one mapping.
264
265         # Note: We treat private and public IP  addresses equally, but
266         # this will change in the future when TOSCA starts to support
267         # multiple private/public IP addresses.
268         log.debug(_('Converting TOSCA attribute for a nodetemplate to a HOT \
269                   attriute.'))
270         if attribute == 'private_address' or \
271            attribute == 'public_address':
272                 attr['get_attr'] = [self.name, 'networks', 'private', 0]
273
274         return attr
275
276     def _log_compute_msg(self, prop, what):
277         msg = _('No value is provided for Compute capability '
278                 'property "%(prop)s". This may set an undesired "%(what)s" '
279                 'in the template.') % {'prop': prop, 'what': what}
280         log.warn(msg)