NSB: fix port topology
[yardstick.git] / yardstick / benchmark / contexts / model.py
1 ##############################################################################
2 # Copyright (c) 2015 Ericsson AB and others.
3 #
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 ##############################################################################
9
10 """ Logical model
11
12 """
13 from __future__ import absolute_import
14
15 import six
16 import logging
17 from six.moves import range
18
19
20 LOG = logging.getLogger(__name__)
21
22
23 class Object(object):
24     """Base class for classes in the logical model
25     Contains common attributes and methods
26     """
27
28     def __init__(self, name, context):
29         # model identities and reference
30         self.name = name
31         self._context = context
32
33         # stack identities
34         self.stack_name = None
35         self.stack_id = None
36
37     @property
38     def dn(self):
39         """returns distinguished name for object"""
40         return self.name + "." + self._context.name
41
42
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)"
47     """
48     map = {}
49
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" %
53                              (name, policy))
54         self.name = name
55         self.members = set()
56         self.stack_name = context.name + "-" + name
57         self.policy = policy
58         PlacementGroup.map[name] = self
59
60     def add_member(self, name):
61         self.members.add(name)
62
63     @staticmethod
64     def get(name):
65         return PlacementGroup.map.get(name)
66
67
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"
71     """
72     map = {}
73
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" %
78                              (name, policy))
79         self.name = name
80         self.members = set()
81         self.stack_name = context.name + "-" + name
82         self.policy = policy
83         ServerGroup.map[name] = self
84
85     def add_member(self, name):
86         self.members.add(name)
87
88     @staticmethod
89     def get(name):
90         return ServerGroup.map.get(name)
91
92
93 class Router(Object):
94     """Class that represents a router in the logical model"""
95
96     def __init__(self, name, network_name, context, external_gateway_info):
97         super(Router, self).__init__(name, context)
98
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
102
103
104 class Network(Object):
105     """Class that represents a network in the logical model"""
106     list = []
107
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')
114         self.router = None
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', [])
122         try:
123             # we require 'null' or '' to disable setting gateway_ip
124             self.gateway_ip = attrs['gateway_ip']
125         except KeyError:
126             # default to explicit None
127             self.gateway_ip = None
128         else:
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"
132
133         if "external_network" in attrs:
134             self.router = Router("router", self.name,
135                                  context, attrs["external_network"])
136
137         Network.list.append(self)
138
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:
142             return True
143         return False
144
145     @staticmethod
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):
150                 return network
151
152     @staticmethod
153     def find_external_network():
154         """return the name of an external network some network in this
155         context has a route to
156         """
157         for network in Network.list:
158             if network.router:
159                 return network.router.external_gateway_info
160         return None
161
162
163 class Server(Object):     # pragma: no cover
164     """Class that represents a server in the logical model"""
165     list = []
166
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
176         self.user_data = ''
177         self.interfaces = {}
178
179         if attrs is None:
180             attrs = {}
181
182         self.placement_groups = []
183         placement = attrs.get("placement", [])
184         placement = placement if isinstance(placement, list) else [placement]
185         for p in placement:
186             pg = PlacementGroup.get(p)
187             if not pg:
188                 raise ValueError("server '%s', placement '%s' is invalid" %
189                                  (name, p))
190             self.placement_groups.append(pg)
191             pg.add_member(self.stack_name)
192
193         self.volume = None
194         if "volume" in attrs:
195             self.volume = attrs.get("volume")
196
197         self.volume_mountpoint = None
198         if "volume_mountpoint" in attrs:
199             self.volume_mountpoint = attrs.get("volume_mountpoint")
200
201         # support servergroup attr
202         self.server_group = None
203         sg = attrs.get("server_group")
204         if sg:
205             server_group = ServerGroup.get(sg)
206             if not server_group:
207                 raise ValueError("server '%s', server_group '%s' is invalid" %
208                                  (name, sg))
209             self.server_group = server_group
210             server_group.add_member(self.stack_name)
211
212         self.instances = 1
213         if "instances" in attrs:
214             self.instances = attrs["instances"]
215
216         # dict with key network name, each item is a dict with port name and ip
217         self.network_ports = attrs.get("network_ports", {})
218         self.ports = {}
219
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 = {}
225
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
230
231         self._image = None
232         if "image" in attrs:
233             self._image = attrs["image"]
234
235         self._flavor = None
236         if "flavor" in attrs:
237             self._flavor = attrs["flavor"]
238
239         self.user_data = attrs.get('user_data', '')
240
241         Server.list.append(self)
242
243     @property
244     def image(self):
245         """returns a server's image name"""
246         if self._image:
247             return self._image
248         else:
249             return self._context.image
250
251     @property
252     def flavor(self):
253         """returns a server's flavor name"""
254         if self._flavor:
255             return self._flavor
256         else:
257             return self._context.flavor
258
259     def _add_instance(self, template, server_name, networks, scheduler_hints):
260         """adds to the template one server and corresponding resources"""
261         port_name_list = []
262         for network in networks:
263             # if explicit mapping skip unused networks
264             if self.network_ports:
265                 try:
266                     ports = self.network_ports[network.name]
267                 except KeyError:
268                     # no port for this network
269                     continue
270                 else:
271                     if isinstance(ports, six.string_types):
272                         if ports.startswith('-'):
273                             LOG.warning("possible YAML error, port name starts with - '%s", ports)
274                         ports = [ports]
275             # otherwise add a port for every network with port name as network name
276             else:
277                 ports = [network.name]
278             for port in ports:
279                 port_name = "{0}-{1}-port".format(server_name, port)
280                 self.ports.setdefault(network.name, []).append(
281                     {"stack_name": port_name, "port": port})
282                 # we can't use secgroups if port_security_enabled is False
283                 if network.port_security_enabled is False:
284                     sec_group_id = None
285                 else:
286                     # if port_security_enabled is None we still need to add to secgroup
287                     sec_group_id = self.secgroup_name
288                 # don't refactor to pass in network object, that causes JSON
289                 # circular ref encode errors
290                 template.add_port(port_name, network.stack_name, network.subnet_stack_name,
291                                   network.vnic_type, sec_group_id=sec_group_id,
292                                   provider=network.provider,
293                                   allowed_address_pairs=network.allowed_address_pairs)
294                 port_name_list.append(port_name)
295
296                 if self.floating_ip:
297                     external_network = self.floating_ip["external_network"]
298                     if network.has_route_to(external_network):
299                         self.floating_ip["stack_name"] = server_name + "-fip"
300                         template.add_floating_ip(self.floating_ip["stack_name"],
301                                                  external_network,
302                                                  port_name,
303                                                  network.router.stack_if_name,
304                                                  sec_group_id)
305                         self.floating_ip_assoc["stack_name"] = \
306                             server_name + "-fip-assoc"
307                         template.add_floating_ip_association(
308                             self.floating_ip_assoc["stack_name"],
309                             self.floating_ip["stack_name"],
310                             port_name)
311         if self.flavor:
312             if isinstance(self.flavor, dict):
313                 self.flavor["name"] = \
314                     self.flavor.setdefault("name", self.stack_name + "-flavor")
315                 template.add_flavor(**self.flavor)
316                 self.flavor_name = self.flavor["name"]
317             else:
318                 self.flavor_name = self.flavor
319
320         if self.volume:
321             if isinstance(self.volume, dict):
322                 self.volume["name"] = \
323                     self.volume.setdefault("name", server_name + "-volume")
324                 template.add_volume(**self.volume)
325                 template.add_volume_attachment(server_name, self.volume["name"],
326                                                mountpoint=self.volume_mountpoint)
327             else:
328                 template.add_volume_attachment(server_name, self.volume,
329                                                mountpoint=self.volume_mountpoint)
330
331         template.add_server(server_name, self.image, flavor=self.flavor_name,
332                             flavors=self.context.flavors,
333                             ports=port_name_list,
334                             user=self.user,
335                             key_name=self.keypair_name,
336                             user_data=self.user_data,
337                             scheduler_hints=scheduler_hints)
338
339     def add_to_template(self, template, networks, scheduler_hints=None):
340         """adds to the template one or more servers (instances)"""
341         if self.instances == 1:
342             server_name = self.stack_name
343             self._add_instance(template, server_name, networks,
344                                scheduler_hints=scheduler_hints)
345         else:
346             # TODO(hafe) fix or remove, no test/sample for this
347             for i in range(self.instances):
348                 server_name = "%s-%d" % (self.stack_name, i)
349                 self._add_instance(template, server_name, networks,
350                                    scheduler_hints=scheduler_hints)
351
352
353 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
354     """update scheduler hints from server's placement configuration
355     TODO: this code is openstack specific and should move somewhere else
356     """
357     if placement_group.policy == "affinity":
358         if "same_host" in scheduler_hints:
359             host_list = scheduler_hints["same_host"]
360         else:
361             host_list = scheduler_hints["same_host"] = []
362     else:
363         if "different_host" in scheduler_hints:
364             host_list = scheduler_hints["different_host"]
365         else:
366             host_list = scheduler_hints["different_host"] = []
367
368     for name in added_servers:
369         if name in placement_group.members:
370             host_list.append({'get_resource': name})