Fix: Missing interfaces for son-monitor are forced to be created early in the code
[osm/vim-emu.git] / src / emuvim / dcemulator / node.py
1 """
2 Copyright (c) 2015 SONATA-NFV and Paderborn University
3 ALL RIGHTS RESERVED.
4
5 Licensed under the Apache License, Version 2.0 (the "License");
6 you may not use this file except in compliance with the License.
7 You may obtain a copy of the License at
8
9 http://www.apache.org/licenses/LICENSE-2.0
10
11 Unless required by applicable law or agreed to in writing, software
12 distributed under the License is distributed on an "AS IS" BASIS,
13 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 See the License for the specific language governing permissions and
15 limitations under the License.
16
17 Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
18 nor the names of its contributors may be used to endorse or promote
19 products derived from this software without specific prior written
20 permission.
21
22 This work has been performed in the framework of the SONATA project,
23 funded by the European Commission under Grant number 671517 through
24 the Horizon 2020 and 5G-PPP programmes. The authors would like to
25 acknowledge the contributions of their colleagues of the SONATA
26 partner consortium (www.sonata-nfv.eu).
27 """
28 from mininet.node import Docker, OVSBridge
29 from mininet.link import Link
30 from emuvim.dcemulator.resourcemodel import NotEnoughResourcesAvailable
31 import logging
32
33
34 LOG = logging.getLogger("dcemulator.node")
35 LOG.setLevel(logging.DEBUG)
36
37
38 DCDPID_BASE = 1000 # start of switch dpid's used for data center switches
39 EXTSAPDPID_BASE = 2000 # start of switch dpid's used for external SAP switches
40
41 class EmulatorCompute(Docker):
42 """
43 Emulator specific compute node class.
44 Inherits from Containernet's Docker host class.
45 Represents a single container connected to a (logical)
46 data center.
47 We can add emulator specific helper functions to it.
48 """
49
50 def __init__(
51 self, name, dimage, **kwargs):
52 self.datacenter = kwargs.get("datacenter") # pointer to current DC
53 self.flavor_name = kwargs.get("flavor_name")
54 LOG.debug("Starting compute instance %r in data center %r" % (name, str(self.datacenter)))
55 # call original Docker.__init__
56 Docker.__init__(self, name, dimage, **kwargs)
57
58 def getNetworkStatus(self):
59 """
60 Helper method to receive information about the virtual networks
61 this compute instance is connected to.
62 """
63 # get all links and find dc switch interface
64 networkStatusList = []
65 for i in self.intfList():
66 vnf_name = self.name
67 vnf_interface = str(i)
68 dc_port_name = self.datacenter.net.find_connected_dc_interface(vnf_name, vnf_interface)
69 # format list of tuples (name, Ip, MAC, isUp, status, dc_portname)
70 intf_dict = {'intf_name': str(i), 'ip': "{0}/{1}".format(i.IP(), i.prefixLen), 'netmask': i.prefixLen, 'mac': i.MAC(), 'up': i.isUp(), 'status': i.status(), 'dc_portname': dc_port_name}
71 networkStatusList.append(intf_dict)
72
73 return networkStatusList
74
75 def getStatus(self):
76 """
77 Helper method to receive information about this compute instance.
78 """
79 status = {}
80 status["name"] = self.name
81 status["network"] = self.getNetworkStatus()
82 status["docker_network"] = self.dcinfo['NetworkSettings']['IPAddress']
83 status["image"] = self.dimage
84 status["flavor_name"] = self.flavor_name
85 status["cpu_quota"] = self.resources.get('cpu_quota')
86 status["cpu_period"] = self.resources.get('cpu_period')
87 status["cpu_shares"] = self.resources.get('cpu_shares')
88 status["cpuset"] = self.resources.get('cpuset_cpus')
89 status["mem_limit"] = self.resources.get('mem_limit')
90 status["memswap_limit"] = self.resources.get('memswap_limit')
91 status["state"] = self.dcli.inspect_container(self.dc)["State"]
92 status["id"] = self.dcli.inspect_container(self.dc)["Id"]
93 status["short_id"] = self.dcli.inspect_container(self.dc)["Id"][:12]
94 status["hostname"] = self.dcli.inspect_container(self.dc)["Config"]['Hostname']
95 status["datacenter"] = (None if self.datacenter is None
96 else self.datacenter.label)
97
98 return status
99
100
101 class EmulatorExtSAP(object):
102 """
103 Emulator specific class that defines an external service access point (SAP) for the service.
104 Inherits from Containernet's OVSBridge class.
105 Represents a single OVS switch connected to a (logical)
106 data center.
107 We can add emulator specific helper functions to it.
108 """
109
110 def __init__(self, sap_name, sap_net, datacenter, **kwargs):
111
112 self.datacenter = datacenter # pointer to current DC
113 self.net = self.datacenter.net
114 self.name = sap_name
115
116 LOG.debug("Starting ext SAP instance %r in data center %r" % (sap_name, str(self.datacenter)))
117
118 # create SAP as separate OVS switch with an assigned ip address
119 self.ip = str(sap_net[1]) + '/' + str(sap_net.prefixlen)
120 self.subnet = sap_net
121 # allow connection to the external internet through the host
122 params = dict(NAT=True)
123 self.switch = self.net.addExtSAP(sap_name, self.ip, dpid=hex(self._get_next_extSAP_dpid())[2:], **params)
124 self.switch.start()
125
126 def _get_next_extSAP_dpid(self):
127 global EXTSAPDPID_BASE
128 EXTSAPDPID_BASE += 1
129 return EXTSAPDPID_BASE
130
131 def getNetworkStatus(self):
132 """
133 Helper method to receive information about the virtual networks
134 this compute instance is connected to.
135 """
136 # get all links and find dc switch interface
137 networkStatusList = []
138 for i in self.switch.intfList():
139 vnf_name = self.name
140 vnf_interface = str(i)
141 if vnf_interface == 'lo':
142 continue
143 dc_port_name = self.datacenter.net.find_connected_dc_interface(vnf_name, vnf_interface)
144 # format list of tuples (name, Ip, MAC, isUp, status, dc_portname)
145 intf_dict = {'intf_name': str(i), 'ip': self.ip, 'netmask': i.prefixLen, 'mac': i.MAC(), 'up': i.isUp(), 'status': i.status(), 'dc_portname': dc_port_name}
146 networkStatusList.append(intf_dict)
147
148 return networkStatusList
149
150 def getStatus(self):
151 return {
152 "name": self.switch.name,
153 "datacenter": self.datacenter.name,
154 "network": self.getNetworkStatus()
155 }
156
157 class Datacenter(object):
158 """
159 Represents a logical data center to which compute resources
160 (Docker containers) can be added at runtime.
161
162 Will also implement resource bookkeeping in later versions.
163 """
164
165 DC_COUNTER = 1
166
167 def __init__(self, label, metadata={}, resource_log_path=None):
168 self.net = None # DCNetwork to which we belong
169 # each node (DC) has a short internal name used by Mininet
170 # this is caused by Mininets naming limitations for swtiches etc.
171 self.name = "dc%d" % Datacenter.DC_COUNTER
172 Datacenter.DC_COUNTER += 1
173 # use this for user defined names that can be longer than self.name
174 self.label = label
175 # dict to store arbitrary metadata (e.g. latitude and longitude)
176 self.metadata = metadata
177 # path to which resource information should be logged (e.g. for experiments). None = no logging
178 self.resource_log_path = resource_log_path
179 # first prototype assumes one "bigswitch" per DC
180 self.switch = None
181 # keep track of running containers
182 self.containers = {}
183 # keep track of attached external access points
184 self.extSAPs = {}
185 # pointer to assigned resource model
186 self._resource_model = None
187
188 def __repr__(self):
189 return self.label
190
191 def _get_next_dc_dpid(self):
192 global DCDPID_BASE
193 DCDPID_BASE += 1
194 return DCDPID_BASE
195
196 def create(self):
197 """
198 Each data center is represented by a single switch to which
199 compute resources can be connected at run time.
200
201 TODO: This will be changed in the future to support multiple networks
202 per data center
203 """
204 self.switch = self.net.addSwitch(
205 "%s.s1" % self.name, dpid=hex(self._get_next_dc_dpid())[2:])
206 LOG.debug("created data center switch: %s" % str(self.switch))
207
208 def start(self):
209 pass
210
211 def startCompute(self, name, image=None, command=None, network=None, flavor_name="tiny", **params):
212 """
213 Create a new container as compute resource and connect it to this
214 data center.
215 :param name: name (string)
216 :param image: image name (string)
217 :param command: command (string)
218 :param network: networks list({"ip": "10.0.0.254/8"}, {"ip": "11.0.0.254/24"})
219 :param flavor_name: name of the flavor for this compute container
220 :return:
221 """
222 assert name is not None
223 # no duplications
224 if name in [c.name for c in self.net.getAllContainers()]:
225 raise Exception("Container with name %s already exists." % name)
226 # set default parameter
227 if image is None:
228 image = "ubuntu:trusty"
229 if network is None:
230 network = {} # {"ip": "10.0.0.254/8"}
231 if isinstance(network, dict):
232 network = [network] # if we have only one network, put it in a list
233 if isinstance(network, list):
234 if len(network) < 1:
235 network.append({})
236
237 # apply hard-set resource limits=0
238 cpu_percentage = params.get('cpu_percent')
239 if cpu_percentage:
240 params['cpu_period'] = self.net.cpu_period
241 params['cpu_quota'] = self.net.cpu_period * float(cpu_percentage)
242
243 # create the container
244 d = self.net.addDocker(
245 "%s" % (name),
246 dimage=image,
247 dcmd=command,
248 datacenter=self,
249 flavor_name=flavor_name,
250 environment = {'VNF_NAME':name},
251 **params
252 )
253
254
255
256 # apply resource limits to container if a resource model is defined
257 if self._resource_model is not None:
258 try:
259 self._resource_model.allocate(d)
260 self._resource_model.write_allocation_log(d, self.resource_log_path)
261 except NotEnoughResourcesAvailable as ex:
262 LOG.warning("Allocation of container %r was blocked by resource model." % name)
263 LOG.info(ex.message)
264 # ensure that we remove the container
265 self.net.removeDocker(name)
266 return None
267
268 # connect all given networks
269 # if no --net option is given, network = [{}], so 1 empty dict in the list
270 # this results in 1 default interface with a default ip address
271 for nw in network:
272 # clean up network configuration (e.g. RTNETLINK does not allow ':' in intf names
273 if nw.get("id") is not None:
274 nw["id"] = self._clean_ifname(nw["id"])
275 # TODO we cannot use TCLink here (see: https://github.com/mpeuster/containernet/issues/3)
276 self.net.addLink(d, self.switch, params1=nw, cls=Link, intfName1=nw.get('id'))
277 # do bookkeeping
278 self.containers[name] = d
279
280 return d # we might use UUIDs for naming later on
281
282 def stopCompute(self, name):
283 """
284 Stop and remove a container from this data center.
285 """
286 assert name is not None
287 if name not in self.containers:
288 raise Exception("Container with name %s not found." % name)
289 LOG.debug("Stopping compute instance %r in data center %r" % (name, str(self)))
290
291 # stop the monitored metrics
292 if self.net.monitor_agent is not None:
293 self.net.monitor_agent.stop_metric(name)
294
295 # call resource model and free resources
296 if self._resource_model is not None:
297 self._resource_model.free(self.containers[name])
298 self._resource_model.write_free_log(self.containers[name], self.resource_log_path)
299
300 # remove links
301 self.net.removeLink(
302 link=None, node1=self.containers[name], node2=self.switch)
303
304 # remove container
305 self.net.removeDocker("%s" % (name))
306 del self.containers[name]
307
308 return True
309
310 def attachExternalSAP(self, sap_name, sap_net, **params):
311 extSAP = EmulatorExtSAP(sap_name, sap_net, self, **params)
312 # link SAP to the DC switch
313 self.net.addLink(extSAP.switch, self.switch, cls=Link)
314 self.extSAPs[sap_name] = extSAP
315
316 def removeExternalSAP(self, sap_name):
317 sap_switch = self.extSAPs[sap_name].switch
318 #sap_switch = self.net.getNodeByName(sap_name)
319 # remove link of SAP to the DC switch
320 self.net.removeLink(link=None, node1=sap_switch, node2=self.switch)
321 self.net.removeExtSAP(sap_name)
322 del self.extSAPs[sap_name]
323
324 def listCompute(self):
325 """
326 Return a list of all running containers assigned to this
327 data center.
328 """
329 return list(self.containers.itervalues())
330
331 def listExtSAPs(self):
332 """
333 Return a list of all external SAPs assigned to this
334 data center.
335 """
336 return list(self.extSAPs.itervalues())
337
338 def getStatus(self):
339 """
340 Return a dict with status information about this DC.
341 """
342 container_list = [name for name in self.containers]
343 ext_saplist = [sap_name for sap_name in self.extSAPs]
344 return {
345 "label": self.label,
346 "internalname": self.name,
347 "switch": self.switch.name,
348 "n_running_containers": len(self.containers),
349 "metadata": self.metadata,
350 "vnf_list" : container_list,
351 "ext SAP list" : ext_saplist
352 }
353
354 def assignResourceModel(self, rm):
355 """
356 Assign a resource model to this DC.
357 :param rm: a BaseResourceModel object
358 :return:
359 """
360 if self._resource_model is not None:
361 raise Exception("There is already an resource model assigned to this DC.")
362 self._resource_model = rm
363 self.net.rm_registrar.register(self, rm)
364 LOG.info("Assigned RM: %r to DC: %r" % (rm, self))
365
366 @staticmethod
367 def _clean_ifname(name):
368 """
369 Cleans up given string to be a
370 RTNETLINK compatible interface name.
371 :param name: string
372 :return: string
373 """
374 if name is None:
375 return "if0"
376 name = name.replace(":", "-")
377 name = name.replace(" ", "-")
378 name = name.replace(".", "-")
379 name = name.replace("_", "-")
380 return name
381