Browse Source

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
tags/2015.11.0b3
tonytan4ever tonytan4ever 4 years ago
parent
commit
df58d6014a
53 changed files with 1784 additions and 12 deletions
  1. +13
    -0
      etc/poppy.conf
  2. +41
    -0
      poppy/distributed_task/taskflow/flow/create_ssl_certificate.py
  3. +94
    -0
      poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py
  4. +3
    -0
      poppy/distributed_task/utils/memoized_controllers.py
  5. +2
    -0
      poppy/manager/base/__init__.py
  6. +10
    -0
      poppy/manager/base/providers.py
  7. +38
    -0
      poppy/manager/base/ssl_certificate.py
  8. +2
    -0
      poppy/manager/default/controllers.py
  9. +4
    -0
      poppy/manager/default/driver.py
  10. +57
    -0
      poppy/manager/default/ssl_certificate.py
  11. +66
    -0
      poppy/model/ssl_certificate.py
  12. +6
    -1
      poppy/notification/mailgun/driver.py
  13. +29
    -0
      poppy/provider/akamai/driver.py
  14. +0
    -0
      poppy/provider/akamai/mod_san_queue/__init__.py
  15. +43
    -0
      poppy/provider/akamai/mod_san_queue/base.py
  16. +65
    -0
      poppy/provider/akamai/mod_san_queue/zookeeper_queue.py
  17. +0
    -0
      poppy/provider/akamai/san_info_storage/__init__.py
  18. +37
    -0
      poppy/provider/akamai/san_info_storage/base.py
  19. +97
    -0
      poppy/provider/akamai/san_info_storage/zookeeper_storage.py
  20. +80
    -0
      poppy/provider/akamai/services.py
  21. +24
    -1
      poppy/provider/akamai/utils.py
  22. +14
    -0
      poppy/provider/base/responder.py
  23. +22
    -0
      poppy/storage/base/services.py
  24. +15
    -0
      poppy/storage/cassandra/migrations/005_domain_certificate_info.cql
  25. +9
    -0
      poppy/storage/cassandra/migrations/config/cassandra.yml
  26. +120
    -0
      poppy/storage/cassandra/services.py
  27. +7
    -0
      poppy/storage/mockdb/services.py
  28. +2
    -0
      poppy/transport/pecan/controllers/v1/__init__.py
  29. +62
    -0
      poppy/transport/pecan/controllers/v1/ssl_certificates.py
  30. +2
    -0
      poppy/transport/pecan/driver.py
  31. +24
    -0
      poppy/transport/pecan/models/request/ssl_certificate.py
  32. +31
    -0
      poppy/transport/pecan/models/response/ssl_certificate.py
  33. +49
    -0
      poppy/transport/validators/schemas/ssl_certificate.py
  34. +40
    -0
      scripts/providers/akamai/san_cert_info/list_san_cert_info.py
  35. +69
    -0
      scripts/providers/akamai/san_cert_info/upsert_san_cert_info.py
  36. +1
    -0
      setup.cfg
  37. +0
    -0
      tests/api/ssl_certificate/__init__.py
  38. +6
    -0
      tests/api/ssl_certificate/data_create_ssl_certificate.json
  39. +22
    -0
      tests/api/ssl_certificate/data_create_ssl_certificate_negative.json
  40. +62
    -0
      tests/api/ssl_certificate/test_create_ssl_certificate.py
  41. +21
    -0
      tests/api/utils/client.py
  42. +18
    -0
      tests/api/utils/models/requests.py
  43. +10
    -0
      tests/functional/transport/pecan/controllers/data_create_ssl_certificate.json
  44. +19
    -0
      tests/functional/transport/pecan/controllers/data_create_ssl_certificate_bad_input_json.json
  45. +93
    -0
      tests/functional/transport/pecan/controllers/test_ssl_certificate.py
  46. +65
    -8
      tests/unit/distributed_task/taskflow/test_flows.py
  47. +1
    -0
      tests/unit/manager/default/test_notification_wrapper.py
  48. +4
    -1
      tests/unit/notification/mailgun/test_driver.py
  49. +4
    -1
      tests/unit/notification/mailgun/test_services.py
  50. +24
    -0
      tests/unit/provider/akamai/test_driver.py
  51. +71
    -0
      tests/unit/provider/akamai/test_mod_san_queue.py
  52. +113
    -0
      tests/unit/provider/akamai/test_san_info_storage.py
  53. +73
    -0
      tests/unit/provider/akamai/test_services.py

