Load Balancing as a Service (LBaaS) for OpenStack
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

576 lines
26KB

  1. # Copyright 2014 Rackspace
  2. # Copyright 2016 Blue Box, an IBM Company
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. from oslo_config import cfg
  16. from oslo_db import exception as odb_exceptions
  17. from oslo_log import log as logging
  18. from oslo_utils import excutils
  19. import pecan
  20. from wsme import types as wtypes
  21. from wsmeext import pecan as wsme_pecan
  22. from octavia.api.drivers import data_models as driver_dm
  23. from octavia.api.drivers import driver_factory
  24. from octavia.api.drivers import utils as driver_utils
  25. from octavia.api.v2.controllers import base
  26. from octavia.api.v2.controllers import l7policy
  27. from octavia.api.v2.types import listener as listener_types
  28. from octavia.common import constants
  29. from octavia.common import data_models
  30. from octavia.common import exceptions
  31. from octavia.common import stats
  32. from octavia.db import api as db_api
  33. from octavia.db import prepare as db_prepare
  34. from octavia.i18n import _
  35. CONF = cfg.CONF
  36. LOG = logging.getLogger(__name__)
  37. class ListenersController(base.BaseController):
  38. RBAC_TYPE = constants.RBAC_LISTENER
  39. def __init__(self):
  40. super(ListenersController, self).__init__()
  41. @wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
  42. [wtypes.text], ignore_extra_args=True)
  43. def get_one(self, id, fields=None):
  44. """Gets a single listener's details."""
  45. context = pecan.request.context.get('octavia_context')
  46. db_listener = self._get_db_listener(context.session, id,
  47. show_deleted=False)
  48. if not db_listener:
  49. raise exceptions.NotFound(resource=data_models.Listener._name(),
  50. id=id)
  51. self._auth_validate_action(context, db_listener.project_id,
  52. constants.RBAC_GET_ONE)
  53. result = self._convert_db_to_type(db_listener,
  54. listener_types.ListenerResponse)
  55. if fields is not None:
  56. result = self._filter_fields([result], fields)[0]
  57. return listener_types.ListenerRootResponse(listener=result)
  58. @wsme_pecan.wsexpose(listener_types.ListenersRootResponse, wtypes.text,
  59. [wtypes.text], ignore_extra_args=True)
  60. def get_all(self, project_id=None, fields=None):
  61. """Lists all listeners."""
  62. pcontext = pecan.request.context
  63. context = pcontext.get('octavia_context')
  64. query_filter = self._auth_get_all(context, project_id)
  65. db_listeners, links = self.repositories.listener.get_all_API_list(
  66. context.session, show_deleted=False,
  67. pagination_helper=pcontext.get(constants.PAGINATION_HELPER),
  68. **query_filter)
  69. result = self._convert_db_to_type(
  70. db_listeners, [listener_types.ListenerResponse])
  71. if fields is not None:
  72. result = self._filter_fields(result, fields)
  73. return listener_types.ListenersRootResponse(
  74. listeners=result, listeners_links=links)
  75. def _test_lb_and_listener_statuses(
  76. self, session, lb_id, id=None,
  77. listener_status=constants.PENDING_UPDATE):
  78. """Verify load balancer is in a mutable state."""
  79. lb_repo = self.repositories.load_balancer
  80. if id:
  81. if not self.repositories.test_and_set_lb_and_listeners_prov_status(
  82. session, lb_id, constants.PENDING_UPDATE,
  83. listener_status, listener_ids=[id]):
  84. LOG.info("Load Balancer %s is immutable.", lb_id)
  85. db_lb = lb_repo.get(session, id=lb_id)
  86. raise exceptions.ImmutableObject(resource=db_lb._name(),
  87. id=lb_id)
  88. else:
  89. if not lb_repo.test_and_set_provisioning_status(
  90. session, lb_id, constants.PENDING_UPDATE):
  91. db_lb = lb_repo.get(session, id=lb_id)
  92. LOG.info("Load Balancer %s is immutable.", db_lb.id)
  93. raise exceptions.ImmutableObject(resource=db_lb._name(),
  94. id=lb_id)
  95. def _validate_pool(self, session, lb_id, pool_id, listener_protocol):
  96. """Validate pool given exists on same load balancer as listener."""
  97. db_pool = self.repositories.pool.get(
  98. session, load_balancer_id=lb_id, id=pool_id)
  99. if not db_pool:
  100. raise exceptions.NotFound(
  101. resource=data_models.Pool._name(), id=pool_id)
  102. if (db_pool.protocol == constants.PROTOCOL_UDP and
  103. db_pool.protocol != listener_protocol):
  104. msg = _("Listeners of type %s can only have pools of "
  105. "type UDP.") % constants.PROTOCOL_UDP
  106. raise exceptions.ValidationException(detail=msg)
  107. def _has_tls_container_refs(self, listener_dict):
  108. return (listener_dict.get('tls_certificate_id') or
  109. listener_dict.get('client_ca_tls_container_id') or
  110. listener_dict.get('sni_containers'))
  111. def _is_tls_or_insert_header(self, listener_dict):
  112. return (self._has_tls_container_refs(listener_dict) or
  113. listener_dict.get('insert_headers'))
  114. def _validate_insert_headers(self, insert_header_list, listener_protocol):
  115. if list(set(insert_header_list) - (
  116. set(constants.SUPPORTED_HTTP_HEADERS +
  117. constants.SUPPORTED_SSL_HEADERS))):
  118. raise exceptions.InvalidOption(
  119. value=insert_header_list,
  120. option='insert_headers')
  121. if not listener_protocol == constants.PROTOCOL_TERMINATED_HTTPS:
  122. is_matched = len(
  123. constants.SUPPORTED_SSL_HEADERS) > len(
  124. list(set(constants.SUPPORTED_SSL_HEADERS) - set(
  125. insert_header_list)))
  126. if is_matched:
  127. headers = []
  128. for header_name in insert_header_list:
  129. if header_name in constants.SUPPORTED_SSL_HEADERS:
  130. headers.append(header_name)
  131. raise exceptions.InvalidOption(
  132. value=headers,
  133. option=('%s protocol listener.' % listener_protocol))
  134. def _validate_create_listener(self, lock_session, listener_dict):
  135. """Validate listener for wrong protocol or duplicate listeners
  136. Update the load balancer db when provisioning status changes.
  137. """
  138. listener_protocol = listener_dict.get('protocol')
  139. if listener_dict and listener_dict.get('insert_headers'):
  140. self._validate_insert_headers(
  141. listener_dict['insert_headers'].keys(), listener_protocol)
  142. # Check for UDP compatibility
  143. if (listener_protocol == constants.PROTOCOL_UDP and
  144. self._is_tls_or_insert_header(listener_dict)):
  145. raise exceptions.ValidationException(detail=_(
  146. "%s protocol listener does not support TLS or header "
  147. "insertion.") % constants.PROTOCOL_UDP)
  148. # Check for TLS disabled
  149. if (not CONF.api_settings.allow_tls_terminated_listeners and
  150. listener_protocol == constants.PROTOCOL_TERMINATED_HTTPS):
  151. raise exceptions.DisabledOption(
  152. value=constants.PROTOCOL_TERMINATED_HTTPS, option='protocol')
  153. # Check for certs when not TERMINATED_HTTPS
  154. if (listener_protocol != constants.PROTOCOL_TERMINATED_HTTPS and
  155. self._has_tls_container_refs(listener_dict)):
  156. raise exceptions.ValidationException(detail=_(
  157. "Certificate container references are only allowed on "
  158. "%s protocol listeners.") %
  159. constants.PROTOCOL_TERMINATED_HTTPS)
  160. # Make sure a base certificate exists if specifying a client ca
  161. if (listener_dict.get('client_ca_tls_certificate_id') and
  162. not (listener_dict.get('tls_certificate_id') or
  163. listener_dict.get('sni_containers'))):
  164. raise exceptions.ValidationException(detail=_(
  165. "An SNI or default certificate container reference must "
  166. "be provided with a client CA container reference."))
  167. # Make sure a certificate container is specified for TERMINATED_HTTPS
  168. if (listener_protocol == constants.PROTOCOL_TERMINATED_HTTPS and
  169. not (listener_dict.get('tls_certificate_id') or
  170. listener_dict.get('sni_containers'))):
  171. raise exceptions.ValidationException(detail=_(
  172. "An SNI or default certificate container reference must "
  173. "be provided for %s protocol listeners.") %
  174. constants.PROTOCOL_TERMINATED_HTTPS)
  175. # Make sure we have a client CA cert if they enable client auth
  176. if (listener_dict.get('client_authentication') !=
  177. constants.CLIENT_AUTH_NONE and not
  178. listener_dict.get('client_ca_tls_certificate_id')):
  179. raise exceptions.ValidationException(detail=_(
  180. "Client authentication setting %s requires a client CA "
  181. "container reference.") %
  182. listener_dict.get('client_authentication'))
  183. # Make sure we have a client CA if they specify a CRL
  184. if (listener_dict.get('client_crl_container_id') and
  185. not listener_dict.get('client_ca_tls_certificate_id')):
  186. raise exceptions.ValidationException(detail=_(
  187. "A client authentication CA reference is required to "
  188. "specify a client authentication revocation list."))
  189. # Validate the TLS containers
  190. sni_containers = listener_dict.pop('sni_containers', [])
  191. tls_refs = [sni['tls_container_id'] for sni in sni_containers]
  192. if listener_dict.get('tls_certificate_id'):
  193. tls_refs.append(listener_dict.get('tls_certificate_id'))
  194. self._validate_tls_refs(tls_refs)
  195. # Validate the client CA cert and optional client CRL
  196. if listener_dict.get('client_ca_tls_certificate_id'):
  197. self._validate_client_ca_and_crl_refs(
  198. listener_dict.get('client_ca_tls_certificate_id'),
  199. listener_dict.get('client_crl_container_id', None))
  200. try:
  201. db_listener = self.repositories.listener.create(
  202. lock_session, **listener_dict)
  203. if sni_containers:
  204. for container in sni_containers:
  205. sni_dict = {'listener_id': db_listener.id,
  206. 'tls_container_id': container.get(
  207. 'tls_container_id')}
  208. self.repositories.sni.create(lock_session, **sni_dict)
  209. db_listener = self.repositories.listener.get(
  210. lock_session, id=db_listener.id)
  211. return db_listener
  212. except odb_exceptions.DBDuplicateEntry as de:
  213. column_list = ['load_balancer_id', 'protocol_port']
  214. constraint_list = ['uq_listener_load_balancer_id_protocol_port']
  215. if ['id'] == de.columns:
  216. raise exceptions.IDAlreadyExists()
  217. elif (set(column_list) == set(de.columns) or
  218. set(constraint_list) == set(de.columns)):
  219. raise exceptions.DuplicateListenerEntry(
  220. port=listener_dict.get('protocol_port'))
  221. except odb_exceptions.DBError:
  222. raise exceptions.InvalidOption(value=listener_dict.get('protocol'),
  223. option='protocol')
  224. @wsme_pecan.wsexpose(listener_types.ListenerRootResponse,
  225. body=listener_types.ListenerRootPOST, status_code=201)
  226. def post(self, listener_):
  227. """Creates a listener on a load balancer."""
  228. listener = listener_.listener
  229. context = pecan.request.context.get('octavia_context')
  230. load_balancer_id = listener.loadbalancer_id
  231. listener.project_id, provider = self._get_lb_project_id_provider(
  232. context.session, load_balancer_id)
  233. self._auth_validate_action(context, listener.project_id,
  234. constants.RBAC_POST)
  235. # Load the driver early as it also provides validation
  236. driver = driver_factory.get_driver(provider)
  237. lock_session = db_api.get_session(autocommit=False)
  238. try:
  239. if self.repositories.check_quota_met(
  240. context.session,
  241. lock_session,
  242. data_models.Listener,
  243. listener.project_id):
  244. raise exceptions.QuotaException(
  245. resource=data_models.Listener._name())
  246. listener_dict = db_prepare.create_listener(
  247. listener.to_dict(render_unsets=True), None)
  248. if listener_dict['default_pool_id']:
  249. self._validate_pool(context.session, load_balancer_id,
  250. listener_dict['default_pool_id'],
  251. listener.protocol)
  252. self._test_lb_and_listener_statuses(
  253. lock_session, lb_id=load_balancer_id)
  254. db_listener = self._validate_create_listener(
  255. lock_session, listener_dict)
  256. # Prepare the data for the driver data model
  257. provider_listener = (
  258. driver_utils.db_listener_to_provider_listener(db_listener))
  259. # re-inject the sni container references lost due to SNI
  260. # being a separate table in the DB
  261. provider_listener.sni_container_refs = listener.sni_container_refs
  262. # Dispatch to the driver
  263. LOG.info("Sending create Listener %s to provider %s",
  264. db_listener.id, driver.name)
  265. driver_utils.call_provider(
  266. driver.name, driver.listener_create, provider_listener)
  267. lock_session.commit()
  268. except Exception:
  269. with excutils.save_and_reraise_exception():
  270. lock_session.rollback()
  271. db_listener = self._get_db_listener(context.session, db_listener.id)
  272. result = self._convert_db_to_type(db_listener,
  273. listener_types.ListenerResponse)
  274. return listener_types.ListenerRootResponse(listener=result)
  275. def _graph_create(self, lock_session, listener_dict,
  276. l7policies=None, pool_name_ids=None):
  277. load_balancer_id = listener_dict['load_balancer_id']
  278. listener_dict = db_prepare.create_listener(
  279. listener_dict, load_balancer_id)
  280. l7policies = listener_dict.pop('l7policies', l7policies)
  281. if listener_dict.get('default_pool_id'):
  282. self._validate_pool(lock_session, load_balancer_id,
  283. listener_dict['default_pool_id'],
  284. listener_dict['protocol'])
  285. db_listener = self._validate_create_listener(
  286. lock_session, listener_dict)
  287. # Now create l7policies
  288. new_l7ps = []
  289. for l7p in l7policies:
  290. l7p['project_id'] = db_listener.project_id
  291. l7p['load_balancer_id'] = load_balancer_id
  292. l7p['listener_id'] = db_listener.id
  293. redirect_pool = l7p.pop('redirect_pool', None)
  294. if redirect_pool:
  295. pool_name = redirect_pool['name']
  296. pool_id = pool_name_ids.get(pool_name)
  297. if not pool_id:
  298. raise exceptions.SingleCreateDetailsMissing(
  299. type='Pool', name=pool_name)
  300. l7p['redirect_pool_id'] = pool_id
  301. new_l7ps.append(l7policy.L7PolicyController()._graph_create(
  302. lock_session, l7p))
  303. db_listener.l7policies = new_l7ps
  304. return db_listener
  305. def _validate_listener_PUT(self, listener, db_listener):
  306. # TODO(rm_work): Do we need something like this? What do we do on an
  307. # empty body for a PUT?
  308. if not listener:
  309. raise exceptions.ValidationException(
  310. detail='No listener object supplied.')
  311. # Check for UDP compatibility
  312. if (db_listener.protocol == constants.PROTOCOL_UDP and
  313. self._is_tls_or_insert_header(listener.to_dict())):
  314. raise exceptions.ValidationException(detail=_(
  315. "%s protocol listener does not support TLS or header "
  316. "insertion.") % constants.PROTOCOL_UDP)
  317. # Check for certs when not TERMINATED_HTTPS
  318. if (db_listener.protocol != constants.PROTOCOL_TERMINATED_HTTPS and
  319. self._has_tls_container_refs(listener.to_dict())):
  320. raise exceptions.ValidationException(detail=_(
  321. "Certificate container references are only allowed on "
  322. "%s protocol listeners.") %
  323. constants.PROTOCOL_TERMINATED_HTTPS)
  324. # Make sure we have a client CA cert if they enable client auth
  325. if ((listener.client_authentication != wtypes.Unset and
  326. listener.client_authentication != constants.CLIENT_AUTH_NONE)
  327. and not (db_listener.client_ca_tls_certificate_id or
  328. listener.client_ca_tls_container_ref)):
  329. raise exceptions.ValidationException(detail=_(
  330. "Client authentication setting %s requires a client CA "
  331. "container reference.") %
  332. listener.client_authentication)
  333. if listener.insert_headers:
  334. self._validate_insert_headers(
  335. list(listener.insert_headers.keys()), db_listener.protocol)
  336. sni_containers = listener.sni_container_refs or []
  337. tls_refs = [sni for sni in sni_containers]
  338. if listener.default_tls_container_ref:
  339. tls_refs.append(listener.default_tls_container_ref)
  340. self._validate_tls_refs(tls_refs)
  341. ca_ref = None
  342. if (listener.client_ca_tls_container_ref and
  343. listener.client_ca_tls_container_ref != wtypes.Unset):
  344. ca_ref = listener.client_ca_tls_container_ref
  345. elif db_listener.client_ca_tls_certificate_id:
  346. ca_ref = db_listener.client_ca_tls_certificate_id
  347. crl_ref = None
  348. if (listener.client_crl_container_ref and
  349. listener.client_crl_container_ref != wtypes.Unset):
  350. crl_ref = listener.client_crl_container_ref
  351. elif db_listener.client_crl_container_id:
  352. crl_ref = db_listener.client_crl_container_id
  353. if crl_ref and not ca_ref:
  354. raise exceptions.ValidationException(detail=_(
  355. "A client authentication CA reference is required to "
  356. "specify a client authentication revocation list."))
  357. if ca_ref or crl_ref:
  358. self._validate_client_ca_and_crl_refs(ca_ref, crl_ref)
  359. def _set_default_on_none(self, listener):
  360. """Reset settings to their default values if None/null was passed in
  361. A None/null value can be passed in to clear a value. PUT values
  362. that were not provided by the user have a type of wtypes.UnsetType.
  363. If the user is attempting to clear values, they should either
  364. be set to None (for example in the name field) or they should be
  365. reset to their default values.
  366. This method is intended to handle those values that need to be set
  367. back to a default value.
  368. """
  369. if listener.connection_limit is None:
  370. listener.connection_limit = constants.DEFAULT_CONNECTION_LIMIT
  371. if listener.timeout_client_data is None:
  372. listener.timeout_client_data = (
  373. CONF.haproxy_amphora.timeout_client_data)
  374. if listener.timeout_member_connect is None:
  375. listener.timeout_member_connect = (
  376. CONF.haproxy_amphora.timeout_member_connect)
  377. if listener.timeout_member_data is None:
  378. listener.timeout_member_data = (
  379. CONF.haproxy_amphora.timeout_member_data)
  380. if listener.timeout_tcp_inspect is None:
  381. listener.timeout_tcp_inspect = (
  382. CONF.haproxy_amphora.timeout_tcp_inspect)
  383. if listener.client_authentication is None:
  384. listener.client_authentication = constants.CLIENT_AUTH_NONE
  385. @wsme_pecan.wsexpose(listener_types.ListenerRootResponse, wtypes.text,
  386. body=listener_types.ListenerRootPUT, status_code=200)
  387. def put(self, id, listener_):
  388. """Updates a listener on a load balancer."""
  389. listener = listener_.listener
  390. context = pecan.request.context.get('octavia_context')
  391. db_listener = self._get_db_listener(context.session, id,
  392. show_deleted=False)
  393. load_balancer_id = db_listener.load_balancer_id
  394. project_id, provider = self._get_lb_project_id_provider(
  395. context.session, load_balancer_id)
  396. self._auth_validate_action(context, project_id, constants.RBAC_PUT)
  397. self._validate_listener_PUT(listener, db_listener)
  398. self._set_default_on_none(listener)
  399. if listener.default_pool_id:
  400. self._validate_pool(context.session, load_balancer_id,
  401. listener.default_pool_id, db_listener.protocol)
  402. # Load the driver early as it also provides validation
  403. driver = driver_factory.get_driver(provider)
  404. with db_api.get_lock_session() as lock_session:
  405. self._test_lb_and_listener_statuses(lock_session,
  406. load_balancer_id, id=id)
  407. # Prepare the data for the driver data model
  408. listener_dict = listener.to_dict(render_unsets=False)
  409. listener_dict['id'] = id
  410. provider_listener_dict = (
  411. driver_utils.listener_dict_to_provider_dict(listener_dict))
  412. # Also prepare the baseline object data
  413. old_provider_listener = (
  414. driver_utils.db_listener_to_provider_listener(db_listener,
  415. for_delete=True))
  416. # Dispatch to the driver
  417. LOG.info("Sending update Listener %s to provider %s", id,
  418. driver.name)
  419. driver_utils.call_provider(
  420. driver.name, driver.listener_update,
  421. old_provider_listener,
  422. driver_dm.Listener.from_dict(provider_listener_dict))
  423. # Update the database to reflect what the driver just accepted
  424. self.repositories.listener.update(
  425. lock_session, id, **listener.to_dict(render_unsets=False))
  426. # Force SQL alchemy to query the DB, otherwise we get inconsistent
  427. # results
  428. context.session.expire_all()
  429. db_listener = self._get_db_listener(context.session, id)
  430. result = self._convert_db_to_type(db_listener,
  431. listener_types.ListenerResponse)
  432. return listener_types.ListenerRootResponse(listener=result)
  433. @wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
  434. def delete(self, id):
  435. """Deletes a listener from a load balancer."""
  436. context = pecan.request.context.get('octavia_context')
  437. db_listener = self._get_db_listener(context.session, id,
  438. show_deleted=False)
  439. load_balancer_id = db_listener.load_balancer_id
  440. project_id, provider = self._get_lb_project_id_provider(
  441. context.session, load_balancer_id)
  442. self._auth_validate_action(context, project_id, constants.RBAC_DELETE)
  443. # Load the driver early as it also provides validation
  444. driver = driver_factory.get_driver(provider)
  445. with db_api.get_lock_session() as lock_session:
  446. self._test_lb_and_listener_statuses(
  447. lock_session, load_balancer_id,
  448. id=id, listener_status=constants.PENDING_DELETE)
  449. LOG.info("Sending delete Listener %s to provider %s", id,
  450. driver.name)
  451. provider_listener = (
  452. driver_utils.db_listener_to_provider_listener(
  453. db_listener, for_delete=True))
  454. driver_utils.call_provider(driver.name, driver.listener_delete,
  455. provider_listener)
  456. @pecan.expose()
  457. def _lookup(self, id, *remainder):
  458. """Overridden pecan _lookup method for custom routing.
  459. Currently it checks if this was a stats request and routes
  460. the request to the StatsController.
  461. """
  462. if id and remainder and remainder[0] == 'stats':
  463. return StatisticsController(listener_id=id), remainder[1:]
  464. return None
  465. class StatisticsController(base.BaseController, stats.StatsMixin):
  466. RBAC_TYPE = constants.RBAC_LISTENER
  467. def __init__(self, listener_id):
  468. super(StatisticsController, self).__init__()
  469. self.id = listener_id
  470. @wsme_pecan.wsexpose(listener_types.StatisticsRootResponse, wtypes.text,
  471. status_code=200)
  472. def get(self):
  473. context = pecan.request.context.get('octavia_context')
  474. db_listener = self._get_db_listener(context.session, self.id,
  475. show_deleted=False)
  476. if not db_listener:
  477. LOG.info("Listener %s not found.", id)
  478. raise exceptions.NotFound(
  479. resource=data_models.Listener._name(),
  480. id=id)
  481. self._auth_validate_action(context, db_listener.project_id,
  482. constants.RBAC_GET_STATS)
  483. listener_stats = self.get_listener_stats(context.session, self.id)
  484. result = self._convert_db_to_type(
  485. listener_stats, listener_types.ListenerStatisticsResponse)
  486. return listener_types.StatisticsRootResponse(stats=result)