initial implementation of vimconn_fos for Eclipse fog05 VIM
[osm/RO.git] / osm_ro / vimconn_fos.py
1 # -*- coding: utf-8 -*-
2
3 ##
4 # Copyright 2019 ADLINK Technology Inc..
5 # This file is part of ETSI OSM
6 # All Rights Reserved.
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License"); you may
9 # not use this file except in compliance with the License. You may obtain
10 # a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17 # License for the specific language governing permissions and limitations
18 # under the License.
19 #
20 #
21
22 """
23 Eclipse fog05 connector, implements methods to interact with fog05 using REST Client + REST Proxy
24
25 Manages LXD containers on x86_64 by default, currently missing EPA and VF/PF
26 Support config dict:
27 - arch : cpu architecture for the VIM
28 - hypervisor: virtualization technology supported by the VIM, can
29 can be one of: LXD, KVM, BARE, XEN, DOCKER, MCU
30 the selected VIM need to have at least a node with support
31 for the selected hypervisor
32
33 """
34 __author__="Gabriele Baldoni"
35 __date__ ="$13-may-2019 10:35:12$"
36
37 import uuid
38 import socket
39 import struct
40 import vimconn
41 import random
42 import yaml
43 from functools import partial
44 from fog05rest import FIMAPI
45 from fog05rest import fimerrors
46
47
48 class vimconnector(vimconn.vimconnector):
49 def __init__(self, uuid, name, tenant_id, tenant_name, url, url_admin=None, user=None, passwd=None, log_level=None,
50 config={}, persistent_info={}):
51 """Constructor of VIM
52 Params:
53 'uuid': id asigned to this VIM
54 'name': name assigned to this VIM, can be used for logging
55 'tenant_id', 'tenant_name': (only one of them is mandatory) VIM tenant to be used
56 'url_admin': (optional), url used for administrative tasks
57 'user', 'passwd': credentials of the VIM user
58 'log_level': provider if it should use a different log_level than the general one
59 'config': dictionary with extra VIM information. This contains a consolidate version of general VIM config
60 at creation and particular VIM config at teh attachment
61 'persistent_info': dict where the class can store information that will be available among class
62 destroy/creation cycles. This info is unique per VIM/credential. At first call it will contain an
63 empty dict. Useful to store login/tokens information for speed up communication
64
65 Returns: Raise an exception is some needed parameter is missing, but it must not do any connectivity
66 check against the VIM
67 """
68
69 vimconn.vimconnector.__init__(self, uuid, name, tenant_id, tenant_name, url, url_admin, user, passwd, log_level,
70 config, persistent_info)
71
72 self.logger.debug('vimconn_fos init with config: {}'.format(config))
73 self.arch = config.get('arch', 'x86_64')
74 self.hv = config.get('hypervisor', 'LXD')
75 self.nodes = config.get('nodes', [])
76 self.fdu_node_map = {}
77 self.fos_api = FIMAPI(locator=self.url)
78
79
80 def __get_ip_range(self, first, count):
81 int_first = struct.unpack('!L', socket.inet_aton(first))[0]
82 int_last = int_first + count
83 last = socket.inet_ntoa(struct.pack('!L', int_last))
84 return (first, last)
85
86 def __name_filter(self, desc, filter_name=None):
87 if filter_name is None:
88 return True
89 return desc.get('name') == filter_name
90
91 def __id_filter(self, desc, filter_id=None):
92 if filter_id is None:
93 return True
94 return desc.get('uuid') == filter_id
95
96 def __checksum_filter(self, desc, filter_checksum=None):
97 if filter_checksum is None:
98 return True
99 return desc.get('checksum') == filter_checksum
100
101 def check_vim_connectivity(self):
102 """Checks VIM can be reached and user credentials are ok.
103 Returns None if success or raised vimconnConnectionException, vimconnAuthException, ...
104 """
105 try:
106 self.fos_api.check()
107 return None
108 except fimerrors.FIMAuthExcetpion as fae:
109 raise vimconn.vimconnAuthException("Unable to authenticate to the VIM. Error {}".format(fae))
110 except Exception as e:
111 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
112
113 def new_network(self, net_name, net_type, ip_profile=None, shared=False, vlan=None):
114 """Adds a tenant network to VIM
115 Params:
116 'net_name': name of the network
117 'net_type': one of:
118 'bridge': overlay isolated network
119 'data': underlay E-LAN network for Passthrough and SRIOV interfaces
120 'ptp': underlay E-LINE network for Passthrough and SRIOV interfaces.
121 'ip_profile': is a dict containing the IP parameters of the network
122 'ip_version': can be "IPv4" or "IPv6" (Currently only IPv4 is implemented)
123 'subnet_address': ip_prefix_schema, that is X.X.X.X/Y
124 'gateway_address': (Optional) ip_schema, that is X.X.X.X
125 'dns_address': (Optional) comma separated list of ip_schema, e.g. X.X.X.X[,X,X,X,X]
126 'dhcp_enabled': True or False
127 'dhcp_start_address': ip_schema, first IP to grant
128 'dhcp_count': number of IPs to grant.
129 'shared': if this network can be seen/use by other tenants/organization
130 'vlan': in case of a data or ptp net_type, the intended vlan tag to be used for the network
131 Returns the network identifier on success or raises and exception on failure
132 """
133 self.logger.debug('new_network: {}'.format(locals()))
134 if net_type in ['data','ptp']:
135 raise vimconn.vimconnNotImplemented('{} type of network not supported'.format(net_type))
136
137 net_uuid = '{}'.format(uuid.uuid4())
138 desc = {
139 'uuid':net_uuid,
140 'name':net_name,
141 'net_type':'ELAN',
142 'is_mgmt':False
143 }
144
145 if ip_profile is not None:
146 ip = {}
147 if ip_profile.get('ip_version') == 'IPv4':
148 ip_info = {}
149 ip_range = self.__get_ip_range(ip_profile.get('dhcp_start_address'), ip_profile.get('dhcp_count'))
150 dhcp_range = '{},{}'.format(ip_range[0],ip_range[1])
151 ip.update({'subnet':ip_profile.get('subnet_address')})
152 ip.update({'dns':ip_profile.get('dns', None)})
153 ip.update({'dhcp_enable':ip_profile.get('dhcp_enabled', False)})
154 ip.update({'dhcp_range': dhcp_range})
155 ip.update({'gateway':ip_profile.get('gateway_address', None)})
156 desc.update({'ip_configuration':ip_info})
157 else:
158 raise vimconn.vimconnNotImplemented('IPV6 network is not implemented at VIM')
159 desc.update({'ip_configuration':ip})
160 self.logger.debug('VIM new_network args: {} - Generated Eclipse fog05 Descriptor {}'.format(locals(), desc))
161 try:
162 self.fos_api.network.add_network(desc)
163 except fimerrors.FIMAResouceExistingException as free:
164 raise vimconn.vimconnConflictException("Network already exists at VIM. Error {}".format(free))
165 except Exception as e:
166 raise vimconn.vimconnException("Unable to create network {}. Error {}".format(net_name, e))
167 # No way from the current rest service to get the actual error, most likely it will be an already existing error
168 return net_uuid
169
170 def get_network_list(self, filter_dict={}):
171 """Obtain tenant networks of VIM
172 Params:
173 'filter_dict' (optional) contains entries to return only networks that matches ALL entries:
174 name: string => returns only networks with this name
175 id: string => returns networks with this VIM id, this imply returns one network at most
176 shared: boolean >= returns only networks that are (or are not) shared
177 tenant_id: sting => returns only networks that belong to this tenant/project
178 ,#(not used yet) admin_state_up: boolean => returns only networks that are (or are not) in admin state active
179 #(not used yet) status: 'ACTIVE','ERROR',... => filter networks that are on this status
180 Returns the network list of dictionaries. each dictionary contains:
181 'id': (mandatory) VIM network id
182 'name': (mandatory) VIM network name
183 'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
184 'network_type': (optional) can be 'vxlan', 'vlan' or 'flat'
185 'segmentation_id': (optional) in case network_type is vlan or vxlan this field contains the segmentation id
186 'error_msg': (optional) text that explains the ERROR status
187 other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
188 List can be empty if no network map the filter_dict. Raise an exception only upon VIM connectivity,
189 authorization, or some other unspecific error
190 """
191 self.logger.debug('get_network_list: {}'.format(filter_dict))
192 res = []
193 try:
194 nets = self.fos_api.network.list()
195 except Exception as e:
196 raise vimconn.vimconnConnectionException("Cannot get network list from VIM, connection error. Error {}".format(e))
197
198 filters = [
199 partial(self.__name_filter, filter_name=filter_dict.get('name')),
200 partial(self.__id_filter,filter_id=filter_dict.get('id'))
201 ]
202
203 r1 = []
204
205 for n in nets:
206 match = True
207 for f in filters:
208 match = match and f(n)
209 if match:
210 r1.append(n)
211
212 for n in r1:
213 osm_net = {
214 'id':n.get('uuid'),
215 'name':n.get('name'),
216 'status':'ACTIVE'
217 }
218 res.append(osm_net)
219 return res
220
221 def get_network(self, net_id):
222 """Obtain network details from the 'net_id' VIM network
223 Return a dict that contains:
224 'id': (mandatory) VIM network id, that is, net_id
225 'name': (mandatory) VIM network name
226 'status': (mandatory) can be 'ACTIVE', 'INACTIVE', 'DOWN', 'BUILD', 'ERROR', 'VIM_ERROR', 'OTHER'
227 'error_msg': (optional) text that explains the ERROR status
228 other VIM specific fields: (optional) whenever possible using the same naming of filter_dict param
229 Raises an exception upon error or when network is not found
230 """
231 self.logger.debug('get_network: {}'.format(net_id))
232 res = self.get_network_list(filter_dict={'id':net_id})
233 if len(res) == 0:
234 raise vimconn.vimconnNotFoundException("Network {} not found at VIM".format(net_id))
235 return res[0]
236
237 def delete_network(self, net_id):
238 """Deletes a tenant network from VIM
239 Returns the network identifier or raises an exception upon error or when network is not found
240 """
241 self.logger.debug('delete_network: {}'.format(net_id))
242 try:
243 self.fos_api.network.remove_network(net_id)
244 except fimerrors.FIMNotFoundException as fnfe:
245 raise vimconn.vimconnNotFoundException("Network {} not found at VIM (already deleted?). Error {}".format(net_id, fnfe))
246 except Exception as e:
247 raise vimconn.vimconnException("Cannot delete network {} from VIM. Error {}".format(net_id, e))
248 return net_id
249
250 def refresh_nets_status(self, net_list):
251 """Get the status of the networks
252 Params:
253 'net_list': a list with the VIM network id to be get the status
254 Returns a dictionary with:
255 'net_id': #VIM id of this network
256 status: #Mandatory. Text with one of:
257 # DELETED (not found at vim)
258 # VIM_ERROR (Cannot connect to VIM, authentication problems, VIM response error, ...)
259 # OTHER (Vim reported other status not understood)
260 # ERROR (VIM indicates an ERROR status)
261 # ACTIVE, INACTIVE, DOWN (admin down),
262 # BUILD (on building process)
263 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
264 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
265 'net_id2': ...
266 """
267 self.logger.debug('Refeshing network status with args: {}'.format(locals()))
268 r = {}
269 for n in net_list:
270 try:
271 osm_n = self.get_network(n)
272 r.update({
273 osm_n.get('id'):{'status':osm_n.get('status')}
274 })
275 except vimconn.vimconnNotFoundException:
276 r.update({
277 n:{'status':'VIM_ERROR'}
278 })
279 return r
280
281 def get_flavor(self, flavor_id):
282 """Obtain flavor details from the VIM
283 Returns the flavor dict details {'id':<>, 'name':<>, other vim specific }
284 Raises an exception upon error or if not found
285 """
286 self.logger.debug('VIM get_flavor with args: {}'.format(locals()))
287 try:
288 r = self.fos_api.flavor.get(flavor_id)
289 except Exception as e:
290 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
291 if r is None:
292 raise vimconn.vimconnNotFoundException("Flavor not found at VIM")
293 return {'id':r.get('uuid'), 'name':r.get('name'), 'fos':r}
294
295 def get_flavor_id_from_data(self, flavor_dict):
296 """Obtain flavor id that match the flavor description
297 Params:
298 'flavor_dict': dictionary that contains:
299 'disk': main hard disk in GB
300 'ram': meomry in MB
301 'vcpus': number of virtual cpus
302 #TODO: complete parameters for EPA
303 Returns the flavor_id or raises a vimconnNotFoundException
304 """
305 self.logger.debug('VIM get_flavor_id_from_data with args : {}'.format(locals()))
306
307 try:
308 flvs = self.fos_api.flavor.list()
309 except Exception as e:
310 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
311 r = [x.get('uuid') for x in flvs if (x.get('cpu_min_count') == flavor_dict.get('vcpus') and x.get('ram_size_mb') == flavor_dict.get('ram') and x.get('storage_size_gb') == flavor_dict.get('disk'))]
312 if len(r) == 0:
313 raise vimconn.vimconnNotFoundException ( "No flavor found" )
314 return r[0]
315
316 def new_flavor(self, flavor_data):
317 """Adds a tenant flavor to VIM
318 flavor_data contains a dictionary with information, keys:
319 name: flavor name
320 ram: memory (cloud type) in MBytes
321 vpcus: cpus (cloud type)
322 extended: EPA parameters
323 - numas: #items requested in same NUMA
324 memory: number of 1G huge pages memory
325 paired-threads|cores|threads: number of paired hyperthreads, complete cores OR individual threads
326 interfaces: # passthrough(PT) or SRIOV interfaces attached to this numa
327 - name: interface name
328 dedicated: yes|no|yes:sriov; for PT, SRIOV or only one SRIOV for the physical NIC
329 bandwidth: X Gbps; requested guarantee bandwidth
330 vpci: requested virtual PCI address
331 disk: disk size
332 is_public:
333 #TODO to concrete
334 Returns the flavor identifier"""
335 self.logger.debug('VIM new_flavor with args: {}'.format(locals()))
336 flv_id = '{}'.format(uuid.uuid4())
337 desc = {
338 'uuid':flv_id,
339 'name':flavor_data.get('name'),
340 'cpu_arch': self.arch,
341 'cpu_min_count': flavor_data.get('vcpus'),
342 'cpu_min_freq': 0.0,
343 'ram_size_mb':float(flavor_data.get('ram')),
344 'storage_size_gb':float(flavor_data.get('disk'))
345 }
346 try:
347 self.fos_api.flavor.add(desc)
348 except fimerrors.FIMAResouceExistingException as free:
349 raise vimconn.vimconnConflictException("Flavor {} already exist at VIM. Error {}".format(flv_id, free))
350 except Exception as e:
351 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
352 return flv_id
353
354
355 def delete_flavor(self, flavor_id):
356 """Deletes a tenant flavor from VIM identify by its id
357 Returns the used id or raise an exception"""
358 try:
359 self.fos_api.flavor.remove(flavor_id)
360 except fimerrors.FIMNotFoundException as fnfe:
361 raise vimconn.vimconnNotFoundException("Flavor {} not found at VIM (already deleted?). Error {}".format(flavor_id, fnfe))
362 except Exception as e:
363 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
364 return flavor_id
365
366 def new_image(self, image_dict):
367 """ Adds a tenant image to VIM. imge_dict is a dictionary with:
368 name: name
369 disk_format: qcow2, vhd, vmdk, raw (by default), ...
370 location: path or URI
371 public: "yes" or "no"
372 metadata: metadata of the image
373 Returns the image id or raises an exception if failed
374 """
375 self.logger.debug('VIM new_image with args: {}'.format(locals()))
376 img_id = '{}'.format(uuid.uuid4())
377 desc = {
378 'name':image_dict.get('name'),
379 'uuid':img_id,
380 'uri':image_dict.get('location')
381 }
382 try:
383 self.fos_api.image.add(desc)
384 except fimerrors.FIMAResouceExistingException as free:
385 raise vimconn.vimconnConflictException("Image {} already exist at VIM. Error {}".format(img_id, free))
386 except Exception as e:
387 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
388 return img_id
389
390 def get_image_id_from_path(self, path):
391
392 """Get the image id from image path in the VIM database.
393 Returns the image_id or raises a vimconnNotFoundException
394 """
395 self.logger.debug('VIM get_image_id_from_path with args: {}'.format(locals()))
396 try:
397 imgs = self.fos_api.image.list()
398 except Exception as e:
399 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
400 res = [x.get('uuid') for x in imgs if x.get('uri')==path]
401 if len(res) == 0:
402 raise vimconn.vimconnNotFoundException("Image with this path was not found")
403 return res[0]
404
405 def get_image_list(self, filter_dict={}):
406 """Obtain tenant images from VIM
407 Filter_dict can be:
408 name: image name
409 id: image uuid
410 checksum: image checksum
411 location: image path
412 Returns the image list of dictionaries:
413 [{<the fields at Filter_dict plus some VIM specific>}, ...]
414 List can be empty
415 """
416 self.logger.debug('VIM get_image_list args: {}'.format(locals()))
417 r = []
418 try:
419 fimgs = self.fos_api.image.list()
420 except Exception as e:
421 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
422
423 filters = [
424 partial(self.__name_filter, filter_name=filter_dict.get('name')),
425 partial(self.__id_filter,filter_id=filter_dict.get('id')),
426 partial(self.__checksum_filter,filter_checksum=filter_dict.get('checksum'))
427 ]
428
429 r1 = []
430
431 for i in fimgs:
432 match = True
433 for f in filters:
434 match = match and f(i)
435 if match:
436 r1.append(i)
437
438 for i in r1:
439 img_info = {
440 'name':i.get('name'),
441 'id':i.get('uuid'),
442 'checksum':i.get('checksum'),
443 'location':i.get('uri'),
444 'fos':i
445 }
446 r.append(img_info)
447 return r
448 #raise vimconnNotImplemented( "Should have implemented this" )
449
450 def new_vminstance(self, name, description, start, image_id, flavor_id, net_list, cloud_config=None, disk_list=None,
451 availability_zone_index=None, availability_zone_list=None):
452 """Adds a VM instance to VIM
453 Params:
454 'start': (boolean) indicates if VM must start or created in pause mode.
455 'image_id','flavor_id': image and flavor VIM id to use for the VM
456 'net_list': list of interfaces, each one is a dictionary with:
457 'name': (optional) name for the interface.
458 'net_id': VIM network id where this interface must be connect to. Mandatory for type==virtual
459 'vpci': (optional) virtual vPCI address to assign at the VM. Can be ignored depending on VIM capabilities
460 'model': (optional and only have sense for type==virtual) interface model: virtio, e1000, ...
461 'mac_address': (optional) mac address to assign to this interface
462 'ip_address': (optional) IP address to assign to this interface
463 #TODO: CHECK if an optional 'vlan' parameter is needed for VIMs when type if VF and net_id is not provided,
464 the VLAN tag to be used. In case net_id is provided, the internal network vlan is used for tagging VF
465 'type': (mandatory) can be one of:
466 'virtual', in this case always connected to a network of type 'net_type=bridge'
467 'PCI-PASSTHROUGH' or 'PF' (passthrough): depending on VIM capabilities it can be connected to a data/ptp network ot it
468 can created unconnected
469 'SR-IOV' or 'VF' (SRIOV with VLAN tag): same as PF for network connectivity.
470 'VFnotShared'(SRIOV without VLAN tag) same as PF for network connectivity. VF where no other VFs
471 are allocated on the same physical NIC
472 'bw': (optional) only for PF/VF/VFnotShared. Minimal Bandwidth required for the interface in GBPS
473 'port_security': (optional) If False it must avoid any traffic filtering at this interface. If missing
474 or True, it must apply the default VIM behaviour
475 After execution the method will add the key:
476 'vim_id': must be filled/added by this method with the VIM identifier generated by the VIM for this
477 interface. 'net_list' is modified
478 'cloud_config': (optional) dictionary with:
479 'key-pairs': (optional) list of strings with the public key to be inserted to the default user
480 'users': (optional) list of users to be inserted, each item is a dict with:
481 'name': (mandatory) user name,
482 'key-pairs': (optional) list of strings with the public key to be inserted to the user
483 'user-data': (optional) can be a string with the text script to be passed directly to cloud-init,
484 or a list of strings, each one contains a script to be passed, usually with a MIMEmultipart file
485 'config-files': (optional). List of files to be transferred. Each item is a dict with:
486 'dest': (mandatory) string with the destination absolute path
487 'encoding': (optional, by default text). Can be one of:
488 'b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
489 'content' (mandatory): string with the content of the file
490 'permissions': (optional) string with file permissions, typically octal notation '0644'
491 'owner': (optional) file owner, string with the format 'owner:group'
492 'boot-data-drive': boolean to indicate if user-data must be passed using a boot drive (hard disk)
493 'disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
494 'image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
495 'size': (mandatory) string with the size of the disk in GB
496 availability_zone_index: Index of availability_zone_list to use for this this VM. None if not AV required
497 availability_zone_list: list of availability zones given by user in the VNFD descriptor. Ignore if
498 availability_zone_index is None
499 Returns a tuple with the instance identifier and created_items or raises an exception on error
500 created_items can be None or a dictionary where this method can include key-values that will be passed to
501 the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
502 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
503 as not present.
504 """
505 self.logger.debug('new_vminstance with rgs: {}'.format(locals()))
506 fdu_uuid = '{}'.format(uuid.uuid4())
507
508 flv = self.fos_api.flavor.get(flavor_id)
509 img = self.fos_api.image.get(image_id)
510
511 if flv is None:
512 raise vimconn.vimconnNotFoundException("Flavor {} not found at VIM".format(flavor_id))
513 if img is None:
514 raise vimconn.vimconnNotFoundException("Image {} not found at VIM".format(image_id))
515
516 created_items = {
517 'fdu_id':'',
518 'node_id':'',
519 'connection_points':[]
520 }
521
522 fdu_desc = {
523 'name':name,
524 'uuid':fdu_uuid,
525 'computation_requirements':flv,
526 'image':img,
527 'hypervisor':self.hv,
528 'migration_kind':'LIVE',
529 'interfaces':[],
530 'io_ports':[],
531 'connection_points':[],
532 'depends_on':[]
533 }
534
535 nets = []
536 cps = []
537 intf_id = 0
538 for n in net_list:
539 cp_id = '{}'.format(uuid.uuid4())
540 n.update({'vim_id':cp_id})
541 pair_id = n.get('net_id')
542
543 cp_d = {
544 'uuid':cp_id,
545 'pair_id':pair_id
546 }
547 intf_d = {
548 'name':n.get('name','eth{}'.format(intf_id)),
549 'is_mgmt':False,
550 'if_type':'INTERNAL',
551 'virtual_interface':{
552 'intf_type':n.get('model','VIRTIO'),
553 'vpci':n.get('vpci','0:0:0'),
554 'bandwidth':int(n.get('bw', 100))
555 }
556 }
557 if n.get('mac_address', None) is not None:
558 intf_d['mac_address'] = n['mac_address']
559
560 created_items['connection_points'].append(cp_id)
561 fdu_desc['connection_points'].append(cp_d)
562 fdu_desc['interfaces'].append(intf_d)
563
564 intf_id = intf_id + 1
565
566 if cloud_config is not None:
567 configuration = {
568 'conf_type':'CLOUD_INIT'
569 }
570 if cloud_config.get('user-data') is not None:
571 configuration.update({'script':cloud_config.get('user-data')})
572 if cloud_config.get('key-pairs') is not None:
573 configuration.update({'ssh_keys':cloud_config.get('key-pairs')})
574
575 if 'script' in configuration:
576 fdu_desc.update({'configuration':configuration})
577
578 ### NODE Selection ###
579 # Infrastructure info
580 # nodes dict with
581 # uuid -> node uuid
582 # computational capabilities -> cpu, ram, and disk available
583 # hypervisors -> list of available hypervisors (eg. KVM, LXD, BARE)
584 #
585 #
586
587 # UPDATING AVAILABLE INFRASTRUCTURE
588
589 if len(self.nodes) == 0:
590 nodes_id = self.fos_api.node.list()
591 else:
592 nodes_id = self.nodes
593 nodes = []
594 for n in nodes_id:
595 n_info = self.fos_api.node.info(n)
596 if n_info is None:
597 continue
598 n_plugs = []
599 for p in self.fos_api.node.plugins(n):
600 n_plugs.append(self.fos_api.plugin.info(n,p))
601
602 n_cpu_number = len(n_info.get('cpu'))
603 n_cpu_arch = n_info.get('cpu')[0].get('arch')
604 n_cpu_freq = n_info.get('cpu')[0].get('frequency')
605 n_ram = n_info.get('ram').get('size')
606 n_disk_size = sorted(list(filter(lambda x: 'sda' in x['local_address'], n_info.get('disks'))), key= lambda k: k['dimension'])[-1].get('dimension')
607
608 hvs = []
609 for p in n_plugs:
610 if p.get('type') == 'runtime':
611 hvs.append(p.get('name'))
612
613 ni = {
614 'uuid':n,
615 'computational_capabilities':{
616 'cpu_count':n_cpu_number,
617 'cpu_arch':n_cpu_arch,
618 'cpu_freq':n_cpu_freq,
619 'ram_size':n_ram,
620 'disk_size':n_disk_size
621 },
622 'hypervisors':hvs
623 }
624 nodes.append(ni)
625
626 # NODE SELECTION
627 compatible_nodes = []
628 for n in nodes:
629 if fdu_desc.get('hypervisor') in n.get('hypervisors'):
630 n_comp = n.get('computational_capabilities')
631 f_comp = fdu_desc.get('computation_requirements')
632 if f_comp.get('cpu_arch') == n_comp.get('cpu_arch'):
633 if f_comp.get('cpu_min_count') <= n_comp.get('cpu_count') and f_comp.get('ram_size_mb') <= n_comp.get('ram_size'):
634 if f_comp.get('disk_size_gb') <= n_comp.get('disk_size'):
635 compatible_nodes.append(n)
636
637 if len(compatible_nodes) == 0:
638 raise vimconn.vimconnConflictException("No available nodes at VIM")
639 selected_node = random.choice(compatible_nodes)
640
641 created_items.update({'fdu_id':fdu_uuid, 'node_id': selected_node.get('uuid')})
642
643 self.logger.debug('FOS Node {} FDU Descriptor: {}'.format(selected_node.get('uuid'), fdu_desc))
644
645 try:
646 self.fos_api.fdu.onboard(fdu_desc)
647 instanceid = self.fos_api.fdu.instantiate(fdu_uuid, selected_node.get('uuid'))
648 created_items.update({'instance_id':instanceid})
649
650 self.fdu_node_map.update({instanceid: selected_node.get('uuid')})
651 self.logger.debug('new_vminstance return: {}'.format((fdu_uuid, created_items)))
652 return (instanceid, created_items)
653 except fimerrors.FIMAResouceExistingException as free:
654 raise vimconn.vimconnConflictException("VM already exists at VIM. Error {}".format(free))
655 except Exception as e:
656 raise vimconn.vimconnException("Error while instantiating VM {}. Error {}".format(name, e))
657
658
659 def get_vminstance(self,vm_id):
660 """Returns the VM instance information from VIM"""
661 self.logger.debug('VIM get_vminstance with args: {}'.format(locals()))
662
663 try:
664 intsinfo = self.fos_api.fdu.instance_info(vm_id)
665 except Exception as e:
666 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))
667 if intsinfo is None:
668 raise vimconn.vimconnNotFoundException('VM with id {} not found!'.format(vm_id))
669 return intsinfo
670
671
672 def delete_vminstance(self, vm_id, created_items=None):
673 """
674 Removes a VM instance from VIM and each associate elements
675 :param vm_id: VIM identifier of the VM, provided by method new_vminstance
676 :param created_items: dictionary with extra items to be deleted. provided by method new_vminstance and/or method
677 action_vminstance
678 :return: None or the same vm_id. Raises an exception on fail
679 """
680 self.logger.debug('FOS delete_vminstance with args: {}'.format(locals()))
681 fduid = created_items.get('fdu_id')
682 try:
683 self.fos_api.fdu.terminate(vm_id)
684 self.fos_api.fdu.offload(fduid)
685 except Exception as e:
686 raise vimconn.vimconnException("Error on deletting VM with id {}. Error {}".format(vm_id,e))
687 return vm_id
688
689 #raise vimconnNotImplemented( "Should have implemented this" )
690
691 def refresh_vms_status(self, vm_list):
692 """Get the status of the virtual machines and their interfaces/ports
693 Params: the list of VM identifiers
694 Returns a dictionary with:
695 vm_id: #VIM id of this Virtual Machine
696 status: #Mandatory. Text with one of:
697 # DELETED (not found at vim)
698 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
699 # OTHER (Vim reported other status not understood)
700 # ERROR (VIM indicates an ERROR status)
701 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
702 # BUILD (on building process), ERROR
703 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
704 #
705 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
706 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
707 interfaces: list with interface info. Each item a dictionary with:
708 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
709 mac_address: #Text format XX:XX:XX:XX:XX:XX
710 vim_net_id: #network id where this interface is connected, if provided at creation
711 vim_interface_id: #interface/port VIM id
712 ip_address: #null, or text with IPv4, IPv6 address
713 compute_node: #identification of compute node where PF,VF interface is allocated
714 pci: #PCI address of the NIC that hosts the PF,VF
715 vlan: #physical VLAN used for VF
716 """
717 self.logger.debug('FOS refresh_vms_status with args: {}'.format(locals()))
718 fos2osm_status = {
719 'DEFINE':'OTHER',
720 'CONFIGURE':'INACTIVE',
721 'RUN':'ACTIVE',
722 'PAUSE':'PAUSED',
723 'ERROR':'ERROR'
724 }
725
726 r = {}
727
728 for vm in vm_list:
729 self.logger.debug('FOS refresh_vms_status for {}'.format(vm))
730
731 info = {}
732 nid = self.fdu_node_map.get(vm)
733 if nid is None:
734 r.update({vm:{
735 'status':'VIM_ERROR',
736 'error_msg':'Not compute node associated for VM'
737 }})
738 continue
739
740 try:
741 vm_info = self.fos_api.fdu.instance_info(vm)
742 except:
743 r.update({vm:{
744 'status':'VIM_ERROR',
745 'error_msg':'unable to connect to VIM'
746 }})
747 continue
748
749 if vm_info is None:
750 r.update({vm:{'status':'DELETED'}})
751 continue
752
753
754 desc = self.fos_api.fdu.info(vm_info['fdu_uuid'])
755 osm_status = fos2osm_status.get(vm_info.get('status'))
756
757 self.logger.debug('FOS status info {}'.format(vm_info))
758 self.logger.debug('FOS status is {} <-> OSM Status {}'.format(vm_info.get('status'), osm_status))
759 info.update({'status':osm_status})
760 if vm_info.get('status') == 'ERROR':
761 info.update({'error_msg':vm_info.get('error_code')})
762 info.update({'vim_info':yaml.safe_dump(vm_info)})
763 faces = []
764 i = 0
765 for intf_name in vm_info.get('hypervisor_info').get('network',[]):
766 intf_info = vm_info.get('hypervisor_info').get('network').get(intf_name)
767 face = {}
768 face['compute_node'] = nid
769 face['vim_info'] = yaml.safe_dump(intf_info)
770 face['mac_address'] = intf_info.get('hwaddr')
771 addrs = []
772 for a in intf_info.get('addresses'):
773 addrs.append(a.get('address'))
774 if len(addrs) >= 0:
775 face['ip_address'] = ','.join(addrs)
776 else:
777 face['ip_address'] = ''
778 face['pci'] = '0:0:0.0'
779 # getting net id by CP
780 try:
781 cp_info = vm_info.get('connection_points')[i]
782 except IndexError:
783 cp_info = None
784 if cp_info is not None:
785 cp_id = cp_info['cp_uuid']
786 cps_d = desc['connection_points']
787 matches = [x for x in cps_d if x['uuid'] == cp_id]
788 if len(matches) > 0:
789 cpd = matches[0]
790 face['vim_net_id'] = cpd.get('pair_id','')
791 else:
792 face['vim_net_id'] = ''
793 face['vim_interface_id'] = cp_id
794 # cp_info.get('uuid')
795 else:
796 face['vim_net_id'] = ''
797 face['vim_interface_id'] = intf_name
798 faces.append(face)
799 i += 1
800
801
802
803 info.update({'interfaces':faces})
804 r.update({vm:info})
805 self.logger.debug('FOS refresh_vms_status res for {} is {}'.format(vm, info))
806 self.logger.debug('FOS refresh_vms_status res is {}'.format(r))
807 return r
808
809
810 #raise vimconnNotImplemented( "Should have implemented this" )
811
812 def action_vminstance(self, vm_id, action_dict, created_items={}):
813 """
814 Send and action over a VM instance. Returns created_items if the action was successfully sent to the VIM.
815 created_items is a dictionary with items that
816 :param vm_id: VIM identifier of the VM, provided by method new_vminstance
817 :param action_dict: dictionary with the action to perform
818 :param created_items: provided by method new_vminstance is a dictionary with key-values that will be passed to
819 the method delete_vminstance. Can be used to store created ports, volumes, etc. Format is vimconnector
820 dependent, but do not use nested dictionaries and a value of None should be the same as not present. This
821 method can modify this value
822 :return: None, or a console dict
823 """
824 self.logger.debug('VIM action_vminstance with args: {}'.format(locals()))
825 nid = self.fdu_node_map.get(vm_id)
826 if nid is None:
827 raise vimconn.vimconnNotFoundException('No node for this VM')
828 try:
829 fdu_info = self.fos_api.fdu.instance_info(vm_id)
830 if "start" in action_dict:
831 if fdu_info.get('status') == 'CONFIGURE':
832 self.fos_api.fdu.start(vm_id)
833 elif fdu_info.get('status') == 'PAUSE':
834 self.fos_api.fdu.resume(vm_id)
835 else:
836 raise vimconn.vimconnConflictException("Cannot start from this state")
837 elif "pause" in action_dict:
838 if fdu_info.get('status') == 'RUN':
839 self.fos_api.fdu.pause(vm_id)
840 else:
841 raise vimconn.vimconnConflictException("Cannot pause from this state")
842 elif "resume" in action_dict:
843 if fdu_info.get('status') == 'PAUSE':
844 self.fos_api.fdu.resume(vm_id)
845 else:
846 raise vimconn.vimconnConflictException("Cannot resume from this state")
847 elif "shutoff" in action_dict or "shutdown" or "forceOff" in action_dict:
848 if fdu_info.get('status') == 'RUN':
849 self.fos_api.fdu.stop(vm_id)
850 else:
851 raise vimconn.vimconnConflictException("Cannot shutoff from this state")
852 elif "terminate" in action_dict:
853 if fdu_info.get('status') == 'RUN':
854 self.fos_api.fdu.stop(vm_id)
855 self.fos_api.fdu.clean(vm_id)
856 self.fos_api.fdu.undefine(vm_id)
857 # self.fos_api.fdu.offload(vm_id)
858 elif fdu_info.get('status') == 'CONFIGURE':
859 self.fos_api.fdu.clean(vm_id)
860 self.fos_api.fdu.undefine(vm_id)
861 # self.fos_api.fdu.offload(vm_id)
862 elif fdu_info.get('status') == 'PAUSE':
863 self.fos_api.fdu.resume(vm_id)
864 self.fos_api.fdu.stop(vm_id)
865 self.fos_api.fdu.clean(vm_id)
866 self.fos_api.fdu.undefine(vm_id)
867 # self.fos_api.fdu.offload(vm_id)
868 else:
869 raise vimconn.vimconnConflictException("Cannot terminate from this state")
870 elif "rebuild" in action_dict:
871 raise vimconnNotImplemented("Rebuild not implememnted")
872 elif "reboot" in action_dict:
873 if fdu_info.get('status') == 'RUN':
874 self.fos_api.fdu.stop(vm_id)
875 self.fos_api.fdu.start(vm_id)
876 else:
877 raise vimconn.vimconnConflictException("Cannot reboot from this state")
878 except Exception as e:
879 raise vimconn.vimconnConnectionException("VIM not reachable. Error {}".format(e))