+ 13
- 0
etc/poppy.conf View File

@@ -118,6 +118,13 @@ delay = 1
[drivers:provider]
default_cache_ttl = 86400

[drivers:notification:mailgun]
mailgun_api_key = "<operator_api_key>"
mailgun_request_url = "https://api.mailgun.net/v2/{0}/events"
sand_box = "<your_sand_box_domain>"
from_address = "<your_send_from_email_address>"
recipients="<a_list_of_email_recipient>"

[drivers:provider:fastly]
apikey = "MYAPIKEY"
# scheme = "https"
@@ -151,6 +158,12 @@ akamai_https_san_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_SAN_NUMBER'
akamai_https_custom_config_numbers = 'MY_AKAMAI_HTTPS_CONFIG_CUSTOM_NUMBER'
san_cert_cnames = "MY_SAN_CERT_LIST"
san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LMIT"
contract_id = "MY_CONTRACT_ID"
group_id = "MY_GROUP_ID"
property_id = "MY_PROPERTY_ID"
storage_backend_type = zookeeper
storage_backend_host = <your_transport_server(s)>
storage_backend_port = <your_transport_port>

[drivers:notification:mailgun]
mailgun_api_key = "<operator_api_key>"


+ 41
- 0
poppy/distributed_task/taskflow/flow/create_ssl_certificate.py 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

+ 94
- 0
poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py 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

+ 3
- 0
poppy/distributed_task/utils/memoized_controllers.py View File

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

+ 2
- 0
poppy/manager/base/__init__.py View File

@@ -17,6 +17,7 @@ from poppy.manager.base import driver
from poppy.manager.base import flavors
from poppy.manager.base import home
from poppy.manager.base import services
from poppy.manager.base import ssl_certificate


Driver = driver.ManagerDriverBase
@@ -24,3 +25,4 @@ Driver = driver.ManagerDriverBase
FlavorsController = flavors.FlavorsControllerBase
ServicesController = services.ServicesControllerBase
HomeController = home.HomeControllerBase
SSLCertificateController = ssl_certificate.SSLCertificateController

+ 10
- 0
poppy/manager/base/providers.py View File

@@ -73,3 +73,13 @@ class ProviderWrapper(object):
service_obj,
hard,
purge_url)

def create_certificate(self, ext, cert_obj):
"""Create a provider

:param ext
:param service_obj
:returns: ext.obj.service_controller.create(service_obj)
"""

return ext.obj.service_controller.create_certificate(cert_obj)

+ 38
- 0
poppy/manager/base/ssl_certificate.py 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

+ 2
- 0
poppy/manager/default/controllers.py View File

@@ -17,9 +17,11 @@ from poppy.manager.default import flavors
from poppy.manager.default import health
from poppy.manager.default import home
from poppy.manager.default import services
from poppy.manager.default import ssl_certificate


Home = home.DefaultHomeController
Flavors = flavors.DefaultFlavorsController
Health = health.DefaultHealthController
Services = services.DefaultServicesController
SSLCertificate = ssl_certificate.DefaultSSLCertificateController

+ 4
- 0
poppy/manager/default/driver.py View File

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

@decorators.lazy_property(write=False)
def ssl_certificate_controller(self):
return controllers.SSLCertificate(self)

+ 57
- 0
poppy/manager/default/ssl_certificate.py 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

+ 66
- 0
poppy/model/ssl_certificate.py 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)
)

+ 6
- 1
poppy/notification/mailgun/driver.py View File

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

MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'
@@ -60,6 +63,8 @@ class MailNotificationDriver(base.Driver):
self.sand_box = self.mail_notification_conf.sand_box
self.from_address = self.mail_notification_conf.from_address
self.recipients = self.mail_notification_conf.recipients
self.notification_subject = (
self.mail_notification_conf.notification_subject)

# validate email addresses
if not validate_email_address(self.from_address):


+ 29
- 0
poppy/provider/akamai/driver.py View File

@@ -23,6 +23,8 @@ import requests

from poppy.openstack.common import log
from poppy.provider.akamai import controllers
from poppy.provider.akamai.mod_san_queue import zookeeper_queue
from poppy.provider.akamai.san_info_storage import zookeeper_storage
from poppy.provider import base

LOG = log.getLogger(__name__)
@@ -89,6 +91,17 @@ AKAMAI_OPTIONS = [
cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can'
' be held by a SAN cert'),

# related info for SPS && PAPI APIs
cfg.StrOpt(
'contract_id',
help='Operator contractID'),
cfg.StrOpt(
'group_id',
help='Operator groupID'),
cfg.StrOpt(
'property_id',
help='Operator propertyID')
]

AKAMAI_GROUP = 'drivers:provider:akamai'
@@ -139,9 +152,25 @@ class CDNProvider(base.Driver):
access_token=self.akamai_conf.ccu_api_access_token
)

self.akamai_sps_api_base_url = ''.join([
str(self.akamai_conf.policy_api_base_url),
'config-secure-provisioning-service/v1'
'/sps-requests/{spsId}?contractId=%s&groupId=%s' % (
self.akamai_conf.contract_id,
self.akamai_conf.group_id
)
])

self.san_cert_cnames = self.akamai_conf.san_cert_cnames
self.san_cert_hostname_limit = self.akamai_conf.san_cert_hostname_limit

self.akamai_sps_api_client = self.akamai_policy_api_client

self.san_info_storage = (
zookeeper_storage.ZookeeperSanInfoStorage(self._conf))
self.mod_san_queue = (
zookeeper_queue.ZookeeperModSanQueue(self._conf))

def is_alive(self):

request_headers = {


+ 0
- 0
poppy/provider/akamai/mod_san_queue/__init__.py View File


+ 43
- 0
poppy/provider/akamai/mod_san_queue/base.py 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

+ 65
- 0
poppy/provider/akamai/mod_san_queue/zookeeper_queue.py 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

+ 0
- 0
poppy/provider/akamai/san_info_storage/__init__.py View File


+ 37
- 0
poppy/provider/akamai/san_info_storage/base.py 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

+ 97
- 0
poppy/provider/akamai/san_info_storage/zookeeper_storage.py 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))

+ 80
- 0
poppy/provider/akamai/services.py View File

@@ -37,12 +37,25 @@ class ServiceController(base.ServiceBase):
def ccu_api_client(self):
return self.driver.ccu_api_client

@property
def sps_api_client(self):
return self.driver.akamai_sps_api_client

@property
def san_info_storage(self):
return self.driver.san_info_storage

@property
def mod_san_queue(self):
return self.driver.mod_san_queue

def __init__(self, driver):
super(ServiceController, self).__init__(driver)

self.driver = driver
self.policy_api_base_url = self.driver.akamai_policy_api_base_url
self.ccu_api_base_url = self.driver.akamai_ccu_api_base_url
self.sps_api_base_url = self.driver.akamai_sps_api_base_url
self.request_header = {'Content-type': 'application/json',
'Accept': 'text/plain'}

@@ -481,6 +494,73 @@ class ServiceController(base.ServiceBase):
format(provider_service_id))
return self.responder.failed(str(e))

