ssl-cert-provision endpoint.This allows user to create a certificate with akamai driver.

Should be letting user assoicate a certificate with ta domain.
Implements blueprint: ssl-certificates
Implements blueprint: akamai-ssl-driver

Change-Id: Iab5dc13d6a0d36bc4e4857364ae3d27a1bcd5113
This commit is contained in:
tonytan4ever 2015-09-16 15:56:08 -04:00 committed by tonytan4ever
parent 72a0d26d01
commit df58d6014a
53 changed files with 1784 additions and 12 deletions

View File

@ -118,6 +118,13 @@ delay = 1
[drivers:provider]
default_cache_ttl = 86400
[drivers:notification:mailgun]
mailgun_api_key = "<operator_api_key>"
mailgun_request_url = "https://api.mailgun.net/v2/{0}/events"
sand_box = "<your_sand_box_domain>"
from_address = "<your_send_from_email_address>"
recipients="<a_list_of_email_recipient>"
[drivers:provider:fastly]
apikey = "MYAPIKEY"
# scheme = "https"
@ -151,6 +158,12 @@ akamai_https_san_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_SAN_NUMBER'
akamai_https_custom_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_CUSTOM_NUMBER'
san_cert_cnames = "MY_SAN_CERT_LIST"
san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LMIT"
contract_id = "MY_CONTRACT_ID"
group_id = "MY_GROUP_ID"
property_id = "MY_PROPERTY_ID"
storage_backend_type = zookeeper
storage_backend_host = <your_transport_server(s)>
storage_backend_port = <your_transport_port>
[drivers:notification:mailgun]
mailgun_api_key = "<operator_api_key>"

View File

@ -0,0 +1,41 @@
# 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.
from oslo_config import cfg
from taskflow.patterns import graph_flow
from taskflow.patterns import linear_flow
from taskflow import retry
from poppy.distributed_task.taskflow.task import create_ssl_certificate_tasks
from poppy.openstack.common import log
LOG = log.getLogger(__name__)
conf = cfg.CONF
conf(project='poppy', prog='poppy', args=[])
def create_ssl_certificate():
flow = graph_flow.Flow('Creating poppy ssl certificate').add(
linear_flow.Flow("Provision poppy ssl certificate",
retry=retry.Times(5)).add(
create_ssl_certificate_tasks.CreateProviderSSLCertificateTask()
),
create_ssl_certificate_tasks.SendNotificationTask(),
create_ssl_certificate_tasks.UpdateCertInfoTask()
)
return flow

View File

@ -0,0 +1,94 @@
# 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_config import cfg
from taskflow import task
from poppy.distributed_task.utils import memoized_controllers
from poppy.openstack.common import log
from poppy.transport.pecan.models.request import ssl_certificate
LOG = log.getLogger(__name__)
conf = cfg.CONF
conf(project='poppy', prog='poppy', args=[])
class CreateProviderSSLCertificateTask(task.Task):
default_provides = "responders"
def execute(self, providers_list_json, cert_obj_json):
service_controller = memoized_controllers.task_controllers('poppy')
# call provider create_ssl_certificate function
providers_list = json.loads(providers_list_json)
cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json))
responders = []
# try to create all service from each provider
for provider in providers_list:
LOG.info('Starting to create ssl certificate: {0}'.format(
cert_obj.to_dict()))
LOG.info('from {0}'.format(provider))
responder = service_controller.provider_wrapper.create_certificate(
service_controller._driver.providers[provider],
cert_obj
)
responders.append(responder)
return responders
class SendNotificationTask(task.Task):
def execute(self, project_id, responders):
service_controller = memoized_controllers.task_controllers('poppy')
notification_content = ""
for responder in responders:
for provider in responder:
notification_content += (
"Project ID: %s, Provider: %s, Detail: %s" %
(project_id, provider, str(responder[provider])))
for n_driver in service_controller._driver.notification:
service_controller.notification_wrapper.send(
n_driver,
n_driver.obj.notification_subject,
notification_content)
return
class UpdateCertInfoTask(task.Task):
def execute(self, project_id, cert_obj_json, responders):
service_controller, self.storage_controller = \
memoized_controllers.task_controllers('poppy', 'storage')
cert_details = {}
for responder in responders:
for provider in responder:
cert_details[provider] = json.dumps(responder[provider])
cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json))
self.storage_controller.update_cert_info(cert_obj.domain_name,
cert_obj.cert_type,
cert_obj.flavor_id,
cert_details)
return

View File

@ -46,5 +46,8 @@ def task_controllers(program, controller=None):
return service_controller, service_controller.storage_controller
if controller == 'dns':
return service_controller, service_controller.dns_controller
if controller == 'ssl_certificate':
return service_controller, (
bootstrap_obj.manager.ssl_certificate_controller)
else:
return service_controller

View File

@ -17,6 +17,7 @@ from poppy.manager.base import driver
from poppy.manager.base import flavors
from poppy.manager.base import home
from poppy.manager.base import services
from poppy.manager.base import ssl_certificate
Driver = driver.ManagerDriverBase
@ -24,3 +25,4 @@ Driver = driver.ManagerDriverBase
FlavorsController = flavors.FlavorsControllerBase
ServicesController = services.ServicesControllerBase
HomeController = home.HomeControllerBase
SSLCertificateController = ssl_certificate.SSLCertificateController

View File

@ -73,3 +73,13 @@ class ProviderWrapper(object):
service_obj,
hard,
purge_url)
def create_certificate(self, ext, cert_obj):
"""Create a provider
:param ext
:param service_obj
:returns: ext.obj.service_controller.create(service_obj)
"""
return ext.obj.service_controller.create_certificate(cert_obj)

View File

@ -0,0 +1,38 @@
# 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 abc
import six
from poppy.manager.base import controller
@six.add_metaclass(abc.ABCMeta)
class SSLCertificateController(controller.ManagerControllerBase):
"""Home controller base class."""
def __init__(self, manager):
super(SSLCertificateController, self).__init__(manager)
@abc.abstractmethod
def create_ssl_certificate(self, project_id, domain_name, **extras):
"""create_ssl_certificate
:param project_id
:param domain_name
:raises: NotImplementedError
"""
raise NotImplementedError

View File

@ -17,9 +17,11 @@ from poppy.manager.default import flavors
from poppy.manager.default import health
from poppy.manager.default import home
from poppy.manager.default import services
from poppy.manager.default import ssl_certificate
Home = home.DefaultHomeController
Flavors = flavors.DefaultFlavorsController
Health = health.DefaultHealthController
Services = services.DefaultServicesController
SSLCertificate = ssl_certificate.DefaultSSLCertificateController

View File

@ -43,3 +43,7 @@ class DefaultManagerDriver(base.Driver):
@decorators.lazy_property(write=False)
def health_controller(self):
return controllers.Health(self)
@decorators.lazy_property(write=False)
def ssl_certificate_controller(self):
return controllers.SSLCertificate(self)

View File

