1 ##############################################################################
2 # Copyright (c) 2015 Ericsson AB and others.
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
13 from __future__ import absolute_import
17 from six.moves import range
20 LOG = logging.getLogger(__name__)
24 """Base class for classes in the logical model
25 Contains common attributes and methods
28 def __init__(self, name, context):
29 # model identities and reference
31 self._context = context
34 self.stack_name = None
39 """returns distinguished name for object"""
40 return self.name + "." + self._context.name
43 class PlacementGroup(Object):
44 """Class that represents a placement group in the logical model
45 Concept comes from the OVF specification. Policy should be one of
46 "availability" or "affinity (there are more but they are not supported)"
50 def __init__(self, name, context, policy):
51 if policy not in ["affinity", "availability"]:
52 raise ValueError("placement group '%s', policy '%s' is not valid" %
56 self.stack_name = context.name + "-" + name
58 PlacementGroup.map[name] = self
60 def add_member(self, name):
61 self.members.add(name)
65 return PlacementGroup.map.get(name)
68 class ServerGroup(Object): # pragma: no cover
69 """Class that represents a server group in the logical model
70 Policy should be one of "anti-affinity" or "affinity"
74 def __init__(self, name, context, policy):
75 super(ServerGroup, self).__init__(name, context)
76 if policy not in {"affinity", "anti-affinity"}:
77 raise ValueError("server group '%s', policy '%s' is not valid" %
81 self.stack_name = context.name + "-" + name
83 ServerGroup.map[name] = self
85 def add_member(self, name):
86 self.members.add(name)
90 return ServerGroup.map.get(name)
94 """Class that represents a router in the logical model"""
96 def __init__(self, name, network_name, context, external_gateway_info):
97 super(Router, self).__init__(name, context)
99 self.stack_name = context.name + "-" + network_name + "-" + self.name
100 self.stack_if_name = self.stack_name + "-if0"
101 self.external_gateway_info = external_gateway_info
104 class Network(Object):
105 """Class that represents a network in the logical model"""
108 def __init__(self, name, context, attrs):
109 super(Network, self).__init__(name, context)
110 self.stack_name = context.name + "-" + self.name
111 self.subnet_stack_name = self.stack_name + "-subnet"
112 self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24')
113 self.enable_dhcp = attrs.get('enable_dhcp', 'true')
115 self.physical_network = attrs.get('physical_network', 'physnet1')
116 self.provider = attrs.get('provider')
117 self.segmentation_id = attrs.get('segmentation_id')
118 self.network_type = attrs.get('network_type')
119 self.port_security_enabled = attrs.get('port_security_enabled')
120 self.vnic_type = attrs.get('vnic_type', 'normal')
121 self.allowed_address_pairs = attrs.get('allowed_address_pairs', [])
123 # we require 'null' or '' to disable setting gateway_ip
124 self.gateway_ip = attrs['gateway_ip']
126 # default to explicit None
127 self.gateway_ip = None
129 # null is None in YAML, so we have to convert back to string
130 if self.gateway_ip is None:
131 self.gateway_ip = "null"
133 if "external_network" in attrs:
134 self.router = Router("router", self.name,
135 context, attrs["external_network"])
137 Network.list.append(self)
139 def has_route_to(self, network_name):
140 """determines if this network has a route to the named network"""
141 if self.router and self.router.external_gateway_info == network_name:
146 def find_by_route_to(external_network):
147 """finds a network that has a route to the specified network"""
148 for network in Network.list:
149 if network.has_route_to(external_network):
153 def find_external_network():
154 """return the name of an external network some network in this
155 context has a route to
157 for network in Network.list:
159 return network.router.external_gateway_info
163 class Server(Object): # pragma: no cover
164 """Class that represents a server in the logical model"""
167 def __init__(self, name, context, attrs):
168 super(Server, self).__init__(name, context)
169 self.stack_name = self.name + "." + context.name
170 self.keypair_name = context.keypair_name
171 self.secgroup_name = context.secgroup_name
172 self.user = context.user
173 self.context = context
174 self.public_ip = None
175 self.private_ip = None
182 self.placement_groups = []
183 placement = attrs.get("placement", [])
184 placement = placement if isinstance(placement, list) else [placement]
186 pg = PlacementGroup.get(p)
188 raise ValueError("server '%s', placement '%s' is invalid" %
190 self.placement_groups.append(pg)
191 pg.add_member(self.stack_name)
194 if "volume" in attrs:
195 self.volume = attrs.get("volume")
197 self.volume_mountpoint = None
198 if "volume_mountpoint" in attrs:
199 self.volume_mountpoint = attrs.get("volume_mountpoint")
201 # support servergroup attr
202 self.server_group = None
203 sg = attrs.get("server_group")
205 server_group = ServerGroup.get(sg)
207 raise ValueError("server '%s', server_group '%s' is invalid" %
209 self.server_group = server_group
210 server_group.add_member(self.stack_name)
213 if "instances" in attrs:
214 self.instances = attrs["instances"]
216 # dict with key network name, each item is a dict with port name and ip
217 self.network_ports = attrs.get("network_ports", {})
220 self.floating_ip = None
221 self.floating_ip_assoc = None
222 if "floating_ip" in attrs:
223 self.floating_ip = {}
224 self.floating_ip_assoc = {}
226 if self.floating_ip is not None:
227 ext_net = Network.find_external_network()
228 assert ext_net is not None
229 self.floating_ip["external_network"] = ext_net
233 self._image = attrs["image"]
236 if "flavor" in attrs:
237 self._flavor = attrs["flavor"]
239 self.user_data = attrs.get('user_data', '')
240 self.availability_zone = attrs.get('availability_zone')
242 Server.list.append(self)
246 """returns a server's image name"""
250 return self._context.image
254 """returns a server's flavor name"""
258 return self._context.flavor
260 def _add_instance(self, template, server_name, networks, scheduler_hints):
261 """adds to the template one server and corresponding resources"""
263 for network in networks:
264 # if explicit mapping skip unused networks
265 if self.network_ports:
267 ports = self.network_ports[network.name]
269 # no port for this network
272 if isinstance(ports, six.string_types):
273 if ports.startswith('-'):
274 LOG.warning("possible YAML error, port name starts with - '%s", ports)
276 # otherwise add a port for every network with port name as network name
278 ports = [network.name]
280 port_name = "{0}-{1}-port".format(server_name, port)
281 self.ports.setdefault(network.name, []).append(
282 {"stack_name": port_name, "port": port})
283 # we can't use secgroups if port_security_enabled is False
284 if network.port_security_enabled is False:
287 # if port_security_enabled is None we still need to add to secgroup
288 sec_group_id = self.secgroup_name
289 # don't refactor to pass in network object, that causes JSON
290 # circular ref encode errors
291 template.add_port(port_name, network.stack_name, network.subnet_stack_name,
292 network.vnic_type, sec_group_id=sec_group_id,
293 provider=network.provider,
294 allowed_address_pairs=network.allowed_address_pairs)
295 port_name_list.append(port_name)
298 external_network = self.floating_ip["external_network"]
299 if network.has_route_to(external_network):
300 self.floating_ip["stack_name"] = server_name + "-fip"
301 template.add_floating_ip(self.floating_ip["stack_name"],
304 network.router.stack_if_name,
306 self.floating_ip_assoc["stack_name"] = \
307 server_name + "-fip-assoc"
308 template.add_floating_ip_association(
309 self.floating_ip_assoc["stack_name"],
310 self.floating_ip["stack_name"],
313 if isinstance(self.flavor, dict):
314 self.flavor["name"] = \
315 self.flavor.setdefault("name", self.stack_name + "-flavor")
316 template.add_flavor(**self.flavor)
317 self.flavor_name = self.flavor["name"]
319 self.flavor_name = self.flavor
322 if isinstance(self.volume, dict):
323 self.volume["name"] = \
324 self.volume.setdefault("name", server_name + "-volume")
325 template.add_volume(**self.volume)
326 template.add_volume_attachment(server_name, self.volume["name"],
327 mountpoint=self.volume_mountpoint)
329 template.add_volume_attachment(server_name, self.volume,
330 mountpoint=self.volume_mountpoint)
332 template.add_server(server_name, self.image, flavor=self.flavor_name,
333 flavors=self.context.flavors, ports=port_name_list,
334 scheduler_hints=scheduler_hints, user=self.user,
335 key_name=self.keypair_name, user_data=self.user_data,
336 availability_zone=self.availability_zone)
338 def add_to_template(self, template, networks, scheduler_hints=None):
339 """adds to the template one or more servers (instances)"""
340 if self.instances == 1:
341 server_name = self.stack_name
342 self._add_instance(template, server_name, networks,
343 scheduler_hints=scheduler_hints)
345 # TODO(hafe) fix or remove, no test/sample for this
346 for i in range(self.instances):
347 server_name = "%s-%d" % (self.stack_name, i)
348 self._add_instance(template, server_name, networks,
349 scheduler_hints=scheduler_hints)
352 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
353 """update scheduler hints from server's placement configuration
354 TODO: this code is openstack specific and should move somewhere else
356 if placement_group.policy == "affinity":
357 if "same_host" in scheduler_hints:
358 host_list = scheduler_hints["same_host"]
360 host_list = scheduler_hints["same_host"] = []
362 if "different_host" in scheduler_hints:
363 host_list = scheduler_hints["different_host"]
365 host_list = scheduler_hints["different_host"] = []
367 for name in added_servers:
368 if name in placement_group.members:
369 host_list.append({'get_resource': name})