| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 1 | from __future__ import print_function # TODO remove when print is no longer needed for debugging |
| 2 | from resources import * |
| 3 | from datetime import datetime |
| 4 | import re |
| 5 | import sys |
| 6 | import uuid |
| 7 | import logging |
| 8 | import ip_handler as IP |
| 9 | |
| 10 | |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 11 | LOG = logging.getLogger("api.openstack.heat.parser") |
| 12 | |
| 13 | |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 14 | class HeatParser: |
| 15 | """ |
| 16 | The HeatParser will parse a heat dictionary and create a stack and its components, to instantiate it within son-emu. |
| 17 | """ |
| 18 | |
| 19 | def __init__(self, compute): |
| 20 | self.description = None |
| 21 | self.parameter_groups = None |
| 22 | self.parameters = None |
| 23 | self.resources = None |
| 24 | self.outputs = None |
| 25 | self.compute = compute |
| 26 | self.bufferResource = list() |
| 27 | |
| 28 | def parse_input(self, input_dict, stack, dc_label, stack_update=False): |
| 29 | """ |
| 30 | It will parse the input dictionary into the corresponding classes, which are then stored within the stack. |
| 31 | |
| 32 | :param input_dict: Dictionary with the template version and resources. |
| 33 | :type input_dict: ``dict`` |
| 34 | :param stack: Reference of the stack that should finally contain all created classes. |
| 35 | :type stack: :class:`heat.resources.stack` |
| 36 | :param dc_label: String that contains the label of the used data center. |
| 37 | :type dc_label: ``str`` |
| 38 | :param stack_update: Specifies if a new stack will be created or a older one will be updated |
| 39 | :type stack_update: ``bool`` |
| 40 | :return: * *True*: If the template version is supported and all resources could be created. |
| 41 | * *False*: Else |
| 42 | :rtype: ``bool`` |
| 43 | """ |
| 44 | if not self.check_template_version(str(input_dict['heat_template_version'])): |
| 45 | print('Unsupported template version: ' + input_dict['heat_template_version'], file=sys.stderr) |
| 46 | return False |
| 47 | |
| 48 | self.description = input_dict.get('description', None) |
| 49 | self.parameter_groups = input_dict.get('parameter_groups', None) |
| 50 | self.parameters = input_dict.get('parameters', None) |
| 51 | self.resources = input_dict.get('resources', None) |
| 52 | self.outputs = input_dict.get('outputs', None) |
| 53 | # clear bufferResources |
| 54 | self.bufferResource = list() |
| 55 | |
| 56 | for resource in self.resources.values(): |
| 57 | self.handle_resource(resource, stack, dc_label, stack_update=stack_update) |
| 58 | |
| 59 | # This loop tries to create all classes which had unresolved dependencies. |
| 60 | unresolved_resources_last_round = len(self.bufferResource) + 1 |
| 61 | while len(self.bufferResource) > 0 and unresolved_resources_last_round > len(self.bufferResource): |
| 62 | unresolved_resources_last_round = len(self.bufferResource) |
| 63 | number_of_items = len(self.bufferResource) |
| 64 | while number_of_items > 0: |
| 65 | self.handle_resource(self.bufferResource.pop(0), stack, dc_label, stack_update=stack_update) |
| 66 | number_of_items -= 1 |
| 67 | |
| 68 | if len(self.bufferResource) > 0: |
| 69 | print(str(len(self.bufferResource)) + |
| peusterm | 7ab1dbe | 2017-06-14 14:56:37 +0200 | [diff] [blame] | 70 | ' classes of the HOT could not be created, because the dependencies could not be found.') |
| 71 | print("the problem classes are:") |
| 72 | for br in self.bufferResource: |
| 73 | print("class: %s" % str(br)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 74 | return False |
| 75 | return True |
| 76 | |
| 77 | def handle_resource(self, resource, stack, dc_label, stack_update=False): |
| 78 | """ |
| 79 | This function will take a resource (from a heat template) and determines which type it is and creates |
| 80 | the corresponding class, with its required parameters, for further calculations (like deploying the stack). |
| 81 | If it is not possible to create the class, because of unresolved dependencies, it will buffer the resource |
| 82 | within the 'self.bufferResource' list. |
| 83 | |
| 84 | :param resource: Dict which contains all important informations about the type and parameters. |
| 85 | :type resource: ``dict`` |
| 86 | :param stack: Reference of the stack that should finally contain the created class. |
| 87 | :type stack: :class:`heat.resources.stack` |
| 88 | :param dc_label: String that contains the label of the used data center |
| 89 | :type dc_label: ``str`` |
| 90 | :param stack_update: Specifies if a new stack will be created or a older one will be updated |
| 91 | :type stack_update: ``bool`` |
| 92 | :return: void |
| 93 | :rtype: ``None`` |
| 94 | """ |
| 95 | if "OS::Neutron::Net" in resource['type']: |
| 96 | try: |
| 97 | net_name = resource['properties']['name'] |
| 98 | if net_name not in stack.nets: |
| 99 | stack.nets[net_name] = self.compute.create_network(net_name, True) |
| 100 | |
| 101 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 102 | LOG.warning('Could not create Net: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 103 | return |
| 104 | |
| 105 | if 'OS::Neutron::Subnet' in resource['type'] and "Net" not in resource['type']: |
| 106 | try: |
| 107 | net_name = resource['properties']['network']['get_resource'] |
| 108 | if net_name not in stack.nets: |
| 109 | net = self.compute.create_network(net_name, stack_update) |
| 110 | stack.nets[net_name] = net |
| 111 | else: |
| 112 | net = stack.nets[net_name] |
| 113 | |
| 114 | net.subnet_name = resource['properties']['name'] |
| 115 | if 'gateway_ip' in resource['properties']: |
| 116 | net.gateway_ip = resource['properties']['gateway_ip'] |
| 117 | net.subnet_id = resource['properties'].get('id', str(uuid.uuid4())) |
| 118 | net.subnet_creation_time = str(datetime.now()) |
| 119 | if not stack_update: |
| 120 | net.set_cidr(IP.get_new_cidr(net.subnet_id)) |
| 121 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 122 | LOG.warning('Could not create Subnet: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 123 | return |
| 124 | |
| 125 | if 'OS::Neutron::Port' in resource['type']: |
| 126 | try: |
| 127 | port_name = resource['properties']['name'] |
| 128 | if port_name not in stack.ports: |
| 129 | port = self.compute.create_port(port_name, stack_update) |
| 130 | stack.ports[port_name] = port |
| 131 | else: |
| 132 | port = stack.ports[port_name] |
| 133 | |
| peusterm | 7ab1dbe | 2017-06-14 14:56:37 +0200 | [diff] [blame] | 134 | if str(resource['properties']['network']['get_resource']) in stack.nets: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 135 | net = stack.nets[resource['properties']['network']['get_resource']] |
| 136 | if net.subnet_id is not None: |
| 137 | port.net_name = net.name |
| 138 | port.ip_address = net.get_new_ip_address(port.name) |
| 139 | return |
| 140 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 141 | LOG.warning('Could not create Port: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 142 | self.bufferResource.append(resource) |
| 143 | return |
| 144 | |
| 145 | if 'OS::Nova::Server' in resource['type']: |
| 146 | try: |
| 147 | compute_name = str(dc_label) + '_' + str(stack.stack_name) + '_' + str(resource['properties']['name']) |
| 148 | shortened_name = str(dc_label) + '_' + str(stack.stack_name) + '_' + \ |
| 149 | self.shorten_server_name(str(resource['properties']['name']), stack) |
| 150 | nw_list = resource['properties']['networks'] |
| 151 | |
| 152 | if shortened_name not in stack.servers: |
| 153 | server = self.compute.create_server(shortened_name, stack_update) |
| 154 | stack.servers[shortened_name] = server |
| 155 | else: |
| 156 | server = stack.servers[shortened_name] |
| 157 | |
| 158 | server.full_name = compute_name |
| 159 | server.template_name = str(resource['properties']['name']) |
| 160 | server.command = resource['properties'].get('command', '/bin/sh') |
| 161 | server.image = resource['properties']['image'] |
| 162 | server.flavor = resource['properties']['flavor'] |
| 163 | |
| 164 | for port in nw_list: |
| 165 | port_name = port['port']['get_resource'] |
| 166 | # just create a port |
| 167 | # we don't know which network it belongs to yet, but the resource will appear later in a valid |
| 168 | # template |
| 169 | if port_name not in stack.ports: |
| 170 | stack.ports[port_name] = self.compute.create_port(port_name, stack_update) |
| 171 | server.port_names.append(port_name) |
| 172 | return |
| 173 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 174 | LOG.warning('Could not create Server: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 175 | return |
| 176 | |
| 177 | if 'OS::Neutron::RouterInterface' in resource['type']: |
| 178 | try: |
| 179 | router_name = None |
| 180 | subnet_name = resource['properties']['subnet']['get_resource'] |
| 181 | |
| 182 | if 'get_resource' in resource['properties']['router']: |
| 183 | router_name = resource['properties']['router']['get_resource'] |
| 184 | else: |
| 185 | router_name = resource['properties']['router'] |
| 186 | |
| 187 | if router_name not in stack.routers: |
| 188 | stack.routers[router_name] = Router(router_name) |
| 189 | |
| 190 | for tmp_net in stack.nets.values(): |
| 191 | if tmp_net.subnet_name == subnet_name: |
| 192 | stack.routers[router_name].add_subnet(subnet_name) |
| 193 | return |
| 194 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 195 | LOG.warning('Could not create RouterInterface: ' + e.__repr__()) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 196 | self.bufferResource.append(resource) |
| 197 | return |
| 198 | |
| 199 | if 'OS::Neutron::FloatingIP' in resource['type']: |
| 200 | try: |
| 201 | port_name = resource['properties']['port_id']['get_resource'] |
| 202 | floating_network_id = resource['properties']['floating_network_id'] |
| 203 | if port_name not in stack.ports: |
| 204 | stack.ports[port_name] = self.compute.create_port(port_name, stack_update) |
| 205 | |
| 206 | stack.ports[port_name].floating_ip = floating_network_id |
| 207 | except Exception as e: |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 208 | LOG.warning('Could not create FloatingIP: ' + e.message) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 209 | return |
| 210 | |
| 211 | if 'OS::Neutron::Router' in resource['type']: |
| 212 | try: |
| 213 | name = resource['properties']['name'] |
| 214 | if name not in stack.routers: |
| 215 | stack.routers[name] = Router(name) |
| 216 | except Exception as e: |
| 217 | print('Could not create Router: ' + e.message) |
| 218 | return |
| 219 | |
| peusterm | bbf4f74 | 2017-06-19 11:22:11 +0200 | [diff] [blame^] | 220 | LOG.warning('Could not determine resource type: {}'.format(resource['type'])) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 221 | return |
| 222 | |
| 223 | def shorten_server_name(self, server_name, stack): |
| 224 | """ |
| 225 | Shortens the server name to a maximum of 12 characters plus the iterator string, if the original name was |
| 226 | used before. |
| 227 | |
| 228 | :param server_name: The original server name. |
| 229 | :type server_name: ``str`` |
| 230 | :param stack: A reference to the used stack. |
| 231 | :type stack: :class:`heat.resources.stack` |
| 232 | :return: A string with max. 12 characters plus iterator string. |
| 233 | :rtype: ``str`` |
| 234 | """ |
| 235 | server_name = self.shorten_name(server_name, 12) |
| 236 | iterator = 0 |
| 237 | while server_name in stack.servers: |
| 238 | server_name = server_name[0:12] + str(iterator) |
| 239 | iterator += 1 |
| 240 | return server_name |
| 241 | |
| 242 | def shorten_name(self, name, max_size): |
| 243 | """ |
| 244 | Shortens the name to max_size characters and replaces all '-' with '_'. |
| 245 | |
| 246 | :param name: The original string. |
| 247 | :type name: ``str`` |
| 248 | :param max_size: The number of allowed characters. |
| 249 | :type max_size: ``int`` |
| 250 | :return: String with at most max_size characters and without '-'. |
| 251 | :rtype: ``str`` |
| 252 | """ |
| 253 | shortened_name = name.split(':', 1)[0] |
| 254 | shortened_name = shortened_name.replace("-", "_") |
| 255 | shortened_name = shortened_name[0:max_size] |
| 256 | return shortened_name |
| 257 | |
| 258 | def check_template_version(self, version_string): |
| 259 | """ |
| 260 | Checks if a version string is equal or later than 30-04-2015 |
| 261 | |
| 262 | :param version_string: String with the version. |
| 263 | :type version_string: ``str`` |
| 264 | :return: * *True*: if the version is equal or later 30-04-2015. |
| 265 | * *False*: else |
| 266 | :rtype: ``bool`` |
| 267 | """ |
| 268 | r = re.compile('\d{4}-\d{2}-\d{2}') |
| 269 | if not r.match(version_string): |
| 270 | return False |
| 271 | |
| 272 | year, month, day = map(int, version_string.split('-', 2)) |
| 273 | if year < 2015: |
| 274 | return False |
| 275 | if year == 2015: |
| 276 | if month < 04: |
| 277 | return False |
| 278 | if month == 04 and day < 30: |
| 279 | return False |
| 280 | return True |