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:
parent
72a0d26d01
commit
df58d6014a
@ -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>"
|
||||
|
@ -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
|
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
38
poppy/manager/base/ssl_certificate.py
Normal file
38
poppy/manager/base/ssl_certificate.py
Normal 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
|
@ -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
|
||||
|
@ -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)
|
||||
|
57
poppy/manager/default/ssl_certificate.py
Normal file
57
poppy/manager/default/ssl_certificate.py
Normal 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
|
66
poppy/model/ssl_certificate.py
Normal file
66
poppy/model/ssl_certificate.py
Normal 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)
|
||||
)
|
@ -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):
|
||||
|
@ -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 = {
|
||||
|
0
poppy/provider/akamai/mod_san_queue/__init__.py
Normal file
0
poppy/provider/akamai/mod_san_queue/__init__.py
Normal file
43
poppy/provider/akamai/mod_san_queue/base.py
Normal file
43
poppy/provider/akamai/mod_san_queue/base.py
Normal 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
|
65
poppy/provider/akamai/mod_san_queue/zookeeper_queue.py
Normal file
65
poppy/provider/akamai/mod_san_queue/zookeeper_queue.py
Normal 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
|
0
poppy/provider/akamai/san_info_storage/__init__.py
Normal file
0
poppy/provider/akamai/san_info_storage/__init__.py
Normal file
37
poppy/provider/akamai/san_info_storage/base.py
Normal file
37
poppy/provider/akamai/san_info_storage/base.py
Normal 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
|
97
poppy/provider/akamai/san_info_storage/zookeeper_storage.py
Normal file
97
poppy/provider/akamai/san_info_storage/zookeeper_storage.py
Normal 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))
|
@ -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
|
||||
|
@ -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])
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
9
poppy/storage/cassandra/migrations/config/cassandra.yml
Normal file
9
poppy/storage/cassandra/migrations/config/cassandra.yml
Normal file
@ -0,0 +1,9 @@
|
||||
# This for running cdeploy command
|
||||
|
||||
development:
|
||||
hosts: [localhost]
|
||||
keyspace: poppy
|
||||
|
||||
production:
|
||||
hosts: [your_production_env_host(s)]
|
||||
keyspace: poppy
|
@ -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.
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
62
poppy/transport/pecan/controllers/v1/ssl_certificates.py
Normal file
62
poppy/transport/pecan/controllers/v1/ssl_certificates.py
Normal 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)
|
@ -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)
|
||||
|
24
poppy/transport/pecan/models/request/ssl_certificate.py
Normal file
24
poppy/transport/pecan/models/request/ssl_certificate.py
Normal 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)
|
31
poppy/transport/pecan/models/response/ssl_certificate.py
Normal file
31
poppy/transport/pecan/models/response/ssl_certificate.py
Normal 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
|
49
poppy/transport/validators/schemas/ssl_certificate.py
Normal file
49
poppy/transport/validators/schemas/ssl_certificate.py
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
40
scripts/providers/akamai/san_cert_info/list_san_cert_info.py
Normal file
40
scripts/providers/akamai/san_cert_info/list_san_cert_info.py
Normal 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()
|
@ -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()
|
@ -64,6 +64,7 @@ poppy.notification =
|
||||
mailgun = poppy.notification.mailgun:Driver
|
||||
|
||||
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
|
0
tests/api/ssl_certificate/__init__.py
Normal file
0
tests/api/ssl_certificate/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"mod_san_test_1": {
|
||||
"cert_type": "san",
|
||||
"domain_name": "www.abc.com"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
62
tests/api/ssl_certificate/test_create_ssl_certificate.py
Normal file
62
tests/api/ssl_certificate/test_create_ssl_certificate.py
Normal 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)
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"all_fields": {
|
||||
"cert_type": "san",
|
||||
"domain_name": "www.abc.com",
|
||||
"flavor_id": "mock"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
@ -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()
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
71
tests/unit/provider/akamai/test_mod_san_queue.py
Normal file
71
tests/unit/provider/akamai/test_mod_san_queue.py
Normal 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()
|
113
tests/unit/provider/akamai/test_san_info_storage.py
Normal file
113
tests/unit/provider/akamai/test_san_info_storage.py
Normal 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))
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user