@ -0,0 +1,57 @@
# 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 poppy.distributed_task.taskflow.flow import create_ssl_certificate
from poppy.manager import base
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_controller = self._driver.storage.services_controller
self.flavor_controller = self._driver.storage.flavors_controller
def create_ssl_certificate(self, project_id, cert_obj):
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_controller.create_cert(
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())
}
self.distributed_task_controller.submit_task(
create_ssl_certificate.create_ssl_certificate,
**kwargs)
return kwargs

View File

@ -0,0 +1,66 @@
# Copyright (c) 2015 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.
from poppy.model import common
VALID_CERT_TYPES = [u'san', u'custom']
class SSLCertificate(common.DictSerializableModel):
"""SSL Certificate Class."""
def __init__(self,
flavor_id,
domain_name,
cert_type):
self._flavor_id = flavor_id
self._domain_name = domain_name
self._cert_type = cert_type
@property
def flavor_id(self):
"""Get or set flavor ref."""
return self._flavor_id
@flavor_id.setter
def flavor_id(self, value):
self._flavor_id = value
@property
def domain_name(self):
"""Get service id."""
return self._domain_name
@domain_name.setter
def domain_name(self, value):
self._domain_name = value
@property
def cert_type(self):
"""Get service id."""
return self._cert_type
@cert_type.setter
def cert_type(self, value):
if (value in VALID_CERT_TYPES):
self._cert_type = value
else:
raise ValueError(
u'Cert type: {0} not in valid options: {1}'.format(
value,
VALID_CERT_TYPES)
)

View File

@ -34,7 +34,10 @@ MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'),
cfg.ListOpt('recipients',
help='A list of emails addresses to receive notification ')
help='A list of emails addresses to receive notification '),
cfg.StrOpt('notification_subject',
default='Poppy SSL Certificate Provisioned',
help='The subject of the email notification ')
]
MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'
@ -60,6 +63,8 @@ class MailNotificationDriver(base.Driver):
self.sand_box = self.mail_notification_conf.sand_box
self.from_address = self.mail_notification_conf.from_address
self.recipients = self.mail_notification_conf.recipients
self.notification_subject = (
self.mail_notification_conf.notification_subject)
# validate email addresses
if not validate_email_address(self.from_address):

View File

