/
contrail_plugin.py
379 lines (317 loc) · 16.1 KB
/
contrail_plugin.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
# Copyright 2014 Juniper Networks. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# @author: Hampapur Ajay, Praneet Bachheti, Rudra Rugge, Atul Moghe
import requests
from neutron.api.v2 import attributes as attr
from neutron.common import exceptions as exc
from neutron.common.config import cfg
from neutron.db import portbindings_base
from neutron.db import quota_db # noqa
from neutron.extensions import allowedaddresspairs
from neutron.extensions import external_net
from neutron.extensions import l3
from neutron.extensions import portbindings
from neutron.extensions import securitygroup
from neutron import neutron_plugin_base_v2
try:
from neutron.openstack.common import importutils
except ImportError:
from oslo_utils import importutils
try:
from neutron.openstack.common import jsonutils as json
except ImportError:
from oslo_serialization import jsonutils as json
try:
from neutron.openstack.common import log as logging
except ImportError:
from oslo_log import log as logging
from simplejson import JSONDecodeError
from eventlet.greenthread import getcurrent
from contrail_plugin_base import HttpResponseError
import contrail_plugin_base as plugin_base
from cfgm_common import utils as cfgmutils
_DEFAULT_KS_CERT_BUNDLE="/tmp/keystonecertbundle.pem"
_DEFAULT_API_CERT_BUNDLE="/tmp/apiservercertbundle.pem"
_DEFAULT_SERVER_CONNECT="http"
_DEFAULT_SECURE_SERVER_CONNECT="https"
LOG = logging.getLogger(__name__)
vnc_opts = [
cfg.StrOpt('api_server_ip', default='127.0.0.1',
help='IP address to connect to VNC controller'),
cfg.StrOpt('api_server_port', default='8082',
help='Port to connect to VNC controller'),
cfg.DictOpt('contrail_extensions', default={},
help='Enable Contrail extensions(policy, ipam)'),
]
analytics_opts = [
cfg.StrOpt('analytics_api_ip', default='127.0.0.1',
help='IP address to connect to VNC collector'),
cfg.StrOpt('analytics_api_port', default='8081',
help='Port to connect to VNC collector'),
]
class InvalidContrailExtensionError(exc.ServiceUnavailable):
message = _("Invalid Contrail Extension: %(ext_name) %(ext_class)")
class NeutronPluginContrailCoreV2(plugin_base.NeutronPluginContrailCoreBase):
PLUGIN_URL_PREFIX = '/neutron'
def _build_auth_details(self):
#keystone
self._authn_token = None
if cfg.CONF.auth_strategy == 'keystone':
kcfg = cfg.CONF.keystone_authtoken
body = '{"auth":{"passwordCredentials":{'
body += ' "username": "%s",' % (kcfg.admin_user)
body += ' "password": "%s"},' % (kcfg.admin_password)
body += ' "tenantName":"%s"}}' % (kcfg.admin_tenant_name)
self._authn_body = body
self._authn_token = cfg.CONF.keystone_authtoken.admin_token
self._keystone_url = "%s://%s:%s%s" % (
cfg.CONF.keystone_authtoken.auth_protocol,
cfg.CONF.keystone_authtoken.auth_host,
cfg.CONF.keystone_authtoken.auth_port,
"/v2.0/tokens")
#Keystone SSL Support
self._ksinsecure=cfg.CONF.keystone_authtoken.insecure
kscertfile=cfg.CONF.keystone_authtoken.certfile
kskeyfile=cfg.CONF.keystone_authtoken.keyfile
kscafile=cfg.CONF.keystone_authtoken.cafile
self._use_ks_certs = False
if (cfg.CONF.keystone_authtoken.auth_protocol ==
_DEFAULT_SECURE_SERVER_CONNECT and kscafile):
certs = [kscafile]
if kscertfile and kskeyfile:
certs = [kscertfile, kskeyfile, kscafile]
self._kscertbundle = cfgmutils.getCertKeyCaBundle(
_DEFAULT_KS_CERT_BUNDLE,certs)
self._use_ks_certs = True
#API Server SSL support
self._apiusessl=cfg.CONF.APISERVER.use_ssl
self._apiinsecure=cfg.CONF.APISERVER.insecure
apicertfile=cfg.CONF.APISERVER.certfile
apikeyfile=cfg.CONF.APISERVER.keyfile
apicafile=cfg.CONF.APISERVER.cafile
if self._apiusessl:
self._apiserverconnect=_DEFAULT_SECURE_SERVER_CONNECT
else:
self._apiserverconnect=_DEFAULT_SERVER_CONNECT
self._use_api_certs = False
if self._apiusessl and apicafile:
certs = [apicafile]
if apicertfile and apikeyfile:
certs = [apicertfile, apikeyfile, apicafile]
self._apicertbundle = cfgmutils.getCertKeyCaBundle(
_DEFAULT_API_CERT_BUNDLE,certs)
self._use_api_certs = True
def _request_api_server(self, url, data=None, headers=None):
# Attempt to post to Api-Server
if self._apiinsecure:
response = requests.post(url, data=data, headers=headers,verify=False)
elif not self._apiinsecure and self._use_api_certs:
response = requests.post(url, data=data, headers=headers,verify=self._apicertbundle)
else:
response = requests.post(url, data=data, headers=headers)
if (response.status_code == requests.codes.unauthorized):
# Get token from keystone and save it for next request
if self._ksinsecure:
response = requests.post(self._keystone_url,
data=self._authn_body,
headers={'Content-type': 'application/json'},verify=False)
elif not self._ksinsecure and self._use_ks_certs:
response = requests.post(self._keystone_url,
data=self._authn_body,
headers={'Content-type': 'application/json'},verify=self._kscertbundle)
else:
response = requests.post(self._keystone_url,
data=self._authn_body,
headers={'Content-type': 'application/json'})
if (response.status_code == requests.codes.ok):
# plan is to re-issue original request with new token
auth_headers = headers or {}
authn_content = json.loads(response.text)
self._authn_token = authn_content['access']['token']['id']
auth_headers['X-AUTH-TOKEN'] = self._authn_token
response = self._request_api_server(url, data, auth_headers)
else:
raise RuntimeError('Authentication Failure')
return response
def _request_api_server_authn(self, url, data=None, headers=None):
# forward user token to API server for RBAC
# token saved earlier in the pipeline
try:
auth_token = getcurrent().contrail_vars.token
except AttributeError:
auth_token = None
authn_headers = headers or {}
if auth_token or self._authn_token:
authn_headers['X-AUTH-TOKEN'] = auth_token or self._authn_token
response = self._request_api_server(url, data, headers=authn_headers)
return response
def _relay_request(self, url_path, data=None):
"""Send received request to api server."""
url = "%s://%s:%s%s" % (self._apiserverconnect,
cfg.CONF.APISERVER.api_server_ip,
cfg.CONF.APISERVER.api_server_port,
url_path)
return self._request_api_server_authn(
url, data=data, headers={'Content-type': 'application/json'})
def _request_backend(self, context, data_dict, obj_name, action):
context_dict = self._encode_context(context, action, obj_name)
data = json.dumps({'context': context_dict, 'data': data_dict})
url_path = "%s/%s" % (self.PLUGIN_URL_PREFIX, obj_name)
response = self._relay_request(url_path, data=data)
try:
return response.status_code, response.json()
except JSONDecodeError:
return response.status_code, {'message': response.content}
def _encode_context(self, context, operation, apitype):
cdict = {'user_id': getattr(context, 'user_id', ''),
'is_admin': getattr(context, 'is_admin', False),
'operation': operation,
'type': apitype,
'tenant_id': getattr(context, 'tenant_id', None)}
if context.roles:
cdict['roles'] = context.roles
if context.tenant:
cdict['tenant'] = context.tenant
return cdict
def _encode_resource(self, resource_id=None, resource=None, fields=None,
filters=None):
resource_dict = {}
if resource_id:
resource_dict['id'] = resource_id
if resource:
resource_dict['resource'] = resource
resource_dict['filters'] = filters
resource_dict['fields'] = fields
return resource_dict
def _prune(self, resource_dict, fields):
if fields:
return dict(((key, item) for key, item in resource_dict.items()
if key in fields))
return resource_dict
def _transform_response(self, status_code, info=None, obj_name=None,
fields=None, propagate_exc=False):
if status_code == requests.codes.ok:
if not isinstance(info, list):
return self._prune(info, fields)
else:
return [self._prune(items, fields) for items in info]
if propagate_exc:
raise HttpResponseError(info)
plugin_base._raise_contrail_error(info, obj_name)
def _create_resource(self, res_type, context, res_data, propagate_exc=False):
"""Create a resource in API server.
This method encodes neutron model, and sends it to the
contrail api server.
"""
for key, value in res_data[res_type].items():
if value == attr.ATTR_NOT_SPECIFIED:
del res_data[res_type][key]
res_dict = self._encode_resource(resource=res_data[res_type])
status_code, res_info = self._request_backend(context, res_dict,
res_type, 'CREATE')
res_dicts = self._transform_response(status_code, info=res_info,
obj_name=res_type,
propagate_exc=propagate_exc)
LOG.debug("create_%(res_type)s(): %(res_dicts)s",
{'res_type': res_type, 'res_dicts': res_dicts})
return res_dicts
def _get_resource(self, res_type, context, id, fields, propagate_exc=False):
"""Get a resource from API server.
This method gets a resource from the contrail api server
"""
res_dict = self._encode_resource(resource_id=id, fields=fields)
status_code, res_info = self._request_backend(context, res_dict,
res_type, 'READ')
res_dicts = self._transform_response(status_code, info=res_info,
fields=fields, obj_name=res_type,
propagate_exc=propagate_exc)
LOG.debug("get_%(res_type)s(): %(res_dicts)s",
{'res_type': res_type, 'res_dicts': res_dicts})
return res_dicts
def _update_resource(self, res_type, context, id, res_data, propagate_exc=False):
"""Update a resource in API server.
This method updates a resource in the contrail api server
"""
res_dict = self._encode_resource(resource_id=id,
resource=res_data[res_type])
status_code, res_info = self._request_backend(context, res_dict,
res_type, 'UPDATE')
res_dicts = self._transform_response(status_code, info=res_info,
obj_name=res_type,
propagate_exc=propagate_exc)
LOG.debug("update_%(res_type)s(): %(res_dicts)s",
{'res_type': res_type, 'res_dicts': res_dicts})
return res_dicts
def _delete_resource(self, res_type, context, id, propagate_exc=False):
"""Delete a resource in API server
This method deletes a resource in the contrail api server
"""
res_dict = self._encode_resource(resource_id=id)
LOG.debug("delete_%(res_type)s(): %(id)s",
{'res_type': res_type, 'id': id})
status_code, res_info = self._request_backend(context, res_dict,
res_type, 'DELETE')
if status_code != requests.codes.ok:
plugin_base._raise_contrail_error(info=res_info,
obj_name=res_type)
def _list_resource(self, res_type, context, filters, fields, propagate_exc=False):
res_dict = self._encode_resource(filters=filters, fields=fields)
status_code, res_info = self._request_backend(context, res_dict,
res_type, 'READALL')
res_dicts = self._transform_response(status_code, info=res_info,
fields=fields, obj_name=res_type,
propagate_exc=propagate_exc)
LOG.debug(
"get_%(res_type)s(): filters: %(filters)r data: %(res_dicts)r",
{'res_type': res_type, 'filters': filters,
'res_dicts': res_dicts})
return res_dicts
def _count_resource(self, res_type, context, filters):
res_dict = self._encode_resource(filters=filters)
status_code, res_count = self._request_backend(context, res_dict,
res_type, 'READCOUNT')
LOG.debug("get_%(res_type)s_count(): %(res_count)r",
{'res_type': res_type, 'res_count': res_count})
return res_count
def add_router_interface(self, context, router_id, interface_info):
"""Add interface to a router."""
if not interface_info:
msg = _("Either subnet_id or port_id must be specified")
raise exc.BadRequest(resource='router', msg=msg)
if 'port_id' in interface_info:
if 'subnet_id' in interface_info:
msg = _("Cannot specify both subnet-id and port-id")
raise exc.BadRequest(resource='router', msg=msg)
res_dict = self._encode_resource(resource_id=router_id,
resource=interface_info)
status_code, res_info = self._request_backend(context, res_dict,
'router', 'ADDINTERFACE')
if status_code != requests.codes.ok:
plugin_base._raise_contrail_error(info=res_info,
obj_name='add_router_interface')
return res_info
def remove_router_interface(self, context, router_id, interface_info):
"""Delete interface from a router."""
if not interface_info:
msg = _("Either subnet_id or port_id must be specified")
raise exc.BadRequest(resource='router', msg=msg)
res_dict = self._encode_resource(resource_id=router_id,
resource=interface_info)
status_code, res_info = self._request_backend(context, res_dict,
'router', 'DELINTERFACE')
if status_code != requests.codes.ok:
plugin_base._raise_contrail_error(info=res_info,
obj_name='remove_router_interface')
return res_info