A modular, vendor-neutral API, that wraps provisioning instructions for all CDN vendors that support it.
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.

ssl_certificate.py 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. # Copyright (c) 2014 Rackspace, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
  12. # implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import itertools
  16. import json
  17. from oslo_context import context as context_utils
  18. from oslo_log import log
  19. from poppy.common import errors
  20. from poppy.distributed_task.taskflow.flow import create_ssl_certificate
  21. from poppy.distributed_task.taskflow.flow import delete_ssl_certificate
  22. from poppy.distributed_task.taskflow.flow import recreate_ssl_certificate
  23. from poppy.manager import base
  24. from poppy.model.helpers import domain
  25. from poppy.model import ssl_certificate
  26. from poppy.transport.validators import helpers as validators
  27. LOG = log.getLogger(__name__)
  28. class DefaultSSLCertificateController(base.SSLCertificateController):
  29. def __init__(self, manager):
  30. super(DefaultSSLCertificateController, self).__init__(manager)
  31. self.distributed_task_controller = (
  32. self._driver.distributed_task.services_controller
  33. )
  34. self.storage = self._driver.storage.certificates_controller
  35. self.service_storage = self._driver.storage.services_controller
  36. self.flavor_controller = self._driver.storage.flavors_controller
  37. def create_ssl_certificate(
  38. self, project_id, cert_obj, https_upgrade=False):
  39. if (not validators.is_valid_domain_name(cert_obj.domain_name)) or \
  40. (validators.is_root_domain(
  41. domain.Domain(cert_obj.domain_name).to_dict())) or \
  42. (not validators.is_valid_tld(cert_obj.domain_name)):
  43. # here created a http domain object but it does not matter http or
  44. # https
  45. raise ValueError('%s must be a valid non-root domain' %
  46. cert_obj.domain_name)
  47. try:
  48. flavor = self.flavor_controller.get(cert_obj.flavor_id)
  49. # raise a lookup error if the flavor is not found
  50. except LookupError as e:
  51. raise e
  52. try:
  53. self.storage.create_certificate(
  54. project_id,
  55. cert_obj
  56. )
  57. # ValueError will be raised if the cert_info has already existed
  58. except ValueError as e:
  59. raise e
  60. providers = [p.provider_id for p in flavor.providers]
  61. kwargs = {
  62. 'providers_list_json': json.dumps(providers),
  63. 'project_id': project_id,
  64. 'cert_obj_json': json.dumps(cert_obj.to_dict()),
  65. 'context_dict': context_utils.get_current().to_dict()
  66. }
  67. if https_upgrade is True:
  68. kwargs['https_upgrade'] = True
  69. self.distributed_task_controller.submit_task(
  70. create_ssl_certificate.create_ssl_certificate,
  71. **kwargs)
  72. return kwargs
  73. def delete_ssl_certificate(self, project_id, domain_name, cert_type):
  74. kwargs = {
  75. 'project_id': project_id,
  76. 'domain_name': domain_name,
  77. 'cert_type': cert_type,
  78. 'context_dict': context_utils.get_current().to_dict()
  79. }
  80. self.distributed_task_controller.submit_task(
  81. delete_ssl_certificate.delete_ssl_certificate,
  82. **kwargs)
  83. return kwargs
  84. def get_certs_info_by_domain(self, domain_name, project_id):
  85. try:
  86. certs_info = self.storage.get_certs_by_domain(
  87. domain_name=domain_name,
  88. project_id=project_id)
  89. if not certs_info:
  90. raise ValueError("certificate information"
  91. "not found for {0} ".format(domain_name))
  92. return certs_info
  93. except ValueError as e:
  94. raise e
  95. def get_san_retry_list(self):
  96. if 'akamai' in self._driver.providers:
  97. akamai_driver = self._driver.providers['akamai'].obj
  98. res = akamai_driver.mod_san_queue.traverse_queue()
  99. # For other providers san_retry_list implementation goes here
  100. else:
  101. # if not using akamai driver just return an empty list
  102. return []
  103. res = [json.loads(r) for r in res]
  104. return [
  105. {"domain_name": r['domain_name'],
  106. "project_id": r['project_id'],
  107. "flavor_id": r['flavor_id'],
  108. "validate_service": r.get('validate_service', True)}
  109. for r in res
  110. ]
  111. def update_san_retry_list(self, queue_data_list):
  112. for r in queue_data_list:
  113. service_obj = self.service_storage\
  114. .get_service_details_by_domain_name(r['domain_name'])
  115. if service_obj is None and r.get('validate_service', True):
  116. raise LookupError(u'Domain {0} does not exist on any service, '
  117. 'are you sure you want to proceed request, '
  118. '{1}? You can set validate_service to False '
  119. 'to retry this san-retry request forcefully'.
  120. format(r['domain_name'], r))
  121. cert_for_domain = self.storage.get_certs_by_domain(
  122. r['domain_name'])
  123. if cert_for_domain != []:
  124. if cert_for_domain.get_cert_status() == "deployed":
  125. raise ValueError(u'Cert on {0} already exists'.
  126. format(r['domain_name']))
  127. new_queue_data = [
  128. json.dumps({'flavor_id': r['flavor_id'], # flavor_id
  129. 'domain_name': r['domain_name'], # domain_name
  130. 'project_id': r['project_id'],
  131. 'validate_service': r.get('validate_service', True)})
  132. for r in queue_data_list
  133. ]
  134. res, deleted = [], []
  135. if 'akamai' in self._driver.providers:
  136. akamai_driver = self._driver.providers['akamai'].obj
  137. orig = [json.loads(r) for r in
  138. akamai_driver.mod_san_queue.traverse_queue()]
  139. res = [json.loads(r) for r in
  140. akamai_driver.mod_san_queue.put_queue_data(new_queue_data)]
  141. deleted = tuple(x for x in orig if x not in res)
  142. # other provider's retry-list implementation goes here
  143. return res, deleted
  144. def rerun_san_retry_list(self):
  145. run_list = []
  146. ignore_list = []
  147. if 'akamai' in self._driver.providers:
  148. akamai_driver = self._driver.providers['akamai'].obj
  149. retry_list = []
  150. while len(akamai_driver.mod_san_queue.mod_san_queue_backend) > 0:
  151. res = akamai_driver.mod_san_queue.dequeue_mod_san_request()
  152. retry_list.append(json.loads(res.decode('utf-8')))
  153. # remove duplicates
  154. # see http://bit.ly/1mX2Vcb for details
  155. def remove_duplicates(data):
  156. """Remove duplicates from the data (normally a list).
  157. The data must be sortable and have an equality operator
  158. """
  159. data = sorted(data)
  160. return [k for k, _ in itertools.groupby(data)]
  161. retry_list = remove_duplicates(retry_list)
  162. # double check in POST. This check should really be first done in
  163. # PUT
  164. for r in retry_list:
  165. err_state = False
  166. service_obj = self.service_storage\
  167. .get_service_details_by_domain_name(r['domain_name'])
  168. if service_obj is None and r.get('validate_service', True):
  169. err_state = True
  170. LOG.error(
  171. u'Domain {0} does not exist on any service, are you '
  172. 'sure you want to proceed request, {1}? You can set '
  173. 'validate_service to False to retry this san-retry '
  174. 'request forcefully'.format(r['domain_name'], r)
  175. )
  176. elif (
  177. service_obj is not None and
  178. service_obj.operator_status.lower() == 'disabled'
  179. ):
  180. err_state = True
  181. LOG.error(
  182. u'The service for domain {0} is disabled.'
  183. 'No certificates will be created for '
  184. 'service {1} while it remains in {2} operator_status'
  185. 'request forcefully'.format(
  186. r['domain_name'],
  187. service_obj.service_id,
  188. service_obj.operator_status
  189. )
  190. )
  191. cert_for_domain = self.storage.get_certs_by_domain(
  192. r['domain_name'])
  193. if cert_for_domain != []:
  194. if cert_for_domain.get_cert_status() == "deployed":
  195. err_state = True
  196. LOG.error(
  197. u'Certificate on {0} has already been provisioned '
  198. 'successfully.'.format(r['domain_name']))
  199. if err_state is False:
  200. run_list.append(r)
  201. else:
  202. ignore_list.append(r)
  203. if not r.get('validate_service', True):
  204. # validation is False, send ignored retry_list
  205. # object back to queue
  206. akamai_driver.mod_san_queue.enqueue_mod_san_request(
  207. json.dumps(r)
  208. )
  209. LOG.warn(
  210. "{0} was skipped because it failed validation.".format(
  211. r['domain_name']
  212. )
  213. )
  214. for cert_obj_dict in run_list:
  215. try:
  216. cert_obj = ssl_certificate.SSLCertificate(
  217. cert_obj_dict['flavor_id'],
  218. cert_obj_dict['domain_name'],
  219. 'san',
  220. project_id=cert_obj_dict['project_id']
  221. )
  222. cert_for_domain = (
  223. self.storage.get_certs_by_domain(
  224. cert_obj.domain_name,
  225. project_id=cert_obj.project_id,
  226. flavor_id=cert_obj.flavor_id,
  227. cert_type=cert_obj.cert_type))
  228. if cert_for_domain == []:
  229. pass
  230. else:
  231. # If this cert has been deployed through manual
  232. # process we ignore the rerun process for this entry
  233. if cert_for_domain.get_cert_status() == 'deployed':
  234. continue
  235. # rerun the san process
  236. try:
  237. flavor = self.flavor_controller.get(cert_obj.flavor_id)
  238. # raise a lookup error if the flavor is not found
  239. except LookupError as e:
  240. raise e
  241. providers = [p.provider_id for p in flavor.providers]
  242. kwargs = {
  243. 'project_id': cert_obj.project_id,
  244. 'domain_name': cert_obj.domain_name,
  245. 'cert_type': 'san',
  246. 'providers_list_json': json.dumps(providers),
  247. 'cert_obj_json': json.dumps(cert_obj.to_dict()),
  248. 'enqueue': False,
  249. }
  250. self.distributed_task_controller.submit_task(
  251. recreate_ssl_certificate.recreate_ssl_certificate,
  252. **kwargs)
  253. except Exception as e:
  254. # When exception happens we log it and re-queue this
  255. # request
  256. LOG.exception(e)
  257. run_list.remove(cert_obj_dict)
  258. ignore_list.append(cert_obj_dict)
  259. akamai_driver.mod_san_queue.enqueue_mod_san_request(
  260. json.dumps(cert_obj_dict)
  261. )
  262. # For other providers post san_retry_list implementation goes here
  263. else:
  264. # if not using akamai driver just return summary of run list and
  265. # ignore list
  266. pass
  267. return run_list, ignore_list
  268. def get_san_cert_configuration(self, san_cert_name):
  269. if 'akamai' in self._driver.providers:
  270. akamai_driver = self._driver.providers['akamai'].obj
  271. if san_cert_name not in akamai_driver.san_cert_cnames:
  272. raise ValueError(
  273. "%s is not a valid san cert, valid san certs are: %s" %
  274. (san_cert_name, akamai_driver.san_cert_cnames))
  275. res = akamai_driver.cert_info_storage.get_cert_config(
  276. san_cert_name
  277. )
  278. else:
  279. # if not using akamai driver just return an empty list
  280. res = {}
  281. return res
  282. def update_san_cert_configuration(self, san_cert_name, new_cert_config):
  283. if 'akamai' in self._driver.providers:
  284. akamai_driver = self._driver.providers['akamai'].obj
  285. if san_cert_name not in akamai_driver.san_cert_cnames:
  286. raise ValueError(
  287. "%s is not a valid san cert, valid san certs are: %s" %
  288. (san_cert_name, akamai_driver.san_cert_cnames))
  289. akamai_driver = self._driver.providers['akamai'].obj
  290. # given the spsId, determine the most recent jobId
  291. # and persist the jobId
  292. if new_cert_config.get('spsId') is not None:
  293. resp = akamai_driver.sps_api_client.get(
  294. akamai_driver.akamai_sps_api_base_url.format(
  295. spsId=new_cert_config['spsId']
  296. ),
  297. )
  298. if resp.status_code != 200:
  299. raise RuntimeError(
  300. 'SPS GET Request failed. Exception: {0}'.format(
  301. resp.text
  302. )
  303. )
  304. else:
  305. resp_json = resp.json()
  306. new_cert_config['jobId'] = (
  307. resp_json['requestList'][0]['jobId']
  308. )
  309. res = akamai_driver.cert_info_storage.update_cert_config(
  310. san_cert_name, new_cert_config)
  311. else:
  312. # if not using akamai driver just return an empty list
  313. res = {}
  314. return res
  315. def get_san_cert_hostname_limit(self):
  316. if 'akamai' in self._driver.providers:
  317. akamai_driver = self._driver.providers['akamai'].obj
  318. res = akamai_driver.cert_info_storage.get_san_cert_hostname_limit()
  319. res = {'san_cert_hostname_limit': res}
  320. else:
  321. # if not using akamai driver just return an empty list
  322. res = {'san_cert_hostname_limit': 0}
  323. return res
  324. def set_san_cert_hostname_limit(self, request_json):
  325. if 'akamai' in self._driver.providers:
  326. try:
  327. new_limit = request_json['san_cert_hostname_limit']
  328. except Exception as exc:
  329. LOG.error("Error attempting to update san settings {0}".format(
  330. exc
  331. ))
  332. raise ValueError('Unknown setting!')
  333. akamai_driver = self._driver.providers['akamai'].obj
  334. res = akamai_driver.cert_info_storage.set_san_cert_hostname_limit(
  335. new_limit
  336. )
  337. else:
  338. # if not using akamai driver just return an empty list
  339. res = 0
  340. return res
  341. def get_certs_by_status(self, status):
  342. certs_by_status = self.storage.get_certs_by_status(status)
  343. return certs_by_status
  344. def update_certificate_status(self, domain_name, certificate_updates):
  345. certificate_old = self.storage.get_certs_by_domain(domain_name)
  346. if not certificate_old:
  347. raise ValueError(
  348. "certificate information not found for {0} ".format(
  349. domain_name
  350. )
  351. )
  352. try:
  353. if (
  354. certificate_updates.get("op") == "replace" and
  355. certificate_updates.get("path") == "status" and
  356. certificate_updates.get("value") is not None
  357. ):
  358. if (
  359. certificate_old.get_cert_status() !=
  360. certificate_updates.get("value")
  361. ):
  362. new_cert_details = certificate_old.cert_details
  363. # update the certificate for the first provider akamai
  364. # this logic changes when multiple certificate providers
  365. # are supported
  366. first_provider = list(new_cert_details.keys())[0]
  367. first_provider_cert_details = (
  368. list(new_cert_details.values())[0]
  369. )
  370. first_provider_cert_details["extra_info"][
  371. "status"] = certificate_updates.get("value")
  372. new_cert_details[first_provider] = json.dumps(
  373. first_provider_cert_details
  374. )
  375. self.storage.update_certificate(
  376. certificate_old.domain_name,
  377. certificate_old.cert_type,
  378. certificate_old.flavor_id,
  379. new_cert_details
  380. )
  381. except Exception as e:
  382. LOG.error(
  383. "Something went wrong during certificate update: {0}".format(
  384. e
  385. )
  386. )
  387. raise errors.CertificateStatusUpdateError(e)