poppy/poppy/manager/default/ssl_certificate.py

462 lines
19 KiB
Python

# Copyright (c) 2014 Rackspace, Inc.
#
# 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.
import json
from oslo_context import context as context_utils
from oslo_log import log
from poppy.common import errors
from poppy.common import util
from poppy.distributed_task.taskflow.flow import create_ssl_certificate
from poppy.distributed_task.taskflow.flow import delete_ssl_certificate
from poppy.distributed_task.taskflow.flow import recreate_ssl_certificate
from poppy.manager import base
from poppy.model.helpers import domain
from poppy.model import ssl_certificate
from poppy.transport.validators import helpers as validators
LOG = log.getLogger(__name__)
class DefaultSSLCertificateController(base.SSLCertificateController):
def __init__(self, manager):
super(DefaultSSLCertificateController, self).__init__(manager)
self.distributed_task_controller = (
self._driver.distributed_task.services_controller
)
self.storage = self._driver.storage.certificates_controller
self.service_storage = self._driver.storage.services_controller
self.flavor_controller = self._driver.storage.flavors_controller
def create_ssl_certificate(
self, project_id, cert_obj, https_upgrade=False):
if (not validators.is_valid_domain_name(cert_obj.domain_name)) or \
(validators.is_root_domain(
domain.Domain(cert_obj.domain_name).to_dict())) or \
(not validators.is_valid_tld(cert_obj.domain_name)):
# here created a http domain object but it does not matter http or
# https
raise ValueError('%s must be a valid non-root domain' %
cert_obj.domain_name)
try:
flavor = self.flavor_controller.get(cert_obj.flavor_id)
# raise a lookup error if the flavor is not found
except LookupError as e:
raise e
try:
self.storage.create_certificate(
project_id,
cert_obj
)
# ValueError will be raised if the cert_info has already existed
except ValueError as e:
raise e
providers = [p.provider_id for p in flavor.providers]
kwargs = {
'providers_list_json': json.dumps(providers),
'project_id': project_id,
'cert_obj_json': json.dumps(cert_obj.to_dict()),
'context_dict': context_utils.get_current().to_dict()
}
if https_upgrade is True:
kwargs['https_upgrade'] = True
self.distributed_task_controller.submit_task(
create_ssl_certificate.create_ssl_certificate,
**kwargs)
return kwargs
def delete_ssl_certificate(self, project_id, domain_name, cert_type):
kwargs = {
'project_id': project_id,
'domain_name': domain_name,
'cert_type': cert_type,
'context_dict': context_utils.get_current().to_dict()
}
self.distributed_task_controller.submit_task(
delete_ssl_certificate.delete_ssl_certificate,
**kwargs)
return kwargs
def get_certs_info_by_domain(self, domain_name, project_id):
try:
certs_info = self.storage.get_certs_by_domain(
domain_name=domain_name,
project_id=project_id)
if not certs_info:
raise ValueError("certificate information"
"not found for {0} ".format(domain_name))
return certs_info
except ValueError as e:
raise e
def get_san_retry_list(self):
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
res = akamai_driver.mod_san_queue.traverse_queue()
# For other providers san_retry_list implementation goes here
else:
# if not using akamai driver just return an empty list
return []
res = [json.loads(r) for r in res]
return [
{"domain_name": r['domain_name'],
"project_id": r['project_id'],
"flavor_id": r['flavor_id'],
"validate_service": r.get('validate_service', True)}
for r in res
]
def update_san_retry_list(self, queue_data_list):
for r in queue_data_list:
service_obj = self.service_storage\
.get_service_details_by_domain_name(r['domain_name'])
if service_obj is None and r.get('validate_service', True):
raise LookupError(u'Domain {0} does not exist on any service, '
'are you sure you want to proceed request, '
'{1}? You can set validate_service to False '
'to retry this san-retry request forcefully'.
format(r['domain_name'], r))
cert_for_domain = self.storage.get_certs_by_domain(
r['domain_name'])
if cert_for_domain != []:
if cert_for_domain.get_cert_status() == "deployed":
raise ValueError(u'Cert on {0} already exists'.
format(r['domain_name']))
new_queue_data = [
json.dumps({'flavor_id': r['flavor_id'], # flavor_id
'domain_name': r['domain_name'], # domain_name
'project_id': r['project_id'],
'validate_service': r.get('validate_service', True)})
for r in queue_data_list
]
res, deleted = [], []
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
orig = [json.loads(r) for r in
akamai_driver.mod_san_queue.traverse_queue()]
res = [json.loads(r) for r in
akamai_driver.mod_san_queue.put_queue_data(new_queue_data)]
deleted = tuple(x for x in orig if x not in res)
# other provider's retry-list implementation goes here
return res, deleted
def rerun_san_retry_list(self):
run_list = []
ignore_list = []
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
retry_list = []
while len(akamai_driver.mod_san_queue.mod_san_queue_backend) > 0:
res = akamai_driver.mod_san_queue.dequeue_mod_san_request()
retry_list.append(json.loads(res.decode('utf-8')))
retry_list = util.remove_duplicates(retry_list)
# double check in POST. This check should really be first done in
# PUT
for r in retry_list:
err_state = False
service_obj = self.service_storage\
.get_service_details_by_domain_name(r['domain_name'])
if service_obj is None and r.get('validate_service', True):
err_state = True
LOG.error(
u'Domain {0} does not exist on any service, are you '
'sure you want to proceed request, {1}? You can set '
'validate_service to False to retry this san-retry '
'request forcefully'.format(r['domain_name'], r)
)
elif (
service_obj is not None and
service_obj.operator_status.lower() == 'disabled'
):
err_state = True
LOG.error(
u'The service for domain {0} is disabled.'
'No certificates will be created for '
'service {1} while it remains in {2} operator_status'
'request forcefully'.format(
r['domain_name'],
service_obj.service_id,
service_obj.operator_status
)
)
cert_for_domain = self.storage.get_certs_by_domain(
r['domain_name'])
if cert_for_domain != []:
if cert_for_domain.get_cert_status() == "deployed":
err_state = True
LOG.error(
u'Certificate on {0} has already been provisioned '
'successfully.'.format(r['domain_name']))
if err_state is False:
run_list.append(r)
else:
ignore_list.append(r)
if not r.get('validate_service', True):
# validation is False, send ignored retry_list
# object back to queue
akamai_driver.mod_san_queue.enqueue_mod_san_request(
json.dumps(r)
)
LOG.warn(
"{0} was skipped because it failed validation.".format(
r['domain_name']
)
)
for cert_obj_dict in run_list:
try:
cert_obj = ssl_certificate.SSLCertificate(
cert_obj_dict['flavor_id'],
cert_obj_dict['domain_name'],
'san',
project_id=cert_obj_dict['project_id']
)
cert_for_domain = (
self.storage.get_certs_by_domain(
cert_obj.domain_name,
project_id=cert_obj.project_id,
flavor_id=cert_obj.flavor_id,
cert_type=cert_obj.cert_type))
if cert_for_domain == []:
pass
else:
# If this cert has been deployed through manual
# process we ignore the rerun process for this entry
if cert_for_domain.get_cert_status() == 'deployed':
continue
# rerun the san process
try:
flavor = self.flavor_controller.get(cert_obj.flavor_id)
# raise a lookup error if the flavor is not found
except LookupError as e:
raise e
providers = [p.provider_id for p in flavor.providers]
kwargs = {
'project_id': cert_obj.project_id,
'domain_name': cert_obj.domain_name,
'cert_type': 'san',
'providers_list_json': json.dumps(providers),
'cert_obj_json': json.dumps(cert_obj.to_dict()),
'enqueue': False,
}
self.distributed_task_controller.submit_task(
recreate_ssl_certificate.recreate_ssl_certificate,
**kwargs)
except Exception as e:
# When exception happens we log it and re-queue this
# request
LOG.exception(e)
run_list.remove(cert_obj_dict)
ignore_list.append(cert_obj_dict)
akamai_driver.mod_san_queue.enqueue_mod_san_request(
json.dumps(cert_obj_dict)
)
# For other providers post san_retry_list implementation goes here
else:
# if not using akamai driver just return summary of run list and
# ignore list
pass
return run_list, ignore_list
def get_san_cert_configuration(self, san_cert_name):
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
if san_cert_name not in akamai_driver.san_cert_cnames:
raise ValueError(
"%s is not a valid san cert, valid san certs are: %s" %
(san_cert_name, akamai_driver.san_cert_cnames))
res = akamai_driver.cert_info_storage.get_cert_config(
san_cert_name
)
else:
# if not using akamai driver just return an empty list
res = {}
return res
def update_san_cert_configuration(self, san_cert_name, new_cert_config):
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
if san_cert_name not in akamai_driver.san_cert_cnames:
raise ValueError(
"%s is not a valid san cert, valid san certs are: %s" %
(san_cert_name, akamai_driver.san_cert_cnames))
# given the spsId, determine the most recent jobId
# and persist the jobId
if new_cert_config.get('spsId') is not None:
resp = akamai_driver.sps_api_client.get(
akamai_driver.akamai_sps_api_base_url.format(
spsId=new_cert_config['spsId']
),
)
if resp.status_code != 200:
raise RuntimeError(
'SPS GET Request failed. Exception: {0}'.format(
resp.text
)
)
else:
resp_json = resp.json()
new_cert_config['jobId'] = (
resp_json['requestList'][0]['jobId']
)
res = akamai_driver.cert_info_storage.update_cert_config(
san_cert_name, new_cert_config)
else:
# if not using akamai driver just return an empty list
res = {}
return res
def get_sni_cert_configuration(self, cert_name):
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
self._validate_sni_cert_name(akamai_driver, cert_name)
res = akamai_driver.cert_info_storage.get_sni_cert_info(cert_name)
else:
# if not using akamai driver just return an empty list
res = {}
return res
def update_sni_cert_configuration(self, cert_name, new_cert_config):
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
self._validate_sni_cert_name(akamai_driver, cert_name)
res = akamai_driver.cert_info_storage.update_sni_cert_config(
cert_name,
new_cert_config
)
else:
# if not using akamai driver just return an empty list
res = {}
return res
def get_san_cert_hostname_limit(self):
if 'akamai' in self._driver.providers:
akamai_driver = self._driver.providers['akamai'].obj
res = akamai_driver.cert_info_storage.get_san_cert_hostname_limit()
res = {'san_cert_hostname_limit': res}
else:
# if not using akamai driver just return an empty list
res = {'san_cert_hostname_limit': 0}
return res
@staticmethod
def _validate_sni_cert_name(provider_driver, cert_name):
if cert_name not in provider_driver.sni_cert_cnames:
raise ValueError(
"{0} is not a valid sni cert, "
"valid sni certs are: {1}".format(
cert_name, provider_driver.sni_cert_cnames))
def set_san_cert_hostname_limit(self, request_json):
if 'akamai' in self._driver.providers:
try:
new_limit = request_json['san_cert_hostname_limit']
except Exception as exc:
LOG.error("Error attempting to update san settings {0}".format(
exc
))
raise ValueError('Unknown setting!')
akamai_driver = self._driver.providers['akamai'].obj
res = akamai_driver.cert_info_storage.set_san_cert_hostname_limit(
new_limit
)
else:
# if not using akamai driver just return an empty list
res = 0
return res
def get_certs_by_status(self, status):
certs_by_status = self.storage.get_certs_by_status(status)
return certs_by_status
def update_certificate_status(self, domain_name, certificate_updates):
certificate_old = self.storage.get_certs_by_domain(domain_name)
if not certificate_old:
raise ValueError(
"certificate information not found for {0} ".format(
domain_name
)
)
try:
if (
certificate_updates.get("op") == "replace" and
certificate_updates.get("path") == "status" and
certificate_updates.get("value") is not None
):
if (
certificate_old.get_cert_status() !=
certificate_updates.get("value")
):
new_cert_details = certificate_old.cert_details
# update the certificate for the first provider akamai
# this logic changes when multiple certificate providers
# are supported
first_provider = list(new_cert_details.keys())[0]
first_provider_cert_details = (
list(new_cert_details.values())[0]
)
first_provider_cert_details["extra_info"][
"status"] = certificate_updates.get("value")
new_cert_details[first_provider] = json.dumps(
first_provider_cert_details
)
self.storage.update_certificate(
certificate_old.domain_name,
certificate_old.cert_type,
certificate_old.flavor_id,
new_cert_details
)
except Exception as e:
LOG.error(
"Something went wrong during certificate update: {0}".format(
e
)
)
raise errors.CertificateStatusUpdateError(e)