From df58d6014ae985a0ca9c01b6471f7833f5d483c1 Mon Sep 17 00:00:00 2001 From: tonytan4ever Date: Wed, 16 Sep 2015 15:56:08 -0400 Subject: [PATCH] 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 --- etc/poppy.conf | 13 ++ .../taskflow/flow/create_ssl_certificate.py | 41 ++++++ .../task/create_ssl_certificate_tasks.py | 94 ++++++++++++++ .../utils/memoized_controllers.py | 3 + poppy/manager/base/__init__.py | 2 + poppy/manager/base/providers.py | 10 ++ poppy/manager/base/ssl_certificate.py | 38 ++++++ poppy/manager/default/controllers.py | 2 + poppy/manager/default/driver.py | 4 + poppy/manager/default/ssl_certificate.py | 57 +++++++++ poppy/model/ssl_certificate.py | 66 ++++++++++ poppy/notification/mailgun/driver.py | 7 +- poppy/provider/akamai/driver.py | 29 +++++ .../provider/akamai/mod_san_queue/__init__.py | 0 poppy/provider/akamai/mod_san_queue/base.py | 43 +++++++ .../akamai/mod_san_queue/zookeeper_queue.py | 65 ++++++++++ .../akamai/san_info_storage/__init__.py | 0 .../provider/akamai/san_info_storage/base.py | 37 ++++++ .../san_info_storage/zookeeper_storage.py | 97 ++++++++++++++ poppy/provider/akamai/services.py | 80 ++++++++++++ poppy/provider/akamai/utils.py | 25 +++- poppy/provider/base/responder.py | 14 ++ poppy/storage/base/services.py | 22 ++++ .../005_domain_certificate_info.cql | 15 +++ .../cassandra/migrations/config/cassandra.yml | 9 ++ poppy/storage/cassandra/services.py | 120 ++++++++++++++++++ poppy/storage/mockdb/services.py | 7 + .../pecan/controllers/v1/__init__.py | 2 + .../pecan/controllers/v1/ssl_certificates.py | 62 +++++++++ poppy/transport/pecan/driver.py | 2 + .../pecan/models/request/ssl_certificate.py | 24 ++++ .../pecan/models/response/ssl_certificate.py | 31 +++++ .../validators/schemas/ssl_certificate.py | 49 +++++++ .../san_cert_info/list_san_cert_info.py | 40 ++++++ .../san_cert_info/upsert_san_cert_info.py | 69 ++++++++++ setup.cfg | 1 + tests/api/ssl_certificate/__init__.py | 0 .../data_create_ssl_certificate.json | 6 + .../data_create_ssl_certificate_negative.json | 22 ++++ .../test_create_ssl_certificate.py | 62 +++++++++ tests/api/utils/client.py | 21 +++ tests/api/utils/models/requests.py | 18 +++ .../data_create_ssl_certificate.json | 10 ++ ...create_ssl_certificate_bad_input_json.json | 19 +++ .../pecan/controllers/test_ssl_certificate.py | 93 ++++++++++++++ .../distributed_task/taskflow/test_flows.py | 73 +++++++++-- .../default/test_notification_wrapper.py | 1 + .../unit/notification/mailgun/test_driver.py | 5 +- .../notification/mailgun/test_services.py | 5 +- tests/unit/provider/akamai/test_driver.py | 24 ++++ .../provider/akamai/test_mod_san_queue.py | 71 +++++++++++ .../provider/akamai/test_san_info_storage.py | 113 +++++++++++++++++ tests/unit/provider/akamai/test_services.py | 73 +++++++++++ 53 files changed, 1784 insertions(+), 12 deletions(-) create mode 100644 poppy/distributed_task/taskflow/flow/create_ssl_certificate.py create mode 100644 poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py create mode 100644 poppy/manager/base/ssl_certificate.py create mode 100644 poppy/manager/default/ssl_certificate.py create mode 100644 poppy/model/ssl_certificate.py create mode 100644 poppy/provider/akamai/mod_san_queue/__init__.py create mode 100644 poppy/provider/akamai/mod_san_queue/base.py create mode 100644 poppy/provider/akamai/mod_san_queue/zookeeper_queue.py create mode 100644 poppy/provider/akamai/san_info_storage/__init__.py create mode 100644 poppy/provider/akamai/san_info_storage/base.py create mode 100644 poppy/provider/akamai/san_info_storage/zookeeper_storage.py create mode 100644 poppy/storage/cassandra/migrations/005_domain_certificate_info.cql create mode 100644 poppy/storage/cassandra/migrations/config/cassandra.yml create mode 100644 poppy/transport/pecan/controllers/v1/ssl_certificates.py create mode 100644 poppy/transport/pecan/models/request/ssl_certificate.py create mode 100644 poppy/transport/pecan/models/response/ssl_certificate.py create mode 100644 poppy/transport/validators/schemas/ssl_certificate.py create mode 100644 scripts/providers/akamai/san_cert_info/list_san_cert_info.py create mode 100644 scripts/providers/akamai/san_cert_info/upsert_san_cert_info.py create mode 100644 tests/api/ssl_certificate/__init__.py create mode 100644 tests/api/ssl_certificate/data_create_ssl_certificate.json create mode 100644 tests/api/ssl_certificate/data_create_ssl_certificate_negative.json create mode 100644 tests/api/ssl_certificate/test_create_ssl_certificate.py create mode 100644 tests/functional/transport/pecan/controllers/data_create_ssl_certificate.json create mode 100644 tests/functional/transport/pecan/controllers/data_create_ssl_certificate_bad_input_json.json create mode 100644 tests/functional/transport/pecan/controllers/test_ssl_certificate.py create mode 100644 tests/unit/provider/akamai/test_mod_san_queue.py create mode 100644 tests/unit/provider/akamai/test_san_info_storage.py diff --git a/etc/poppy.conf b/etc/poppy.conf index 3d884d1d..3490bb02 100644 --- a/etc/poppy.conf +++ b/etc/poppy.conf @@ -118,6 +118,13 @@ delay = 1 [drivers:provider] default_cache_ttl = 86400 +[drivers:notification:mailgun] +mailgun_api_key = "" +mailgun_request_url = "https://api.mailgun.net/v2/{0}/events" +sand_box = "" +from_address = "" +recipients="" + [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 = +storage_backend_port = [drivers:notification:mailgun] mailgun_api_key = "" diff --git a/poppy/distributed_task/taskflow/flow/create_ssl_certificate.py b/poppy/distributed_task/taskflow/flow/create_ssl_certificate.py new file mode 100644 index 00000000..9c26f6f3 --- /dev/null +++ b/poppy/distributed_task/taskflow/flow/create_ssl_certificate.py @@ -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 diff --git a/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py b/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py new file mode 100644 index 00000000..a0832658 --- /dev/null +++ b/poppy/distributed_task/taskflow/task/create_ssl_certificate_tasks.py @@ -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 diff --git a/poppy/distributed_task/utils/memoized_controllers.py b/poppy/distributed_task/utils/memoized_controllers.py index 6059857f..476d4261 100644 --- a/poppy/distributed_task/utils/memoized_controllers.py +++ b/poppy/distributed_task/utils/memoized_controllers.py @@ -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 diff --git a/poppy/manager/base/__init__.py b/poppy/manager/base/__init__.py index c0f0433a..6f13cabb 100644 --- a/poppy/manager/base/__init__.py +++ b/poppy/manager/base/__init__.py @@ -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 diff --git a/poppy/manager/base/providers.py b/poppy/manager/base/providers.py index baff30c8..478f3f0d 100644 --- a/poppy/manager/base/providers.py +++ b/poppy/manager/base/providers.py @@ -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) diff --git a/poppy/manager/base/ssl_certificate.py b/poppy/manager/base/ssl_certificate.py new file mode 100644 index 00000000..b6cb774e --- /dev/null +++ b/poppy/manager/base/ssl_certificate.py @@ -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 diff --git a/poppy/manager/default/controllers.py b/poppy/manager/default/controllers.py index 66d0cf25..d136667e 100644 --- a/poppy/manager/default/controllers.py +++ b/poppy/manager/default/controllers.py @@ -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 diff --git a/poppy/manager/default/driver.py b/poppy/manager/default/driver.py index 24e7a7e5..b12c1fb9 100644 --- a/poppy/manager/default/driver.py +++ b/poppy/manager/default/driver.py @@ -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) diff --git a/poppy/manager/default/ssl_certificate.py b/poppy/manager/default/ssl_certificate.py new file mode 100644 index 00000000..e38e6933 --- /dev/null +++ b/poppy/manager/default/ssl_certificate.py @@ -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 diff --git a/poppy/model/ssl_certificate.py b/poppy/model/ssl_certificate.py new file mode 100644 index 00000000..019302b4 --- /dev/null +++ b/poppy/model/ssl_certificate.py @@ -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) + ) diff --git a/poppy/notification/mailgun/driver.py b/poppy/notification/mailgun/driver.py index 6bfd958a..cb2889d4 100644 --- a/poppy/notification/mailgun/driver.py +++ b/poppy/notification/mailgun/driver.py @@ -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): diff --git a/poppy/provider/akamai/driver.py b/poppy/provider/akamai/driver.py index 49293200..9dd270d2 100644 --- a/poppy/provider/akamai/driver.py +++ b/poppy/provider/akamai/driver.py @@ -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 = { diff --git a/poppy/provider/akamai/mod_san_queue/__init__.py b/poppy/provider/akamai/mod_san_queue/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poppy/provider/akamai/mod_san_queue/base.py b/poppy/provider/akamai/mod_san_queue/base.py new file mode 100644 index 00000000..e927ba85 --- /dev/null +++ b/poppy/provider/akamai/mod_san_queue/base.py @@ -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 diff --git a/poppy/provider/akamai/mod_san_queue/zookeeper_queue.py b/poppy/provider/akamai/mod_san_queue/zookeeper_queue.py new file mode 100644 index 00000000..66ff6bb6 --- /dev/null +++ b/poppy/provider/akamai/mod_san_queue/zookeeper_queue.py @@ -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 diff --git a/poppy/provider/akamai/san_info_storage/__init__.py b/poppy/provider/akamai/san_info_storage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poppy/provider/akamai/san_info_storage/base.py b/poppy/provider/akamai/san_info_storage/base.py new file mode 100644 index 00000000..5cc6aa67 --- /dev/null +++ b/poppy/provider/akamai/san_info_storage/base.py @@ -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 diff --git a/poppy/provider/akamai/san_info_storage/zookeeper_storage.py b/poppy/provider/akamai/san_info_storage/zookeeper_storage.py new file mode 100644 index 00000000..7f4e93cc --- /dev/null +++ b/poppy/provider/akamai/san_info_storage/zookeeper_storage.py @@ -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)) diff --git a/poppy/provider/akamai/services.py b/poppy/provider/akamai/services.py index f430a06a..7bffaab1 100644 --- a/poppy/provider/akamai/services.py +++ b/poppy/provider/akamai/services.py @@ -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 diff --git a/poppy/provider/akamai/utils.py b/poppy/provider/akamai/utils.py index 83d0e454..a783caef 100644 --- a/poppy/provider/akamai/utils.py +++ b/poppy/provider/akamai/utils.py @@ -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 ' % sys.argv[0]) diff --git a/poppy/provider/base/responder.py b/poppy/provider/base/responder.py index 32db9102..47cf3232 100644 --- a/poppy/provider/base/responder.py +++ b/poppy/provider/base/responder.py @@ -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 + } + } diff --git a/poppy/storage/base/services.py b/poppy/storage/base/services.py index 26e0801d..a34f00e5 100644 --- a/poppy/storage/base/services.py +++ b/poppy/storage/base/services.py @@ -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 diff --git a/poppy/storage/cassandra/migrations/005_domain_certificate_info.cql b/poppy/storage/cassandra/migrations/005_domain_certificate_info.cql new file mode 100644 index 00000000..85ebe1da --- /dev/null +++ b/poppy/storage/cassandra/migrations/005_domain_certificate_info.cql @@ -0,0 +1,15 @@ +CREATE TABLE certificate_info ( + project_id VARCHAR, + flavor_id VARCHAR, + cert_type VARCHAR, + domain_name VARCHAR, + cert_details MAP, + PRIMARY KEY (domain_name) +); + +CREATE INDEX idx_cert_type + ON certificate_info (cert_type); + +--//@UNDO + +DROP TABLE IF EXISTS certificate_info; \ No newline at end of file diff --git a/poppy/storage/cassandra/migrations/config/cassandra.yml b/poppy/storage/cassandra/migrations/config/cassandra.yml new file mode 100644 index 00000000..25e65f3c --- /dev/null +++ b/poppy/storage/cassandra/migrations/config/cassandra.yml @@ -0,0 +1,9 @@ +# This for running cdeploy command + +development: + hosts: [localhost] + keyspace: poppy + +production: + hosts: [your_production_env_host(s)] + keyspace: poppy \ No newline at end of file diff --git a/poppy/storage/cassandra/services.py b/poppy/storage/cassandra/services.py index acd327fd..d367a895 100644 --- a/poppy/storage/cassandra/services.py +++ b/poppy/storage/cassandra/services.py @@ -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. diff --git a/poppy/storage/mockdb/services.py b/poppy/storage/mockdb/services.py index 9dde5945..4ffeb762 100644 --- a/poppy/storage/mockdb/services.py +++ b/poppy/storage/mockdb/services.py @@ -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') diff --git a/poppy/transport/pecan/controllers/v1/__init__.py b/poppy/transport/pecan/controllers/v1/__init__.py index 378bfb5a..55745a37 100644 --- a/poppy/transport/pecan/controllers/v1/__init__.py +++ b/poppy/transport/pecan/controllers/v1/__init__.py @@ -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 diff --git a/poppy/transport/pecan/controllers/v1/ssl_certificates.py b/poppy/transport/pecan/controllers/v1/ssl_certificates.py new file mode 100644 index 00000000..a16a941f --- /dev/null +++ b/poppy/transport/pecan/controllers/v1/ssl_certificates.py @@ -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) diff --git a/poppy/transport/pecan/driver.py b/poppy/transport/pecan/driver.py index 0b6b4274..93924330 100644 --- a/poppy/transport/pecan/driver.py +++ b/poppy/transport/pecan/driver.py @@ -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) diff --git a/poppy/transport/pecan/models/request/ssl_certificate.py b/poppy/transport/pecan/models/request/ssl_certificate.py new file mode 100644 index 00000000..6debabef --- /dev/null +++ b/poppy/transport/pecan/models/request/ssl_certificate.py @@ -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) diff --git a/poppy/transport/pecan/models/response/ssl_certificate.py b/poppy/transport/pecan/models/response/ssl_certificate.py new file mode 100644 index 00000000..9efeb3bf --- /dev/null +++ b/poppy/transport/pecan/models/response/ssl_certificate.py @@ -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 diff --git a/poppy/transport/validators/schemas/ssl_certificate.py b/poppy/transport/validators/schemas/ssl_certificate.py new file mode 100644 index 00000000..2b744512 --- /dev/null +++ b/poppy/transport/validators/schemas/ssl_certificate.py @@ -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 + } + } + } + } + } diff --git a/scripts/providers/akamai/san_cert_info/list_san_cert_info.py b/scripts/providers/akamai/san_cert_info/list_san_cert_info.py new file mode 100644 index 00000000..61901843 --- /dev/null +++ b/scripts/providers/akamai/san_cert_info/list_san_cert_info.py @@ -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() diff --git a/scripts/providers/akamai/san_cert_info/upsert_san_cert_info.py b/scripts/providers/akamai/san_cert_info/upsert_san_cert_info.py new file mode 100644 index 00000000..a61fdac8 --- /dev/null +++ b/scripts/providers/akamai/san_cert_info/upsert_san_cert_info.py @@ -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() diff --git a/setup.cfg b/setup.cfg index 0946dca3..a3b23ff7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,7 @@ poppy.notification = mailgun = poppy.notification.mailgun:Driver + [wheel] universal = 1 diff --git a/tests/api/ssl_certificate/__init__.py b/tests/api/ssl_certificate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/api/ssl_certificate/data_create_ssl_certificate.json b/tests/api/ssl_certificate/data_create_ssl_certificate.json new file mode 100644 index 00000000..22bc5c82 --- /dev/null +++ b/tests/api/ssl_certificate/data_create_ssl_certificate.json @@ -0,0 +1,6 @@ +{ + "mod_san_test_1": { + "cert_type": "san", + "domain_name": "www.abc.com" + } +} \ No newline at end of file diff --git a/tests/api/ssl_certificate/data_create_ssl_certificate_negative.json b/tests/api/ssl_certificate/data_create_ssl_certificate_negative.json new file mode 100644 index 00000000..1c95661d --- /dev/null +++ b/tests/api/ssl_certificate/data_create_ssl_certificate_negative.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/api/ssl_certificate/test_create_ssl_certificate.py b/tests/api/ssl_certificate/test_create_ssl_certificate.py new file mode 100644 index 00000000..a469c1d6 --- /dev/null +++ b/tests/api/ssl_certificate/test_create_ssl_certificate.py @@ -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) diff --git a/tests/api/utils/client.py b/tests/api/utils/client.py index 96b93549..0754243f 100644 --- a/tests/api/utils/client.py +++ b/tests/api/utils/client.py @@ -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) diff --git a/tests/api/utils/models/requests.py b/tests/api/utils/models/requests.py index 353b2dd0..21b701d9 100644 --- a/tests/api/utils/models/requests.py +++ b/tests/api/utils/models/requests.py @@ -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) diff --git a/tests/functional/transport/pecan/controllers/data_create_ssl_certificate.json b/tests/functional/transport/pecan/controllers/data_create_ssl_certificate.json new file mode 100644 index 00000000..aa86dbba --- /dev/null +++ b/tests/functional/transport/pecan/controllers/data_create_ssl_certificate.json @@ -0,0 +1,10 @@ +{ + "all_fields": { + "cert_type": "san", + "domain_name": "www.abc.com", + "flavor_id": "mock" + } +} + + + diff --git a/tests/functional/transport/pecan/controllers/data_create_ssl_certificate_bad_input_json.json b/tests/functional/transport/pecan/controllers/data_create_ssl_certificate_bad_input_json.json new file mode 100644 index 00000000..d2f8147e --- /dev/null +++ b/tests/functional/transport/pecan/controllers/data_create_ssl_certificate_bad_input_json.json @@ -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" + } +} \ No newline at end of file diff --git a/tests/functional/transport/pecan/controllers/test_ssl_certificate.py b/tests/functional/transport/pecan/controllers/test_ssl_certificate.py new file mode 100644 index 00000000..9c0bbde4 --- /dev/null +++ b/tests/functional/transport/pecan/controllers/test_ssl_certificate.py @@ -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() diff --git a/tests/unit/distributed_task/taskflow/test_flows.py b/tests/unit/distributed_task/taskflow/test_flows.py index 63d01e67..325a2564 100644 --- a/tests/unit/distributed_task/taskflow/test_flows.py +++ b/tests/unit/distributed_task/taskflow/test_flows.py @@ -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) diff --git a/tests/unit/manager/default/test_notification_wrapper.py b/tests/unit/manager/default/test_notification_wrapper.py index 8b98bb1e..f773b824 100644 --- a/tests/unit/manager/default/test_notification_wrapper.py +++ b/tests/unit/manager/default/test_notification_wrapper.py @@ -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") diff --git a/tests/unit/notification/mailgun/test_driver.py b/tests/unit/notification/mailgun/test_driver.py index 5b7f34ff..0856bf3c 100644 --- a/tests/unit/notification/mailgun/test_driver.py +++ b/tests/unit/notification/mailgun/test_driver.py @@ -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' diff --git a/tests/unit/notification/mailgun/test_services.py b/tests/unit/notification/mailgun/test_services.py index c933894e..00a88774 100644 --- a/tests/unit/notification/mailgun/test_services.py +++ b/tests/unit/notification/mailgun/test_services.py @@ -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' diff --git a/tests/unit/provider/akamai/test_driver.py b/tests/unit/provider/akamai/test_driver.py index 71b135a9..68a47bb7 100644 --- a/tests/unit/provider/akamai/test_driver.py +++ b/tests/unit/provider/akamai/test_driver.py @@ -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) diff --git a/tests/unit/provider/akamai/test_mod_san_queue.py b/tests/unit/provider/akamai/test_mod_san_queue.py new file mode 100644 index 00000000..3a9bd13b --- /dev/null +++ b/tests/unit/provider/akamai/test_mod_san_queue.py @@ -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() diff --git a/tests/unit/provider/akamai/test_san_info_storage.py b/tests/unit/provider/akamai/test_san_info_storage.py new file mode 100644 index 00000000..91f5a48a --- /dev/null +++ b/tests/unit/provider/akamai/test_san_info_storage.py @@ -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)) diff --git a/tests/unit/provider/akamai/test_services.py b/tests/unit/provider/akamai/test_services.py index 7085b66a..fe5ebb50 100644 --- a/tests/unit/provider/akamai/test_services.py +++ b/tests/unit/provider/akamai/test_services.py @@ -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