Merge "add unit test for ping"
[yardstick.git] / yardstick / benchmark / context / 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
14 import sys
15
16 from yardstick.orchestrator.heat import HeatTemplate
17
18
19 class Object(object):
20     '''Base class for classes in the logical model
21     Contains common attributes and methods
22     '''
23     def __init__(self, name, context):
24         # model identities and reference
25         self.name = name
26         self._context = context
27
28         # stack identities
29         self.stack_name = None
30         self.stack_id = None
31
32     @property
33     def dn(self):
34         '''returns distinguished name for object'''
35         return self.name + "." + self._context.name
36
37
38 class PlacementGroup(Object):
39     '''Class that represents a placement group in the logical model
40     Concept comes from the OVF specification. Policy should be one of
41     "availability" or "affinity (there are more but they are not supported)"
42     '''
43     map = {}
44
45     def __init__(self, name, context, policy):
46         if policy not in ["affinity", "availability"]:
47             raise ValueError("placement group '%s', policy '%s' is not valid" %
48                              (name, policy))
49         self.name = name
50         self.members = set()
51         self.stack_name = context.name + "-" + name
52         self.policy = policy
53         PlacementGroup.map[name] = self
54
55     def add_member(self, name):
56         self.members.add(name)
57
58     @staticmethod
59     def get(name):
60         if name in PlacementGroup.map:
61             return PlacementGroup.map[name]
62         else:
63             return None
64
65
66 class Router(Object):
67     '''Class that represents a router in the logical model'''
68     def __init__(self, name, network_name, context, external_gateway_info):
69         super(Router, self).__init__(name, context)
70
71         self.stack_name = context.name + "-" + network_name + "-" + self.name
72         self.stack_if_name = self.stack_name + "-if0"
73         self.external_gateway_info = external_gateway_info
74
75
76 class Network(Object):
77     '''Class that represents a network in the logical model'''
78     list = []
79
80     def __init__(self, name, context, attrs):
81         super(Network, self).__init__(name, context)
82         self.stack_name = context.name + "-" + self.name
83         self.subnet_stack_name = self.stack_name + "-subnet"
84         self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24')
85         self.router = None
86
87         if "external_network" in attrs:
88             self.router = Router("router", self.name,
89                                  context, attrs["external_network"])
90
91         Network.list.append(self)
92
93     def has_route_to(self, network_name):
94         '''determines if this network has a route to the named network'''
95         if self.router and self.router.external_gateway_info == network_name:
96             return True
97         return False
98
99     @staticmethod
100     def find_by_route_to(external_network):
101         '''finds a network that has a route to the specified network'''
102         for network in Network.list:
103             if network.has_route_to(external_network):
104                 return network
105
106     @staticmethod
107     def find_external_network():
108         '''return the name of an external network some network in this
109         context has a route to'''
110         for network in Network.list:
111             if network.router:
112                 return network.router.external_gateway_info
113         return None
114
115
116 class Server(Object):
117     '''Class that represents a server in the logical model'''
118     list = []
119
120     def __init__(self, name, context, attrs):
121         super(Server, self).__init__(name, context)
122         self.stack_name = context.name + "-" + self.name
123         self.keypair_name = context.keypair_name
124         self.secgroup_name = context.secgroup_name
125         self.context = context
126         self.public_ip = None
127         self.private_ip = None
128
129         if attrs is None:
130             attrs = {}
131
132         self.placement_groups = []
133         placement = attrs.get("placement", [])
134         placement = placement if type(placement) is list else [placement]
135         for p in placement:
136             pg = PlacementGroup.get(p)
137             if not pg:
138                 raise ValueError("server '%s', placement '%s' is invalid" %
139                                  (name, p))
140             self.placement_groups.append(pg)
141             pg.add_member(self.stack_name)
142
143         self.instances = 1
144         if "instances" in attrs:
145             self.instances = attrs["instances"]
146
147         # dict with key network name, each item is a dict with port name and ip
148         self.ports = {}
149
150         self.floating_ip = None
151         if "floating_ip" in attrs:
152             self.floating_ip = {}
153
154         if self.floating_ip is not None:
155             ext_net = Network.find_external_network()
156             assert ext_net is not None
157             self.floating_ip["external_network"] = ext_net
158
159         self._image = None
160         if "image" in attrs:
161             self._image = attrs["image"]
162
163         self._flavor = None
164         if "flavor" in attrs:
165             self._flavor = attrs["flavor"]
166
167         Server.list.append(self)
168
169     @property
170     def image(self):
171         '''returns a server's image name'''
172         if self._image:
173             return self._image
174         else:
175             return self._context.image
176
177     @property
178     def flavor(self):
179         '''returns a server's flavor name'''
180         if self._flavor:
181             return self._flavor
182         else:
183             return self._context.flavor
184
185     def _add_instance(self, template, server_name, networks, scheduler_hints):
186         '''adds to the template one server and corresponding resources'''
187         port_name_list = []
188         for network in networks:
189             port_name = server_name + "-" + network.name + "-port"
190             self.ports[network.name] = {"stack_name": port_name}
191             template.add_port(port_name, network.stack_name,
192                               network.subnet_stack_name,
193                               sec_group_id=self.secgroup_name)
194             port_name_list.append(port_name)
195
196             if self.floating_ip:
197                 external_network = self.floating_ip["external_network"]
198                 if network.has_route_to(external_network):
199                     self.floating_ip["stack_name"] = server_name + "-fip"
200                     template.add_floating_ip(self.floating_ip["stack_name"],
201                                              external_network,
202                                              port_name,
203                                              network.router.stack_if_name,
204                                              self.secgroup_name)
205
206         template.add_server(server_name, self.image, self.flavor,
207                             ports=port_name_list,
208                             key_name=self.keypair_name,
209                             scheduler_hints=scheduler_hints)
210
211     def add_to_template(self, template, networks, scheduler_hints=None):
212         '''adds to the template one or more servers (instances)'''
213         if self.instances == 1:
214             server_name = "%s-%s" % (template.name, self.name)
215             self._add_instance(template, server_name, networks,
216                                scheduler_hints=scheduler_hints)
217         else:
218             for i in range(self.instances):
219                 server_name = "%s-%s-%d" % (template.name, self.name, i)
220                 self._add_instance(template, server_name, networks,
221                                    scheduler_hints=scheduler_hints)
222
223
224 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
225     ''' update scheduler hints from server's placement configuration
226     TODO: this code is openstack specific and should move somewhere else
227     '''
228     if placement_group.policy == "affinity":
229         if "same_host" in scheduler_hints:
230             host_list = scheduler_hints["same_host"]
231         else:
232             host_list = scheduler_hints["same_host"] = []
233     else:
234         if "different_host" in scheduler_hints:
235             host_list = scheduler_hints["different_host"]
236         else:
237             host_list = scheduler_hints["different_host"] = []
238
239     for name in added_servers:
240         if name in placement_group.members:
241             host_list.append({'get_resource': name})
242
243
244 class Context(object):
245     '''Class that represents a context in the logical model'''
246     list = []
247
248     def __init__(self):
249         self.name = None
250         self.stack = None
251         self.networks = []
252         self.servers = []
253         self.placement_groups = []
254         self.keypair_name = None
255         self.secgroup_name = None
256         self._server_map = {}
257         self._image = None
258         self._flavor = None
259         self._user = None
260         self.template_file = None
261         self.heat_parameters = None
262         Context.list.append(self)
263
264     def init(self, attrs):
265         '''initializes itself from the supplied arguments'''
266         self.name = attrs["name"]
267
268         if "user" in attrs:
269             self._user = attrs["user"]
270
271         if "heat_template" in attrs:
272             self.template_file = attrs["heat_template"]
273             self.heat_parameters = attrs.get("heat_parameters", None)
274             return
275
276         self.keypair_name = self.name + "-key"
277         self.secgroup_name = self.name + "-secgroup"
278
279         if "image" in attrs:
280             self._image = attrs["image"]
281
282         if "flavor" in attrs:
283             self._flavor = attrs["flavor"]
284
285         if "placement_groups" in attrs:
286             for name, pgattrs in attrs["placement_groups"].items():
287                 pg = PlacementGroup(name, self, pgattrs["policy"])
288                 self.placement_groups.append(pg)
289
290         for name, netattrs in attrs["networks"].items():
291             network = Network(name, self, netattrs)
292             self.networks.append(network)
293
294         for name, serverattrs in attrs["servers"].items():
295             server = Server(name, self, serverattrs)
296             self.servers.append(server)
297             self._server_map[server.dn] = server
298
299     @property
300     def image(self):
301         '''returns application's default image name'''
302         return self._image
303
304     @property
305     def flavor(self):
306         '''returns application's default flavor name'''
307         return self._flavor
308
309     @property
310     def user(self):
311         '''return login user name corresponding to image'''
312         return self._user
313
314     def _add_resources_to_template(self, template):
315         '''add to the template the resources represented by this context'''
316         template.add_keypair(self.keypair_name)
317         template.add_security_group(self.secgroup_name)
318
319         for network in self.networks:
320             template.add_network(network.stack_name)
321             template.add_subnet(network.subnet_stack_name, network.stack_name,
322                                 network.subnet_cidr)
323
324             if network.router:
325                 template.add_router(network.router.stack_name,
326                                     network.router.external_gateway_info,
327                                     network.subnet_stack_name)
328                 template.add_router_interface(network.router.stack_if_name,
329                                               network.router.stack_name,
330                                               network.subnet_stack_name)
331
332         # create a list of servers sorted by increasing no of placement groups
333         list_of_servers = sorted(self.servers,
334                                  key=lambda s: len(s.placement_groups))
335
336         #
337         # add servers with scheduler hints derived from placement groups
338         #
339
340         # create list of servers with availability policy
341         availability_servers = []
342         for server in list_of_servers:
343             for pg in server.placement_groups:
344                 if pg.policy == "availability":
345                     availability_servers.append(server)
346                     break
347
348         # add servers with availability policy
349         added_servers = []
350         for server in availability_servers:
351             scheduler_hints = {}
352             for pg in server.placement_groups:
353                 update_scheduler_hints(scheduler_hints, added_servers, pg)
354             server.add_to_template(template, self.networks, scheduler_hints)
355             added_servers.append(server.stack_name)
356
357         # create list of servers with affinity policy
358         affinity_servers = []
359         for server in list_of_servers:
360             for pg in server.placement_groups:
361                 if pg.policy == "affinity":
362                     affinity_servers.append(server)
363                     break
364
365         # add servers with affinity policy
366         for server in affinity_servers:
367             if server.stack_name in added_servers:
368                 continue
369             scheduler_hints = {}
370             for pg in server.placement_groups:
371                 update_scheduler_hints(scheduler_hints, added_servers, pg)
372             server.add_to_template(template, self.networks, scheduler_hints)
373             added_servers.append(server.stack_name)
374
375         # add remaining servers with no placement group configured
376         for server in list_of_servers:
377             if len(server.placement_groups) == 0:
378                 server.add_to_template(template, self.networks, {})
379
380     def deploy(self):
381         '''deploys template into a stack using cloud'''
382         print "Deploying context '%s'" % self.name
383
384         heat_template = HeatTemplate(self.name, self.template_file,
385                                      self.heat_parameters)
386
387         if self.template_file is None:
388             self._add_resources_to_template(heat_template)
389
390         try:
391             self.stack = heat_template.create()
392         except KeyboardInterrupt:
393             sys.exit("\nStack create interrupted")
394         except RuntimeError as err:
395             sys.exit("error: failed to deploy stack: '%s'" % err.args)
396         except Exception as err:
397             sys.exit("error: failed to deploy stack: '%s'" % err)
398
399         # copy some vital stack output into server objects
400         for server in self.servers:
401             if len(server.ports) > 0:
402                 # TODO(hafe) can only handle one internal network for now
403                 port = server.ports.values()[0]
404                 server.private_ip = self.stack.outputs[port["stack_name"]]
405
406             if server.floating_ip:
407                 server.public_ip = \
408                     self.stack.outputs[server.floating_ip["stack_name"]]
409
410         print "Context '%s' deployed" % self.name
411
412     def undeploy(self):
413         '''undeploys stack from cloud'''
414         if self.stack:
415             print "Undeploying context '%s'" % self.name
416             self.stack.delete()
417             self.stack = None
418             print "Context '%s' undeployed" % self.name
419
420     @staticmethod
421     def get_server_by_name(dn):
422         '''lookup server object by DN
423
424         dn is a distinguished name including the context name'''
425         if "." not in dn:
426             raise ValueError("dn '%s' is malformed" % dn)
427
428         for context in Context.list:
429             if dn in context._server_map:
430                 return context._server_map[dn]
431
432         return None
433
434     @staticmethod
435     def get_context_by_name(name):
436         for context in Context.list:
437             if name == context.name:
438                 return context
439         return None
440
441     @staticmethod
442     def get_server(attr_name):
443         '''lookup server object by name from context
444         attr_name: either a name for a server created by yardstick or a dict
445         with attribute name mapping when using external heat templates
446         '''
447         if type(attr_name) is dict:
448             cname = attr_name["name"].split(".")[1]
449             context = Context.get_context_by_name(cname)
450             if context is None:
451                 raise ValueError("context not found for server '%s'" %
452                                  attr_name["name"])
453
454             public_ip = None
455             private_ip = None
456             if "public_ip_attr" in attr_name:
457                 public_ip = context.stack.outputs[attr_name["public_ip_attr"]]
458             if "private_ip_attr" in attr_name:
459                 private_ip = context.stack.outputs[
460                     attr_name["private_ip_attr"]]
461
462             # Create a dummy server instance for holding the *_ip attributes
463             server = Server(attr_name["name"].split(".")[0], context, {})
464             server.public_ip = public_ip
465             server.private_ip = private_ip
466             return server
467         else:
468             return Context.get_server_by_name(attr_name)