@ -23,6 +23,8 @@ import requests
from poppy.openstack.common import log
from poppy.provider.akamai import controllers
from poppy.provider.akamai.mod_san_queue import zookeeper_queue
from poppy.provider.akamai.san_info_storage import zookeeper_storage
from poppy.provider import base
LOG = log.getLogger(__name__)
@ -89,6 +91,17 @@ AKAMAI_OPTIONS = [
cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can'
' be held by a SAN cert'),
# related info for SPS && PAPI APIs
cfg.StrOpt(
'contract_id',
help='Operator contractID'),
cfg.StrOpt(
'group_id',
help='Operator groupID'),
cfg.StrOpt(
'property_id',
help='Operator propertyID')
]
AKAMAI_GROUP = 'drivers:provider:akamai'
@ -139,9 +152,25 @@ class CDNProvider(base.Driver):
access_token=self.akamai_conf.ccu_api_access_token
)
self.akamai_sps_api_base_url = ''.join([
str(self.akamai_conf.policy_api_base_url),
'config-secure-provisioning-service/v1'
'/sps-requests/{spsId}?contractId=%s&groupId=%s' % (
self.akamai_conf.contract_id,
self.akamai_conf.group_id
)
])
self.san_cert_cnames = self.akamai_conf.san_cert_cnames
self.san_cert_hostname_limit = self.akamai_conf.san_cert_hostname_limit
self.akamai_sps_api_client = self.akamai_policy_api_client
self.san_info_storage = (
zookeeper_storage.ZookeeperSanInfoStorage(self._conf))
self.mod_san_queue = (
zookeeper_queue.ZookeeperModSanQueue(self._conf))
def is_alive(self):
request_headers = {

View File

@ -0,0 +1,43 @@
# 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 abc
import six
@six.add_metaclass(abc.ABCMeta)
class ModSanQueue(object):
"""Interface definition for Akamai Mod San Queue.
The purpose of this queue is to buffer the client's
mod_san request (Currently one request will make one
san_cert pending, if currently there is no active san
cert to serve the client request, it is needed to keep
the request in a queue)
"""
def __init__(self, conf):
self._conf = conf
def enqueue_mod_san_request(self, domain_name):
raise NotImplementedError
def dequeue_mod_san_request(self):
raise NotImplementedError
def move_request_to_top(self):
raise NotImplementedError

View File

@ -0,0 +1,65 @@
# 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.
from kazoo.recipe import queue
from oslo_config import cfg
from poppy.common import decorators
from poppy.provider.akamai.mod_san_queue import base
from poppy.provider.akamai import utils
AKAMAI_OPTIONS = [
# queue backend configs
cfg.StrOpt(
'queue_backend_type',
help='SAN Cert Queueing backend'),
cfg.ListOpt('queue_backend_host', default=['localhost'],
help='default queue backend server hosts'),
cfg.IntOpt('queue_backend_port', default=2181, help='default'
' default queue backend server port (e.g: 2181)'),
cfg.StrOpt(
'mod_san_queue_path', default='/mod_san_queue', help='Zookeeper path '
'for mod_san_queue'),
]
AKAMAI_GROUP = 'drivers:provider:akamai'
class ZookeeperModSanQueue(base.ModSanQueue):
def __init__(self, conf):
super(ZookeeperModSanQueue, self).__init__(conf)
self._conf.register_opts(AKAMAI_OPTIONS,
group=AKAMAI_GROUP)
self.akamai_conf = self._conf[AKAMAI_GROUP]
self.mod_san_queue_backend = queue.LockingQueue(
self.zk_client,
self.akamai_conf.mod_san_queue_path)
@decorators.lazy_property(write=False)
def zk_client(self):
return utils.connect_to_zookeeper_queue_backend(self.akamai_conf)
def enqueue_mod_san_request(self, cert_obj_json):
self.mod_san_queue_backend.put(cert_obj_json)
def dequeue_mod_san_request(self, consume=True):
res = self.mod_san_queue_backend.get()
if consume:
self.mod_san_queue_backend.consume()
return res

View File

@ -0,0 +1,37 @@
# 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 abc
import six
@six.add_metaclass(abc.ABCMeta)
class BaseAkamaiSanInfoStorage(object):
"""Interface definition for Akamai San Info Storage.
"""
def __init__(self, conf):
self._conf = conf
def get_cert_info(self, san_cert_name):
raise NotImplementedError
def save_cert_last_spsid(self, san_cert_name, sps_id_value):
raise NotImplementedError
def get_cert_last_spsid(self, san_cert_name):
raise NotImplementedError

View File

@ -0,0 +1,97 @@
# Copyright (c) 2015 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.
from oslo_config import cfg
from poppy.provider.akamai.san_info_storage import base
from poppy.provider.akamai import utils
AKAMAI_OPTIONS = [
# storage backend configs for long running tasks
cfg.StrOpt(
'storage_backend_type',
default='zookeeper',
help='SAN Cert info storage backend'),
cfg.ListOpt('storage_backend_host', default=['localhost'],
help='default san info storage backend server hosts'),
cfg.IntOpt('storage_backend_port', default=2181, help='default'
' default san info storage backend server port (e.g: 2181)'),
cfg.StrOpt(
'san_info_storage_path', default='/san_info', help='zookeeper backend'
' path for san cert info'),
]
AKAMAI_GROUP = 'drivers:provider:akamai'
class ZookeeperSanInfoStorage(base.BaseAkamaiSanInfoStorage):
def __init__(self, conf):
super(ZookeeperSanInfoStorage, self).__init__(conf)
self._conf.register_opts(AKAMAI_OPTIONS,
group=AKAMAI_GROUP)
self.akamai_conf = self._conf[AKAMAI_GROUP]
self.san_info_storage_path = self.akamai_conf.san_info_storage_path
self.zookeeper_client = utils.connect_to_zookeeper_storage_backend(
self.akamai_conf)
def _zk_path(self, san_cert_name, property_name=None):
path_names_list = [self.san_info_storage_path, san_cert_name,
property_name] if property_name else (
[self.san_info_storage_path, san_cert_name])
return '/'.join(path_names_list)
def list_all_san_cert_names(self):
self.zookeeper_client.ensure_path(self.san_info_storage_path)
return self.zookeeper_client.get_children(self.san_info_storage_path)
def get_cert_info(self, san_cert_name):
self.zookeeper_client.ensure_path(self._zk_path(san_cert_name, None))
jobId, _ = self.zookeeper_client.get(self._zk_path(san_cert_name,
"jobId"))
issuer, _ = self.zookeeper_client.get(self._zk_path(san_cert_name,
"issuer"))
ipVersion, _ = self.zookeeper_client.get(
self._zk_path(san_cert_name, "ipVersion"))
slot_deployment_klass, _ = self.zookeeper_client.get(
self._zk_path(san_cert_name, "slot_deployment_klass"))
return {
# This will always be the san cert name
'cnameHostname': san_cert_name,
'jobId': jobId,
'issuer': issuer,
'createType': 'modSan',
'ipVersion': ipVersion,
'slot-deployment.class': slot_deployment_klass
}
def save_cert_last_spsid(self, san_cert_name, sps_id_value):
self._save_cert_property_value(san_cert_name,
'spsId', sps_id_value)
def get_cert_last_spsid(self, san_cert_name):
my_sps_id_path = self._zk_path(san_cert_name, 'spsId')
self.zookeeper_client.ensure_path(my_sps_id_path)
spsId, _ = self.zookeeper_client.get(my_sps_id_path)
return spsId
def _save_cert_property_value(self, san_cert_name,
property_name, value):
property_name_path = self._zk_path(san_cert_name, property_name)
self.zookeeper_client.ensure_path(property_name_path)
self.zookeeper_client.set(property_name_path, str(value))

View File

@ -37,12 +37,25 @@ class ServiceController(base.ServiceBase):
def ccu_api_client(self):
return self.driver.ccu_api_client
@property
def sps_api_client(self):
return self.driver.akamai_sps_api_client
@property
def san_info_storage(self):
return self.driver.san_info_storage
@property
def mod_san_queue(self):
return self.driver.mod_san_queue
def __init__(self, driver):
super(ServiceController, self).__init__(driver)
self.driver = driver
self.policy_api_base_url = self.driver.akamai_policy_api_base_url
self.ccu_api_base_url = self.driver.akamai_ccu_api_base_url
self.sps_api_base_url = self.driver.akamai_sps_api_base_url
self.request_header = {'Content-type': 'application/json',
'Accept': 'text/plain'}
@ -481,6 +494,73 @@ class ServiceController(base.ServiceBase):
format(provider_service_id))
return self.responder.failed(str(e))
def create_certificate(self, cert_obj):
if cert_obj.cert_type == 'san':
for san_cert_name in self.san_cert_cnames:
lastSpsId = (
self.san_info_storage.get_cert_last_spsid(san_cert_name))
if lastSpsId not in [None, ""]:
LOG.info('Latest spsId for %s is: %s' % (san_cert_name,
lastSpsId))
resp = self.sps_api_client.get(
self.sps_api_base_url.format(spsId=lastSpsId),
)
if resp.status_code != 200:
raise RuntimeError('SPS API Request Failed'
'Exception: %s' % resp.text)
status = json.loads(resp.text)['requestList'][0]['status']
# This SAN Cert is on pending status
if status != 'SPS Request Complete':
LOG.info("SPS Not completed for %s..." %
self.san_cert_name)
continue
# issue modify san_cert sps request
cert_info = self.san_info_storage.get_cert_info(san_cert_name)
cert_info['add.sans'] = cert_obj.domain_name
string_post_data = '&'.join(
['%s=%s' % (k, v) for (k, v) in cert_info.items()])
LOG.info('Post modSan request with request data: %s' %
string_post_data)
resp = self.sps_api_client.post(
self.sps_api_base_url.format(spsId=""),
data=string_post_data
)
if resp.status_code != 202:
raise RuntimeError('SPS Request failed.'
'Exception: %s' % resp.text)
else:
resp_dict = json.loads(resp.text)
LOG.info('modSan request submitted. Response: %s' %
str(resp_dict))
this_sps_id = resp_dict['spsId']
self.san_info_storage.save_cert_last_spsid(san_cert_name,
this_sps_id)
return self.responder.ssl_certificate_provisioned(
san_cert_name, {
'status': 'create_in_progress',
'san cert': san_cert_name,
'akamai_spsId': this_sps_id,
'create_at': str(datetime.datetime.now()),
'action': 'Waiting for customer domain '
'validation for %s' %
(cert_obj.domain_name)
})
else:
self.mod_san_queue.enqueue_mod_san_request(
json.dumps(cert_obj.to_dict()))
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'san cert': None,
'action': 'No available san cert for %s right now.'
' More provisioning might be needed' %
(cert_obj.domain_name)
})
else:
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'reason': 'Cert type : %s hasn\'t been implemented'
})
@decorators.lazy_property(write=False)
def current_customer(self):
return None

View File

@ -16,6 +16,7 @@
import ssl
import sys
from kazoo import client
from OpenSSL import crypto
import six
@ -65,7 +66,7 @@ def get_ssl_number_of_hosts(remote_host):
# We can actually print all the Subject Alternative Names
# for san in sans:
# print san
# print(san)
result = len(sans)
break
else:
@ -73,6 +74,28 @@ def get_ssl_number_of_hosts(remote_host):
return result
def connect_to_zookeeper_storage_backend(conf):
"""Connect to a zookeeper cluster"""
storage_backend_hosts = ','.join(['%s:%s' % (
host, conf.storage_backend_port)
for host in
conf.storage_backend_host])
zk_client = client.KazooClient(storage_backend_hosts)
zk_client.start()
return zk_client
def connect_to_zookeeper_queue_backend(conf):
"""Connect to a zookeeper cluster"""
storage_backend_hosts = ','.join(['%s:%s' % (
host, conf.queue_backend_port)
for host in
conf.queue_backend_host])
zk_client = client.KazooClient(storage_backend_hosts)
zk_client.start()
return zk_client
if __name__ == "__main__":
if len(sys.argv) != 2:
print('Usage: %s <remote_host_you_want_get_cert_on>' % sys.argv[0])

