| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1 | # Copyright (c) 2015 SONATA-NFV and Paderborn University |
| 2 | # ALL RIGHTS RESERVED. |
| 3 | # |
| 4 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | # you may not use this file except in compliance with the License. |
| 6 | # You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | # See the License for the specific language governing permissions and |
| 14 | # limitations under the License. |
| 15 | # |
| 16 | # Neither the name of the SONATA-NFV, Paderborn University |
| 17 | # nor the names of its contributors may be used to endorse or promote |
| 18 | # products derived from this software without specific prior written |
| 19 | # permission. |
| 20 | # |
| 21 | # This work has been performed in the framework of the SONATA project, |
| 22 | # funded by the European Commission under Grant number 671517 through |
| 23 | # the Horizon 2020 and 5G-PPP programmes. The authors would like to |
| 24 | # acknowledge the contributions of their colleagues of the SONATA |
| 25 | # partner consortium (www.sonata-nfv.eu). |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 26 | from __future__ import print_function # TODO remove when print is no longer needed for debugging |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 27 | from resources.router import Router |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 28 | from datetime import datetime |
| 29 | import re |
| 30 | import sys |
| 31 | import uuid |
| 32 | import logging |
| 33 | import ip_handler as IP |
| 34 | |
| 35 | |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame] | 36 | LOG = logging.getLogger("api.openstack.heat.parser") |
| 37 | |
| 38 | |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 39 | class HeatParser: |
| 40 | """ |
| 41 | The HeatParser will parse a heat dictionary and create a stack and its components, to instantiate it within son-emu. |
| 42 | """ |
| 43 | |
| 44 | def __init__(self, compute): |
| 45 | self.description = None |
| 46 | self.parameter_groups = None |
| 47 | self.parameters = None |
| 48 | self.resources = None |
| 49 | self.outputs = None |
| 50 | self.compute = compute |
| 51 | self.bufferResource = list() |
| 52 | |
| 53 | def parse_input(self, input_dict, stack, dc_label, stack_update=False): |
| 54 | """ |
| 55 | It will parse the input dictionary into the corresponding classes, which are then stored within the stack. |
| 56 | |
| 57 | :param input_dict: Dictionary with the template version and resources. |
| 58 | :type input_dict: ``dict`` |
| 59 | :param stack: Reference of the stack that should finally contain all created classes. |
| 60 | :type stack: :class:`heat.resources.stack` |
| 61 | :param dc_label: String that contains the label of the used data center. |
| 62 | :type dc_label: ``str`` |
| 63 | :param stack_update: Specifies if a new stack will be created or a older one will be updated |
| 64 | :type stack_update: ``bool`` |
| 65 | :return: * *True*: If the template version is supported and all resources could be created. |
| 66 | * *False*: Else |
| 67 | :rtype: ``bool`` |
| 68 | """ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 69 | if not self.check_template_version( |
| 70 | str(input_dict['heat_template_version'])): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 71 | print('Unsupported template version: ' + input_dict['heat_template_version'], file=sys.stderr) |
| 72 | return False |
| 73 | |
| 74 | self.description = input_dict.get('description', None) |
| 75 | self.parameter_groups = input_dict.get('parameter_groups', None) |
| 76 | self.parameters = input_dict.get('parameters', None) |
| 77 | self.resources = input_dict.get('resources', None) |
| 78 | self.outputs = input_dict.get('outputs', None) |
| 79 | # clear bufferResources |
| 80 | self.bufferResource = list() |
| 81 | |
| 82 | for resource in self.resources.values(): |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 83 | self.handle_resource(resource, stack, dc_label, |
| 84 | stack_update=stack_update) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 85 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 86 | # This loop tries to create all classes which had unresolved |
| 87 | # dependencies. |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 88 | unresolved_resources_last_round = len(self.bufferResource) + 1 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 89 | while len(self.bufferResource) > 0 and unresolved_resources_last_round > len( |
| 90 | self.bufferResource): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 91 | unresolved_resources_last_round = len(self.bufferResource) |
| 92 | number_of_items = len(self.bufferResource) |
| 93 | while number_of_items > 0: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 94 | self.handle_resource(self.bufferResource.pop( |
| 95 | 0), stack, dc_label, stack_update=stack_update) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 96 | number_of_items -= 1 |
| 97 | |
| 98 | if len(self.bufferResource) > 0: |
| 99 | print(str(len(self.bufferResource)) + |
| peusterm | 7ab1dbe | 2017-06-14 14:56:37 +0200 | [diff] [blame] | 100 | ' classes of the HOT could not be created, because the dependencies could not be found.') |
| 101 | print("the problem classes are:") |
| 102 | for br in self.bufferResource: |
| 103 | print("class: %s" % str(br)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 104 | return False |
| 105 | return True |
| 106 | |
| 107 | def handle_resource(self, resource, stack, dc_label, stack_update=False): |
| 108 | """ |
| 109 | This function will take a resource (from a heat template) and determines which type it is and creates |
| 110 | the corresponding class, with its required parameters, for further calculations (like deploying the stack). |
| 111 | If it is not possible to create the class, because of unresolved dependencies, it will buffer the resource |
| 112 | within the 'self.bufferResource' list. |
| 113 | |
| 114 | :param resource: Dict which contains all important informations about the type and parameters. |
| 115 | :type resource: ``dict`` |
| 116 | :param stack: Reference of the stack that should finally contain the created class. |
| 117 | :type stack: :class:`heat.resources.stack` |
| 118 | :param dc_label: String that contains the label of the used data center |
| 119 | :type dc_label: ``str`` |
| 120 | :param stack_update: Specifies if a new stack will be created or a older one will be updated |
| 121 | :type stack_update: ``bool`` |
| 122 | :return: void |
| 123 | :rtype: ``None`` |
| 124 | """ |
| 125 | if "OS::Neutron::Net" in resource['type']: |
| 126 | try: |
| 127 | net_name = resource['properties']['name'] |
| 128 | if net_name not in stack.nets: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 129 | stack.nets[net_name] = self.compute.create_network( |
| 130 | net_name, True) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 131 | |
| 132 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame] | 133 | LOG.warning('Could not create Net: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 134 | return |
| 135 | |
| 136 | if 'OS::Neutron::Subnet' in resource['type'] and "Net" not in resource['type']: |
| 137 | try: |
| 138 | net_name = resource['properties']['network']['get_resource'] |
| 139 | if net_name not in stack.nets: |
| 140 | net = self.compute.create_network(net_name, stack_update) |
| 141 | stack.nets[net_name] = net |
| 142 | else: |
| 143 | net = stack.nets[net_name] |
| 144 | |
| 145 | net.subnet_name = resource['properties']['name'] |
| 146 | if 'gateway_ip' in resource['properties']: |
| 147 | net.gateway_ip = resource['properties']['gateway_ip'] |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 148 | net.subnet_id = resource['properties'].get( |
| 149 | 'id', str(uuid.uuid4())) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 150 | net.subnet_creation_time = str(datetime.now()) |
| 151 | if not stack_update: |
| 152 | net.set_cidr(IP.get_new_cidr(net.subnet_id)) |
| 153 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame] | 154 | LOG.warning('Could not create Subnet: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 155 | return |
| 156 | |
| 157 | if 'OS::Neutron::Port' in resource['type']: |
| 158 | try: |
| 159 | port_name = resource['properties']['name'] |
| 160 | if port_name not in stack.ports: |
| 161 | port = self.compute.create_port(port_name, stack_update) |
| 162 | stack.ports[port_name] = port |
| 163 | else: |
| 164 | port = stack.ports[port_name] |
| 165 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 166 | if str(resource['properties']['network'] |
| 167 | ['get_resource']) in stack.nets: |
| 168 | net = stack.nets[resource['properties'] |
| 169 | ['network']['get_resource']] |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 170 | if net.subnet_id is not None: |
| 171 | port.net_name = net.name |
| 172 | port.ip_address = net.get_new_ip_address(port.name) |
| 173 | return |
| 174 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame] | 175 | LOG.warning('Could not create Port: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 176 | self.bufferResource.append(resource) |
| 177 | return |
| 178 | |
| 179 | if 'OS::Nova::Server' in resource['type']: |
| 180 | try: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 181 | compute_name = str(dc_label) + '_' + str(stack.stack_name) + \ |
| 182 | '_' + str(resource['properties']['name']) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 183 | shortened_name = str(dc_label) + '_' + str(stack.stack_name) + '_' + \ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 184 | self.shorten_server_name( |
| 185 | str(resource['properties']['name']), stack) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 186 | nw_list = resource['properties']['networks'] |
| 187 | |
| 188 | if shortened_name not in stack.servers: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 189 | server = self.compute.create_server( |
| 190 | shortened_name, stack_update) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 191 | stack.servers[shortened_name] = server |
| 192 | else: |
| 193 | server = stack.servers[shortened_name] |
| 194 | |
| 195 | server.full_name = compute_name |
| 196 | server.template_name = str(resource['properties']['name']) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 197 | server.command = resource['properties'].get( |
| 198 | 'command', '/bin/sh') |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 199 | server.image = resource['properties']['image'] |
| 200 | server.flavor = resource['properties']['flavor'] |
| 201 | |
| 202 | for port in nw_list: |
| 203 | port_name = port['port']['get_resource'] |
| 204 | # just create a port |
| 205 | # we don't know which network it belongs to yet, but the resource will appear later in a valid |
| 206 | # template |
| 207 | if port_name not in stack.ports: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 208 | stack.ports[port_name] = self.compute.create_port( |
| 209 | port_name, stack_update) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 210 | server.port_names.append(port_name) |
| 211 | return |
| 212 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame] | 213 | LOG.warning('Could not create Server: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 214 | return |
| 215 | |
| 216 | if 'OS::Neutron::RouterInterface' in resource['type']: |
| 217 | try: |
| 218 | router_name = None |
| 219 | subnet_name = resource['properties']['subnet']['get_resource'] |
| 220 | |
| 221 | if 'get_resource' in resource['properties']['router']: |
| 222 | router_name = resource['properties']['router']['get_resource'] |
| 223 | else: |
| 224 | router_name = resource['properties']['router'] |
| 225 | |
| 226 | if router_name not in stack.routers: |
| 227 | stack.routers[router_name] = Router(router_name) |
| 228 | |
| 229 | for tmp_net in stack.nets.values(): |
| 230 | if tmp_net.subnet_name == subnet_name: |
| 231 | stack.routers[router_name].add_subnet(subnet_name) |
| 232 | return |
| 233 | except Exception as e: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 234 | LOG.warning( |
| 235 | 'Could not create RouterInterface: ' + e.__repr__()) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 236 | self.bufferResource.append(resource) |
| 237 | return |
| 238 | |
| 239 | if 'OS::Neutron::FloatingIP' in resource['type']: |
| 240 | try: |
| 241 | port_name = resource['properties']['port_id']['get_resource'] |
| 242 | floating_network_id = resource['properties']['floating_network_id'] |
| 243 | if port_name not in stack.ports: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 244 | stack.ports[port_name] = self.compute.create_port( |
| 245 | port_name, stack_update) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 246 | |
| 247 | stack.ports[port_name].floating_ip = floating_network_id |
| 248 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame] | 249 | LOG.warning('Could not create FloatingIP: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 250 | return |
| 251 | |
| 252 | if 'OS::Neutron::Router' in resource['type']: |
| 253 | try: |
| 254 | name = resource['properties']['name'] |
| 255 | if name not in stack.routers: |
| 256 | stack.routers[name] = Router(name) |
| 257 | except Exception as e: |
| 258 | print('Could not create Router: ' + e.message) |
| 259 | return |
| 260 | |
| peusterm | eb398a2 | 2017-06-19 12:01:13 +0200 | [diff] [blame] | 261 | if 'OS::Heat::ResourceGroup' in resource['type']: |
| 262 | try: |
| 263 | embedded_resource = resource['properties']['resource_def'] |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 264 | LOG.debug("Found resource in resource group: {}".format( |
| 265 | embedded_resource)) |
| peusterm | eb398a2 | 2017-06-19 12:01:13 +0200 | [diff] [blame] | 266 | # recursively parse embedded resource |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 267 | self.handle_resource( |
| 268 | embedded_resource, stack, dc_label, stack_update) |
| peusterm | eb398a2 | 2017-06-19 12:01:13 +0200 | [diff] [blame] | 269 | except Exception as e: |
| 270 | print('Could not create Router: ' + e.message) |
| 271 | return |
| 272 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 273 | LOG.warning( |
| 274 | 'Could not determine resource type: {}'.format(resource['type'])) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 275 | return |
| 276 | |
| 277 | def shorten_server_name(self, server_name, stack): |
| 278 | """ |
| 279 | Shortens the server name to a maximum of 12 characters plus the iterator string, if the original name was |
| 280 | used before. |
| 281 | |
| 282 | :param server_name: The original server name. |
| 283 | :type server_name: ``str`` |
| 284 | :param stack: A reference to the used stack. |
| 285 | :type stack: :class:`heat.resources.stack` |
| 286 | :return: A string with max. 12 characters plus iterator string. |
| 287 | :rtype: ``str`` |
| 288 | """ |
| 289 | server_name = self.shorten_name(server_name, 12) |
| 290 | iterator = 0 |
| 291 | while server_name in stack.servers: |
| 292 | server_name = server_name[0:12] + str(iterator) |
| 293 | iterator += 1 |
| 294 | return server_name |
| 295 | |
| 296 | def shorten_name(self, name, max_size): |
| 297 | """ |
| 298 | Shortens the name to max_size characters and replaces all '-' with '_'. |
| 299 | |
| 300 | :param name: The original string. |
| 301 | :type name: ``str`` |
| 302 | :param max_size: The number of allowed characters. |
| 303 | :type max_size: ``int`` |
| 304 | :return: String with at most max_size characters and without '-'. |
| 305 | :rtype: ``str`` |
| 306 | """ |
| 307 | shortened_name = name.split(':', 1)[0] |
| 308 | shortened_name = shortened_name.replace("-", "_") |
| 309 | shortened_name = shortened_name[0:max_size] |
| 310 | return shortened_name |
| 311 | |
| 312 | def check_template_version(self, version_string): |
| 313 | """ |
| 314 | Checks if a version string is equal or later than 30-04-2015 |
| 315 | |
| 316 | :param version_string: String with the version. |
| 317 | :type version_string: ``str`` |
| 318 | :return: * *True*: if the version is equal or later 30-04-2015. |
| 319 | * *False*: else |
| 320 | :rtype: ``bool`` |
| 321 | """ |
| 322 | r = re.compile('\d{4}-\d{2}-\d{2}') |
| 323 | if not r.match(version_string): |
| 324 | return False |
| 325 | |
| 326 | year, month, day = map(int, version_string.split('-', 2)) |
| 327 | if year < 2015: |
| 328 | return False |
| 329 | if year == 2015: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 330 | if month < 0o4: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 331 | return False |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 332 | if month == 0o4 and day < 30: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 333 | return False |
| 334 | return True |