def create_certificate(self, cert_obj):
if cert_obj.cert_type == 'san':
for san_cert_name in self.san_cert_cnames:
lastSpsId = (
self.san_info_storage.get_cert_last_spsid(san_cert_name))
if lastSpsId not in [None, ""]:
LOG.info('Latest spsId for %s is: %s' % (san_cert_name,
lastSpsId))
resp = self.sps_api_client.get(
self.sps_api_base_url.format(spsId=lastSpsId),
)
if resp.status_code != 200:
raise RuntimeError('SPS API Request Failed'
'Exception: %s' % resp.text)
status = json.loads(resp.text)['requestList'][0]['status']
# This SAN Cert is on pending status
if status != 'SPS Request Complete':
LOG.info("SPS Not completed for %s..." %
self.san_cert_name)
continue
# issue modify san_cert sps request
cert_info = self.san_info_storage.get_cert_info(san_cert_name)
cert_info['add.sans'] = cert_obj.domain_name
string_post_data = '&'.join(
['%s=%s' % (k, v) for (k, v) in cert_info.items()])
LOG.info('Post modSan request with request data: %s' %
string_post_data)
resp = self.sps_api_client.post(
self.sps_api_base_url.format(spsId=""),
data=string_post_data
)
if resp.status_code != 202:
raise RuntimeError('SPS Request failed.'
'Exception: %s' % resp.text)
else:
resp_dict = json.loads(resp.text)
LOG.info('modSan request submitted. Response: %s' %
str(resp_dict))
this_sps_id = resp_dict['spsId']
self.san_info_storage.save_cert_last_spsid(san_cert_name,
this_sps_id)
return self.responder.ssl_certificate_provisioned(
san_cert_name, {
'status': 'create_in_progress',
'san cert': san_cert_name,
'akamai_spsId': this_sps_id,
'create_at': str(datetime.datetime.now()),
'action': 'Waiting for customer domain '
'validation for %s' %
(cert_obj.domain_name)
})
else:
self.mod_san_queue.enqueue_mod_san_request(
json.dumps(cert_obj.to_dict()))
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'san cert': None,
'action': 'No available san cert for %s right now.'
' More provisioning might be needed' %
(cert_obj.domain_name)
})
else:
return self.responder.ssl_certificate_provisioned(None, {
'status': 'failed',
'reason': 'Cert type : %s hasn\'t been implemented'
})

@decorators.lazy_property(write=False)
def current_customer(self):
return None


+ 24
- 1
poppy/provider/akamai/utils.py View File

@@ -16,6 +16,7 @@
import ssl
import sys

from kazoo import client
from OpenSSL import crypto
import six

@@ -65,7 +66,7 @@ def get_ssl_number_of_hosts(remote_host):

# We can actually print all the Subject Alternative Names
# for san in sans:
# print san
# print(san)
result = len(sans)
break
else:
@@ -73,6 +74,28 @@ def get_ssl_number_of_hosts(remote_host):
return result


def connect_to_zookeeper_storage_backend(conf):
"""Connect to a zookeeper cluster"""
storage_backend_hosts = ','.join(['%s:%s' % (
host, conf.storage_backend_port)
for host in
conf.storage_backend_host])
zk_client = client.KazooClient(storage_backend_hosts)
zk_client.start()
return zk_client


def connect_to_zookeeper_queue_backend(conf):
"""Connect to a zookeeper cluster"""
storage_backend_hosts = ','.join(['%s:%s' % (
host, conf.queue_backend_port)
for host in
conf.queue_backend_host])
zk_client = client.KazooClient(storage_backend_hosts)
zk_client.start()
return zk_client


if __name__ == "__main__":
if len(sys.argv) != 2:
print('Usage: %s <remote_host_you_want_get_cert_on>' % sys.argv[0])


+ 14
- 0
poppy/provider/base/responder.py View File

@@ -123,3 +123,17 @@ class Responder(object):
'caching': cache_list
}
}

def ssl_certificate_provisioned(self, cert_domain, extra_info=None):
"""ssl_certificate_provisioned.

:param cert_domain
:param extra_info
:returns provider msg{cert_domain, extra_info}
"""
return {
self.provider: {
'cert_domain': cert_domain,
'extra_info': extra_info
}
}

+ 22
- 0
poppy/storage/base/services.py View File

@@ -112,6 +112,28 @@ class ServicesControllerBase(controller.StorageControllerBase):
"""
raise NotImplementedError

@abc.abstractmethod
def create_cert(self, project_id, cert_obj):
"""create_cert