View File

@ -123,3 +123,17 @@ class Responder(object):
'caching': cache_list
}
}
def ssl_certificate_provisioned(self, cert_domain, extra_info=None):
"""ssl_certificate_provisioned.
:param cert_domain
:param extra_info
:returns provider msg{cert_domain, extra_info}
"""
return {
self.provider: {
'cert_domain': cert_domain,
'extra_info': extra_info
}
}

View File

@ -112,6 +112,28 @@ class ServicesControllerBase(controller.StorageControllerBase):
"""
raise NotImplementedError
@abc.abstractmethod
def create_cert(self, project_id, cert_obj):
"""create_cert
:param project_id
:param cert_obj
:raise NotImplementedError
"""
raise NotImplementedError
@abc.abstractmethod
def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
"""update_cert_info.
:param domain_name
:param cert_type
:param flavor_id
:param cert_info
"""
raise NotImplementedError
@staticmethod
def format_result(result):
"""format_result

View File

@ -0,0 +1,15 @@
CREATE TABLE certificate_info (
project_id VARCHAR,
flavor_id VARCHAR,
cert_type VARCHAR,
domain_name VARCHAR,
cert_details MAP<TEXT, TEXT>,
PRIMARY KEY (domain_name)
);
CREATE INDEX idx_cert_type
ON certificate_info (cert_type);
--//@UNDO
DROP TABLE IF EXISTS certificate_info;

View File

@ -0,0 +1,9 @@
# This for running cdeploy command
development:
hosts: [localhost]
keyspace: poppy
production:
hosts: [your_production_env_host(s)]
keyspace: poppy

View File

