1 # -*- coding: utf-8 -*-
4 # Copyright 2015 Telefonica Investigacion y Desarrollo, S.A.U.
5 # This file is part of openmano
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
12 # http://www.apache.org/licenses/LICENSE-2.0
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
22 osconnector implements all the methods to interact with openstack using the python-neutronclient.
24 For the VNF forwarding graph, The OpenStack VIM connector calls the
25 networking-sfc Neutron extension methods, whose resources are mapped
26 to the VIM connector's SFC resources as follows:
27 - Classification (OSM) -> Flow Classifier (Neutron)
28 - Service Function Instance (OSM) -> Port Pair (Neutron)
29 - Service Function (OSM) -> Port Pair Group (Neutron)
30 - Service Function Path (OSM) -> Port Chain (Neutron)
32 __author__
= "Alfonso Tierno, Gerardo Garcia, Pablo Montes, xFlow Research, Igor D.C., Eduardo Sousa"
33 __date__
= "$22-sep-2017 23:59:59$"
35 from osm_ro
import vimconn
44 from pprint
import pformat
46 from novaclient
import client
as nClient
, exceptions
as nvExceptions
47 from keystoneauth1
.identity
import v2
, v3
48 from keystoneauth1
import session
49 import keystoneclient
.exceptions
as ksExceptions
50 import keystoneclient
.v3
.client
as ksClient_v3
51 import keystoneclient
.v2_0
.client
as ksClient_v2
52 from glanceclient
import client
as glClient
53 import glanceclient
.exc
as gl1Exceptions
54 from cinderclient
import client
as cClient
55 from http
.client
import HTTPException
# TODO py3 check that this base exception matches python2 httplib.HTTPException
56 from neutronclient
.neutron
import client
as neClient
57 from neutronclient
.common
import exceptions
as neExceptions
58 from requests
.exceptions
import ConnectionError
61 """contain the openstack virtual machine status to openmano status"""
62 vmStatus2manoFormat
={'ACTIVE':'ACTIVE',
64 'SUSPENDED': 'SUSPENDED',
67 'ERROR':'ERROR','DELETED':'DELETED'
69 netStatus2manoFormat
={'ACTIVE':'ACTIVE','PAUSED':'PAUSED','INACTIVE':'INACTIVE','BUILD':'BUILD','ERROR':'ERROR','DELETED':'DELETED'
72 supportedClassificationTypes
= ['legacy_flow_classifier']
74 #global var to have a timeout creating and deleting volumes
79 class SafeDumper(yaml
.SafeDumper
):
80 def represent_data(self
, data
):
81 # Openstack APIs use custom subclasses of dict and YAML safe dumper
82 # is designed to not handle that (reference issue 142 of pyyaml)
83 if isinstance(data
, dict) and data
.__class
__ != dict:
84 # A simple solution is to convert those items back to dicts
85 data
= dict(data
.items())
87 return super(SafeDumper
, self
).represent_data(data
)
90 class vimconnector(vimconn
.vimconnector
):
91 def __init__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
=None, user
=None, passwd
=None,
92 log_level
=None, config
={}, persistent_info
={}):
93 '''using common constructor parameters. In this case
94 'url' is the keystone authorization url,
95 'url_admin' is not use
97 api_version
= config
.get('APIversion')
98 if api_version
and api_version
not in ('v3.3', 'v2.0', '2', '3'):
99 raise vimconn
.vimconnException("Invalid value '{}' for config:APIversion. "
100 "Allowed values are 'v3.3', 'v2.0', '2' or '3'".format(api_version
))
101 vim_type
= config
.get('vim_type')
102 if vim_type
and vim_type
not in ('vio', 'VIO'):
103 raise vimconn
.vimconnException("Invalid value '{}' for config:vim_type."
104 "Allowed values are 'vio' or 'VIO'".format(vim_type
))
106 if config
.get('dataplane_net_vlan_range') is not None:
107 #validate vlan ranges provided by user
108 self
._validate
_vlan
_ranges
(config
.get('dataplane_net_vlan_range'), 'dataplane_net_vlan_range')
110 if config
.get('multisegment_vlan_range') is not None:
111 #validate vlan ranges provided by user
112 self
._validate
_vlan
_ranges
(config
.get('multisegment_vlan_range'), 'multisegment_vlan_range')
114 vimconn
.vimconnector
.__init
__(self
, uuid
, name
, tenant_id
, tenant_name
, url
, url_admin
, user
, passwd
, log_level
,
117 if self
.config
.get("insecure") and self
.config
.get("ca_cert"):
118 raise vimconn
.vimconnException("options insecure and ca_cert are mutually exclusive")
120 if self
.config
.get("insecure"):
122 if self
.config
.get("ca_cert"):
123 self
.verify
= self
.config
.get("ca_cert")
126 raise TypeError('url param can not be NoneType')
127 self
.persistent_info
= persistent_info
128 self
.availability_zone
= persistent_info
.get('availability_zone', None)
129 self
.session
= persistent_info
.get('session', {'reload_client': True})
130 self
.my_tenant_id
= self
.session
.get('my_tenant_id')
131 self
.nova
= self
.session
.get('nova')
132 self
.neutron
= self
.session
.get('neutron')
133 self
.cinder
= self
.session
.get('cinder')
134 self
.glance
= self
.session
.get('glance')
135 # self.glancev1 = self.session.get('glancev1')
136 self
.keystone
= self
.session
.get('keystone')
137 self
.api_version3
= self
.session
.get('api_version3')
138 self
.vim_type
= self
.config
.get("vim_type")
140 self
.vim_type
= self
.vim_type
.upper()
141 if self
.config
.get("use_internal_endpoint"):
142 self
.endpoint_type
= "internalURL"
144 self
.endpoint_type
= None
146 logging
.getLogger('urllib3').setLevel(logging
.WARNING
)
147 logging
.getLogger('keystoneauth').setLevel(logging
.WARNING
)
148 logging
.getLogger('novaclient').setLevel(logging
.WARNING
)
149 self
.logger
= logging
.getLogger('openmano.vim.openstack')
151 # allow security_groups to be a list or a single string
152 if isinstance(self
.config
.get('security_groups'), str):
153 self
.config
['security_groups'] = [self
.config
['security_groups']]
154 self
.security_groups_id
= None
156 ####### VIO Specific Changes #########
157 if self
.vim_type
== "VIO":
158 self
.logger
= logging
.getLogger('openmano.vim.vio')
161 self
.logger
.setLevel( getattr(logging
, log_level
))
163 def __getitem__(self
, index
):
164 """Get individuals parameters.
166 if index
== 'project_domain_id':
167 return self
.config
.get("project_domain_id")
168 elif index
== 'user_domain_id':
169 return self
.config
.get("user_domain_id")
171 return vimconn
.vimconnector
.__getitem
__(self
, index
)
173 def __setitem__(self
, index
, value
):
174 """Set individuals parameters and it is marked as dirty so to force connection reload.
176 if index
== 'project_domain_id':
177 self
.config
["project_domain_id"] = value
178 elif index
== 'user_domain_id':
179 self
.config
["user_domain_id"] = value
181 vimconn
.vimconnector
.__setitem
__(self
, index
, value
)
182 self
.session
['reload_client'] = True
184 def serialize(self
, value
):
185 """Serialization of python basic types.
187 In the case value is not serializable a message will be logged and a
188 simple representation of the data that cannot be converted back to
191 if isinstance(value
, str):
195 return yaml
.dump(value
, Dumper
=SafeDumper
,
196 default_flow_style
=True, width
=256)
197 except yaml
.representer
.RepresenterError
:
198 self
.logger
.debug('The following entity cannot be serialized in YAML:\n\n%s\n\n', pformat(value
),
202 def _reload_connection(self
):
203 '''Called before any operation, it check if credentials has changed
204 Throw keystoneclient.apiclient.exceptions.AuthorizationFailure
206 #TODO control the timing and possible token timeout, but it seams that python client does this task for us :-)
207 if self
.session
['reload_client']:
208 if self
.config
.get('APIversion'):
209 self
.api_version3
= self
.config
['APIversion'] == 'v3.3' or self
.config
['APIversion'] == '3'
210 else: # get from ending auth_url that end with v3 or with v2.0
211 self
.api_version3
= self
.url
.endswith("/v3") or self
.url
.endswith("/v3/")
212 self
.session
['api_version3'] = self
.api_version3
213 if self
.api_version3
:
214 if self
.config
.get('project_domain_id') or self
.config
.get('project_domain_name'):
215 project_domain_id_default
= None
217 project_domain_id_default
= 'default'
218 if self
.config
.get('user_domain_id') or self
.config
.get('user_domain_name'):
219 user_domain_id_default
= None
221 user_domain_id_default
= 'default'
222 auth
= v3
.Password(auth_url
=self
.url
,
224 password
=self
.passwd
,
225 project_name
=self
.tenant_name
,
226 project_id
=self
.tenant_id
,
227 project_domain_id
=self
.config
.get('project_domain_id', project_domain_id_default
),
228 user_domain_id
=self
.config
.get('user_domain_id', user_domain_id_default
),
229 project_domain_name
=self
.config
.get('project_domain_name'),
230 user_domain_name
=self
.config
.get('user_domain_name'))
232 auth
= v2
.Password(auth_url
=self
.url
,
234 password
=self
.passwd
,
235 tenant_name
=self
.tenant_name
,
236 tenant_id
=self
.tenant_id
)
237 sess
= session
.Session(auth
=auth
, verify
=self
.verify
)
238 # addedd region_name to keystone, nova, neutron and cinder to support distributed cloud for Wind River Titanium cloud and StarlingX
239 region_name
= self
.config
.get('region_name')
240 if self
.api_version3
:
241 self
.keystone
= ksClient_v3
.Client(session
=sess
, endpoint_type
=self
.endpoint_type
, region_name
=region_name
)
243 self
.keystone
= ksClient_v2
.Client(session
=sess
, endpoint_type
=self
.endpoint_type
)
244 self
.session
['keystone'] = self
.keystone
245 # In order to enable microversion functionality an explicit microversion must be specified in 'config'.
246 # This implementation approach is due to the warning message in
247 # https://developer.openstack.org/api-guide/compute/microversions.html
248 # where it is stated that microversion backwards compatibility is not guaranteed and clients should
249 # always require an specific microversion.
250 # To be able to use 'device role tagging' functionality define 'microversion: 2.32' in datacenter config
251 version
= self
.config
.get("microversion")
254 # addedd region_name to keystone, nova, neutron and cinder to support distributed cloud for Wind River Titanium cloud and StarlingX
255 self
.nova
= self
.session
['nova'] = nClient
.Client(str(version
), session
=sess
, endpoint_type
=self
.endpoint_type
, region_name
=region_name
)
256 self
.neutron
= self
.session
['neutron'] = neClient
.Client('2.0', session
=sess
, endpoint_type
=self
.endpoint_type
, region_name
=region_name
)
257 self
.cinder
= self
.session
['cinder'] = cClient
.Client(2, session
=sess
, endpoint_type
=self
.endpoint_type
, region_name
=region_name
)
259 self
.my_tenant_id
= self
.session
['my_tenant_id'] = sess
.get_project_id()
260 except Exception as e
:
261 self
.logger
.error("Cannot get project_id from session", exc_info
=True)
262 if self
.endpoint_type
== "internalURL":
263 glance_service_id
= self
.keystone
.services
.list(name
="glance")[0].id
264 glance_endpoint
= self
.keystone
.endpoints
.list(glance_service_id
, interface
="internal")[0].url
266 glance_endpoint
= None
267 self
.glance
= self
.session
['glance'] = glClient
.Client(2, session
=sess
, endpoint
=glance_endpoint
)
268 # using version 1 of glance client in new_image()
269 # self.glancev1 = self.session['glancev1'] = glClient.Client('1', session=sess,
270 # endpoint=glance_endpoint)
271 self
.session
['reload_client'] = False
272 self
.persistent_info
['session'] = self
.session
273 # add availablity zone info inside self.persistent_info
274 self
._set
_availablity
_zones
()
275 self
.persistent_info
['availability_zone'] = self
.availability_zone
276 self
.security_groups_id
= None # force to get again security_groups_ids next time they are needed
278 def __net_os2mano(self
, net_list_dict
):
279 '''Transform the net openstack format to mano format
280 net_list_dict can be a list of dict or a single dict'''
281 if type(net_list_dict
) is dict:
282 net_list_
=(net_list_dict
,)
283 elif type(net_list_dict
) is list:
284 net_list_
=net_list_dict
286 raise TypeError("param net_list_dict must be a list or a dictionary")
287 for net
in net_list_
:
288 if net
.get('provider:network_type') == "vlan":
293 def __classification_os2mano(self
, class_list_dict
):
294 """Transform the openstack format (Flow Classifier) to mano format
295 (Classification) class_list_dict can be a list of dict or a single dict
297 if isinstance(class_list_dict
, dict):
298 class_list_
= [class_list_dict
]
299 elif isinstance(class_list_dict
, list):
300 class_list_
= class_list_dict
303 "param class_list_dict must be a list or a dictionary")
304 for classification
in class_list_
:
305 id = classification
.pop('id')
306 name
= classification
.pop('name')
307 description
= classification
.pop('description')
308 project_id
= classification
.pop('project_id')
309 tenant_id
= classification
.pop('tenant_id')
310 original_classification
= copy
.deepcopy(classification
)
311 classification
.clear()
312 classification
['ctype'] = 'legacy_flow_classifier'
313 classification
['definition'] = original_classification
314 classification
['id'] = id
315 classification
['name'] = name
316 classification
['description'] = description
317 classification
['project_id'] = project_id
318 classification
['tenant_id'] = tenant_id
320 def __sfi_os2mano(self
, sfi_list_dict
):
321 """Transform the openstack format (Port Pair) to mano format (SFI)
322 sfi_list_dict can be a list of dict or a single dict
324 if isinstance(sfi_list_dict
, dict):
325 sfi_list_
= [sfi_list_dict
]
326 elif isinstance(sfi_list_dict
, list):
327 sfi_list_
= sfi_list_dict
330 "param sfi_list_dict must be a list or a dictionary")
331 for sfi
in sfi_list_
:
332 sfi
['ingress_ports'] = []
333 sfi
['egress_ports'] = []
334 if sfi
.get('ingress'):
335 sfi
['ingress_ports'].append(sfi
['ingress'])
336 if sfi
.get('egress'):
337 sfi
['egress_ports'].append(sfi
['egress'])
340 params
= sfi
.get('service_function_parameters')
343 correlation
= params
.get('correlation')
346 sfi
['sfc_encap'] = sfc_encap
347 del sfi
['service_function_parameters']
349 def __sf_os2mano(self
, sf_list_dict
):
350 """Transform the openstack format (Port Pair Group) to mano format (SF)
351 sf_list_dict can be a list of dict or a single dict
353 if isinstance(sf_list_dict
, dict):
354 sf_list_
= [sf_list_dict
]
355 elif isinstance(sf_list_dict
, list):
356 sf_list_
= sf_list_dict
359 "param sf_list_dict must be a list or a dictionary")
361 del sf
['port_pair_group_parameters']
362 sf
['sfis'] = sf
['port_pairs']
365 def __sfp_os2mano(self
, sfp_list_dict
):
366 """Transform the openstack format (Port Chain) to mano format (SFP)
367 sfp_list_dict can be a list of dict or a single dict
369 if isinstance(sfp_list_dict
, dict):
370 sfp_list_
= [sfp_list_dict
]
371 elif isinstance(sfp_list_dict
, list):
372 sfp_list_
= sfp_list_dict
375 "param sfp_list_dict must be a list or a dictionary")
376 for sfp
in sfp_list_
:
377 params
= sfp
.pop('chain_parameters')
380 correlation
= params
.get('correlation')
383 sfp
['sfc_encap'] = sfc_encap
384 sfp
['spi'] = sfp
.pop('chain_id')
385 sfp
['classifications'] = sfp
.pop('flow_classifiers')
386 sfp
['service_functions'] = sfp
.pop('port_pair_groups')
388 # placeholder for now; read TODO note below
389 def _validate_classification(self
, type, definition
):
390 # only legacy_flow_classifier Type is supported at this point
392 # TODO(igordcard): this method should be an abstract method of an
393 # abstract Classification class to be implemented by the specific
394 # Types. Also, abstract vimconnector should call the validation
395 # method before the implemented VIM connectors are called.
397 def _format_exception(self
, exception
):
398 '''Transform a keystone, nova, neutron exception into a vimconn exception'''
400 message_error
= exception
.message
402 if isinstance(exception
, (neExceptions
.NetworkNotFoundClient
, nvExceptions
.NotFound
, ksExceptions
.NotFound
,
403 gl1Exceptions
.HTTPNotFound
)):
404 raise vimconn
.vimconnNotFoundException(type(exception
).__name
__ + ": " + message_error
)
405 elif isinstance(exception
, (HTTPException
, gl1Exceptions
.HTTPException
, gl1Exceptions
.CommunicationError
,
406 ConnectionError
, ksExceptions
.ConnectionError
, neExceptions
.ConnectionFailed
)):
407 raise vimconn
.vimconnConnectionException(type(exception
).__name
__ + ": " + message_error
)
408 elif isinstance(exception
, (KeyError, nvExceptions
.BadRequest
, ksExceptions
.BadRequest
)):
409 raise vimconn
.vimconnException(type(exception
).__name
__ + ": " + message_error
)
410 elif isinstance(exception
, (nvExceptions
.ClientException
, ksExceptions
.ClientException
,
411 neExceptions
.NeutronException
)):
412 raise vimconn
.vimconnUnexpectedResponse(type(exception
).__name
__ + ": " + message_error
)
413 elif isinstance(exception
, nvExceptions
.Conflict
):
414 raise vimconn
.vimconnConflictException(type(exception
).__name
__ + ": " + message_error
)
415 elif isinstance(exception
, vimconn
.vimconnException
):
418 self
.logger
.error("General Exception " + message_error
, exc_info
=True)
419 raise vimconn
.vimconnConnectionException(type(exception
).__name
__ + ": " + message_error
)
421 def _get_ids_from_name(self
):
423 Obtain ids from name of tenant and security_groups. Store at self .security_groups_id"
426 # get tenant_id if only tenant_name is supplied
427 self
._reload
_connection
()
428 if not self
.my_tenant_id
:
429 raise vimconn
.vimconnConnectionException("Error getting tenant information from name={} id={}".
430 format(self
.tenant_name
, self
.tenant_id
))
431 if self
.config
.get('security_groups') and not self
.security_groups_id
:
432 # convert from name to id
433 neutron_sg_list
= self
.neutron
.list_security_groups(tenant_id
=self
.my_tenant_id
)["security_groups"]
435 self
.security_groups_id
= []
436 for sg
in self
.config
.get('security_groups'):
437 for neutron_sg
in neutron_sg_list
:
438 if sg
in (neutron_sg
["id"], neutron_sg
["name"]):
439 self
.security_groups_id
.append(neutron_sg
["id"])
442 self
.security_groups_id
= None
443 raise vimconn
.vimconnConnectionException("Not found security group {} for this tenant".format(sg
))
445 def check_vim_connectivity(self
):
446 # just get network list to check connectivity and credentials
447 self
.get_network_list(filter_dict
={})
449 def get_tenant_list(self
, filter_dict
={}):
450 '''Obtain tenants of VIM
451 filter_dict can contain the following keys:
452 name: filter by tenant name
453 id: filter by tenant uuid/id
455 Returns the tenant list of dictionaries: [{'name':'<name>, 'id':'<id>, ...}, ...]
457 self
.logger
.debug("Getting tenants from VIM filter: '%s'", str(filter_dict
))
459 self
._reload
_connection
()
460 if self
.api_version3
:
461 project_class_list
= self
.keystone
.projects
.list(name
=filter_dict
.get("name"))
463 project_class_list
= self
.keystone
.tenants
.findall(**filter_dict
)
465 for project
in project_class_list
:
466 if filter_dict
.get('id') and filter_dict
["id"] != project
.id:
468 project_list
.append(project
.to_dict())
470 except (ksExceptions
.ConnectionError
, ksExceptions
.ClientException
, ConnectionError
) as e
:
471 self
._format
_exception
(e
)
473 def new_tenant(self
, tenant_name
, tenant_description
):
474 '''Adds a new tenant to openstack VIM. Returns the tenant identifier'''
475 self
.logger
.debug("Adding a new tenant name: %s", tenant_name
)
477 self
._reload
_connection
()
478 if self
.api_version3
:
479 project
= self
.keystone
.projects
.create(tenant_name
, self
.config
.get("project_domain_id", "default"),
480 description
=tenant_description
, is_domain
=False)
482 project
= self
.keystone
.tenants
.create(tenant_name
, tenant_description
)
484 except (ksExceptions
.ConnectionError
, ksExceptions
.ClientException
, ksExceptions
.BadRequest
, ConnectionError
) as e
:
485 self
._format
_exception
(e
)
487 def delete_tenant(self
, tenant_id
):
488 '''Delete a tenant from openstack VIM. Returns the old tenant identifier'''
489 self
.logger
.debug("Deleting tenant %s from VIM", tenant_id
)
491 self
._reload
_connection
()
492 if self
.api_version3
:
493 self
.keystone
.projects
.delete(tenant_id
)
495 self
.keystone
.tenants
.delete(tenant_id
)
497 except (ksExceptions
.ConnectionError
, ksExceptions
.ClientException
, ksExceptions
.NotFound
, ConnectionError
) as e
:
498 self
._format
_exception
(e
)
500 def new_network(self
,net_name
, net_type
, ip_profile
=None, shared
=False, provider_network_profile
=None):
501 """Adds a tenant network to VIM
503 'net_name': name of the network
505 'bridge': overlay isolated network
506 'data': underlay E-LAN network for Passthrough and SRIOV interfaces
507 'ptp': underlay E-LINE network for Passthrough and SRIOV interfaces.
508 'ip_profile': is a dict containing the IP parameters of the network
509 'ip_version': can be "IPv4" or "IPv6" (Currently only IPv4 is implemented)
510 'subnet_address': ip_prefix_schema, that is X.X.X.X/Y
511 'gateway_address': (Optional) ip_schema, that is X.X.X.X
512 'dns_address': (Optional) comma separated list of ip_schema, e.g. X.X.X.X[,X,X,X,X]
513 'dhcp_enabled': True or False
514 'dhcp_start_address': ip_schema, first IP to grant
515 'dhcp_count': number of IPs to grant.
516 'shared': if this network can be seen/use by other tenants/organization
517 'provider_network_profile': (optional) contains {segmentation-id: vlan, provider-network: vim_netowrk}
518 Returns a tuple with the network identifier and created_items, or raises an exception on error
519 created_items can be None or a dictionary where this method can include key-values that will be passed to
520 the method delete_network. Can be used to store created segments, created l2gw connections, etc.
521 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
524 self
.logger
.debug("Adding a new network to VIM name '%s', type '%s'", net_name
, net_type
)
525 # self.logger.debug(">>>>>>>>>>>>>>>>>> IP profile %s", str(ip_profile))
529 if provider_network_profile
:
530 vlan
= provider_network_profile
.get("segmentation-id")
533 self
._reload
_connection
()
534 network_dict
= {'name': net_name
, 'admin_state_up': True}
535 if net_type
=="data" or net_type
=="ptp":
536 if self
.config
.get('dataplane_physical_net') == None:
537 raise vimconn
.vimconnConflictException("You must provide a 'dataplane_physical_net' at config value before creating sriov network")
538 if not self
.config
.get('multisegment_support'):
539 network_dict
["provider:physical_network"] = self
.config
[
540 'dataplane_physical_net'] # "physnet_sriov" #TODO physical
541 network_dict
["provider:network_type"] = "vlan"
543 network_dict
["provider:network_type"] = vlan
545 ###### Multi-segment case ######
548 segment1_dict
["provider:physical_network"] = ''
549 segment1_dict
["provider:network_type"] = 'vxlan'
550 segment_list
.append(segment1_dict
)
552 segment2_dict
["provider:physical_network"] = self
.config
['dataplane_physical_net']
553 segment2_dict
["provider:network_type"] = "vlan"
554 if self
.config
.get('multisegment_vlan_range'):
555 vlanID
= self
._generate
_multisegment
_vlanID
()
556 segment2_dict
["provider:segmentation_id"] = vlanID
558 # raise vimconn.vimconnConflictException(
559 # "You must provide 'multisegment_vlan_range' at config dict before creating a multisegment network")
560 segment_list
.append(segment2_dict
)
561 network_dict
["segments"] = segment_list
563 ####### VIO Specific Changes #########
564 if self
.vim_type
== "VIO":
566 network_dict
["provider:segmentation_id"] = vlan
568 if self
.config
.get('dataplane_net_vlan_range') is None:
569 raise vimconn
.vimconnConflictException("You must provide "\
570 "'dataplane_net_vlan_range' in format [start_ID - end_ID]"\
571 "at config value before creating sriov network with vlan tag")
573 network_dict
["provider:segmentation_id"] = self
._generate
_vlanID
()
575 network_dict
["shared"] = shared
576 if self
.config
.get("disable_network_port_security"):
577 network_dict
["port_security_enabled"] = False
578 new_net
= self
.neutron
.create_network({'network':network_dict
})
580 # create subnetwork, even if there is no profile
583 if not ip_profile
.get('subnet_address'):
584 #Fake subnet is required
585 subnet_rand
= random
.randint(0, 255)
586 ip_profile
['subnet_address'] = "192.168.{}.0/24".format(subnet_rand
)
587 if 'ip_version' not in ip_profile
:
588 ip_profile
['ip_version'] = "IPv4"
589 subnet
= {"name": net_name
+"-subnet",
590 "network_id": new_net
["network"]["id"],
591 "ip_version": 4 if ip_profile
['ip_version']=="IPv4" else 6,
592 "cidr": ip_profile
['subnet_address']
594 # Gateway should be set to None if not needed. Otherwise openstack assigns one by default
595 if ip_profile
.get('gateway_address'):
596 subnet
['gateway_ip'] = ip_profile
['gateway_address']
598 subnet
['gateway_ip'] = None
599 if ip_profile
.get('dns_address'):
600 subnet
['dns_nameservers'] = ip_profile
['dns_address'].split(";")
601 if 'dhcp_enabled' in ip_profile
:
602 subnet
['enable_dhcp'] = False if \
603 ip_profile
['dhcp_enabled']=="false" or ip_profile
['dhcp_enabled']==False else True
604 if ip_profile
.get('dhcp_start_address'):
605 subnet
['allocation_pools'] = []
606 subnet
['allocation_pools'].append(dict())
607 subnet
['allocation_pools'][0]['start'] = ip_profile
['dhcp_start_address']
608 if ip_profile
.get('dhcp_count'):
609 #parts = ip_profile['dhcp_start_address'].split('.')
610 #ip_int = (int(parts[0]) << 24) + (int(parts[1]) << 16) + (int(parts[2]) << 8) + int(parts[3])
611 ip_int
= int(netaddr
.IPAddress(ip_profile
['dhcp_start_address']))
612 ip_int
+= ip_profile
['dhcp_count'] - 1
613 ip_str
= str(netaddr
.IPAddress(ip_int
))
614 subnet
['allocation_pools'][0]['end'] = ip_str
615 #self.logger.debug(">>>>>>>>>>>>>>>>>> Subnet: %s", str(subnet))
616 self
.neutron
.create_subnet({"subnet": subnet
} )
618 if net_type
== "data" and self
.config
.get('multisegment_support'):
619 if self
.config
.get('l2gw_support'):
620 l2gw_list
= self
.neutron
.list_l2_gateways().get("l2_gateways", ())
621 for l2gw
in l2gw_list
:
623 l2gw_conn
["l2_gateway_id"] = l2gw
["id"]
624 l2gw_conn
["network_id"] = new_net
["network"]["id"]
625 l2gw_conn
["segmentation_id"] = str(vlanID
)
626 new_l2gw_conn
= self
.neutron
.create_l2_gateway_connection({"l2_gateway_connection": l2gw_conn
})
627 created_items
["l2gwconn:" + str(new_l2gw_conn
["l2_gateway_connection"]["id"])] = True
628 return new_net
["network"]["id"], created_items
629 except Exception as e
:
630 #delete l2gw connections (if any) before deleting the network
631 for k
, v
in created_items
.items():
632 if not v
: # skip already deleted
635 k_item
, _
, k_id
= k
.partition(":")
636 if k_item
== "l2gwconn":
637 self
.neutron
.delete_l2_gateway_connection(k_id
)
638 except Exception as e2
:
639 self
.logger
.error("Error deleting l2 gateway connection: {}: {}".format(type(e2
).__name
__, e2
))
641 self
.neutron
.delete_network(new_net
['network']['id'])
642 self
._format
_exception
(e
)
644 def get_network_list(self
, filter_dict
={}):
645 '''Obtain tenant networks of VIM
651 admin_state_up: boolean
653 Returns the network list of dictionaries
655 self
.logger
.debug("Getting network from VIM filter: '%s'", str(filter_dict
))
657 self
._reload
_connection
()
658 filter_dict_os
= filter_dict
.copy()
659 if self
.api_version3
and "tenant_id" in filter_dict_os
:
660 filter_dict_os
['project_id'] = filter_dict_os
.pop('tenant_id') #T ODO check
661 net_dict
= self
.neutron
.list_networks(**filter_dict_os
)
662 net_list
= net_dict
["networks"]
663 self
.__net
_os
2mano
(net_list
)
665 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
, neExceptions
.NeutronException
, ConnectionError
) as e
:
666 self
._format
_exception
(e
)
668 def get_network(self
, net_id
):
669 '''Obtain details of network from VIM
670 Returns the network information from a network id'''
671 self
.logger
.debug(" Getting tenant network %s from VIM", net_id
)
672 filter_dict
={"id": net_id
}
673 net_list
= self
.get_network_list(filter_dict
)
675 raise vimconn
.vimconnNotFoundException("Network '{}' not found".format(net_id
))
676 elif len(net_list
)>1:
677 raise vimconn
.vimconnConflictException("Found more than one network with this criteria")
680 for subnet_id
in net
.get("subnets", () ):
682 subnet
= self
.neutron
.show_subnet(subnet_id
)
683 except Exception as e
:
684 self
.logger
.error("osconnector.get_network(): Error getting subnet %s %s" % (net_id
, str(e
)))
685 subnet
= {"id": subnet_id
, "fault": str(e
)}
686 subnets
.append(subnet
)
687 net
["subnets"] = subnets
688 net
["encapsulation"] = net
.get('provider:network_type')
689 net
["encapsulation_type"] = net
.get('provider:network_type')
690 net
["segmentation_id"] = net
.get('provider:segmentation_id')
691 net
["encapsulation_id"] = net
.get('provider:segmentation_id')
694 def delete_network(self
, net_id
, created_items
=None):
696 Removes a tenant network from VIM and its associated elements
697 :param net_id: VIM identifier of the network, provided by method new_network
698 :param created_items: dictionary with extra items to be deleted. provided by method new_network
699 Returns the network identifier or raises an exception upon error or when network is not found
701 self
.logger
.debug("Deleting network '%s' from VIM", net_id
)
702 if created_items
== None:
705 self
._reload
_connection
()
706 #delete l2gw connections (if any) before deleting the network
707 for k
, v
in created_items
.items():
708 if not v
: # skip already deleted
711 k_item
, _
, k_id
= k
.partition(":")
712 if k_item
== "l2gwconn":
713 self
.neutron
.delete_l2_gateway_connection(k_id
)
714 except Exception as e
:
715 self
.logger
.error("Error deleting l2 gateway connection: {}: {}".format(type(e
).__name
__, e
))
716 #delete VM ports attached to this networks before the network
717 ports
= self
.neutron
.list_ports(network_id
=net_id
)
718 for p
in ports
['ports']:
720 self
.neutron
.delete_port(p
["id"])
721 except Exception as e
:
722 self
.logger
.error("Error deleting port %s: %s", p
["id"], str(e
))
723 self
.neutron
.delete_network(net_id
)
725 except (neExceptions
.ConnectionFailed
, neExceptions
.NetworkNotFoundClient
, neExceptions
.NeutronException
,
726 ksExceptions
.ClientException
, neExceptions
.NeutronException
, ConnectionError
) as e
:
727 self
._format
_exception
(e
)
729 def refresh_nets_status(self
, net_list
):
730 '''Get the status of the networks
731 Params: the list of network identifiers
732 Returns a dictionary with:
733 net_id: #VIM id of this network
734 status: #Mandatory. Text with one of:
735 # DELETED (not found at vim)
736 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
737 # OTHER (Vim reported other status not understood)
738 # ERROR (VIM indicates an ERROR status)
739 # ACTIVE, INACTIVE, DOWN (admin down),
740 # BUILD (on building process)
742 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
743 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
747 for net_id
in net_list
:
750 net_vim
= self
.get_network(net_id
)
751 if net_vim
['status'] in netStatus2manoFormat
:
752 net
["status"] = netStatus2manoFormat
[ net_vim
['status'] ]
754 net
["status"] = "OTHER"
755 net
["error_msg"] = "VIM status reported " + net_vim
['status']
757 if net
['status'] == "ACTIVE" and not net_vim
['admin_state_up']:
758 net
['status'] = 'DOWN'
760 net
['vim_info'] = self
.serialize(net_vim
)
762 if net_vim
.get('fault'): #TODO
763 net
['error_msg'] = str(net_vim
['fault'])
764 except vimconn
.vimconnNotFoundException
as e
:
765 self
.logger
.error("Exception getting net status: %s", str(e
))
766 net
['status'] = "DELETED"
767 net
['error_msg'] = str(e
)
768 except vimconn
.vimconnException
as e
:
769 self
.logger
.error("Exception getting net status: %s", str(e
))
770 net
['status'] = "VIM_ERROR"
771 net
['error_msg'] = str(e
)
772 net_dict
[net_id
] = net
775 def get_flavor(self
, flavor_id
):
776 '''Obtain flavor details from the VIM. Returns the flavor dict details'''
777 self
.logger
.debug("Getting flavor '%s'", flavor_id
)
779 self
._reload
_connection
()
780 flavor
= self
.nova
.flavors
.find(id=flavor_id
)
781 #TODO parse input and translate to VIM format (openmano_schemas.new_vminstance_response_schema)
782 return flavor
.to_dict()
783 except (nvExceptions
.NotFound
, nvExceptions
.ClientException
, ksExceptions
.ClientException
, ConnectionError
) as e
:
784 self
._format
_exception
(e
)
786 def get_flavor_id_from_data(self
, flavor_dict
):
787 """Obtain flavor id that match the flavor description
788 Returns the flavor_id or raises a vimconnNotFoundException
789 flavor_dict: contains the required ram, vcpus, disk
790 If 'use_existing_flavors' is set to True at config, the closer flavor that provides same or more ram, vcpus
791 and disk is returned. Otherwise a flavor with exactly same ram, vcpus and disk is returned or a
792 vimconnNotFoundException is raised
794 exact_match
= False if self
.config
.get('use_existing_flavors') else True
796 self
._reload
_connection
()
797 flavor_candidate_id
= None
798 flavor_candidate_data
= (10000, 10000, 10000)
799 flavor_target
= (flavor_dict
["ram"], flavor_dict
["vcpus"], flavor_dict
["disk"])
801 extended
= flavor_dict
.get("extended", {})
804 raise vimconn
.vimconnNotFoundException("Flavor with EPA still not implemented")
806 # raise vimconn.vimconnNotFoundException("Cannot find any flavor with more than one numa")
808 # numas = extended.get("numas")
809 for flavor
in self
.nova
.flavors
.list():
810 epa
= flavor
.get_keys()
814 flavor_data
= (flavor
.ram
, flavor
.vcpus
, flavor
.disk
)
815 if flavor_data
== flavor_target
:
817 elif not exact_match
and flavor_target
< flavor_data
< flavor_candidate_data
:
818 flavor_candidate_id
= flavor
.id
819 flavor_candidate_data
= flavor_data
820 if not exact_match
and flavor_candidate_id
:
821 return flavor_candidate_id
822 raise vimconn
.vimconnNotFoundException("Cannot find any flavor matching '{}'".format(str(flavor_dict
)))
823 except (nvExceptions
.NotFound
, nvExceptions
.ClientException
, ksExceptions
.ClientException
, ConnectionError
) as e
:
824 self
._format
_exception
(e
)
826 def process_resource_quota(self
, quota
, prefix
, extra_specs
):
833 extra_specs
["quota:" + prefix
+ "_limit"] = quota
['limit']
834 if 'reserve' in quota
:
835 extra_specs
["quota:" + prefix
+ "_reservation"] = quota
['reserve']
836 if 'shares' in quota
:
837 extra_specs
["quota:" + prefix
+ "_shares_level"] = "custom"
838 extra_specs
["quota:" + prefix
+ "_shares_share"] = quota
['shares']
840 def new_flavor(self
, flavor_data
, change_name_if_used
=True):
841 '''Adds a tenant flavor to openstack VIM
842 if change_name_if_used is True, it will change name in case of conflict, because it is not supported name repetition
843 Returns the flavor identifier
845 self
.logger
.debug("Adding flavor '%s'", str(flavor_data
))
850 name
=flavor_data
['name']
851 while retry
<max_retries
:
854 self
._reload
_connection
()
855 if change_name_if_used
:
858 fl
=self
.nova
.flavors
.list()
860 fl_names
.append(f
.name
)
861 while name
in fl_names
:
863 name
= flavor_data
['name']+"-" + str(name_suffix
)
865 ram
= flavor_data
.get('ram',64)
866 vcpus
= flavor_data
.get('vcpus',1)
869 extended
= flavor_data
.get("extended")
871 numas
=extended
.get("numas")
873 numa_nodes
= len(numas
)
875 return -1, "Can not add flavor with more than one numa"
876 extra_specs
["hw:numa_nodes"] = str(numa_nodes
)
877 extra_specs
["hw:mem_page_size"] = "large"
878 extra_specs
["hw:cpu_policy"] = "dedicated"
879 extra_specs
["hw:numa_mempolicy"] = "strict"
880 if self
.vim_type
== "VIO":
881 extra_specs
["vmware:extra_config"] = '{"numa.nodeAffinity":"0"}'
882 extra_specs
["vmware:latency_sensitivity_level"] = "high"
884 #overwrite ram and vcpus
885 #check if key 'memory' is present in numa else use ram value at flavor
887 ram
= numa
['memory']*1024
888 #See for reference: https://specs.openstack.org/openstack/nova-specs/specs/mitaka/implemented/virt-driver-cpu-thread-pinning.html
889 extra_specs
["hw:cpu_sockets"] = 1
890 if 'paired-threads' in numa
:
891 vcpus
= numa
['paired-threads']*2
892 #cpu_thread_policy "require" implies that the compute node must have an STM architecture
893 extra_specs
["hw:cpu_thread_policy"] = "require"
894 extra_specs
["hw:cpu_policy"] = "dedicated"
895 elif 'cores' in numa
:
896 vcpus
= numa
['cores']
897 # cpu_thread_policy "prefer" implies that the host must not have an SMT architecture, or a non-SMT architecture will be emulated
898 extra_specs
["hw:cpu_thread_policy"] = "isolate"
899 extra_specs
["hw:cpu_policy"] = "dedicated"
900 elif 'threads' in numa
:
901 vcpus
= numa
['threads']
902 # cpu_thread_policy "prefer" implies that the host may or may not have an SMT architecture
903 extra_specs
["hw:cpu_thread_policy"] = "prefer"
904 extra_specs
["hw:cpu_policy"] = "dedicated"
905 # for interface in numa.get("interfaces",() ):
906 # if interface["dedicated"]=="yes":
907 # raise vimconn.vimconnException("Passthrough interfaces are not supported for the openstack connector", http_code=vimconn.HTTP_Service_Unavailable)
908 # #TODO, add the key 'pci_passthrough:alias"="<label at config>:<number ifaces>"' when a way to connect it is available
909 elif extended
.get("cpu-quota"):
910 self
.process_resource_quota(extended
.get("cpu-quota"), "cpu", extra_specs
)
911 if extended
.get("mem-quota"):
912 self
.process_resource_quota(extended
.get("mem-quota"), "memory", extra_specs
)
913 if extended
.get("vif-quota"):
914 self
.process_resource_quota(extended
.get("vif-quota"), "vif", extra_specs
)
915 if extended
.get("disk-io-quota"):
916 self
.process_resource_quota(extended
.get("disk-io-quota"), "disk_io", extra_specs
)
918 new_flavor
=self
.nova
.flavors
.create(name
,
921 flavor_data
.get('disk',0),
922 is_public
=flavor_data
.get('is_public', True)
926 new_flavor
.set_keys(extra_specs
)
928 except nvExceptions
.Conflict
as e
:
929 if change_name_if_used
and retry
< max_retries
:
931 self
._format
_exception
(e
)
932 #except nvExceptions.BadRequest as e:
933 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
, ConnectionError
, KeyError) as e
:
934 self
._format
_exception
(e
)
936 def delete_flavor(self
,flavor_id
):
937 '''Deletes a tenant flavor from openstack VIM. Returns the old flavor_id
940 self
._reload
_connection
()
941 self
.nova
.flavors
.delete(flavor_id
)
943 #except nvExceptions.BadRequest as e:
944 except (nvExceptions
.NotFound
, ksExceptions
.ClientException
, nvExceptions
.ClientException
, ConnectionError
) as e
:
945 self
._format
_exception
(e
)
947 def new_image(self
,image_dict
):
949 Adds a tenant image to VIM. imge_dict is a dictionary with:
951 disk_format: qcow2, vhd, vmdk, raw (by default), ...
952 location: path or URI
953 public: "yes" or "no"
954 metadata: metadata of the image
959 while retry
<max_retries
:
962 self
._reload
_connection
()
963 #determine format http://docs.openstack.org/developer/glance/formats.html
964 if "disk_format" in image_dict
:
965 disk_format
=image_dict
["disk_format"]
966 else: #autodiscover based on extension
967 if image_dict
['location'].endswith(".qcow2"):
969 elif image_dict
['location'].endswith(".vhd"):
971 elif image_dict
['location'].endswith(".vmdk"):
973 elif image_dict
['location'].endswith(".vdi"):
975 elif image_dict
['location'].endswith(".iso"):
977 elif image_dict
['location'].endswith(".aki"):
979 elif image_dict
['location'].endswith(".ari"):
981 elif image_dict
['location'].endswith(".ami"):
985 self
.logger
.debug("new_image: '%s' loading from '%s'", image_dict
['name'], image_dict
['location'])
986 if self
.vim_type
== "VIO":
987 container_format
= "bare"
988 if 'container_format' in image_dict
:
989 container_format
= image_dict
['container_format']
990 new_image
= self
.glance
.images
.create(name
=image_dict
['name'], container_format
=container_format
,
991 disk_format
=disk_format
)
993 new_image
= self
.glance
.images
.create(name
=image_dict
['name'])
994 if image_dict
['location'].startswith("http"):
995 # TODO there is not a method to direct download. It must be downloaded locally with requests
996 raise vimconn
.vimconnNotImplemented("Cannot create image from URL")
998 with
open(image_dict
['location']) as fimage
:
999 self
.glance
.images
.upload(new_image
.id, fimage
)
1000 #new_image = self.glancev1.images.create(name=image_dict['name'], is_public=image_dict.get('public',"yes")=="yes",
1001 # container_format="bare", data=fimage, disk_format=disk_format)
1002 metadata_to_load
= image_dict
.get('metadata')
1003 # TODO location is a reserved word for current openstack versions. fixed for VIO please check for openstack
1004 if self
.vim_type
== "VIO":
1005 metadata_to_load
['upload_location'] = image_dict
['location']
1007 metadata_to_load
['location'] = image_dict
['location']
1008 self
.glance
.images
.update(new_image
.id, **metadata_to_load
)
1010 except (nvExceptions
.Conflict
, ksExceptions
.ClientException
, nvExceptions
.ClientException
) as e
:
1011 self
._format
_exception
(e
)
1012 except (HTTPException
, gl1Exceptions
.HTTPException
, gl1Exceptions
.CommunicationError
, ConnectionError
) as e
:
1013 if retry
==max_retries
:
1015 self
._format
_exception
(e
)
1016 except IOError as e
: #can not open the file
1017 raise vimconn
.vimconnConnectionException(type(e
).__name
__ + ": " + str(e
)+ " for " + image_dict
['location'],
1018 http_code
=vimconn
.HTTP_Bad_Request
)
1020 def delete_image(self
, image_id
):
1021 '''Deletes a tenant image from openstack VIM. Returns the old id
1024 self
._reload
_connection
()
1025 self
.glance
.images
.delete(image_id
)
1027 except (nvExceptions
.NotFound
, ksExceptions
.ClientException
, nvExceptions
.ClientException
, gl1Exceptions
.CommunicationError
, gl1Exceptions
.HTTPNotFound
, ConnectionError
) as e
: #TODO remove
1028 self
._format
_exception
(e
)
1030 def get_image_id_from_path(self
, path
):
1031 '''Get the image id from image path in the VIM database. Returns the image_id'''
1033 self
._reload
_connection
()
1034 images
= self
.glance
.images
.list()
1035 for image
in images
:
1036 if image
.metadata
.get("location")==path
:
1038 raise vimconn
.vimconnNotFoundException("image with location '{}' not found".format( path
))
1039 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
, gl1Exceptions
.CommunicationError
, ConnectionError
) as e
:
1040 self
._format
_exception
(e
)
1042 def get_image_list(self
, filter_dict
={}):
1043 '''Obtain tenant images from VIM
1047 checksum: image checksum
1048 Returns the image list of dictionaries:
1049 [{<the fields at Filter_dict plus some VIM specific>}, ...]
1052 self
.logger
.debug("Getting image list from VIM filter: '%s'", str(filter_dict
))
1054 self
._reload
_connection
()
1055 filter_dict_os
= filter_dict
.copy()
1056 #First we filter by the available filter fields: name, id. The others are removed.
1057 image_list
= self
.glance
.images
.list()
1059 for image
in image_list
:
1061 if filter_dict
.get("name") and image
["name"] != filter_dict
["name"]:
1063 if filter_dict
.get("id") and image
["id"] != filter_dict
["id"]:
1065 if filter_dict
.get("checksum") and image
["checksum"] != filter_dict
["checksum"]:
1068 filtered_list
.append(image
.copy())
1069 except gl1Exceptions
.HTTPNotFound
:
1071 return filtered_list
1072 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
, gl1Exceptions
.CommunicationError
, ConnectionError
) as e
:
1073 self
._format
_exception
(e
)
1075 def __wait_for_vm(self
, vm_id
, status
):
1076 """wait until vm is in the desired status and return True.
1077 If the VM gets in ERROR status, return false.
1078 If the timeout is reached generate an exception"""
1080 while elapsed_time
< server_timeout
:
1081 vm_status
= self
.nova
.servers
.get(vm_id
).status
1082 if vm_status
== status
:
1084 if vm_status
== 'ERROR':
1089 # if we exceeded the timeout rollback
1090 if elapsed_time
>= server_timeout
:
1091 raise vimconn
.vimconnException('Timeout waiting for instance ' + vm_id
+ ' to get ' + status
,
1092 http_code
=vimconn
.HTTP_Request_Timeout
)
1094 def _get_openstack_availablity_zones(self
):
1096 Get from openstack availability zones available
1100 openstack_availability_zone
= self
.nova
.availability_zones
.list()
1101 openstack_availability_zone
= [str(zone
.zoneName
) for zone
in openstack_availability_zone
1102 if zone
.zoneName
!= 'internal']
1103 return openstack_availability_zone
1104 except Exception as e
:
1107 def _set_availablity_zones(self
):
1109 Set vim availablity zone
1113 if 'availability_zone' in self
.config
:
1114 vim_availability_zones
= self
.config
.get('availability_zone')
1115 if isinstance(vim_availability_zones
, str):
1116 self
.availability_zone
= [vim_availability_zones
]
1117 elif isinstance(vim_availability_zones
, list):
1118 self
.availability_zone
= vim_availability_zones
1120 self
.availability_zone
= self
._get
_openstack
_availablity
_zones
()
1122 def _get_vm_availability_zone(self
, availability_zone_index
, availability_zone_list
):
1124 Return thge availability zone to be used by the created VM.
1125 :return: The VIM availability zone to be used or None
1127 if availability_zone_index
is None:
1128 if not self
.config
.get('availability_zone'):
1130 elif isinstance(self
.config
.get('availability_zone'), str):
1131 return self
.config
['availability_zone']
1133 # TODO consider using a different parameter at config for default AV and AV list match
1134 return self
.config
['availability_zone'][0]
1136 vim_availability_zones
= self
.availability_zone
1137 # check if VIM offer enough availability zones describe in the VNFD
1138 if vim_availability_zones
and len(availability_zone_list
) <= len(vim_availability_zones
):
1139 # check if all the names of NFV AV match VIM AV names
1140 match_by_index
= False
1141 for av
in availability_zone_list
:
1142 if av
not in vim_availability_zones
:
1143 match_by_index
= True
1146 return vim_availability_zones
[availability_zone_index
]
1148 return availability_zone_list
[availability_zone_index
]
1150 raise vimconn
.vimconnConflictException("No enough availability zones at VIM for this deployment")
1152 def new_vminstance(self
, name
, description
, start
, image_id
, flavor_id
, net_list
, cloud_config
=None, disk_list
=None,
1153 availability_zone_index
=None, availability_zone_list
=None):
1154 """Adds a VM instance to VIM
1156 start: indicates if VM must start or boot in pause mode. Ignored
1157 image_id,flavor_id: iamge and flavor uuid
1158 net_list: list of interfaces, each one is a dictionary with:
1160 net_id: network uuid to connect
1161 vpci: virtual vcpi to assign, ignored because openstack lack #TODO
1162 model: interface model, ignored #TODO
1163 mac_address: used for SR-IOV ifaces #TODO for other types
1164 use: 'data', 'bridge', 'mgmt'
1165 type: 'virtual', 'PCI-PASSTHROUGH'('PF'), 'SR-IOV'('VF'), 'VFnotShared'
1166 vim_id: filled/added by this function
1167 floating_ip: True/False (or it can be None)
1168 'cloud_config': (optional) dictionary with:
1169 'key-pairs': (optional) list of strings with the public key to be inserted to the default user
1170 'users': (optional) list of users to be inserted, each item is a dict with:
1171 'name': (mandatory) user name,
1172 'key-pairs': (optional) list of strings with the public key to be inserted to the user
1173 'user-data': (optional) string is a text script to be passed directly to cloud-init
1174 'config-files': (optional). List of files to be transferred. Each item is a dict with:
1175 'dest': (mandatory) string with the destination absolute path
1176 'encoding': (optional, by default text). Can be one of:
1177 'b64', 'base64', 'gz', 'gz+b64', 'gz+base64', 'gzip+b64', 'gzip+base64'
1178 'content' (mandatory): string with the content of the file
1179 'permissions': (optional) string with file permissions, typically octal notation '0644'
1180 'owner': (optional) file owner, string with the format 'owner:group'
1181 'boot-data-drive': boolean to indicate if user-data must be passed using a boot drive (hard disk)
1182 'disk_list': (optional) list with additional disks to the VM. Each item is a dict with:
1183 'image_id': (optional). VIM id of an existing image. If not provided an empty disk must be mounted
1184 'size': (mandatory) string with the size of the disk in GB
1185 'vim_id' (optional) should use this existing volume id
1186 availability_zone_index: Index of availability_zone_list to use for this this VM. None if not AV required
1187 availability_zone_list: list of availability zones given by user in the VNFD descriptor. Ignore if
1188 availability_zone_index is None
1189 #TODO ip, security groups
1190 Returns a tuple with the instance identifier and created_items or raises an exception on error
1191 created_items can be None or a dictionary where this method can include key-values that will be passed to
1192 the method delete_vminstance and action_vminstance. Can be used to store created ports, volumes, etc.
1193 Format is vimconnector dependent, but do not use nested dictionaries and a value of None should be the same
1196 self
.logger
.debug("new_vminstance input: image='%s' flavor='%s' nics='%s'",image_id
, flavor_id
,str(net_list
))
1202 external_network
= [] # list of external networks to be connected to instance, later on used to create floating_ip
1203 no_secured_ports
= [] # List of port-is with port-security disabled
1204 self
._reload
_connection
()
1205 # metadata_vpci = {} # For a specific neutron plugin
1206 block_device_mapping
= None
1208 for net
in net_list
:
1209 if not net
.get("net_id"): # skip non connected iface
1213 "network_id": net
["net_id"],
1214 "name": net
.get("name"),
1215 "admin_state_up": True
1217 if self
.config
.get("security_groups") and net
.get("port_security") is not False and \
1218 not self
.config
.get("no_port_security_extension"):
1219 if not self
.security_groups_id
:
1220 self
._get
_ids
_from
_name
()
1221 port_dict
["security_groups"] = self
.security_groups_id
1223 if net
["type"]=="virtual":
1226 # metadata_vpci[ net["net_id"] ] = [[ net["vpci"], "" ]]
1227 elif net
["type"] == "VF" or net
["type"] == "SR-IOV": # for VF
1229 # if "VF" not in metadata_vpci:
1230 # metadata_vpci["VF"]=[]
1231 # metadata_vpci["VF"].append([ net["vpci"], "" ])
1232 port_dict
["binding:vnic_type"]="direct"
1233 # VIO specific Changes
1234 if self
.vim_type
== "VIO":
1235 # Need to create port with port_security_enabled = False and no-security-groups
1236 port_dict
["port_security_enabled"]=False
1237 port_dict
["provider_security_groups"]=[]
1238 port_dict
["security_groups"]=[]
1239 else: # For PT PCI-PASSTHROUGH
1240 # VIO specific Changes
1241 # Current VIO release does not support port with type 'direct-physical'
1242 # So no need to create virtual port in case of PCI-device.
1243 # Will update port_dict code when support gets added in next VIO release
1244 if self
.vim_type
== "VIO":
1245 raise vimconn
.vimconnNotSupportedException(
1246 "Current VIO release does not support full passthrough (PT)")
1248 # if "PF" not in metadata_vpci:
1249 # metadata_vpci["PF"]=[]
1250 # metadata_vpci["PF"].append([ net["vpci"], "" ])
1251 port_dict
["binding:vnic_type"]="direct-physical"
1252 if not port_dict
["name"]:
1253 port_dict
["name"]=name
1254 if net
.get("mac_address"):
1255 port_dict
["mac_address"]=net
["mac_address"]
1256 if net
.get("ip_address"):
1257 port_dict
["fixed_ips"] = [{'ip_address': net
["ip_address"]}]
1258 # TODO add 'subnet_id': <subnet_id>
1259 new_port
= self
.neutron
.create_port({"port": port_dict
})
1260 created_items
["port:" + str(new_port
["port"]["id"])] = True
1261 net
["mac_adress"] = new_port
["port"]["mac_address"]
1262 net
["vim_id"] = new_port
["port"]["id"]
1263 # if try to use a network without subnetwork, it will return a emtpy list
1264 fixed_ips
= new_port
["port"].get("fixed_ips")
1266 net
["ip"] = fixed_ips
[0].get("ip_address")
1270 port
= {"port-id": new_port
["port"]["id"]}
1271 if float(self
.nova
.api_version
.get_string()) >= 2.32:
1272 port
["tag"] = new_port
["port"]["name"]
1273 net_list_vim
.append(port
)
1275 if net
.get('floating_ip', False):
1276 net
['exit_on_floating_ip_error'] = True
1277 external_network
.append(net
)
1278 elif net
['use'] == 'mgmt' and self
.config
.get('use_floating_ip'):
1279 net
['exit_on_floating_ip_error'] = False
1280 external_network
.append(net
)
1281 net
['floating_ip'] = self
.config
.get('use_floating_ip')
1283 # If port security is disabled when the port has not yet been attached to the VM, then all vm traffic is dropped.
1284 # As a workaround we wait until the VM is active and then disable the port-security
1285 if net
.get("port_security") == False and not self
.config
.get("no_port_security_extension"):
1286 no_secured_ports
.append(new_port
["port"]["id"])
1289 # metadata = {"pci_assignement": json.dumps(metadata_vpci)}
1290 # if len(metadata["pci_assignement"]) >255:
1291 # #limit the metadata size
1292 # #metadata["pci_assignement"] = metadata["pci_assignement"][0:255]
1293 # self.logger.warn("Metadata deleted since it exceeds the expected length (255) ")
1296 self
.logger
.debug("name '%s' image_id '%s'flavor_id '%s' net_list_vim '%s' description '%s'",
1297 name
, image_id
, flavor_id
, str(net_list_vim
), description
)
1300 config_drive
, userdata
= self
._create
_user
_data
(cloud_config
)
1302 # Create additional volumes in case these are present in disk_list
1303 base_disk_index
= ord('b')
1305 block_device_mapping
= {}
1306 for disk
in disk_list
:
1307 if disk
.get('vim_id'):
1308 block_device_mapping
['_vd' + chr(base_disk_index
)] = disk
['vim_id']
1310 if 'image_id' in disk
:
1311 volume
= self
.cinder
.volumes
.create(size
=disk
['size'], name
=name
+ '_vd' +
1312 chr(base_disk_index
), imageRef
=disk
['image_id'])
1314 volume
= self
.cinder
.volumes
.create(size
=disk
['size'], name
=name
+ '_vd' +
1315 chr(base_disk_index
))
1316 created_items
["volume:" + str(volume
.id)] = True
1317 block_device_mapping
['_vd' + chr(base_disk_index
)] = volume
.id
1318 base_disk_index
+= 1
1320 # Wait until created volumes are with status available
1322 while elapsed_time
< volume_timeout
:
1323 for created_item
in created_items
:
1324 v
, _
, volume_id
= created_item
.partition(":")
1326 if self
.cinder
.volumes
.get(volume_id
).status
!= 'available':
1328 else: # all ready: break from while
1332 # If we exceeded the timeout rollback
1333 if elapsed_time
>= volume_timeout
:
1334 raise vimconn
.vimconnException('Timeout creating volumes for instance ' + name
,
1335 http_code
=vimconn
.HTTP_Request_Timeout
)
1336 # get availability Zone
1337 vm_av_zone
= self
._get
_vm
_availability
_zone
(availability_zone_index
, availability_zone_list
)
1339 self
.logger
.debug("nova.servers.create({}, {}, {}, nics={}, security_groups={}, "
1340 "availability_zone={}, key_name={}, userdata={}, config_drive={}, "
1341 "block_device_mapping={})".format(name
, image_id
, flavor_id
, net_list_vim
,
1342 self
.config
.get("security_groups"), vm_av_zone
,
1343 self
.config
.get('keypair'), userdata
, config_drive
,
1344 block_device_mapping
))
1345 server
= self
.nova
.servers
.create(name
, image_id
, flavor_id
, nics
=net_list_vim
,
1346 security_groups
=self
.config
.get("security_groups"),
1347 # TODO remove security_groups in future versions. Already at neutron port
1348 availability_zone
=vm_av_zone
,
1349 key_name
=self
.config
.get('keypair'),
1351 config_drive
=config_drive
,
1352 block_device_mapping
=block_device_mapping
1353 ) # , description=description)
1355 vm_start_time
= time
.time()
1356 # Previously mentioned workaround to wait until the VM is active and then disable the port-security
1357 if no_secured_ports
:
1358 self
.__wait
_for
_vm
(server
.id, 'ACTIVE')
1360 for port_id
in no_secured_ports
:
1362 self
.neutron
.update_port(port_id
,
1363 {"port": {"port_security_enabled": False, "security_groups": None}})
1364 except Exception as e
:
1365 raise vimconn
.vimconnException("It was not possible to disable port security for port {}".format(
1367 # print "DONE :-)", server
1370 if external_network
:
1371 floating_ips
= self
.neutron
.list_floatingips().get("floatingips", ())
1372 for floating_network
in external_network
:
1377 ip
= floating_ips
.pop(0)
1378 if ip
.get("port_id", False) or ip
.get('tenant_id') != server
.tenant_id
:
1380 if isinstance(floating_network
['floating_ip'], str):
1381 if ip
.get("floating_network_id") != floating_network
['floating_ip']:
1383 free_floating_ip
= ip
["id"]
1385 if isinstance(floating_network
['floating_ip'], str) and \
1386 floating_network
['floating_ip'].lower() != "true":
1387 pool_id
= floating_network
['floating_ip']
1389 # Find the external network
1390 external_nets
= list()
1391 for net
in self
.neutron
.list_networks()['networks']:
1392 if net
['router:external']:
1393 external_nets
.append(net
)
1395 if len(external_nets
) == 0:
1396 raise vimconn
.vimconnException("Cannot create floating_ip automatically since no external "
1397 "network is present",
1398 http_code
=vimconn
.HTTP_Conflict
)
1399 if len(external_nets
) > 1:
1400 raise vimconn
.vimconnException("Cannot create floating_ip automatically since multiple "
1401 "external networks are present",
1402 http_code
=vimconn
.HTTP_Conflict
)
1404 pool_id
= external_nets
[0].get('id')
1405 param
= {'floatingip': {'floating_network_id': pool_id
, 'tenant_id': server
.tenant_id
}}
1407 # self.logger.debug("Creating floating IP")
1408 new_floating_ip
= self
.neutron
.create_floatingip(param
)
1409 free_floating_ip
= new_floating_ip
['floatingip']['id']
1410 except Exception as e
:
1411 raise vimconn
.vimconnException(type(e
).__name
__ + ": Cannot create new floating_ip " +
1412 str(e
), http_code
=vimconn
.HTTP_Conflict
)
1416 # the vim_id key contains the neutron.port_id
1417 self
.neutron
.update_floatingip(free_floating_ip
,
1418 {"floatingip": {"port_id": floating_network
["vim_id"]}})
1419 # Using nove is deprecated on nova client 10.0
1421 except Exception as e
:
1422 # openstack need some time after VM creation to asign an IP. So retry if fails
1423 vm_status
= self
.nova
.servers
.get(server
.id).status
1424 if vm_status
!= 'ACTIVE' and vm_status
!= 'ERROR':
1425 if time
.time() - vm_start_time
< server_timeout
:
1428 raise vimconn
.vimconnException(
1429 "Cannot create floating_ip: {} {}".format(type(e
).__name
__, e
),
1430 http_code
=vimconn
.HTTP_Conflict
)
1432 except Exception as e
:
1433 if not floating_network
['exit_on_floating_ip_error']:
1434 self
.logger
.warning("Cannot create floating_ip. %s", str(e
))
1438 return server
.id, created_items
1439 # except nvExceptions.NotFound as e:
1440 # error_value=-vimconn.HTTP_Not_Found
1441 # error_text= "vm instance %s not found" % vm_id
1442 # except TypeError as e:
1443 # raise vimconn.vimconnException(type(e).__name__ + ": "+ str(e), http_code=vimconn.HTTP_Bad_Request)
1445 except Exception as e
:
1448 server_id
= server
.id
1450 self
.delete_vminstance(server_id
, created_items
)
1451 except Exception as e2
:
1452 self
.logger
.error("new_vminstance rollback fail {}".format(e2
))
1454 self
._format
_exception
(e
)
1456 def get_vminstance(self
,vm_id
):
1457 '''Returns the VM instance information from VIM'''
1458 #self.logger.debug("Getting VM from VIM")
1460 self
._reload
_connection
()
1461 server
= self
.nova
.servers
.find(id=vm_id
)
1462 #TODO parse input and translate to VIM format (openmano_schemas.new_vminstance_response_schema)
1463 return server
.to_dict()
1464 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
, nvExceptions
.NotFound
, ConnectionError
) as e
:
1465 self
._format
_exception
(e
)
1467 def get_vminstance_console(self
,vm_id
, console_type
="vnc"):
1469 Get a console for the virtual machine
1471 vm_id: uuid of the VM
1472 console_type, can be:
1473 "novnc" (by default), "xvpvnc" for VNC types,
1474 "rdp-html5" for RDP types, "spice-html5" for SPICE types
1475 Returns dict with the console parameters:
1476 protocol: ssh, ftp, http, https, ...
1477 server: usually ip address
1478 port: the http, ssh, ... port
1479 suffix: extra text, e.g. the http path and query string
1481 self
.logger
.debug("Getting VM CONSOLE from VIM")
1483 self
._reload
_connection
()
1484 server
= self
.nova
.servers
.find(id=vm_id
)
1485 if console_type
== None or console_type
== "novnc":
1486 console_dict
= server
.get_vnc_console("novnc")
1487 elif console_type
== "xvpvnc":
1488 console_dict
= server
.get_vnc_console(console_type
)
1489 elif console_type
== "rdp-html5":
1490 console_dict
= server
.get_rdp_console(console_type
)
1491 elif console_type
== "spice-html5":
1492 console_dict
= server
.get_spice_console(console_type
)
1494 raise vimconn
.vimconnException("console type '{}' not allowed".format(console_type
), http_code
=vimconn
.HTTP_Bad_Request
)
1496 console_dict1
= console_dict
.get("console")
1498 console_url
= console_dict1
.get("url")
1501 protocol_index
= console_url
.find("//")
1502 suffix_index
= console_url
[protocol_index
+2:].find("/") + protocol_index
+2
1503 port_index
= console_url
[protocol_index
+2:suffix_index
].find(":") + protocol_index
+2
1504 if protocol_index
< 0 or port_index
<0 or suffix_index
<0:
1505 return -vimconn
.HTTP_Internal_Server_Error
, "Unexpected response from VIM"
1506 console_dict
={"protocol": console_url
[0:protocol_index
],
1507 "server": console_url
[protocol_index
+2:port_index
],
1508 "port": console_url
[port_index
:suffix_index
],
1509 "suffix": console_url
[suffix_index
+1:]
1513 raise vimconn
.vimconnUnexpectedResponse("Unexpected response from VIM")
1515 except (nvExceptions
.NotFound
, ksExceptions
.ClientException
, nvExceptions
.ClientException
, nvExceptions
.BadRequest
, ConnectionError
) as e
:
1516 self
._format
_exception
(e
)
1518 def delete_vminstance(self
, vm_id
, created_items
=None):
1519 '''Removes a VM instance from VIM. Returns the old identifier
1521 #print "osconnector: Getting VM from VIM"
1522 if created_items
== None:
1525 self
._reload
_connection
()
1526 # delete VM ports attached to this networks before the virtual machine
1527 for k
, v
in created_items
.items():
1528 if not v
: # skip already deleted
1531 k_item
, _
, k_id
= k
.partition(":")
1532 if k_item
== "port":
1533 self
.neutron
.delete_port(k_id
)
1534 except Exception as e
:
1535 self
.logger
.error("Error deleting port: {}: {}".format(type(e
).__name
__, e
))
1537 # #commented because detaching the volumes makes the servers.delete not work properly ?!?
1538 # #dettach volumes attached
1539 # server = self.nova.servers.get(vm_id)
1540 # volumes_attached_dict = server._info['os-extended-volumes:volumes_attached'] #volume['id']
1541 # #for volume in volumes_attached_dict:
1542 # # self.cinder.volumes.detach(volume['id'])
1545 self
.nova
.servers
.delete(vm_id
)
1547 # delete volumes. Although having detached, they should have in active status before deleting
1548 # we ensure in this loop
1551 while keep_waiting
and elapsed_time
< volume_timeout
:
1552 keep_waiting
= False
1553 for k
, v
in created_items
.items():
1554 if not v
: # skip already deleted
1557 k_item
, _
, k_id
= k
.partition(":")
1558 if k_item
== "volume":
1559 if self
.cinder
.volumes
.get(k_id
).status
!= 'available':
1562 self
.cinder
.volumes
.delete(k_id
)
1563 except Exception as e
:
1564 self
.logger
.error("Error deleting volume: {}: {}".format(type(e
).__name
__, e
))
1569 except (nvExceptions
.NotFound
, ksExceptions
.ClientException
, nvExceptions
.ClientException
, ConnectionError
) as e
:
1570 self
._format
_exception
(e
)
1572 def refresh_vms_status(self
, vm_list
):
1573 '''Get the status of the virtual machines and their interfaces/ports
1574 Params: the list of VM identifiers
1575 Returns a dictionary with:
1576 vm_id: #VIM id of this Virtual Machine
1577 status: #Mandatory. Text with one of:
1578 # DELETED (not found at vim)
1579 # VIM_ERROR (Cannot connect to VIM, VIM response error, ...)
1580 # OTHER (Vim reported other status not understood)
1581 # ERROR (VIM indicates an ERROR status)
1582 # ACTIVE, PAUSED, SUSPENDED, INACTIVE (not running),
1583 # CREATING (on building process), ERROR
1584 # ACTIVE:NoMgmtIP (Active but any of its interface has an IP address
1586 error_msg: #Text with VIM error message, if any. Or the VIM connection ERROR
1587 vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1589 - vim_info: #Text with plain information obtained from vim (yaml.safe_dump)
1590 mac_address: #Text format XX:XX:XX:XX:XX:XX
1591 vim_net_id: #network id where this interface is connected
1592 vim_interface_id: #interface/port VIM id
1593 ip_address: #null, or text with IPv4, IPv6 address
1594 compute_node: #identification of compute node where PF,VF interface is allocated
1595 pci: #PCI address of the NIC that hosts the PF,VF
1596 vlan: #physical VLAN used for VF
1599 self
.logger
.debug("refresh_vms status: Getting tenant VM instance information from VIM")
1600 for vm_id
in vm_list
:
1603 vm_vim
= self
.get_vminstance(vm_id
)
1604 if vm_vim
['status'] in vmStatus2manoFormat
:
1605 vm
['status'] = vmStatus2manoFormat
[ vm_vim
['status'] ]
1607 vm
['status'] = "OTHER"
1608 vm
['error_msg'] = "VIM status reported " + vm_vim
['status']
1610 vm
['vim_info'] = self
.serialize(vm_vim
)
1612 vm
["interfaces"] = []
1613 if vm_vim
.get('fault'):
1614 vm
['error_msg'] = str(vm_vim
['fault'])
1617 self
._reload
_connection
()
1618 port_dict
= self
.neutron
.list_ports(device_id
=vm_id
)
1619 for port
in port_dict
["ports"]:
1621 interface
['vim_info'] = self
.serialize(port
)
1622 interface
["mac_address"] = port
.get("mac_address")
1623 interface
["vim_net_id"] = port
["network_id"]
1624 interface
["vim_interface_id"] = port
["id"]
1625 # check if OS-EXT-SRV-ATTR:host is there,
1626 # in case of non-admin credentials, it will be missing
1627 if vm_vim
.get('OS-EXT-SRV-ATTR:host'):
1628 interface
["compute_node"] = vm_vim
['OS-EXT-SRV-ATTR:host']
1629 interface
["pci"] = None
1631 # check if binding:profile is there,
1632 # in case of non-admin credentials, it will be missing
1633 if port
.get('binding:profile'):
1634 if port
['binding:profile'].get('pci_slot'):
1635 # TODO: At the moment sr-iov pci addresses are converted to PF pci addresses by setting the slot to 0x00
1636 # TODO: This is just a workaround valid for niantinc. Find a better way to do so
1637 # CHANGE DDDD:BB:SS.F to DDDD:BB:00.(F%2) assuming there are 2 ports per nic
1638 pci
= port
['binding:profile']['pci_slot']
1639 # interface["pci"] = pci[:-4] + "00." + str(int(pci[-1]) % 2)
1640 interface
["pci"] = pci
1641 interface
["vlan"] = None
1642 #if network is of type vlan and port is of type direct (sr-iov) then set vlan id
1643 network
= self
.neutron
.show_network(port
["network_id"])
1644 if network
['network'].get('provider:network_type') == 'vlan' and \
1645 port
.get("binding:vnic_type") == "direct":
1646 interface
["vlan"] = network
['network'].get('provider:segmentation_id')
1648 #look for floating ip address
1650 floating_ip_dict
= self
.neutron
.list_floatingips(port_id
=port
["id"])
1651 if floating_ip_dict
.get("floatingips"):
1652 ips
.append(floating_ip_dict
["floatingips"][0].get("floating_ip_address") )
1656 for subnet
in port
["fixed_ips"]:
1657 ips
.append(subnet
["ip_address"])
1658 interface
["ip_address"] = ";".join(ips
)
1659 vm
["interfaces"].append(interface
)
1660 except Exception as e
:
1661 self
.logger
.error("Error getting vm interface information {}: {}".format(type(e
).__name
__, e
),
1663 except vimconn
.vimconnNotFoundException
as e
:
1664 self
.logger
.error("Exception getting vm status: %s", str(e
))
1665 vm
['status'] = "DELETED"
1666 vm
['error_msg'] = str(e
)
1667 except vimconn
.vimconnException
as e
:
1668 self
.logger
.error("Exception getting vm status: %s", str(e
))
1669 vm
['status'] = "VIM_ERROR"
1670 vm
['error_msg'] = str(e
)
1674 def action_vminstance(self
, vm_id
, action_dict
, created_items
={}):
1675 '''Send and action over a VM instance from VIM
1676 Returns None or the console dict if the action was successfully sent to the VIM'''
1677 self
.logger
.debug("Action over VM '%s': %s", vm_id
, str(action_dict
))
1679 self
._reload
_connection
()
1680 server
= self
.nova
.servers
.find(id=vm_id
)
1681 if "start" in action_dict
:
1682 if action_dict
["start"]=="rebuild":
1685 if server
.status
=="PAUSED":
1687 elif server
.status
=="SUSPENDED":
1689 elif server
.status
=="SHUTOFF":
1691 elif "pause" in action_dict
:
1693 elif "resume" in action_dict
:
1695 elif "shutoff" in action_dict
or "shutdown" in action_dict
:
1697 elif "forceOff" in action_dict
:
1699 elif "terminate" in action_dict
:
1701 elif "createImage" in action_dict
:
1702 server
.create_image()
1703 #"path":path_schema,
1704 #"description":description_schema,
1705 #"name":name_schema,
1706 #"metadata":metadata_schema,
1707 #"imageRef": id_schema,
1708 #"disk": {"oneOf":[{"type": "null"}, {"type":"string"}] },
1709 elif "rebuild" in action_dict
:
1710 server
.rebuild(server
.image
['id'])
1711 elif "reboot" in action_dict
:
1712 server
.reboot() #reboot_type='SOFT'
1713 elif "console" in action_dict
:
1714 console_type
= action_dict
["console"]
1715 if console_type
== None or console_type
== "novnc":
1716 console_dict
= server
.get_vnc_console("novnc")
1717 elif console_type
== "xvpvnc":
1718 console_dict
= server
.get_vnc_console(console_type
)
1719 elif console_type
== "rdp-html5":
1720 console_dict
= server
.get_rdp_console(console_type
)
1721 elif console_type
== "spice-html5":
1722 console_dict
= server
.get_spice_console(console_type
)
1724 raise vimconn
.vimconnException("console type '{}' not allowed".format(console_type
),
1725 http_code
=vimconn
.HTTP_Bad_Request
)
1727 console_url
= console_dict
["console"]["url"]
1729 protocol_index
= console_url
.find("//")
1730 suffix_index
= console_url
[protocol_index
+2:].find("/") + protocol_index
+2
1731 port_index
= console_url
[protocol_index
+2:suffix_index
].find(":") + protocol_index
+2
1732 if protocol_index
< 0 or port_index
<0 or suffix_index
<0:
1733 raise vimconn
.vimconnException("Unexpected response from VIM " + str(console_dict
))
1734 console_dict2
={"protocol": console_url
[0:protocol_index
],
1735 "server": console_url
[protocol_index
+2 : port_index
],
1736 "port": int(console_url
[port_index
+1 : suffix_index
]),
1737 "suffix": console_url
[suffix_index
+1:]
1739 return console_dict2
1740 except Exception as e
:
1741 raise vimconn
.vimconnException("Unexpected response from VIM " + str(console_dict
))
1744 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
, nvExceptions
.NotFound
, ConnectionError
) as e
:
1745 self
._format
_exception
(e
)
1746 #TODO insert exception vimconn.HTTP_Unauthorized
1748 ####### VIO Specific Changes #########
1749 def _generate_vlanID(self
):
1751 Method to get unused vlanID
1759 networks
= self
.get_network_list()
1760 for net
in networks
:
1761 if net
.get('provider:segmentation_id'):
1762 usedVlanIDs
.append(net
.get('provider:segmentation_id'))
1763 used_vlanIDs
= set(usedVlanIDs
)
1765 #find unused VLAN ID
1766 for vlanID_range
in self
.config
.get('dataplane_net_vlan_range'):
1768 start_vlanid
, end_vlanid
= map(int, vlanID_range
.replace(" ", "").split("-"))
1769 for vlanID
in range(start_vlanid
, end_vlanid
+ 1):
1770 if vlanID
not in used_vlanIDs
:
1772 except Exception as exp
:
1773 raise vimconn
.vimconnException("Exception {} occurred while generating VLAN ID.".format(exp
))
1775 raise vimconn
.vimconnConflictException("Unable to create the SRIOV VLAN network."\
1776 " All given Vlan IDs {} are in use.".format(self
.config
.get('dataplane_net_vlan_range')))
1779 def _generate_multisegment_vlanID(self
):
1781 Method to get unused vlanID
1789 networks
= self
.get_network_list()
1790 for net
in networks
:
1791 if net
.get('provider:network_type') == "vlan" and net
.get('provider:segmentation_id'):
1792 usedVlanIDs
.append(net
.get('provider:segmentation_id'))
1793 elif net
.get('segments'):
1794 for segment
in net
.get('segments'):
1795 if segment
.get('provider:network_type') == "vlan" and segment
.get('provider:segmentation_id'):
1796 usedVlanIDs
.append(segment
.get('provider:segmentation_id'))
1797 used_vlanIDs
= set(usedVlanIDs
)
1799 #find unused VLAN ID
1800 for vlanID_range
in self
.config
.get('multisegment_vlan_range'):
1802 start_vlanid
, end_vlanid
= map(int, vlanID_range
.replace(" ", "").split("-"))
1803 for vlanID
in range(start_vlanid
, end_vlanid
+ 1):
1804 if vlanID
not in used_vlanIDs
:
1806 except Exception as exp
:
1807 raise vimconn
.vimconnException("Exception {} occurred while generating VLAN ID.".format(exp
))
1809 raise vimconn
.vimconnConflictException("Unable to create the VLAN segment."\
1810 " All VLAN IDs {} are in use.".format(self
.config
.get('multisegment_vlan_range')))
1813 def _validate_vlan_ranges(self
, input_vlan_range
, text_vlan_range
):
1815 Method to validate user given vlanID ranges
1819 for vlanID_range
in input_vlan_range
:
1820 vlan_range
= vlanID_range
.replace(" ", "")
1822 vlanID_pattern
= r
'(\d)*-(\d)*$'
1823 match_obj
= re
.match(vlanID_pattern
, vlan_range
)
1825 raise vimconn
.vimconnConflictException("Invalid VLAN range for {}: {}.You must provide "\
1826 "'{}' in format [start_ID - end_ID].".format(text_vlan_range
, vlanID_range
, text_vlan_range
))
1828 start_vlanid
, end_vlanid
= map(int,vlan_range
.split("-"))
1829 if start_vlanid
<= 0 :
1830 raise vimconn
.vimconnConflictException("Invalid VLAN range for {}: {}."\
1831 "Start ID can not be zero. For VLAN "\
1832 "networks valid IDs are 1 to 4094 ".format(text_vlan_range
, vlanID_range
))
1833 if end_vlanid
> 4094 :
1834 raise vimconn
.vimconnConflictException("Invalid VLAN range for {}: {}."\
1835 "End VLAN ID can not be greater than 4094. For VLAN "\
1836 "networks valid IDs are 1 to 4094 ".format(text_vlan_range
, vlanID_range
))
1838 if start_vlanid
> end_vlanid
:
1839 raise vimconn
.vimconnConflictException("Invalid VLAN range for {}: {}."\
1840 "You must provide '{}' in format start_ID - end_ID and "\
1841 "start_ID < end_ID ".format(text_vlan_range
, vlanID_range
, text_vlan_range
))
1845 def new_external_port(self
, port_data
):
1846 #TODO openstack if needed
1847 '''Adds a external port to VIM'''
1848 '''Returns the port identifier'''
1849 return -vimconn
.HTTP_Internal_Server_Error
, "osconnector.new_external_port() not implemented"
1851 def connect_port_network(self
, port_id
, network_id
, admin
=False):
1852 #TODO openstack if needed
1853 '''Connects a external port to a network'''
1854 '''Returns status code of the VIM response'''
1855 return -vimconn
.HTTP_Internal_Server_Error
, "osconnector.connect_port_network() not implemented"
1857 def new_user(self
, user_name
, user_passwd
, tenant_id
=None):
1858 '''Adds a new user to openstack VIM'''
1859 '''Returns the user identifier'''
1860 self
.logger
.debug("osconnector: Adding a new user to VIM")
1862 self
._reload
_connection
()
1863 user
=self
.keystone
.users
.create(user_name
, password
=user_passwd
, default_project
=tenant_id
)
1864 #self.keystone.tenants.add_user(self.k_creds["username"], #role)
1866 except ksExceptions
.ConnectionError
as e
:
1867 error_value
=-vimconn
.HTTP_Bad_Request
1868 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1869 except ksExceptions
.ClientException
as e
: #TODO remove
1870 error_value
=-vimconn
.HTTP_Bad_Request
1871 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1872 #TODO insert exception vimconn.HTTP_Unauthorized
1873 #if reaching here is because an exception
1874 self
.logger
.debug("new_user " + error_text
)
1875 return error_value
, error_text
1877 def delete_user(self
, user_id
):
1878 '''Delete a user from openstack VIM'''
1879 '''Returns the user identifier'''
1881 print("osconnector: Deleting a user from VIM")
1883 self
._reload
_connection
()
1884 self
.keystone
.users
.delete(user_id
)
1886 except ksExceptions
.ConnectionError
as e
:
1887 error_value
=-vimconn
.HTTP_Bad_Request
1888 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1889 except ksExceptions
.NotFound
as e
:
1890 error_value
=-vimconn
.HTTP_Not_Found
1891 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1892 except ksExceptions
.ClientException
as e
: #TODO remove
1893 error_value
=-vimconn
.HTTP_Bad_Request
1894 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1895 #TODO insert exception vimconn.HTTP_Unauthorized
1896 #if reaching here is because an exception
1897 self
.logger
.debug("delete_tenant " + error_text
)
1898 return error_value
, error_text
1900 def get_hosts_info(self
):
1901 '''Get the information of deployed hosts
1902 Returns the hosts content'''
1904 print("osconnector: Getting Host info from VIM")
1907 self
._reload
_connection
()
1908 hypervisors
= self
.nova
.hypervisors
.list()
1909 for hype
in hypervisors
:
1910 h_list
.append( hype
.to_dict() )
1911 return 1, {"hosts":h_list
}
1912 except nvExceptions
.NotFound
as e
:
1913 error_value
=-vimconn
.HTTP_Not_Found
1914 error_text
= (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1915 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
) as e
:
1916 error_value
=-vimconn
.HTTP_Bad_Request
1917 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1918 #TODO insert exception vimconn.HTTP_Unauthorized
1919 #if reaching here is because an exception
1920 self
.logger
.debug("get_hosts_info " + error_text
)
1921 return error_value
, error_text
1923 def get_hosts(self
, vim_tenant
):
1924 '''Get the hosts and deployed instances
1925 Returns the hosts content'''
1926 r
, hype_dict
= self
.get_hosts_info()
1929 hypervisors
= hype_dict
["hosts"]
1931 servers
= self
.nova
.servers
.list()
1932 for hype
in hypervisors
:
1933 for server
in servers
:
1934 if server
.to_dict()['OS-EXT-SRV-ATTR:hypervisor_hostname']==hype
['hypervisor_hostname']:
1936 hype
['vm'].append(server
.id)
1938 hype
['vm'] = [server
.id]
1940 except nvExceptions
.NotFound
as e
:
1941 error_value
=-vimconn
.HTTP_Not_Found
1942 error_text
= (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1943 except (ksExceptions
.ClientException
, nvExceptions
.ClientException
) as e
:
1944 error_value
=-vimconn
.HTTP_Bad_Request
1945 error_text
= type(e
).__name
__ + ": "+ (str(e
) if len(e
.args
)==0 else str(e
.args
[0]))
1946 #TODO insert exception vimconn.HTTP_Unauthorized
1947 #if reaching here is because an exception
1948 self
.logger
.debug("get_hosts " + error_text
)
1949 return error_value
, error_text
1951 def new_classification(self
, name
, ctype
, definition
):
1952 self
.logger
.debug('Adding a new (Traffic) Classification to VIM, named %s', name
)
1955 self
._reload
_connection
()
1956 if ctype
not in supportedClassificationTypes
:
1957 raise vimconn
.vimconnNotSupportedException(
1958 'OpenStack VIM connector doesn\'t support provided '
1959 'Classification Type {}, supported ones are: '
1960 '{}'.format(ctype
, supportedClassificationTypes
))
1961 if not self
._validate
_classification
(ctype
, definition
):
1962 raise vimconn
.vimconnException(
1963 'Incorrect Classification definition '
1964 'for the type specified.')
1965 classification_dict
= definition
1966 classification_dict
['name'] = name
1968 new_class
= self
.neutron
.create_sfc_flow_classifier(
1969 {'flow_classifier': classification_dict
})
1970 return new_class
['flow_classifier']['id']
1971 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
1972 neExceptions
.NeutronException
, ConnectionError
) as e
:
1974 'Creation of Classification failed.')
1975 self
._format
_exception
(e
)
1977 def get_classification(self
, class_id
):
1978 self
.logger
.debug(" Getting Classification %s from VIM", class_id
)
1979 filter_dict
= {"id": class_id
}
1980 class_list
= self
.get_classification_list(filter_dict
)
1981 if len(class_list
) == 0:
1982 raise vimconn
.vimconnNotFoundException(
1983 "Classification '{}' not found".format(class_id
))
1984 elif len(class_list
) > 1:
1985 raise vimconn
.vimconnConflictException(
1986 "Found more than one Classification with this criteria")
1987 classification
= class_list
[0]
1988 return classification
1990 def get_classification_list(self
, filter_dict
={}):
1991 self
.logger
.debug("Getting Classifications from VIM filter: '%s'",
1994 filter_dict_os
= filter_dict
.copy()
1995 self
._reload
_connection
()
1996 if self
.api_version3
and "tenant_id" in filter_dict_os
:
1997 filter_dict_os
['project_id'] = filter_dict_os
.pop('tenant_id')
1998 classification_dict
= self
.neutron
.list_sfc_flow_classifiers(
2000 classification_list
= classification_dict
["flow_classifiers"]
2001 self
.__classification
_os
2mano
(classification_list
)
2002 return classification_list
2003 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2004 neExceptions
.NeutronException
, ConnectionError
) as e
:
2005 self
._format
_exception
(e
)
2007 def delete_classification(self
, class_id
):
2008 self
.logger
.debug("Deleting Classification '%s' from VIM", class_id
)
2010 self
._reload
_connection
()
2011 self
.neutron
.delete_sfc_flow_classifier(class_id
)
2013 except (neExceptions
.ConnectionFailed
, neExceptions
.NeutronException
,
2014 ksExceptions
.ClientException
, neExceptions
.NeutronException
,
2015 ConnectionError
) as e
:
2016 self
._format
_exception
(e
)
2018 def new_sfi(self
, name
, ingress_ports
, egress_ports
, sfc_encap
=True):
2019 self
.logger
.debug("Adding a new Service Function Instance to VIM, named '%s'", name
)
2022 self
._reload
_connection
()
2026 if len(ingress_ports
) != 1:
2027 raise vimconn
.vimconnNotSupportedException(
2028 "OpenStack VIM connector can only have "
2029 "1 ingress port per SFI")
2030 if len(egress_ports
) != 1:
2031 raise vimconn
.vimconnNotSupportedException(
2032 "OpenStack VIM connector can only have "
2033 "1 egress port per SFI")
2034 sfi_dict
= {'name': name
,
2035 'ingress': ingress_ports
[0],
2036 'egress': egress_ports
[0],
2037 'service_function_parameters': {
2038 'correlation': correlation
}}
2039 new_sfi
= self
.neutron
.create_sfc_port_pair({'port_pair': sfi_dict
})
2040 return new_sfi
['port_pair']['id']
2041 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2042 neExceptions
.NeutronException
, ConnectionError
) as e
:
2045 self
.neutron
.delete_sfc_port_pair(
2046 new_sfi
['port_pair']['id'])
2049 'Creation of Service Function Instance failed, with '
2050 'subsequent deletion failure as well.')
2051 self
._format
_exception
(e
)
2053 def get_sfi(self
, sfi_id
):
2054 self
.logger
.debug('Getting Service Function Instance %s from VIM', sfi_id
)
2055 filter_dict
= {"id": sfi_id
}
2056 sfi_list
= self
.get_sfi_list(filter_dict
)
2057 if len(sfi_list
) == 0:
2058 raise vimconn
.vimconnNotFoundException("Service Function Instance '{}' not found".format(sfi_id
))
2059 elif len(sfi_list
) > 1:
2060 raise vimconn
.vimconnConflictException(
2061 'Found more than one Service Function Instance '
2062 'with this criteria')
2066 def get_sfi_list(self
, filter_dict
={}):
2067 self
.logger
.debug("Getting Service Function Instances from VIM filter: '%s'", str(filter_dict
))
2069 self
._reload
_connection
()
2070 filter_dict_os
= filter_dict
.copy()
2071 if self
.api_version3
and "tenant_id" in filter_dict_os
:
2072 filter_dict_os
['project_id'] = filter_dict_os
.pop('tenant_id')
2073 sfi_dict
= self
.neutron
.list_sfc_port_pairs(**filter_dict_os
)
2074 sfi_list
= sfi_dict
["port_pairs"]
2075 self
.__sfi
_os
2mano
(sfi_list
)
2077 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2078 neExceptions
.NeutronException
, ConnectionError
) as e
:
2079 self
._format
_exception
(e
)
2081 def delete_sfi(self
, sfi_id
):
2082 self
.logger
.debug("Deleting Service Function Instance '%s' "
2085 self
._reload
_connection
()
2086 self
.neutron
.delete_sfc_port_pair(sfi_id
)
2088 except (neExceptions
.ConnectionFailed
, neExceptions
.NeutronException
,
2089 ksExceptions
.ClientException
, neExceptions
.NeutronException
,
2090 ConnectionError
) as e
:
2091 self
._format
_exception
(e
)
2093 def new_sf(self
, name
, sfis
, sfc_encap
=True):
2094 self
.logger
.debug("Adding a new Service Function to VIM, named '%s'", name
)
2097 self
._reload
_connection
()
2098 # correlation = None
2100 # correlation = 'nsh'
2101 for instance
in sfis
:
2102 sfi
= self
.get_sfi(instance
)
2103 if sfi
.get('sfc_encap') != sfc_encap
:
2104 raise vimconn
.vimconnNotSupportedException(
2105 "OpenStack VIM connector requires all SFIs of the "
2106 "same SF to share the same SFC Encapsulation")
2107 sf_dict
= {'name': name
,
2109 new_sf
= self
.neutron
.create_sfc_port_pair_group({
2110 'port_pair_group': sf_dict
})
2111 return new_sf
['port_pair_group']['id']
2112 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2113 neExceptions
.NeutronException
, ConnectionError
) as e
:
2116 self
.neutron
.delete_sfc_port_pair_group(
2117 new_sf
['port_pair_group']['id'])
2120 'Creation of Service Function failed, with '
2121 'subsequent deletion failure as well.')
2122 self
._format
_exception
(e
)
2124 def get_sf(self
, sf_id
):
2125 self
.logger
.debug("Getting Service Function %s from VIM", sf_id
)
2126 filter_dict
= {"id": sf_id
}
2127 sf_list
= self
.get_sf_list(filter_dict
)
2128 if len(sf_list
) == 0:
2129 raise vimconn
.vimconnNotFoundException(
2130 "Service Function '{}' not found".format(sf_id
))
2131 elif len(sf_list
) > 1:
2132 raise vimconn
.vimconnConflictException(
2133 "Found more than one Service Function with this criteria")
2137 def get_sf_list(self
, filter_dict
={}):
2138 self
.logger
.debug("Getting Service Function from VIM filter: '%s'",
2141 self
._reload
_connection
()
2142 filter_dict_os
= filter_dict
.copy()
2143 if self
.api_version3
and "tenant_id" in filter_dict_os
:
2144 filter_dict_os
['project_id'] = filter_dict_os
.pop('tenant_id')
2145 sf_dict
= self
.neutron
.list_sfc_port_pair_groups(**filter_dict_os
)
2146 sf_list
= sf_dict
["port_pair_groups"]
2147 self
.__sf
_os
2mano
(sf_list
)
2149 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2150 neExceptions
.NeutronException
, ConnectionError
) as e
:
2151 self
._format
_exception
(e
)
2153 def delete_sf(self
, sf_id
):
2154 self
.logger
.debug("Deleting Service Function '%s' from VIM", sf_id
)
2156 self
._reload
_connection
()
2157 self
.neutron
.delete_sfc_port_pair_group(sf_id
)
2159 except (neExceptions
.ConnectionFailed
, neExceptions
.NeutronException
,
2160 ksExceptions
.ClientException
, neExceptions
.NeutronException
,
2161 ConnectionError
) as e
:
2162 self
._format
_exception
(e
)
2164 def new_sfp(self
, name
, classifications
, sfs
, sfc_encap
=True, spi
=None):
2165 self
.logger
.debug("Adding a new Service Function Path to VIM, named '%s'", name
)
2168 self
._reload
_connection
()
2169 # In networking-sfc the MPLS encapsulation is legacy
2170 # should be used when no full SFC Encapsulation is intended
2171 correlation
= 'mpls'
2174 sfp_dict
= {'name': name
,
2175 'flow_classifiers': classifications
,
2176 'port_pair_groups': sfs
,
2177 'chain_parameters': {'correlation': correlation
}}
2179 sfp_dict
['chain_id'] = spi
2180 new_sfp
= self
.neutron
.create_sfc_port_chain({'port_chain': sfp_dict
})
2181 return new_sfp
["port_chain"]["id"]
2182 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2183 neExceptions
.NeutronException
, ConnectionError
) as e
:
2186 self
.neutron
.delete_sfc_port_chain(new_sfp
['port_chain']['id'])
2189 'Creation of Service Function Path failed, with '
2190 'subsequent deletion failure as well.')
2191 self
._format
_exception
(e
)
2193 def get_sfp(self
, sfp_id
):
2194 self
.logger
.debug(" Getting Service Function Path %s from VIM", sfp_id
)
2195 filter_dict
= {"id": sfp_id
}
2196 sfp_list
= self
.get_sfp_list(filter_dict
)
2197 if len(sfp_list
) == 0:
2198 raise vimconn
.vimconnNotFoundException(
2199 "Service Function Path '{}' not found".format(sfp_id
))
2200 elif len(sfp_list
) > 1:
2201 raise vimconn
.vimconnConflictException(
2202 "Found more than one Service Function Path with this criteria")
2206 def get_sfp_list(self
, filter_dict
={}):
2207 self
.logger
.debug("Getting Service Function Paths from VIM filter: '%s'", str(filter_dict
))
2209 self
._reload
_connection
()
2210 filter_dict_os
= filter_dict
.copy()
2211 if self
.api_version3
and "tenant_id" in filter_dict_os
:
2212 filter_dict_os
['project_id'] = filter_dict_os
.pop('tenant_id')
2213 sfp_dict
= self
.neutron
.list_sfc_port_chains(**filter_dict_os
)
2214 sfp_list
= sfp_dict
["port_chains"]
2215 self
.__sfp
_os
2mano
(sfp_list
)
2217 except (neExceptions
.ConnectionFailed
, ksExceptions
.ClientException
,
2218 neExceptions
.NeutronException
, ConnectionError
) as e
:
2219 self
._format
_exception
(e
)
2221 def delete_sfp(self
, sfp_id
):
2222 self
.logger
.debug("Deleting Service Function Path '%s' from VIM", sfp_id
)
2224 self
._reload
_connection
()
2225 self
.neutron
.delete_sfc_port_chain(sfp_id
)
2227 except (neExceptions
.ConnectionFailed
, neExceptions
.NeutronException
,
2228 ksExceptions
.ClientException
, neExceptions
.NeutronException
,
2229 ConnectionError
) as e
:
2230 self
._format
_exception
(e
)