:param project_id
:param cert_obj
:raise NotImplementedError
"""
raise NotImplementedError

@abc.abstractmethod
def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
"""update_cert_info.

:param domain_name
:param cert_type
:param flavor_id
:param cert_info
"""
raise NotImplementedError

@staticmethod
def format_result(result):
"""format_result


+ 15
- 0
poppy/storage/cassandra/migrations/005_domain_certificate_info.cql 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;

+ 9
- 0
poppy/storage/cassandra/migrations/config/cassandra.yml 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

+ 120
- 0
poppy/storage/cassandra/services.py View File

@@ -166,6 +166,30 @@ CQL_CREATE_SERVICE = '''
%(log_delivery)s)
'''

CQL_CREATE_CERT = '''
INSERT INTO certificate_info (project_id,
flavor_id,
cert_type,
domain_name,
cert_details
)
VALUES (%(project_id)s,
%(flavor_id)s,
%(cert_type)s,
%(domain_name)s,
%(cert_details)s)
'''

CQL_VERIFY_CERT = '''
SELECT project_id,
flavor_id,
cert_type,
domain_name
FROM certificate_info
WHERE domain_name = %(domain_name)s
ALLOW FILTERING
'''

CQL_UPDATE_SERVICE = CQL_CREATE_SERVICE

