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] [drivers:provider]
default_cache_ttl = 86400 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] [drivers:provider:fastly]
apikey = "MYAPIKEY" apikey = "MYAPIKEY"
# scheme = "https" # 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' akamai_https_custom_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_CUSTOM_NUMBER'
san_cert_cnames = "MY_SAN_CERT_LIST" san_cert_cnames = "MY_SAN_CERT_LIST"
san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LMIT" 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] [drivers:notification:mailgun]
mailgun_api_key = "<operator_api_key>" 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 return service_controller, service_controller.storage_controller
if controller == 'dns': if controller == 'dns':
return service_controller, service_controller.dns_controller return service_controller, service_controller.dns_controller
if controller == 'ssl_certificate':
return service_controller, (
bootstrap_obj.manager.ssl_certificate_controller)
else: else:
return service_controller 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 flavors
from poppy.manager.base import home from poppy.manager.base import home
from poppy.manager.base import services from poppy.manager.base import services
from poppy.manager.base import ssl_certificate
Driver = driver.ManagerDriverBase Driver = driver.ManagerDriverBase
@ -24,3 +25,4 @@ Driver = driver.ManagerDriverBase
FlavorsController = flavors.FlavorsControllerBase FlavorsController = flavors.FlavorsControllerBase
ServicesController = services.ServicesControllerBase ServicesController = services.ServicesControllerBase
HomeController = home.HomeControllerBase HomeController = home.HomeControllerBase
SSLCertificateController = ssl_certificate.SSLCertificateController

View File

@ -73,3 +73,13 @@ class ProviderWrapper(object):
service_obj, service_obj,
hard, hard,
purge_url) 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 health
from poppy.manager.default import home from poppy.manager.default import home
from poppy.manager.default import services from poppy.manager.default import services
from poppy.manager.default import ssl_certificate
Home = home.DefaultHomeController Home = home.DefaultHomeController
Flavors = flavors.DefaultFlavorsController Flavors = flavors.DefaultFlavorsController
Health = health.DefaultHealthController Health = health.DefaultHealthController
Services = services.DefaultServicesController Services = services.DefaultServicesController
SSLCertificate = ssl_certificate.DefaultSSLCertificateController

View File

@ -43,3 +43,7 @@ class DefaultManagerDriver(base.Driver):
@decorators.lazy_property(write=False) @decorators.lazy_property(write=False)
def health_controller(self): def health_controller(self):
return controllers.Health(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', cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'), help='Sent from email address'),
cfg.ListOpt('recipients', 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' MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'
@ -60,6 +63,8 @@ class MailNotificationDriver(base.Driver):
self.sand_box = self.mail_notification_conf.sand_box self.sand_box = self.mail_notification_conf.sand_box
self.from_address = self.mail_notification_conf.from_address self.from_address = self.mail_notification_conf.from_address
self.recipients = self.mail_notification_conf.recipients self.recipients = self.mail_notification_conf.recipients
self.notification_subject = (
self.mail_notification_conf.notification_subject)
# validate email addresses # validate email addresses
if not validate_email_address(self.from_address): if not validate_email_address(self.from_address):

View File

@ -23,6 +23,8 @@ import requests
from poppy.openstack.common import log from poppy.openstack.common import log
from poppy.provider.akamai import controllers 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 from poppy.provider import base
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
@ -89,6 +91,17 @@ AKAMAI_OPTIONS = [
cfg.IntOpt('san_cert_hostname_limit', default=80, cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can' help='default limit on how many hostnames can'
' be held by a SAN cert'), ' 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' AKAMAI_GROUP = 'drivers:provider:akamai'
@ -139,9 +152,25 @@ class CDNProvider(base.Driver):
access_token=self.akamai_conf.ccu_api_access_token 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_cnames = self.akamai_conf.san_cert_cnames
self.san_cert_hostname_limit = self.akamai_conf.san_cert_hostname_limit 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): def is_alive(self):
request_headers = { 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): def ccu_api_client(self):
return self.driver.ccu_api_client 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): def __init__(self, driver):
super(ServiceController, self).__init__(driver) super(ServiceController, self).__init__(driver)
self.driver = driver self.driver = driver
self.policy_api_base_url = self.driver.akamai_policy_api_base_url 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.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', self.request_header = {'Content-type': 'application/json',
'Accept': 'text/plain'} 'Accept': 'text/plain'}
@ -481,6 +494,73 @@ class ServiceController(base.ServiceBase):
format(provider_service_id)) format(provider_service_id))
return self.responder.failed(str(e)) 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) @decorators.lazy_property(write=False)
def current_customer(self): def current_customer(self):
return None return None