@ -166,6 +166,30 @@ CQL_CREATE_SERVICE = '''
%(log_delivery)s)
'''
CQL_CREATE_CERT = '''
INSERT INTO certificate_info (project_id,
flavor_id,
cert_type,
domain_name,
cert_details
)
VALUES (%(project_id)s,
%(flavor_id)s,
%(cert_type)s,
%(domain_name)s,
%(cert_details)s)
'''
CQL_VERIFY_CERT = '''
SELECT project_id,
flavor_id,
cert_type,
domain_name
FROM certificate_info
WHERE domain_name = %(domain_name)s
ALLOW FILTERING
'''
CQL_UPDATE_SERVICE = CQL_CREATE_SERVICE
CQL_GET_PROVIDER_DETAILS = '''
@ -180,6 +204,13 @@ CQL_UPDATE_PROVIDER_DETAILS = '''
WHERE project_id = %(project_id)s AND service_id = %(service_id)s
'''
CQL_UPDATE_CERT_DETAILS = '''
UPDATE certificate_info
set cert_details = %(cert_details)s
WHERE domain_name = %(domain_name)s
IF cert_type = %(cert_type)s AND flavor_id = %(flavor_id)s
'''
class ServicesController(base.ServicesController):
@ -291,6 +322,51 @@ class ServicesController(base.ServicesController):
LOG.exception(ex)
return False
def cert_already_exist(self, domain_name, comparing_cert_type,
comparing_flavor_id,
comparing_project_id):
"""cert_already_exist
Check if a cert with this domain name and type has already been
created, or if the domain has been taken by other customers
:param domain_name
:param cert_type
:param comparing_project_id
:raises ValueError
:returns Boolean if the cert with same type exists with another user.
"""
LOG.info("Check if cert on '{0}' exists".format(domain_name))
args = {
'domain_name': domain_name.lower()
}
stmt = query.SimpleStatement(
CQL_VERIFY_CERT,
consistency_level=self._driver.consistency_level)
results = self.session.execute(stmt, args)
if results:
msg = None
for r in results:
if str(r.get('project_id')) != str(comparing_project_id):
msg = "Domain '{0}' has already been created cert by {1}"\
.format(domain_name, r.get('project_id'))
LOG.warn(msg)
raise ValueError(msg)
elif (str(r.get('flavor_id')) == str(comparing_flavor_id)
and
str(r.get('cert_type')) == str(comparing_cert_type)):
msg = "{0} have already created cert of type {1} on {2}"\
.format(str(comparing_project_id),
comparing_cert_type,
domain_name)
LOG.warn(msg)
raise ValueError(msg)
return False
else:
return False
def create(self, project_id, service_obj):
"""create.
@ -506,6 +582,29 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, delete_args)
def create_cert(self, project_id, cert_obj):
if not self.cert_already_exist(cert_obj.domain_name,
cert_obj.cert_type,
cert_obj.flavor_id,
project_id):
pass
args = {
'project_id': project_id,
'flavor_id': cert_obj.flavor_id,
'cert_type': cert_obj.cert_type,
'domain_name': cert_obj.domain_name,
# when create the cert, cert domain has not been assigned yet
# In future we can tweak the logic to assign cert_domain
'cert_domain': '',
'cert_details': {}
}
stmt = query.SimpleStatement(
CQL_CREATE_CERT,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
def get_provider_details(self, project_id, service_id):
"""get_provider_details.
@ -613,6 +712,27 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
"""update_cert_info.
:param domain_name
:param cert_type
:param flavor_id
:param cert_info
"""
args = {
'domain_name': domain_name,
'cert_type': cert_type,
'flavor_id': flavor_id,
'cert_details': cert_details
}
stmt = query.SimpleStatement(
CQL_UPDATE_CERT_DETAILS,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)
@staticmethod
def format_result(result):
"""format_result.

View File

@ -123,6 +123,13 @@ class ServicesController(base.ServicesController):
def domain_exists_elsewhere(self, domain_name, service_id):
return domain_name in self.claimed_domains
def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
pass
def create_cert(self, project_id, cert_obj):
pass
@staticmethod
def format_result(result):
service_id = result.get('service_id')

View File

@ -21,6 +21,7 @@ from poppy.transport.pecan.controllers.v1 import health
from poppy.transport.pecan.controllers.v1 import home
from poppy.transport.pecan.controllers.v1 import ping
from poppy.transport.pecan.controllers.v1 import services
from poppy.transport.pecan.controllers.v1 import ssl_certificates
# Hoist into package namespace
@ -33,3 +34,4 @@ DNSHealth = health.DNSHealthController
StorageHealth = health.StorageHealthController
ProviderHealth = health.ProviderHealthController
Admin = admin.AdminController
SSLCertificate = ssl_certificates.SSLCertificateController

View File

@ -0,0 +1,62 @@
# 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
import pecan
from pecan import hooks
from poppy.transport.pecan.controllers import base
from poppy.transport.pecan import hooks as poppy_hooks
from poppy.transport.pecan.models.request import ssl_certificate
from poppy.transport.validators import helpers
from poppy.transport.validators.schemas import ssl_certificate\
as ssl_certificate_validation
from poppy.transport.validators.stoplight import decorators
from poppy.transport.validators.stoplight import helpers as stoplight_helpers
from poppy.transport.validators.stoplight import rule
class SSLCertificateController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
@pecan.expose('json')
@decorators.validate(
request=rule.Rule(
helpers.json_matches_service_schema(
ssl_certificate_validation.SSLCertificateSchema.get_schema(
"ssl_certificate",
"POST")),
helpers.abort_with_message,
stoplight_helpers.pecan_getter))
def post(self):
ssl_certificate_controller = (
self._driver.manager.ssl_certificate_controller)
certificate_info_dict = json.loads(pecan.request.body.decode('utf-8'))
try:
cert_obj = ssl_certificate.load_from_json(certificate_info_dict)
ssl_certificate_controller.create_ssl_certificate(self.project_id,
cert_obj)
except LookupError as e:
pecan.abort(400, detail='Provisioning ssl certificate failed. '
'Reason: %s' % str(e))
except ValueError as e:
pecan.abort(400, detail='Provisioning ssl certificate failed. '
'Reason: %s' % str(e))
return pecan.Response(None, 202)

View File

@ -63,6 +63,8 @@ class PecanTransportDriver(transport.Driver):
home_controller.add_controller('services', v1.Services(self))
home_controller.add_controller('flavors', v1.Flavors(self))
home_controller.add_controller('admin', v1.Admin(self))
home_controller.add_controller('ssl_certificate',
v1.SSLCertificate(self))
self._app = pecan.make_app(root_controller,
guess_content_type_from_ext=False)

View File

@ -0,0 +1,24 @@
# 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.
from poppy.model import ssl_certificate
def load_from_json(json_data):
flavor_id = json_data.get("flavor_id")
domain_name = json_data.get("domain_name")
cert_type = json_data.get("cert_type")
return ssl_certificate.SSLCertificate(flavor_id, domain_name, cert_type)

View File

@ -0,0 +1,31 @@
# 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.
try:
import ordereddict as collections
except ImportError: # pragma: no cover
import collections # pragma: no cover
from poppy.common import util
class Model(collections.OrderedDict):
'response class for SSLCertificate'
def __init__(self, ssl_certificate):
super(Model, self).__init__()
self["flavor_id"] = ssl_certificate.flavor_id
self['domain_name'] = util.help_escape(ssl_certificate.domain_name)
self['cert_type'] = ssl_certificate.cert_type

View File

@ -0,0 +1,49 @@
# Copyright (c) 2015 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.
from poppy.transport.validators import schema_base
class SSLCertificateSchema(schema_base.SchemaBase):
'''JSON Schmema validation for /ssl_certificate.'''
schema = {
'ssl_certificate': {
'POST': {
'type': 'object',
'additionalProperties': False,
'properties': {
'flavor_id': {
'type': 'string',
'required': True,
'minLength': 1,
'maxLength': 256
},
'cert_type': {
'type': 'string',
'required': True,
'enum': ['san'],
},
'domain_name': {
'type': 'string',
'required': True,
'minLength': 3,
'maxLength': 253
}
}
}
}
}

View File

@ -0,0 +1,40 @@
# Copyright (c) 2015 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.
from oslo_config import cfg
from poppy.provider.akamai.san_info_storage import zookeeper_storage
CONF = cfg.CONF
CONF.register_cli_opts(zookeeper_storage.AKAMAI_OPTIONS,
group=zookeeper_storage.AKAMAI_GROUP)
CONF(prog='akamai-config')
def main():
zk_storage = zookeeper_storage.ZookeeperSanInfoStorage(CONF)
all_san_cert_names = zk_storage.list_all_san_cert_names()
if not all_san_cert_names:
print ("Currently no SAN cert info has been intialized")
for san_cert_name in all_san_cert_names:
print("%s:%s" % (san_cert_name,
str(zk_storage.get_cert_info(san_cert_name))))
if __name__ == "__main__":
main()

View File

@ -0,0 +1,69 @@
# Copyright (c) 2015 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.
from oslo_config import cfg
from poppy.provider.akamai.san_info_storage import zookeeper_storage
CONF = cfg.CONF
CONF.register_cli_opts(zookeeper_storage.AKAMAI_OPTIONS,
group=zookeeper_storage.AKAMAI_GROUP)
CONF.register_cli_opt(
cfg.ListOpt('san_cert_cnames',
help='A list of san certs cnamehost names'),
group=zookeeper_storage.AKAMAI_GROUP)
CONF(prog='akamai-config')
def main():
zk_storage = zookeeper_storage.ZookeeperSanInfoStorage(CONF)
san_attribute_default_list = {
'issuer': 'symentec',
'ipVersion': 'ipv4',
'slot_deployment_klass': 'esslType',
'jobId': None}
for san_cert_name in CONF[zookeeper_storage.AKAMAI_GROUP].san_cert_cnames:
print("Upsert SAN info for :%s" % (san_cert_name))
for attr in san_attribute_default_list:
user_input = None
while ((user_input or "").strip() or user_input) in ["", None]:
user_input = raw_input('Please input value for attr: %s, '
'San cert: %s,'
'default value: %s'
' (if default is None, '
'that means a real value has to'
' be input): ' %
(attr,
san_cert_name,
san_attribute_default_list[attr]))
if san_attribute_default_list[attr] is None:
continue
else:
user_input = san_attribute_default_list[attr]
break
zk_storage._save_cert_property_value(san_cert_name, attr,
user_input)
if __name__ == "__main__":
'''example usage:
python upsert_san_cert_info.py '
'--drivers:provider:akamai-storage_backend_type zookeeper'
'--drivers:provider:akamai-storage_backend_host 192.168.59.103'
'--drivers:provider:akamai-san_cert_cnames'
secure1.san1.altcdn.com,secure2.san1.altcdn.com'''
main()

View File

@ -64,6 +64,7 @@ poppy.notification =
mailgun = poppy.notification.mailgun:Driver
[wheel]
universal = 1

View File

View File

@ -0,0 +1,6 @@
{
"mod_san_test_1": {
"cert_type": "san",
"domain_name": "www.abc.com"
}
}

View File

@ -0,0 +1,22 @@
{
"missing_cert_type": {
"domain_name": "www.abc.com"
},
"invalid_cert_type": {
"cert_type": "not_a_valid_cert_type",
"domain_name": "www.abc.com"
},
"missing_domain_name": {
"cert_type": "san"
},
"missing_flavor_id": {
"cert_type": "san",
"domain_name": "www.abc.com",
"missing_flavor_id": true
},
"invalid_flavor_id": {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "not_a_valid_flavor_id"
}
}

View File

@ -0,0 +1,62 @@
# Copyright (c) 2015 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 ddt
from tests.api import base
@ddt.ddt
class TestCreateSSLCertificate(base.TestBase):
"""Tests for Create SSL Certificate."""
def setUp(self):
super(TestCreateSSLCertificate, self).setUp()
self.flavor_id = self.test_flavor
@ddt.file_data('data_create_ssl_certificate_negative.json')
def test_create_ssl_certificate_negative(self, test_data):
cert_type = test_data.get('cert_type')
domain_name = test_data.get('domain_name')
flavor_id = test_data.get('flavor_id') or self.flavor_id
if test_data.get("missing_flavor_id", False):
flavor_id = None
resp = self.client.create_ssl_certificate(
cert_type=cert_type,
domain_name=domain_name,
flavor_id=flavor_id
)
self.assertEqual(resp.status_code, 400)
@ddt.file_data('data_create_ssl_certificate.json')
def test_create_ssl_certificate_positive(self, test_data):
if self.test_config.run_ssl_tests is False:
self.skipTest('Create ssl certificate needs to'
' be run when commanded')
cert_type = test_data.get('cert_type')
domain_name = test_data.get('domain_name')
flavor_id = test_data.get('flavor_id') or self.flavor_id
resp = self.client.create_ssl_certificate(
cert_type=cert_type,
domain_name=domain_name,
flavor_id=flavor_id
)
self.assertEqual(resp.status_code, 202)

View File

@ -404,3 +404,24 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
assert False, ('Timed out waiting for service '
'to be deleted, after '
'waiting {0} seconds'.format(retry_timeout))
def create_ssl_certificate(self, cert_type=None,
domain_name=None, flavor_id=None,
requestslib_kwargs=None,):
"""Creates SSL Certificate
:return: Response Object containing response code 200 and body with
details of service
POST
ssl_certificate
"""
url = '{0}/ssl_certificate'.format(self.url)
requests_object = requests.CreateSSLCertificate(
cert_type=cert_type,
domain_name=domain_name,
flavor_id=flavor_id
)
return self.request('POST', url, request_entity=requests_object,
requestslib_kwargs=requestslib_kwargs)

View File

@ -109,3 +109,21 @@ class CreateFlavor(base.AutoMarshallingModel):
"providers": self.provider_list,
"limits": self.limits}
return json.dumps(create_flavor_request)
class CreateSSLCertificate(base.AutoMarshallingModel):
"""Marshalling for Create Flavor requests."""
def __init__(self, cert_type=None, domain_name=None, flavor_id=None):
super(CreateSSLCertificate, self).__init__()
self.cert_type = cert_type
self.domain_name = domain_name
self.flavor_id = flavor_id
def _obj_to_json(self):
create_ssl_certificate_request = {
"cert_type": self.cert_type,
"domain_name": self.domain_name,
"flavor_id": self.flavor_id}
return json.dumps(create_ssl_certificate_request)

View File

@ -0,0 +1,10 @@
{
"all_fields": {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "mock"
}
}

View File

@ -0,0 +1,19 @@
{
"missing_domain_name": {
"cert_type": "san",
"flavor_id": "mock"
},
"missing_cert_type": {
"domain_name": "www.abc.com",
"flavor_id": "mock"
},
"non_existing_flavor_input": {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "non_exist"
},
"missing_flavor_id": {
"cert_type": "san",
"domain_name": "www.abc.com"
}
}

View File

@ -0,0 +1,93 @@
# 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
import uuid
import ddt
from tests.functional.transport.pecan import base
@ddt.ddt
class SSLCertificateControllerTest(base.FunctionalTest):
def setUp(self):
super(SSLCertificateControllerTest, self).setUp()
self.project_id = str(uuid.uuid1())
self.service_name = str(uuid.uuid1())
self.flavor_id = str(uuid.uuid1())
# create a mock flavor to be used by new service creations
flavor_json = {
"id": self.flavor_id,
"providers": [
{
"provider": "mock",
"links": [
{
"href": "http://mock.cdn",
"rel": "provider_url"
}
]
}
]
}
response = self.app.post('/v1.0/flavors',
params=json.dumps(flavor_json),
headers={
"Content-Type": "application/json",
"X-Project-ID": self.project_id})
self.assertEqual(201, response.status_code)
@ddt.file_data("data_create_ssl_certificate.json")
def test_create_ssl_certificate(self, ssl_certificate_json):
# override the hardcoded flavor_id in the ddt file with
# a custom one defined in setUp()
ssl_certificate_json['flavor_id'] = self.flavor_id
# create with good data
response = self.app.post('/v1.0/ssl_certificate',
params=json.dumps(ssl_certificate_json),
headers={
'Content-Type': 'application/json',
'X-Project-ID': self.project_id})
self.assertEqual(202, response.status_code)
def test_create_with_invalid_json(self):
# create with errorenous data: invalid json data
response = self.app.post('/v1.0/ssl_certificate',
params="{",
headers={
'Content-Type': 'application/json',
'X-Project-ID': self.project_id},
expect_errors=True)
self.assertEqual(400, response.status_code)
@ddt.file_data("data_create_ssl_certificate_bad_input_json.json")
def test_create_with_bad_input_json(self, ssl_certificate_json):
# create with errorenous data
response = self.app.post('/v1.0/ssl_certificate',
params=json.dumps(ssl_certificate_json),
headers={'Content-Type': 'application/json',
'X-Project-ID': self.project_id},
expect_errors=True)
self.assertEqual(400, response.status_code)
def tearDown(self):
super(SSLCertificateControllerTest, self).tearDown()

View File

@ -19,6 +19,7 @@ import mock
from taskflow import engines
from poppy.distributed_task.taskflow.flow import create_service
from poppy.distributed_task.taskflow.flow import create_ssl_certificate
from poppy.distributed_task.taskflow.flow import delete_service
from poppy.distributed_task.taskflow.flow import purge_service
from poppy.distributed_task.taskflow.flow import update_service
@ -28,6 +29,7 @@ from poppy.distributed_task.utils import memoized_controllers
from poppy.model.helpers import domain
from poppy.model.helpers import origin
from poppy.model import service
from poppy.model import ssl_certificate
from tests.unit import base
from tests.unit.manager.default.test_services import MonkeyPatchControllers
@ -142,8 +144,24 @@ class TestFlowRuns(base.TestCase):
dns_controller.disable = mock.Mock()
dns_controller.disable._mock_return_value = []
def patch_create_ssl_certificate_flow(self, service_controller,
storage_controller, dns_controller):
storage_controller.get = mock.Mock()
storage_controller.update = mock.Mock()
storage_controller._driver.close_connection = mock.Mock()
service_controller.provider_wrapper.create_certificate = mock.Mock()
service_controller.provider_wrapper.create_certificate.\
_mock_return_value = []
service_controller._driver = mock.Mock()
service_controller._driver.providers.__getitem__ = mock.Mock()
service_controller._driver.notification = [mock.Mock()]
dns_controller.create = mock.Mock()
dns_controller.create._mock_return_value = []
common.create_log_delivery_container = mock.Mock()
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_create_flow_normal(self, mock_creds):
def test_create_flow_normal(self, mock_creds, mock_dns_client):
providers = ['cdn_provider']
kwargs = {
'providers_list_json': json.dumps(providers),
@ -167,8 +185,9 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(create_service.create_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_update_flow_normal(self, mock_creds):
def test_update_flow_normal(self, mock_creds, mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
domains_new = domain.Domain(domain='mycdn.poppy.org')
@ -206,8 +225,9 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(update_service.update_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_delete_flow_normal(self, mock_creds):
def test_delete_flow_normal(self, mock_creds, mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@ -239,8 +259,9 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(delete_service.delete_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_purge_flow_normal(self, mock_creds):
def test_purge_flow_normal(self, mock_creds, mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@ -273,8 +294,10 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(purge_service.purge_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_service_state_flow_normal(self, mock_creds):
def test_service_state_flow_normal(self, mock_creds,
mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@ -311,8 +334,10 @@ class TestFlowRuns(base.TestCase):
engines.run(update_service_state.disable_service(),
store=disable_kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_create_flow_dns_exception(self, mock_creds):
def test_create_flow_dns_exception(self, mock_creds,
mock_dns_client):
providers = ['cdn_provider']
kwargs = {
'providers_list_json': json.dumps(providers),
@ -343,8 +368,10 @@ class TestFlowRuns(base.TestCase):
}
engines.run(create_service.create_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_update_flow_dns_exception(self, mock_creds):
def test_update_flow_dns_exception(self, mock_creds,
mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
domains_new = domain.Domain(domain='mycdn.poppy.org')
@ -391,8 +418,10 @@ class TestFlowRuns(base.TestCase):
engines.run(update_service.update_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_delete_flow_dns_exception(self, mock_creds):
def test_delete_flow_dns_exception(self, mock_creds,
mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@ -818,3 +847,31 @@ class TestFlowRuns(base.TestCase):
store=enable_kwargs)
engines.run(update_service_state.disable_service(),
store=disable_kwargs)
# Keep create credentials for now
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_create_ssl_certificate_normal(self, mock_creds, mock_dns_client):
providers = ['cdn_provider']
cert_obj_json = ssl_certificate.SSLCertificate('cdn',
'mytestsite.com',
'san')
kwargs = {
'providers_list_json': json.dumps(providers),
'project_id': json.dumps(str(uuid.uuid4())),
'cert_obj_json': json.dumps(cert_obj_json.to_dict()),
}
service_controller, storage_controller, dns_controller = \
self.all_controllers()
with MonkeyPatchControllers(service_controller,
dns_controller,
storage_controller,
memoized_controllers.task_controllers):
self.patch_create_ssl_certificate_flow(service_controller,
storage_controller,
dns_controller)
engines.run(create_ssl_certificate.create_ssl_certificate(),
store=kwargs)

View File

@ -31,5 +31,6 @@ class TestProviderWrapper(base.TestCase):
self.notifications_wrapper_obj.send(mock_ext,
"test_subject",
"test_mail_content")
mock_ext.obj.services_controller.send.assert_called_once_with(
"test_subject", "test_mail_content")

View File

@ -34,7 +34,10 @@ MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'),
cfg.ListOpt('recipients', default=['recipient@gmail.com'],
help='A list of emails addresses to receive notification ')
help='A list of emails addresses to receive notification '),
cfg.StrOpt('notification_subject',
default='Poppy SSL Certificate Provisioned',
help='The subject of the email notification ')
]
MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'

View File

@ -35,7 +35,10 @@ MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'),
cfg.ListOpt('recipients', default=['recipient@gmail.com'],
help='A list of emails addresses to receive notification ')
help='A list of emails addresses to receive notification '),
cfg.StrOpt('notification_subject',
default='Poppy SSL Certificate Provisioned',
help='The subject of the email notification ')
]
MAIL_NOTIFICATION_GROUP = 'drivers:notification:mail'

View File

@ -84,6 +84,17 @@ AKAMAI_OPTIONS = [
cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can'
' be held by a SAN cert'),
# related info for SPS && PAPI APIs
cfg.StrOpt(
'contract_id',
help='Operator contractID'),
cfg.StrOpt(
'group_id',
help='Operator groupID'),
cfg.StrOpt(
'property_id',
help='Operator propertyID')
]
@ -115,6 +126,12 @@ class TestDriver(base.TestCase):
self.conf = cfg.ConfigOpts()
zookeeper_client_patcher = mock.patch(
'kazoo.client.KazooClient'
)
zookeeper_client_patcher.start()
self.addCleanup(zookeeper_client_patcher.stop)
@mock.patch('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_init(self, mock_connect):
@ -158,3 +175,10 @@ class TestDriver(base.TestCase):
provider = driver.CDNProvider(self.conf)
self.assertNotEqual(None, provider.policy_api_client)
self.assertNotEqual(None, provider.ccu_api_client)
@mock.patch('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_san_info_storage(self, mock_connect):
mock_connect.return_value = mock.Mock()
provider = driver.CDNProvider(self.conf)
self.assertNotEqual(None, provider.san_info_storage)

View File

@ -0,0 +1,71 @@
# Copyright (c) 2015 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 mock
from oslo_config import cfg
from poppy.provider.akamai.mod_san_queue import zookeeper_queue
from tests.unit import base
AKAMAI_OPTIONS = [
# queue backend configs
cfg.StrOpt(
'queue_backend_type',
help='SAN Cert Queueing backend'),
cfg.ListOpt('queue_backend_host', default=['localhost'],
help='default queue backend server hosts'),
cfg.IntOpt('queue_backend_port', default=2181, help='default'
' default queue backend server port (e.g: 2181)'),
cfg.StrOpt(
'mod_san_queue_path', default='/mod_san_queue', help='Zookeeper path '
'for mod_san_queue'),
]
AKAMAI_GROUP = 'drivers:provider:akamai'
class TestModSanQueue(base.TestCase):
def setUp(self):
super(TestModSanQueue, self).setUp()
self.cert_obj_json = {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
zookeeper_client_patcher = mock.patch(
'kazoo.client.KazooClient'
)
zookeeper_client_patcher.start()
self.addCleanup(zookeeper_client_patcher.stop)
self.conf = cfg.ConfigOpts()
self.zk_queue = zookeeper_queue.ZookeeperModSanQueue(self.conf)
self.zk_queue.mod_san_queue_backend = mock.Mock()
def test_enqueue_mod_san_request(self):
self.zk_queue.enqueue_mod_san_request(self.cert_obj_json)
self.zk_queue.mod_san_queue_backend.put.assert_called_once_with(
self.cert_obj_json)
def test_dequeue_mod_san_request(self):
self.zk_queue.dequeue_mod_san_request()
self.zk_queue.dequeue_mod_san_request(False)
calls = [mock.call(), mock.call()]
self.zk_queue.mod_san_queue_backend.get.assert_has_calls(calls)
self.zk_queue.mod_san_queue_backend.consume.assert_called_once_with()

View File

@ -0,0 +1,113 @@
# Copyright (c) 2015 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 mock
from oslo_config import cfg
from poppy.provider.akamai.san_info_storage import zookeeper_storage
from tests.unit import base
AKAMAI_OPTIONS = [
# storage backend configs for long running tasks
cfg.StrOpt(
'storage_backend_type',
help='SAN Cert info storage backend'),
cfg.ListOpt('storage_backend_host', default=['localhost'],
help='default san info storage backend server hosts'),
cfg.IntOpt('storage_backend_port', default=2181, help='default'
' default san info storage backend server port (e.g: 2181)'),
cfg.StrOpt(
'san_info_storage_path', default='/san_info', help='zookeeper backend'
' path for san cert info'),
]
AKAMAI_GROUP = 'drivers:provider:akamai'
class TestSANInfoStorage(base.TestCase):
def setUp(self):
super(TestSANInfoStorage, self).setUp()
zookeeper_client_patcher = mock.patch(
'kazoo.client.KazooClient'
)
zookeeper_client_patcher.start()
self.addCleanup(zookeeper_client_patcher.stop)
self.conf = cfg.ConfigOpts()
self.zk_storage = zookeeper_storage.ZookeeperSanInfoStorage(self.conf)
def zk_get_value_func(zk_path):
stat = "good"
if 'jobId' in zk_path:
return stat, 1789
if 'spsId' in zk_path:
return stat, 4809
if 'issuer' in zk_path:
return stat, 'symantec'
if 'ipVersion' in zk_path:
return stat, 'ipv4'
if 'slot_deployment_klass' in zk_path:
return stat, 'esslType'
return None, None
self.zk_storage.zookeeper_client.get.side_effect = zk_get_value_func
def test__zk_path(self):
path1 = self.zk_storage._zk_path('secure.san1.poppycdn.com', 'jobId')
self.assertTrue(path1 == '/san_info/secure.san1.poppycdn.com/jobId')
path2 = self.zk_storage._zk_path('secure.san1.poppycdn.com', None)
self.assertTrue(path2 == '/san_info/secure.san1.poppycdn.com')
def test__save_cert_property_value(self):
self.zk_storage._save_cert_property_value('secure.san1.poppycdn.com',
'spsId', str(1789))
self.zk_storage.zookeeper_client.ensure_path.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com/spsId')
self.zk_storage.zookeeper_client.set.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com/spsId', str(1789))
def test_save_cert_last_spsid(self):
self.zk_storage.save_cert_last_spsid('secure.san1.poppycdn.com', 1789)
self.zk_storage.zookeeper_client.ensure_path.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com/spsId')
self.zk_storage.zookeeper_client.set.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com/spsId', str(1789))
def test_get_cert_last_spsid(self):
self.zk_storage.get_cert_last_spsid('secure.san1.poppycdn.com')
self.zk_storage.zookeeper_client.ensure_path.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com/spsId')
self.zk_storage.zookeeper_client.get.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com/spsId')
def list_all_san_cert_names(self):
self.zk_storage.list_all_san_cert_names()
self.zk_storage.zookeeper_client.get_children.assert_create_once_with(
'/san_info/secure.san1.poppycdn.com'
)
def test_get_cert_info(self):
res = self.zk_storage.get_cert_info('secure.san1.poppycdn.com')
self.zk_storage.zookeeper_client.ensure_path.assert_called_once_with(
'/san_info/secure.san1.poppycdn.com'
)
calls = [mock.call('/san_info/secure.san1.poppycdn.com/jobId'),
mock.call('/san_info/secure.san1.poppycdn.com/issuer'),
mock.call('/san_info/secure.san1.poppycdn.com/ipVersion'),
mock.call(
'/san_info/secure.san1.poppycdn.com/slot_deployment_klass')]
self.zk_storage.zookeeper_client.get.assert_has_calls(calls)
self.assertTrue(isinstance(res, dict))

View File

@ -28,6 +28,7 @@ from poppy.model.helpers import rule
from poppy.model.service import Service
from poppy.provider.akamai import services
from poppy.transport.pecan.models.request import service
from poppy.transport.pecan.models.request import ssl_certificate
from tests.unit import base
@ -447,3 +448,75 @@ class TestServices(base.TestCase):
break
self.assertTrue(restriction_rule_valid)
def test_create_ssl_certificate_happy_path(self):
controller = services.ServiceController(self.driver)
data = {
"cert_type": "san",
"domain_name": "www.abc.com",
"flavor_id": "premium"
}
controller.san_cert_cnames = ["secure.san1.poppycdn.com",
"secure.san2.poppycdn.com"]
lastSpsId = (
controller.san_info_storage.get_cert_last_spsid(
"secure.san1.poppycdn.com"))
controller.san_info_storage.get_cert_info.return_value = {
'cnameHostname': "secure.san1.poppycdn.com",
'jobId': "secure.san1.poppycdn.com",
'issuer': 1789,
'createType': 'modSan',
'ipVersion': 'ipv4',
'slot-deployment.class': 'esslType'
}
cert_info = controller.san_info_storage.get_cert_info(
"secure.san1.poppycdn.com")
cert_info['add.sans'] = "www.abc.com"
string_post_cert_info = '&'.join(
['%s=%s' % (k, v) for (k, v) in cert_info.items()])
controller.sps_api_client.get.return_value = mock.Mock(
status_code=200,
# Mock an SPS request
text=json.dumps({
"requestList":
[{"resourceUrl": "/config-secure-provisioning-service/"
"v1/sps-requests/1849",
"parameters": [{
"name": "cnameHostname",
"value": "secure.san1.poppycdn.com"
}, {"name": "createType", "value": "modSan"},
{"name": "csr.cn",
"value": "secure.san3.poppycdn.com"},
{"name": "add.sans",
"value": "www.abc.com"}],
"lastStatusChange": "2015-03-19T21:47:10Z",
"spsId": 1789,
"status": "SPS Request Complete",
"jobId": 44306}]})
)
controller.sps_api_client.post.return_value = mock.Mock(
status_code=202,
text=json.dumps({
"spsId": 1789,
"resourceLocation":
"/config-secure-provisioning-service/v1/sps-requests/1856",
"Results": {
"size": 1,
"data": [{
"text": None,
"results": {
"type": "SUCCESS",
"jobID": 44434}
}]}})
)
controller.create_certificate(ssl_certificate.load_from_json(data))
controller.sps_api_client.get.assert_called_once_with(
controller.sps_api_base_url.format(spsId=lastSpsId))
controller.sps_api_client.post.assert_called_once_with(
controller.sps_api_base_url.format(spsId=lastSpsId),
data=string_post_cert_info)
return