CQL_GET_PROVIDER_DETAILS = '''
@@ -180,6 +204,13 @@ CQL_UPDATE_PROVIDER_DETAILS = '''
WHERE project_id = %(project_id)s AND service_id = %(service_id)s
'''

CQL_UPDATE_CERT_DETAILS = '''
UPDATE certificate_info
set cert_details = %(cert_details)s
WHERE domain_name = %(domain_name)s
IF cert_type = %(cert_type)s AND flavor_id = %(flavor_id)s
'''


class ServicesController(base.ServicesController):

@@ -291,6 +322,51 @@ class ServicesController(base.ServicesController):
LOG.exception(ex)
return False

def cert_already_exist(self, domain_name, comparing_cert_type,
comparing_flavor_id,
comparing_project_id):
"""cert_already_exist

Check if a cert with this domain name and type has already been
created, or if the domain has been taken by other customers

:param domain_name
:param cert_type
:param comparing_project_id

:raises ValueError
:returns Boolean if the cert with same type exists with another user.
"""
LOG.info("Check if cert on '{0}' exists".format(domain_name))
args = {
'domain_name': domain_name.lower()
}
stmt = query.SimpleStatement(
CQL_VERIFY_CERT,
consistency_level=self._driver.consistency_level)
results = self.session.execute(stmt, args)

if results:
msg = None
for r in results:
if str(r.get('project_id')) != str(comparing_project_id):
msg = "Domain '{0}' has already been created cert by {1}"\
.format(domain_name, r.get('project_id'))
LOG.warn(msg)
raise ValueError(msg)
elif (str(r.get('flavor_id')) == str(comparing_flavor_id)
and
str(r.get('cert_type')) == str(comparing_cert_type)):
msg = "{0} have already created cert of type {1} on {2}"\
.format(str(comparing_project_id),
comparing_cert_type,
domain_name)
LOG.warn(msg)
raise ValueError(msg)
return False
else:
return False

def create(self, project_id, service_obj):
"""create.

@@ -506,6 +582,29 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, delete_args)

def create_cert(self, project_id, cert_obj):

if not self.cert_already_exist(cert_obj.domain_name,
cert_obj.cert_type,
cert_obj.flavor_id,
project_id):
pass

args = {
'project_id': project_id,
'flavor_id': cert_obj.flavor_id,
'cert_type': cert_obj.cert_type,
'domain_name': cert_obj.domain_name,
# when create the cert, cert domain has not been assigned yet
# In future we can tweak the logic to assign cert_domain
'cert_domain': '',
'cert_details': {}
}
stmt = query.SimpleStatement(
CQL_CREATE_CERT,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)

def get_provider_details(self, project_id, service_id):
"""get_provider_details.

@@ -613,6 +712,27 @@ class ServicesController(base.ServicesController):
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)

def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
"""update_cert_info.

:param domain_name
:param cert_type
:param flavor_id
:param cert_info
"""

args = {
'domain_name': domain_name,
'cert_type': cert_type,
'flavor_id': flavor_id,
'cert_details': cert_details
}
stmt = query.SimpleStatement(
CQL_UPDATE_CERT_DETAILS,
consistency_level=self._driver.consistency_level)
self.session.execute(stmt, args)

@staticmethod
def format_result(result):
"""format_result.


+ 7
- 0
poppy/storage/mockdb/services.py View File

@@ -123,6 +123,13 @@ class ServicesController(base.ServicesController):
def domain_exists_elsewhere(self, domain_name, service_id):
return domain_name in self.claimed_domains

def update_cert_info(self, domain_name, cert_type, flavor_id,
cert_details):
pass

def create_cert(self, project_id, cert_obj):
pass

@staticmethod
def format_result(result):
service_id = result.get('service_id')


+ 2
- 0
poppy/transport/pecan/controllers/v1/__init__.py View File

@@ -21,6 +21,7 @@ from poppy.transport.pecan.controllers.v1 import health
from poppy.transport.pecan.controllers.v1 import home
from poppy.transport.pecan.controllers.v1 import ping
from poppy.transport.pecan.controllers.v1 import services
from poppy.transport.pecan.controllers.v1 import ssl_certificates


# Hoist into package namespace
@@ -33,3 +34,4 @@ DNSHealth = health.DNSHealthController
StorageHealth = health.StorageHealthController
ProviderHealth = health.ProviderHealthController
Admin = admin.AdminController
SSLCertificate = ssl_certificates.SSLCertificateController

+ 62
- 0
poppy/transport/pecan/controllers/v1/ssl_certificates.py 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)

+ 2
- 0
poppy/transport/pecan/driver.py View File

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

self._app = pecan.make_app(root_controller,
guess_content_type_from_ext=False)


+ 24
- 0
poppy/transport/pecan/models/request/ssl_certificate.py 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)

+ 31
- 0
poppy/transport/pecan/models/response/ssl_certificate.py 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

+ 49
- 0
poppy/transport/validators/schemas/ssl_certificate.py 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
}
}
}
}
}

+ 40
- 0
scripts/providers/akamai/san_cert_info/list_san_cert_info.py 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()

+ 69
- 0
scripts/providers/akamai/san_cert_info/upsert_san_cert_info.py 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()

+ 1
- 0
setup.cfg View File

@@ -64,6 +64,7 @@ poppy.notification =
mailgun = poppy.notification.mailgun:Driver



[wheel]
universal = 1



+ 0
- 0
tests/api/ssl_certificate/__init__.py View File


+ 6
- 0
tests/api/ssl_certificate/data_create_ssl_certificate.json View File

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

+ 22
- 0
tests/api/ssl_certificate/data_create_ssl_certificate_negative.json 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"
}
}

+ 62
- 0
tests/api/ssl_certificate/test_create_ssl_certificate.py 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)

+ 21
- 0
tests/api/utils/client.py View File

@@ -404,3 +404,24 @@ class PoppyClient(client.AutoMarshallingHTTPClient):
assert False, ('Timed out waiting for service '
'to be deleted, after '
'waiting {0} seconds'.format(retry_timeout))

def create_ssl_certificate(self, cert_type=None,
domain_name=None, flavor_id=None,
requestslib_kwargs=None,):
"""Creates SSL Certificate

:return: Response Object containing response code 200 and body with
details of service
POST
ssl_certificate
"""
url = '{0}/ssl_certificate'.format(self.url)

requests_object = requests.CreateSSLCertificate(
cert_type=cert_type,
domain_name=domain_name,
flavor_id=flavor_id
)

return self.request('POST', url, request_entity=requests_object,
requestslib_kwargs=requestslib_kwargs)

+ 18
- 0
tests/api/utils/models/requests.py View File

@@ -109,3 +109,21 @@ class CreateFlavor(base.AutoMarshallingModel):
"providers": self.provider_list,
"limits": self.limits}
return json.dumps(create_flavor_request)


class CreateSSLCertificate(base.AutoMarshallingModel):
"""Marshalling for Create Flavor requests."""

def __init__(self, cert_type=None, domain_name=None, flavor_id=None):
super(CreateSSLCertificate, self).__init__()

self.cert_type = cert_type
self.domain_name = domain_name
self.flavor_id = flavor_id

def _obj_to_json(self):
create_ssl_certificate_request = {
"cert_type": self.cert_type,
"domain_name": self.domain_name,
"flavor_id": self.flavor_id}
return json.dumps(create_ssl_certificate_request)

+ 10
- 0
tests/functional/transport/pecan/controllers/data_create_ssl_certificate.json View File

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




+ 19
- 0
tests/functional/transport/pecan/controllers/data_create_ssl_certificate_bad_input_json.json 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"
}
}

+ 93
- 0
tests/functional/transport/pecan/controllers/test_ssl_certificate.py 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()

+ 65
- 8
tests/unit/distributed_task/taskflow/test_flows.py View File

@@ -19,6 +19,7 @@ import mock
from taskflow import engines

from poppy.distributed_task.taskflow.flow import create_service
from poppy.distributed_task.taskflow.flow import create_ssl_certificate
from poppy.distributed_task.taskflow.flow import delete_service
from poppy.distributed_task.taskflow.flow import purge_service
from poppy.distributed_task.taskflow.flow import update_service
@@ -28,6 +29,7 @@ from poppy.distributed_task.utils import memoized_controllers
from poppy.model.helpers import domain
from poppy.model.helpers import origin
from poppy.model import service
from poppy.model import ssl_certificate
from tests.unit import base
from tests.unit.manager.default.test_services import MonkeyPatchControllers

@@ -142,8 +144,24 @@ class TestFlowRuns(base.TestCase):
dns_controller.disable = mock.Mock()
dns_controller.disable._mock_return_value = []

def patch_create_ssl_certificate_flow(self, service_controller,
storage_controller, dns_controller):
storage_controller.get = mock.Mock()
storage_controller.update = mock.Mock()
storage_controller._driver.close_connection = mock.Mock()
service_controller.provider_wrapper.create_certificate = mock.Mock()
service_controller.provider_wrapper.create_certificate.\
_mock_return_value = []
service_controller._driver = mock.Mock()
service_controller._driver.providers.__getitem__ = mock.Mock()
service_controller._driver.notification = [mock.Mock()]
dns_controller.create = mock.Mock()
dns_controller.create._mock_return_value = []
common.create_log_delivery_container = mock.Mock()

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_create_flow_normal(self, mock_creds):
def test_create_flow_normal(self, mock_creds, mock_dns_client):
providers = ['cdn_provider']
kwargs = {
'providers_list_json': json.dumps(providers),
@@ -167,8 +185,9 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(create_service.create_service(), store=kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_update_flow_normal(self, mock_creds):
def test_update_flow_normal(self, mock_creds, mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
domains_new = domain.Domain(domain='mycdn.poppy.org')
@@ -206,8 +225,9 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(update_service.update_service(), store=kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_delete_flow_normal(self, mock_creds):
def test_delete_flow_normal(self, mock_creds, mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@@ -239,8 +259,9 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(delete_service.delete_service(), store=kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_purge_flow_normal(self, mock_creds):
def test_purge_flow_normal(self, mock_creds, mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@@ -273,8 +294,10 @@ class TestFlowRuns(base.TestCase):
dns_controller)
engines.run(purge_service.purge_service(), store=kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_service_state_flow_normal(self, mock_creds):
def test_service_state_flow_normal(self, mock_creds,
mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@@ -311,8 +334,10 @@ class TestFlowRuns(base.TestCase):
engines.run(update_service_state.disable_service(),
store=disable_kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_create_flow_dns_exception(self, mock_creds):
def test_create_flow_dns_exception(self, mock_creds,
mock_dns_client):
providers = ['cdn_provider']
kwargs = {
'providers_list_json': json.dumps(providers),
@@ -343,8 +368,10 @@ class TestFlowRuns(base.TestCase):
}
engines.run(create_service.create_service(), store=kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_update_flow_dns_exception(self, mock_creds):
def test_update_flow_dns_exception(self, mock_creds,
mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
domains_new = domain.Domain(domain='mycdn.poppy.org')
@@ -391,8 +418,10 @@ class TestFlowRuns(base.TestCase):

engines.run(update_service.update_service(), store=kwargs)

@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_delete_flow_dns_exception(self, mock_creds):
def test_delete_flow_dns_exception(self, mock_creds,
mock_dns_client):
service_id = str(uuid.uuid4())
domains_old = domain.Domain(domain='cdn.poppy.org')
current_origin = origin.Origin(origin='poppy.org')
@@ -818,3 +847,31 @@ class TestFlowRuns(base.TestCase):
store=enable_kwargs)
engines.run(update_service_state.disable_service(),
store=disable_kwargs)

# Keep create credentials for now
@mock.patch('pyrax.cloud_dns')
@mock.patch('pyrax.set_credentials')
def test_create_ssl_certificate_normal(self, mock_creds, mock_dns_client):
providers = ['cdn_provider']
cert_obj_json = ssl_certificate.SSLCertificate('cdn',
'mytestsite.com',
'san')
kwargs = {
'providers_list_json': json.dumps(providers),
'project_id': json.dumps(str(uuid.uuid4())),
'cert_obj_json': json.dumps(cert_obj_json.to_dict()),
}

service_controller, storage_controller, dns_controller = \
self.all_controllers()

with MonkeyPatchControllers(service_controller,
dns_controller,
storage_controller,
memoized_controllers.task_controllers):

self.patch_create_ssl_certificate_flow(service_controller,
storage_controller,
dns_controller)
engines.run(create_ssl_certificate.create_ssl_certificate(),
store=kwargs)

+ 1
- 0
tests/unit/manager/default/test_notification_wrapper.py View File

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

mock_ext.obj.services_controller.send.assert_called_once_with(
"test_subject", "test_mail_content")

+ 4
- 1
tests/unit/notification/mailgun/test_driver.py View File

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

MAIL_NOTIFICATION_GROUP = 'drivers:notification:mailgun'


+ 4
- 1
tests/unit/notification/mailgun/test_services.py View File

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

MAIL_NOTIFICATION_GROUP = 'drivers:notification:mail'


+ 24
- 0
tests/unit/provider/akamai/test_driver.py View File

@@ -84,6 +84,17 @@ AKAMAI_OPTIONS = [
cfg.IntOpt('san_cert_hostname_limit', default=80,
help='default limit on how many hostnames can'
' be held by a SAN cert'),

# related info for SPS && PAPI APIs
cfg.StrOpt(
'contract_id',
help='Operator contractID'),
cfg.StrOpt(
'group_id',
help='Operator groupID'),
cfg.StrOpt(
'property_id',
help='Operator propertyID')
]


@@ -115,6 +126,12 @@ class TestDriver(base.TestCase):

self.conf = cfg.ConfigOpts()

zookeeper_client_patcher = mock.patch(
'kazoo.client.KazooClient'
)
zookeeper_client_patcher.start()
self.addCleanup(zookeeper_client_patcher.stop)

@mock.patch('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_init(self, mock_connect):
@@ -158,3 +175,10 @@ class TestDriver(base.TestCase):
provider = driver.CDNProvider(self.conf)
self.assertNotEqual(None, provider.policy_api_client)
self.assertNotEqual(None, provider.ccu_api_client)

@mock.patch('akamai.edgegrid.EdgeGridAuth')
@mock.patch.object(driver, 'AKAMAI_OPTIONS', new=AKAMAI_OPTIONS)
def test_san_info_storage(self, mock_connect):
mock_connect.return_value = mock.Mock()
provider = driver.CDNProvider(self.conf)
self.assertNotEqual(None, provider.san_info_storage)

+ 71
- 0
tests/unit/provider/akamai/test_mod_san_queue.py 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,