View File

@ -16,6 +16,7 @@
import ssl import ssl
import sys import sys
from kazoo import client
from OpenSSL import crypto from OpenSSL import crypto
import six import six
@ -65,7 +66,7 @@ def get_ssl_number_of_hosts(remote_host):
# We can actually print all the Subject Alternative Names # We can actually print all the Subject Alternative Names
# for san in sans: # for san in sans:
# print san # print(san)
result = len(sans) result = len(sans)
break break
else: else:
@ -73,6 +74,28 @@ def get_ssl_number_of_hosts(remote_host):
return result 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 __name__ == "__main__":
if len(sys.argv) != 2: if len(sys.argv) != 2:
print('Usage: %s <remote_host_you_want_get_cert_on>' % sys.argv[0]) 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 '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 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 @staticmethod
def format_result(result): def format_result(result):
"""format_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) %(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_UPDATE_SERVICE = CQL_CREATE_SERVICE
CQL_GET_PROVIDER_DETAILS = ''' CQL_GET_PROVIDER_DETAILS = '''
@ -180,6 +204,13 @@ CQL_UPDATE_PROVIDER_DETAILS = '''
WHERE project_id = %(project_id)s AND service_id = %(service_id)s 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): class ServicesController(base.ServicesController):
@ -291,6 +322,51 @@ class ServicesController(base.ServicesController):
LOG.exception(ex) LOG.exception(ex)
return False 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): def create(self, project_id, service_obj):
"""create. """create.
@ -506,6 +582,29 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level) consistency_level=self._driver.consistency_level)
self.session.execute(stmt, delete_args) 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): def get_provider_details(self, project_id, service_id):
"""get_provider_details. """get_provider_details.
@ -613,6 +712,27 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level) consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args) 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 @staticmethod
def format_result(result): def format_result(result):
"""format_result. """format_result.

View File

@ -123,6 +123,13 @@ class ServicesController(base.ServicesController):
def domain_exists_elsewhere(self, domain_name, service_id): def domain_exists_elsewhere(self, domain_name, service_id):
return domain_name in self.claimed_domains 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 @staticmethod
def format_result(result): def format_result(result):
service_id = result.get('service_id') 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 home
from poppy.transport.pecan.controllers.v1 import ping from poppy.transport.pecan.controllers.v1 import ping
from poppy.transport.pecan.controllers.v1 import services from poppy.transport.pecan.controllers.v1 import services
from poppy.transport.pecan.controllers.v1 import ssl_certificates
# Hoist into package namespace # Hoist into package namespace
@ -33,3 +34,4 @@ DNSHealth = health.DNSHealthController
StorageHealth = health.StorageHealthController StorageHealth = health.StorageHealthController
ProviderHealth = health.ProviderHealthController ProviderHealth = health.ProviderHealthController
Admin = admin.AdminController 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('services', v1.Services(self))
home_controller.add_controller('flavors', v1.Flavors(self)) home_controller.add_controller('flavors', v1.Flavors(self))
home_controller.add_controller('admin', v1.Admin(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, self._app = pecan.make_app(root_controller,
guess_content_type_from_ext=False) 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 mailgun = poppy.notification.mailgun:Driver
[wheel] [wheel]
universal = 1 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 ' assert False, ('Timed out waiting for service '
'to be deleted, after ' 'to be deleted, after '
'waiting {0} seconds'.format(retry_timeout)) '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, "providers": self.provider_list,
"limits": self.limits} "limits": self.limits}
return json.dumps(create_flavor_request) 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 taskflow import engines
from poppy.distributed_task.taskflow.flow import create_service 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 delete_service
from poppy.distributed_task.taskflow.flow import purge_service from poppy.distributed_task.taskflow.flow import purge_service
from poppy.distributed_task.taskflow.flow import update_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 domain
from poppy.model.helpers import origin from poppy.model.helpers import origin
from poppy.model import service from poppy.model import service
from poppy.model import ssl_certificate
from tests.unit import base from tests.unit import base
from tests.unit.manager.default.test_services import MonkeyPatchControllers 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.Mock()
dns_controller.disable._mock_return_value = [] 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') @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'] providers = ['cdn_provider']
kwargs = { kwargs = {
'providers_list_json': json.dumps(providers), 'providers_list_json': json.dumps(providers),
@ -167,8 +185,9 @@ class TestFlowRuns(base.TestCase):
dns_controller) dns_controller)
engines.run(create_service.create_service(), store=kwargs) engines.run(create_service.create_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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()) service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org') domains_old = domain.Domain(domain='cdn.poppy.org')
domains_new = domain.Domain(domain='mycdn.poppy.org') domains_new = domain.Domain(domain='mycdn.poppy.org')
@ -206,8 +225,9 @@ class TestFlowRuns(base.TestCase):
dns_controller) dns_controller)
engines.run(update_service.update_service(), store=kwargs) engines.run(update_service.update_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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()) service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org') domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org') current_origin = origin.Origin(origin='poppy.org')
@ -239,8 +259,9 @@ class TestFlowRuns(base.TestCase):
dns_controller) dns_controller)
engines.run(delete_service.delete_service(), store=kwargs) engines.run(delete_service.delete_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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()) service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org') domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org') current_origin = origin.Origin(origin='poppy.org')
@ -273,8 +294,10 @@ class TestFlowRuns(base.TestCase):
dns_controller) dns_controller)
engines.run(purge_service.purge_service(), store=kwargs) engines.run(purge_service.purge_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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()) service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org') domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org') current_origin = origin.Origin(origin='poppy.org')
@ -311,8 +334,10 @@ class TestFlowRuns(base.TestCase):
engines.run(update_service_state.disable_service(), engines.run(update_service_state.disable_service(),
store=disable_kwargs) store=disable_kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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'] providers = ['cdn_provider']
kwargs = { kwargs = {
'providers_list_json': json.dumps(providers), 'providers_list_json': json.dumps(providers),
@ -343,8 +368,10 @@ class TestFlowRuns(base.TestCase):
} }
engines.run(create_service.create_service(), store=kwargs) engines.run(create_service.create_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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()) service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org') domains_old = domain.Domain(domain='cdn.poppy.org')
domains_new = domain.Domain(domain='mycdn.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) engines.run(update_service.update_service(), store=kwargs)
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials') @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()) service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org') domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org') current_origin = origin.Origin(origin='poppy.org')
@ -818,3 +847,31 @@ class TestFlowRuns(base.TestCase):
store=enable_kwargs) store=enable_kwargs)
engines.run(update_service_state.disable_service(), engines.run(update_service_state.disable_service(),
store=disable_kwargs) 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, self.notifications_wrapper_obj.send(mock_ext,
"test_subject", "test_subject",
"test_mail_content") "test_mail_content")
mock_ext.obj.services_controller.send.assert_called_once_with( mock_ext.obj.services_controller.send.assert_called_once_with(
"test_subject", "test_mail_content") "test_subject", "test_mail_content")

View File

@ -34,7 +34,10 @@ MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('from_address', default='noreply@poppycdn.org', cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'), help='Sent from email address'),
cfg.ListOpt('recipients', default=['recipient@gmail.com'], 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' MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'

View File

@ -35,7 +35,10 @@ MAIL_NOTIFICATION_OPTIONS = [
cfg.StrOpt('from_address', default='noreply@poppycdn.org', cfg.StrOpt('from_address', default='noreply@poppycdn.org',
help='Sent from email address'), help='Sent from email address'),
cfg.ListOpt('recipients', default=['recipient@gmail.com'], 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' MAIL_NOTIFICATION_GROUP = 'drivers:notification:mail'

View File

@ -84,6 +84,17 @@ AKAMAI_OPTIONS = [
cfg.IntOpt('san_cert_hostname_limit', default=80, cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can' help='default limit on how many hostnames can'
' be held by a SAN cert'), ' 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() 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('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS) @mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_init(self, mock_connect): def test_init(self, mock_connect):
@ -158,3 +175,10 @@ class TestDriver(base.TestCase):
provider = driver.CDNProvider(self.conf) provider = driver.CDNProvider(self.conf)
self.assertNotEqual(None, provider.policy_api_client) self.assertNotEqual(None, provider.policy_api_client)
self.assertNotEqual(None, provider.ccu_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.model.service import Service
from poppy.provider.akamai import services from poppy.provider.akamai import services
from poppy.transport.pecan.models.request import service from poppy.transport.pecan.models.request import service
from poppy.transport.pecan.models.request import ssl_certificate
from tests.unit import base from tests.unit import base
@ -447,3 +448,75 @@ class TestServices(base.TestCase):
break break
self.assertTrue(restriction_rule_valid) 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