Merge "Add send socket commands function"
[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
18 from collections import Mapping
19 from six.moves import range
20
21 from yardstick.common import constants as consts
22
23
24 LOG = logging.getLogger(__name__)
25
26
27 class Object(object):
28     """Base class for classes in the logical model
29     Contains common attributes and methods
30     """
31
32     def __init__(self, name, context):
33         # model identities and reference
34         self.name = name
35         self._context = context
36
37         # stack identities
38         self.stack_name = None
39         self.stack_id = None
40
41     @property
42     def dn(self):
43         """returns distinguished name for object"""
44         return self.name + "." + self._context.name
45
46
47 class PlacementGroup(Object):
48     """Class that represents a placement group in the logical model
49     Concept comes from the OVF specification. Policy should be one of
50     "availability" or "affinity (there are more but they are not supported)"
51     """
52     map = {}
53
54     def __init__(self, name, context, policy):
55         if policy not in ["affinity", "availability"]:
56             raise ValueError("placement group '%s', policy '%s' is not valid" %
57                              (name, policy))
58         self.name = name
59         self.members = set()
60         self.stack_name = context.name + "-" + name
61         self.policy = policy
62         PlacementGroup.map[name] = self
63
64     def add_member(self, name):
65         self.members.add(name)
66
67     @staticmethod
68     def get(name):
69         return PlacementGroup.map.get(name)
70
71
72 class ServerGroup(Object):     # pragma: no cover
73     """Class that represents a server group in the logical model
74     Policy should be one of "anti-affinity" or "affinity"
75     """
76     map = {}
77
78     def __init__(self, name, context, policy):
79         super(ServerGroup, self).__init__(name, context)
80         if policy not in {"affinity", "anti-affinity"}:
81             raise ValueError("server group '%s', policy '%s' is not valid" %
82                              (name, policy))
83         self.name = name
84         self.members = set()
85         self.stack_name = context.name + "-" + name
86         self.policy = policy
87         ServerGroup.map[name] = self
88
89     def add_member(self, name):
90         self.members.add(name)
91
92     @staticmethod
93     def get(name):
94         return ServerGroup.map.get(name)
95
96
97 class Router(Object):
98     """Class that represents a router in the logical model"""
99
100     def __init__(self, name, network_name, context, external_gateway_info):
101         super(Router, self).__init__(name, context)
102
103         self.stack_name = context.name + "-" + network_name + "-" + self.name
104         self.stack_if_name = self.stack_name + "-if0"
105         self.external_gateway_info = external_gateway_info
106
107
108 class Network(Object):
109     """Class that represents a network in the logical model"""
110     list = []
111
112     def __init__(self, name, context, attrs):
113         super(Network, self).__init__(name, context)
114         self.stack_name = context.name + "-" + self.name
115         self.subnet_stack_name = self.stack_name + "-subnet"
116         self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24')
117         self.enable_dhcp = attrs.get('enable_dhcp', 'true')
118         self.router = None
119         self.physical_network = attrs.get('physical_network', 'physnet1')
120         self.provider = attrs.get('provider')
121         self.segmentation_id = attrs.get('segmentation_id')
122         self.network_type = attrs.get('network_type')
123         self.port_security_enabled = attrs.get('port_security_enabled')
124         self.vnic_type = attrs.get('vnic_type', 'normal')
125         self.allowed_address_pairs = attrs.get('allowed_address_pairs', [])
126         try:
127             # we require 'null' or '' to disable setting gateway_ip
128             self.gateway_ip = attrs['gateway_ip']
129         except KeyError:
130             # default to explicit None
131             self.gateway_ip = None
132         else:
133             # null is None in YAML, so we have to convert back to string
134             if self.gateway_ip is None:
135                 self.gateway_ip = "null"
136
137         self.net_flags = attrs.get('net_flags', {})
138         if self.is_existing():
139             self.subnet = attrs.get('subnet')
140             if not self.subnet:
141                 raise Warning('No subnet set in existing netwrok!')
142         else:
143             if "external_network" in attrs:
144                 self.router = Router("router", self.name,
145                                      context, attrs["external_network"])
146             Network.list.append(self)
147
148     def is_existing(self):
149         net_is_existing = self.net_flags.get(consts.IS_EXISTING)
150         if net_is_existing and not isinstance(net_is_existing, bool):
151             raise SyntaxError('Network flags should be bool type!')
152         return net_is_existing
153
154     def is_public(self):
155         net_is_public = self.net_flags.get(consts.IS_PUBLIC)
156         if net_is_public and not isinstance(net_is_public, bool):
157             raise SyntaxError('Network flags should be bool type!')
158         return net_is_public
159
160     def has_route_to(self, network_name):
161         """determines if this network has a route to the named network"""
162         if self.router and self.router.external_gateway_info == network_name:
163             return True
164         return False
165
166     @staticmethod
167     def find_by_route_to(external_network):
168         """finds a network that has a route to the specified network"""
169         for network in Network.list:
170             if network.has_route_to(external_network):
171                 return network
172
173     @staticmethod
174     def find_external_network():
175         """return the name of an external network some network in this
176         context has a route to
177         """
178         for network in Network.list:
179             if network.router:
180                 return network.router.external_gateway_info
181         return None
182
183
184 class Server(Object):     # pragma: no cover
185     """Class that represents a server in the logical model"""
186     list = []
187
188     def __init__(self, name, context, attrs):
189         super(Server, self).__init__(name, context)
190         self.stack_name = self.name + "." + context.name
191         self.keypair_name = context.keypair_name
192         self.secgroup_name = context.secgroup_name
193         self.user = context.user
194         self.context = context
195         self.public_ip = None
196         self.private_ip = None
197         self.user_data = ''
198         self.interfaces = {}
199
200         if attrs is None:
201             attrs = {}
202
203         self.placement_groups = []
204         placement = attrs.get("placement", [])
205         placement = placement if isinstance(placement, list) else [placement]
206         for p in placement:
207             pg = PlacementGroup.get(p)
208             if not pg:
209                 raise ValueError("server '%s', placement '%s' is invalid" %
210                                  (name, p))
211             self.placement_groups.append(pg)
212             pg.add_member(self.stack_name)
213
214         self.volume = None
215         if "volume" in attrs:
216             self.volume = attrs.get("volume")
217
218         self.volume_mountpoint = None
219         if "volume_mountpoint" in attrs:
220             self.volume_mountpoint = attrs.get("volume_mountpoint")
221
222         # support servergroup attr
223         self.server_group = None
224         sg = attrs.get("server_group")
225         if sg:
226             server_group = ServerGroup.get(sg)
227             if not server_group:
228                 raise ValueError("server '%s', server_group '%s' is invalid" %
229                                  (name, sg))
230             self.server_group = server_group
231             server_group.add_member(self.stack_name)
232
233         self.instances = 1
234         if "instances" in attrs:
235             self.instances = attrs["instances"]
236
237         # dict with key network name, each item is a dict with port name and ip
238         self.network_ports = attrs.get("network_ports", {})
239         self.ports = {}
240
241         self.floating_ip = None
242         self.floating_ip_assoc = None
243         if "floating_ip" in attrs:
244             self.floating_ip = {}
245             self.floating_ip_assoc = {}
246
247         if self.floating_ip is not None:
248             ext_net = Network.find_external_network()
249             assert ext_net is not None
250             self.floating_ip["external_network"] = ext_net
251
252         self._image = None
253         if "image" in attrs:
254             self._image = attrs["image"]
255
256         self._flavor = None
257         if "flavor" in attrs:
258             self._flavor = attrs["flavor"]
259
260         self.user_data = attrs.get('user_data', '')
261         self.availability_zone = attrs.get('availability_zone')
262
263         Server.list.append(self)
264
265     def override_ip(self, network_name, port):
266         def find_port_overrides():
267             for p in ports:
268                 # p can be string or dict
269                 # we can't just use p[port['port'] in case p is a string
270                 # and port['port'] is an int?
271                 if isinstance(p, Mapping):
272                     g = p.get(port['port'])
273                     # filter out empty dicts
274                     if g:
275                         yield g
276
277         ports = self.network_ports.get(network_name, [])
278         intf = self.interfaces[port['port']]
279         for override in find_port_overrides():
280             intf['local_ip'] = override.get('local_ip', intf['local_ip'])
281             intf['netmask'] = override.get('netmask', intf['netmask'])
282             # only use the first value
283             break
284
285     @property
286     def image(self):
287         """returns a server's image name"""
288         if self._image:
289             return self._image
290         else:
291             return self._context.image
292
293     @property
294     def flavor(self):
295         """returns a server's flavor name"""
296         if self._flavor:
297             return self._flavor
298         else:
299             return self._context.flavor
300
301     def _add_instance(self, template, server_name, networks, scheduler_hints):
302         """adds to the template one server and corresponding resources"""
303         port_name_list = []
304         for network in networks:
305             # if explicit mapping skip unused networks
306             if self.network_ports:
307                 try:
308                     ports = self.network_ports[network.name]
309                 except KeyError:
310                     # no port for this network
311                     continue
312                 else:
313                     if isinstance(ports, six.string_types):
314                         # because strings are iterable we have to check specifically
315                         raise SyntaxError("network_port must be a list '{}'".format(ports))
316                     # convert port subdicts into their just port name
317                     # port subdicts are used to override Heat IP address,
318                     # but we just need the port name
319                     # we allow duplicates here and let Heat raise the error
320                     ports = [next(iter(p)) if isinstance(p, dict) else p for p in ports]
321             # otherwise add a port for every network with port name as network name
322             else:
323                 ports = [network.name]
324             net_flags = network.net_flags
325             for port in ports:
326                 port_name = "{0}-{1}-port".format(server_name, port)
327                 port_info = {"stack_name": port_name, "port": port}
328                 if net_flags:
329                     port_info['net_flags'] = net_flags
330                 self.ports.setdefault(network.name, []).append(port_info)
331                 # we can't use secgroups if port_security_enabled is False
332                 if network.port_security_enabled is False:
333                     sec_group_id = None
334                 else:
335                     # if port_security_enabled is None we still need to add to secgroup
336                     sec_group_id = self.secgroup_name
337                 # don't refactor to pass in network object, that causes JSON
338                 # circular ref encode errors
339                 template.add_port(port_name, network,
340                                   sec_group_id=sec_group_id,
341                                   provider=network.provider,
342                                   allowed_address_pairs=network.allowed_address_pairs)
343                 if network.is_public():
344                     port_name_list.insert(0, port_name)
345                 else:
346                     port_name_list.append(port_name)
347
348                 if self.floating_ip:
349                     external_network = self.floating_ip["external_network"]
350                     if network.has_route_to(external_network):
351                         self.floating_ip["stack_name"] = server_name + "-fip"
352                         template.add_floating_ip(self.floating_ip["stack_name"],
353                                                  external_network,
354                                                  port_name,
355                                                  network.router.stack_if_name,
356                                                  sec_group_id)
357                         self.floating_ip_assoc["stack_name"] = \
358                             server_name + "-fip-assoc"
359                         template.add_floating_ip_association(
360                             self.floating_ip_assoc["stack_name"],
361                             self.floating_ip["stack_name"],
362                             port_name)
363         if self.flavor:
364             if isinstance(self.flavor, dict):
365                 self.flavor["name"] = \
366                     self.flavor.setdefault("name", self.stack_name + "-flavor")
367                 template.add_flavor(**self.flavor)
368                 self.flavor_name = self.flavor["name"]
369             else:
370                 self.flavor_name = self.flavor
371
372         if self.volume:
373             if isinstance(self.volume, dict):
374                 self.volume["name"] = \
375                     self.volume.setdefault("name", server_name + "-volume")
376                 template.add_volume(**self.volume)
377                 template.add_volume_attachment(server_name, self.volume["name"],
378                                                mountpoint=self.volume_mountpoint)
379             else:
380                 template.add_volume_attachment(server_name, self.volume,
381                                                mountpoint=self.volume_mountpoint)
382
383         template.add_server(server_name, self.image, flavor=self.flavor_name,
384                             flavors=self.context.flavors, ports=port_name_list,
385                             scheduler_hints=scheduler_hints, user=self.user,
386                             key_name=self.keypair_name, user_data=self.user_data,
387                             availability_zone=self.availability_zone)
388
389     def add_to_template(self, template, networks, scheduler_hints=None):
390         """adds to the template one or more servers (instances)"""
391         if self.instances == 1:
392             server_name = self.stack_name
393             self._add_instance(template, server_name, networks,
394                                scheduler_hints=scheduler_hints)
395         else:
396             # TODO(hafe) fix or remove, no test/sample for this
397             for i in range(self.instances):
398                 server_name = "%s-%d" % (self.stack_name, i)
399                 self._add_instance(template, server_name, networks,
400                                    scheduler_hints=scheduler_hints)
401
402
403 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
404     """update scheduler hints from server's placement configuration
405     TODO: this code is openstack specific and should move somewhere else
406     """
407     if placement_group.policy == "affinity":
408         if "same_host" in scheduler_hints:
409             host_list = scheduler_hints["same_host"]
410         else:
411             host_list = scheduler_hints["same_host"] = []
412     else:
413         if "different_host" in scheduler_hints:
414             host_list = scheduler_hints["different_host"]
415         else:
416             host_list = scheduler_hints["different_host"] = []
417
418     for name in added_servers:
419         if name in placement_group.members:
420             host_list.append({'get_resource': name})