From 7a2c4c56e453bf0aa3ffdf6dc6965263da9fd323 Mon Sep 17 00:00:00 2001 From: Sriram Madapusi Vasudevan Date: Thu, 15 Oct 2015 12:15:07 -0400 Subject: [PATCH] feat: Add akamai background job admin endpoint REQUEST: POST /admin/provider/akamai/background_job { "job_type": "akamai_update_papi_property_for_mod_san", "domain_name": "testabc.cnamecdn.com", "san_cert_name": "secure1.san1.test_cdn.com" } RESPONSE: 202 ACCEPTED Note: The admin endpoints are also exposed as scripts: akamai-property-udpate-mod-san --domain_name --san_cert_name akamai-cert-status-check-and-update --domain_name --cert_type "san" --project_id --flavor_id Change-Id: I9211d9f24cbd46c8818993d3ff8493d5fba4bc28 --- .../akamai_check_and_update_cert_status.py | 64 ++++ ...akamai_update_papi_property_for_mod_san.py | 78 +++++ .../taskflow/task/create_service_tasks.py | 11 +- .../utils/memoized_controllers.py | 2 + poppy/manager/base/__init__.py | 2 + poppy/manager/base/background_job.py | 36 ++ poppy/manager/default/background_job.py | 95 ++++++ poppy/manager/default/controllers.py | 2 + poppy/manager/default/driver.py | 4 + poppy/manager/default/services.py | 22 +- .../akamai/background_jobs/__init__.py | 0 .../check_cert_status_and_update/__init__.py | 0 .../check_cert_status_and_update_flow.py | 52 +++ .../check_cert_status_and_update_tasks.py | 124 +++++++ .../update_property/__init__.py | 0 .../update_property/update_property_flow.py | 50 +++ .../update_property/update_property_tasks.py | 276 +++++++++++++++ poppy/provider/akamai/driver.py | 31 +- poppy/storage/cassandra/services.py | 5 +- poppy/storage/mockdb/services.py | 28 +- poppy/transport/pecan/controllers/v1/admin.py | 31 ++ .../pecan/models/request/ssl_certificate.py | 5 +- .../pecan/models/response/service.py | 1 + .../validators/schemas/background_jobs.py | 88 +++++ setup.cfg | 2 + tests/etc/default_functional.conf | 9 +- .../data_post_background_jobs.json | 14 + .../data_post_background_jobs_bad_input.json | 22 ++ .../pecan/controllers/test_background_jobs.py | 51 +++ .../pecan/controllers/test_ssl_certificate.py | 2 +- .../akamai/background_jobs/__init__.py | 0 .../akamai/background_jobs/akamai_mocks.py | 321 ++++++++++++++++++ .../akamai/background_jobs/test_flows.py | 66 ++++ ...ain.json => data_get_certs_by_domain.json} | 0 tests/unit/storage/cassandra/test_services.py | 6 +- 35 files changed, 1482 insertions(+), 18 deletions(-) create mode 100644 poppy/cmd/akamai_check_and_update_cert_status.py create mode 100644 poppy/cmd/akamai_update_papi_property_for_mod_san.py create mode 100644 poppy/manager/base/background_job.py create mode 100644 poppy/manager/default/background_job.py create mode 100644 poppy/provider/akamai/background_jobs/__init__.py create mode 100644 poppy/provider/akamai/background_jobs/check_cert_status_and_update/__init__.py create mode 100644 poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_flow.py create mode 100644 poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py create mode 100644 poppy/provider/akamai/background_jobs/update_property/__init__.py create mode 100644 poppy/provider/akamai/background_jobs/update_property/update_property_flow.py create mode 100644 poppy/provider/akamai/background_jobs/update_property/update_property_tasks.py create mode 100644 poppy/transport/validators/schemas/background_jobs.py create mode 100644 tests/functional/transport/pecan/controllers/data_post_background_jobs.json create mode 100644 tests/functional/transport/pecan/controllers/data_post_background_jobs_bad_input.json create mode 100644 tests/functional/transport/pecan/controllers/test_background_jobs.py create mode 100644 tests/unit/provider/akamai/background_jobs/__init__.py create mode 100644 tests/unit/provider/akamai/background_jobs/akamai_mocks.py create mode 100644 tests/unit/provider/akamai/background_jobs/test_flows.py rename tests/unit/storage/cassandra/{data_get_cert_by_domain.json => data_get_certs_by_domain.json} (100%) diff --git a/poppy/cmd/akamai_check_and_update_cert_status.py b/poppy/cmd/akamai_check_and_update_cert_status.py new file mode 100644 index 00000000..164ab73e --- /dev/null +++ b/poppy/cmd/akamai_check_and_update_cert_status.py @@ -0,0 +1,64 @@ +# 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.common import cli +from poppy.openstack.common import log +from poppy.provider.akamai.background_jobs.check_cert_status_and_update import \ + check_cert_status_and_update_flow + +LOG = log.getLogger(__name__) + +CLI_OPT = [ + cfg.StrOpt('domain_name', + required=True, + help='The domain you want to check cert status on'), + cfg.StrOpt('cert_type', + default='san', + help='Cert type of this cert'), + cfg.StrOpt('project_id', + required=True, + help='project id of this cert'), + cfg.StrOpt('flavor_id', + default='cdn', + help='flavor id of this cert'), +] + + +@cli.runnable +def run(): + # TODO(kgriffs): For now, we have to use the global config + # to pick up common options from openstack.common.log, since + # that module uses the global CONF instance exclusively. + conf = cfg.ConfigOpts() + conf.register_cli_opts(CLI_OPT) + conf(prog='akamai-cert-check') + + LOG.info('Starting to check status on domain: %s, for project_id: %s' + 'flavor_id: %s, cert_type: %s' % + ( + conf.domain_name, + conf.project_id, + conf.flavor_id, + conf.cert_type + )) + + check_cert_status_and_update_flow.run_check_cert_status_and_update_flow( + conf.domain_name, + conf.cert_type, + conf.flavor_id, + conf.project_id + ) diff --git a/poppy/cmd/akamai_update_papi_property_for_mod_san.py b/poppy/cmd/akamai_update_papi_property_for_mod_san.py new file mode 100644 index 00000000..03636668 --- /dev/null +++ b/poppy/cmd/akamai_update_papi_property_for_mod_san.py @@ -0,0 +1,78 @@ +# 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 json + +from oslo_config import cfg + +from poppy.common import cli +from poppy.openstack.common import log +from poppy.provider.akamai.background_jobs.update_property import \ + update_property_flow + +LOG = log.getLogger(__name__) + +CLI_OPT = [ + cfg.StrOpt('domain_name', + required=True, + help='The domain you want to add in host name (cnameFrom)'), + cfg.StrOpt('san_cert_name', + required=True, + help='Cert type of this cert'), + cfg.StrOpt('update_type', + default="hostsnames", + help='Update type for this update, available types are:' + 'hostsnames, secureEdgeHost, rules'), + cfg.StrOpt('action', + default="add", + help='What kind of action, do you want "add" or "remove" ' + 'hostnames'), + cfg.StrOpt('property_spec', + default='akamai_https_san_config_numbers', + help='Property spec of the property to be updated'), + cfg.StrOpt('san_cert_domain_suffix', + default='edgekey.net', + help='Property spec of the property to be updated'), +] + + +@cli.runnable +def run(): + # TODO(kgriffs): For now, we have to use the global config + # to pick up common options from openstack.common.log, since + # that module uses the global CONF instance exclusively. + conf = cfg.ConfigOpts() + conf.register_cli_opts(CLI_OPT) + conf(prog='akamai-papi-update') + + LOG.info("%s: %s to %s, on property: %s" % ( + conf.action, + conf.domain_name, + conf.san_cert_name, + conf.property_spec + )) + + update_info_list = json.dumps([ + (conf.action, + { + "cnameFrom": conf.domain_name, + "cnameTo": '.'.join([conf.san_cert_name, + conf.san_cert_domain_suffix]), + "cnameType": "EDGE_HOSTNAME" + }) + ]) + + update_property_flow.run_update_property_flow( + conf.property_spec, conf.update_type, update_info_list) diff --git a/poppy/distributed_task/taskflow/task/create_service_tasks.py b/poppy/distributed_task/taskflow/task/create_service_tasks.py index 8893977b..881df950 100644 --- a/poppy/distributed_task/taskflow/task/create_service_tasks.py +++ b/poppy/distributed_task/taskflow/task/create_service_tasks.py @@ -55,9 +55,14 @@ class CreateProviderServicesTask(task.Task): for domain in service_obj.domains: if domain.certificate == 'san': cert_for_domain = ( - self.storage_controller.get_cert_by_domain( - domain.domain, domain.certificate, - service_obj.flavor_id, project_id)) + self.storage_controller.get_certs_by_domain( + domain.domain, + project_id=project_id, + flavor_id=service_obj.flavor_id, + cert_type=domain.certificate + )) + if cert_for_domain == []: + cert_for_domain = None domain.cert_info = cert_for_domain except ValueError: msg = 'Creating service {0} from Poppy failed. ' \ diff --git a/poppy/distributed_task/utils/memoized_controllers.py b/poppy/distributed_task/utils/memoized_controllers.py index 476d4261..c1e50de7 100644 --- a/poppy/distributed_task/utils/memoized_controllers.py +++ b/poppy/distributed_task/utils/memoized_controllers.py @@ -46,6 +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 == 'providers': + return service_controller, bootstrap_obj.manager.providers if controller == 'ssl_certificate': return service_controller, ( bootstrap_obj.manager.ssl_certificate_controller) diff --git a/poppy/manager/base/__init__.py b/poppy/manager/base/__init__.py index 6f13cabb..5be28586 100644 --- a/poppy/manager/base/__init__.py +++ b/poppy/manager/base/__init__.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from poppy.manager.base import background_job from poppy.manager.base import driver from poppy.manager.base import flavors from poppy.manager.base import home @@ -22,6 +23,7 @@ from poppy.manager.base import ssl_certificate Driver = driver.ManagerDriverBase +BackgroundJobController = background_job.BackgroundJobControllerBase FlavorsController = flavors.FlavorsControllerBase ServicesController = services.ServicesControllerBase HomeController = home.HomeControllerBase diff --git a/poppy/manager/base/background_job.py b/poppy/manager/base/background_job.py new file mode 100644 index 00000000..8bc72df7 --- /dev/null +++ b/poppy/manager/base/background_job.py @@ -0,0 +1,36 @@ +# 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 BackgroundJobControllerBase(controller.ManagerControllerBase): + """Health controller base class.""" + + def __init__(self, manager): + super(BackgroundJobControllerBase, self).__init__(manager) + + @abc.abstractmethod + def post_job(self, job_type, args): + """Returns the health of storage and providers + + :raises: NotImplementedError + """ + raise NotImplementedError diff --git a/poppy/manager/default/background_job.py b/poppy/manager/default/background_job.py new file mode 100644 index 00000000..eb76112f --- /dev/null +++ b/poppy/manager/default/background_job.py @@ -0,0 +1,95 @@ +# 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 poppy.manager import base +from poppy.notification.mailgun import driver as n_driver +from poppy.openstack.common import log +from poppy.provider.akamai.background_jobs.check_cert_status_and_update import \ + check_cert_status_and_update_flow +from poppy.provider.akamai.background_jobs.update_property import \ + update_property_flow +from poppy.provider.akamai import driver as a_driver + +conf = cfg.CONF +conf(project='poppy', prog='poppy', args=[]) +LOG = log.getLogger(__name__) + + +class BackgroundJobController(base.BackgroundJobController): + + def __init__(self, manager): + super(BackgroundJobController, self).__init__(manager) + self.distributed_task_controller = ( + self._driver.distributed_task.services_controller) + self.akamai_san_cert_suffix = ( + conf[a_driver.AKAMAI_GROUP].akamai_https_access_url_suffix) + self.notify_email_list = ( + conf[n_driver.MAIL_NOTIFICATION_GROUP].recipients) + + def post_job(self, job_type, kwargs): + kwargs = kwargs + if job_type == "akamai_check_and_update_cert_status": + LOG.info('Starting to check status on domain: %s,' + 'for project_id: %s' + 'flavor_id: %s, cert_type: %s' % + ( + kwargs.get("domain_name"), + kwargs.get("project_id"), + kwargs.get("flavor_id"), + kwargs.get("cert_type") + )) + self.distributed_task_controller.submit_task( + check_cert_status_and_update_flow. + check_cert_status_and_update_flow, + **kwargs) + elif job_type == "akamai_update_papi_property_for_mod_san": + LOG.info("%s: %s to %s, on property: %s" % ( + kwargs.get("action", 'add'), + kwargs.get("domain_name"), + kwargs.get("san_cert_name"), + kwargs.get("property_spec", 'akamai_https_san_config_numbers') + )) + + t_kwargs = {} + + update_info_list = json.dumps([ + (kwargs.get("property_spec", 'add'), + { + "cnameFrom": kwargs.get("domain_name"), + "cnameTo": '.'.join([kwargs.get("san_cert_name"), + kwargs.get( + "san_cert_domain_suffix", + self.akamai_san_cert_suffix)]), + "cnameType": "EDGE_HOSTNAME" + }) + ]) + + t_kwargs = { + "property_spec": kwargs.get("property_spec", + 'akamai_https_san_config_numbers'), + "update_type": kwargs.get("update_type", 'hostnames'), + "update_info_list": update_info_list, + "notify_email_list": self.notify_email_list + } + + self.distributed_task_controller.submit_task( + update_property_flow.update_property_flow, + **t_kwargs) + else: + raise NotImplementedError('job type: %s has not been implemented') diff --git a/poppy/manager/default/controllers.py b/poppy/manager/default/controllers.py index d136667e..2f9b72d4 100644 --- a/poppy/manager/default/controllers.py +++ b/poppy/manager/default/controllers.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from poppy.manager.default import background_job from poppy.manager.default import flavors from poppy.manager.default import health from poppy.manager.default import home @@ -20,6 +21,7 @@ from poppy.manager.default import services from poppy.manager.default import ssl_certificate +BackgroundJob = background_job.BackgroundJobController Home = home.DefaultHomeController Flavors = flavors.DefaultFlavorsController Health = health.DefaultHealthController diff --git a/poppy/manager/default/driver.py b/poppy/manager/default/driver.py index b12c1fb9..1586e7f9 100644 --- a/poppy/manager/default/driver.py +++ b/poppy/manager/default/driver.py @@ -44,6 +44,10 @@ class DefaultManagerDriver(base.Driver): def health_controller(self): return controllers.Health(self) + @decorators.lazy_property(write=False) + def background_job_controller(self): + return controllers.BackgroundJob(self) + @decorators.lazy_property(write=False) def ssl_certificate_controller(self): return controllers.SSLCertificate(self) diff --git a/poppy/manager/default/services.py b/poppy/manager/default/services.py index e0035ce4..d38f627b 100644 --- a/poppy/manager/default/services.py +++ b/poppy/manager/default/services.py @@ -239,6 +239,18 @@ class DefaultServicesController(base.ServicesController): existing_shared_domains[customer_domain] = domain.domain domain.domain = customer_domain + # old domains need to bind as well + elif domain.certificate == 'san': + cert_for_domain = ( + self.storage_controller.get_certs_by_domain( + domain.domain, + project_id=project_id, + flavor_id=service_old.flavor_id, + cert_type=domain.certificate)) + if cert_for_domain == []: + cert_for_domain = None + domain.cert_info = cert_for_domain + service_old_json = json.loads(json.dumps(service_old.to_dict())) # remove fields that cannot be part of PATCH @@ -293,9 +305,13 @@ class DefaultServicesController(base.ServicesController): elif domain.certificate == 'san': cert_for_domain = ( - self.storage_controller.get_cert_by_domain( - domain.domain, domain.certificate, - service_new.flavor_id, project_id)) + self.storage_controller.get_certs_by_domain( + domain.domain, + project_id=project_id, + flavor_id=service_new.flavor_id, + cert_type=domain.certificate)) + if cert_for_domain == []: + cert_for_domain = None domain.cert_info = cert_for_domain # retrofit the access url info into diff --git a/poppy/provider/akamai/background_jobs/__init__.py b/poppy/provider/akamai/background_jobs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poppy/provider/akamai/background_jobs/check_cert_status_and_update/__init__.py b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_flow.py b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_flow.py new file mode 100644 index 00000000..412b549f --- /dev/null +++ b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_flow.py @@ -0,0 +1,52 @@ +# 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 taskflow import engines +from taskflow.patterns import linear_flow + +from poppy.openstack.common import log +from poppy.provider.akamai.background_jobs.check_cert_status_and_update import \ + check_cert_status_and_update_tasks + + +LOG = log.getLogger(__name__) + + +conf = cfg.CONF +conf(project='poppy', prog='poppy', args=[]) + + +def check_cert_status_and_update_flow(): + flow = linear_flow.Flow('Update Akamai Property').add( + check_cert_status_and_update_tasks.GetCertInfoTask(), + check_cert_status_and_update_tasks.CheckCertStatusTask(), + check_cert_status_and_update_tasks.UpdateCertStatusTask() + ) + return flow + + +def run_check_cert_status_and_update_flow(domain_name, cert_type, flavor_id, + project_id): + e = engines.load( + check_cert_status_and_update_flow(), + store={ + 'domain_name': domain_name, + 'cert_type': cert_type, + 'flavor_id': flavor_id, + 'project_id': project_id + }, + engine='serial') + e.run() diff --git a/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py new file mode 100644 index 00000000..3c2ca828 --- /dev/null +++ b/poppy/provider/akamai/background_jobs/check_cert_status_and_update/check_cert_status_and_update_tasks.py @@ -0,0 +1,124 @@ +# 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 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 GetCertInfoTask(task.Task): + default_provides = "cert_obj_json" + + def execute(self, domain_name, cert_type, flavor_id, project_id): + service_controller, self.storage_controller = \ + memoized_controllers.task_controllers('poppy', 'storage') + res = self.storage_controller.get_certs_by_domain( + domain_name, project_id=project_id, + flavor_id=flavor_id, cert_type=cert_type) + if res is None: + return "" + return json.dumps(res.to_dict()) + + +class CheckCertStatusTask(task.Task): + default_provides = "status_change_to" + + def __init__(self): + super(CheckCertStatusTask, self).__init__() + service_controller, self.providers = \ + memoized_controllers.task_controllers('poppy', 'providers') + self.akamai_driver = self.providers['akamai'].obj + + def execute(self, cert_obj_json): + if cert_obj_json != "": + cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json) + ) + latest_sps_id = cert_obj.cert_details['Akamai']['extra_info'].get( + 'akamai_spsId') + + if latest_sps_id is None: + return "" + + resp = self.akamai_driver.akamai_sps_api_client.get( + self.akamai_driver.akamai_sps_api_base_url.format( + spsId=latest_sps_id + ) + ) + + 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.cert) + return "" + else: + LOG.info("SPS completed for %s..." % + cert_obj.get_san_edge_name()) + return "deployed" + + +class UpdateCertStatusTask(task.Task): + + def __init__(self): + super(UpdateCertStatusTask, self).__init__() + service_controller, self.storage_controller = \ + memoized_controllers.task_controllers('poppy', 'storage') + + def execute(self, project_id, cert_obj_json, status_change_to): + if cert_obj_json != "": + cert_obj = ssl_certificate.load_from_json(json.loads(cert_obj_json) + ) + cert_details = cert_obj.cert_details + + if status_change_to == "deployed": + cert_details['Akamai']['extra_info']['status'] = 'deployed' + cert_details['Akamai'] = json.dumps(cert_details['Akamai']) + self.storage_controller.update_cert_info(cert_obj.domain_name, + cert_obj.cert_type, + cert_obj.flavor_id, + cert_details) + + service_obj = ( + self.storage_controller. + get_service_details_by_domain_name(cert_obj.domain_name) + ) + # Update provider details + if service_obj is not None: + service_obj.provider_details['Akamai'].\ + domains_certificate_status.\ + set_domain_certificate_status(cert_obj.domain_name, + 'deployed') + self.storage_controller.update_provider_details( + project_id, + service_obj.service_id, + service_obj.provider_details + ) + else: + pass diff --git a/poppy/provider/akamai/background_jobs/update_property/__init__.py b/poppy/provider/akamai/background_jobs/update_property/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/poppy/provider/akamai/background_jobs/update_property/update_property_flow.py b/poppy/provider/akamai/background_jobs/update_property/update_property_flow.py new file mode 100644 index 00000000..a14d0df4 --- /dev/null +++ b/poppy/provider/akamai/background_jobs/update_property/update_property_flow.py @@ -0,0 +1,50 @@ +# 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 taskflow import engines +from taskflow.patterns import linear_flow + +from poppy.openstack.common import log +from poppy.provider.akamai.background_jobs.update_property import ( + update_property_tasks) + + +LOG = log.getLogger(__name__) + + +conf = cfg.CONF +conf(project='poppy', prog='poppy', args=[]) + + +def update_property_flow(): + flow = linear_flow.Flow('Update Akamai Property').add( + update_property_tasks.PropertyGetLatestVersionTask(), + update_property_tasks.PropertyUpdateTask(), + update_property_tasks.PropertyActivateTask() + ) + return flow + + +def run_update_property_flow(property_spec, update_type, update_info_list): + e = engines.load( + update_property_flow(), + store={ + "property_spec": property_spec, + "update_type": update_type, + "update_info_list": update_info_list + }, + engine='serial') + e.run() diff --git a/poppy/provider/akamai/background_jobs/update_property/update_property_tasks.py b/poppy/provider/akamai/background_jobs/update_property/update_property_tasks.py new file mode 100644 index 00000000..96dd7565 --- /dev/null +++ b/poppy/provider/akamai/background_jobs/update_property/update_property_tasks.py @@ -0,0 +1,276 @@ +# 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 json + +from oslo_config import cfg +from taskflow import task + +from poppy.distributed_task.utils import memoized_controllers +from poppy.openstack.common import log + + +LOG = log.getLogger(__name__) +conf = cfg.CONF +conf(project='poppy', prog='poppy', args=[]) + + +class PropertyGetLatestVersionTask(task.Task): + default_provides = "new_version_number" + + def __init__(self): + super(PropertyGetLatestVersionTask, self).__init__() + service_controller, self.providers = \ + memoized_controllers.task_controllers('poppy', 'providers') + self.akamai_driver = self.providers['akamai'].obj + self.sc = self.akamai_driver.service_controller + self.akamai_conf = self.akamai_driver.akamai_conf + + def execute(self, property_spec): + """Get/Create a new Akamai property update version if necessary""" + self.property_id = self.akamai_driver.papi_property_id(property_spec) + + LOG.info('Starting to get next version for property: %s' + % self.property_id) + resp = self.akamai_driver.akamai_papi_api_client.get( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s' % self.property_id) + ) + if resp.status_code != 200: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + else: + a_property = json.loads(resp.text)['properties']['items'][0] + latestVersion = a_property['latestVersion'] or 0 + production_version = a_property['productionVersion'] or -1 + staging_version = a_property['stagingVersion'] or -1 + + max_version = max(latestVersion, production_version, + staging_version) + + if production_version == -1 and staging_version == -1: + # if the max version has not been activated yet + # we just reuse the max version + LOG.info("New version for : %s is %s" % (self.property_id, + str(max_version))) + return max_version + else: + # else now we need to create a new version (bump up a version) + resp = self.akamai_driver.akamai_papi_api_client.get( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s/versions/%s' % ( + self.property_id, str(max_version))) + ) + if resp.status_code != 200: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + etag = json.loads(resp.text)['versions']['items'][0]['etag'] + # create a new version + resp = self.akamai_driver.akamai_papi_api_client.post( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s/versions' % ( + self.property_id)), + data=json.dumps({ + 'createFromVersion': max_version, + 'createFromEtag': etag + }), + headers={'Content-type': 'application/json'} + ) + + if resp.status_code != 201: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + LOG.info("New version for : %s is %s" % (self.property_id, + str(max_version+1))) + return max_version + 1 + + +class PropertyUpdateTask(task.Task): + default_provides = 'update_detail' + + def __init__(self): + super(PropertyUpdateTask, self).__init__() + service_controller, self.providers = \ + memoized_controllers.task_controllers('poppy', 'providers') + self.akamai_driver = self.providers['akamai'].obj + self.sc = self.akamai_driver.service_controller + self.akamai_conf = self.akamai_driver.akamai_conf + + self.existing_hosts = [] + self.existing_edgehostnames = [] + + def execute(self, property_spec, new_version_number, update_type, + update_info_list): + """Update an Akamai property""" + self.property_id = self.akamai_driver.papi_property_id(property_spec) + + update_info_list = json.loads(update_info_list) + update_detail = "" + + # avoid loading hostnames multiple times + if update_type == 'hostnames': + if self.existing_hosts == []: + LOG.info("Getting Hostnames...") + resp = self.akamai_driver.akamai_papi_api_client.get( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s/versions/%s/hostnames' % + (self.property_id, + str(new_version_number))) + ) + if resp.status_code != 200: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + self.existing_hosts = json.loads(resp.text)['hostnames'][ + 'items'] + # message should be a list assembled hosts dictionary + for action, host_info in update_info_list: + # add new hosts + if action == 'add': + cnameToEdgeHostname = host_info['cnameTo'] + # avoid loading edgehostnames multiple times + if self.existing_edgehostnames == []: + LOG.info("Getting EdgeHostnames...") + resp = self.akamai_driver.akamai_papi_api_client.\ + get( + self.akamai_driver. + akamai_papi_api_base_url.format( + middle_part='edgehostnames') + ) + + if resp.status_code != 200: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + self.existing_edgehostnames = ( + json.loads(resp.text)['edgeHostnames']['items'] + ) + + for edgehostname in self.existing_edgehostnames: + if (edgehostname['domainPrefix'] == + cnameToEdgeHostname.replace( + edgehostname['domainSuffix'], "")[:-1]): + host_info['edgeHostnameId'] = ( + edgehostname['edgeHostnameId']) + + self.existing_hosts.append(host_info) + update_detail = "Add cnameFrom: %s to cnameTo %s" % ( + host_info['cnameFrom'], host_info['cnameTo']) + # remove a hosts + elif action == 'remove': + for idx, existing_host_info in enumerate( + self.existing_hosts): + if existing_host_info['cnameFrom'] == ( + host_info['cnameFrom']): + del self.existing_hosts[idx] + break + update_detail = ("Remove cnameFrom: %s to cnameTo %s" + % (host_info['cnameFrom'], + host_info['cnameTo'])) + + LOG.info('Start Updating Hostnames: %s' % + str(self.existing_hosts)) + resp = self.akamai_driver.akamai_papi_api_client.put( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s/versions/%s/hostnames' % ( + self.property_id, + str(new_version_number))), + data=json.dumps(self.existing_hosts), + headers={'Content-type': 'application/json'} + ) + + if resp.status_code != 200: + LOG.info("Updating property hostnames response code: %s" % + str(resp.status_code)) + LOG.info("Updating property hostnames response text: %s" % + str(resp.text)) + else: + LOG.info("Update property hostnames successful...") + # Handle secureEdgeHost addition + elif update_type == 'secureEdgeHost': + # Note(tonytan4ever): This will be used when adding custom cert + pass + elif update_type == 'rules': + pass + + return update_detail + + +class PropertyActivateTask(task.Task): + + def __init__(self): + super(PropertyActivateTask, self).__init__() + service_controller, self.providers = \ + memoized_controllers.task_controllers('poppy', 'providers') + self.akamai_driver = self.providers['akamai'].obj + self.akamai_conf = self.akamai_driver.akamai_conf + self.sc = self.akamai_driver.service_controller + + def execute(self, property_spec, new_version_number, update_detail, + notify_email_list=[]): + """Update an Akamai property""" + self.property_id = self.akamai_driver.papi_property_id(property_spec) + + # This request needs json + LOG.info('Starting activating version: %s for property: %s' % + (new_version_number, + self.property_id)) + data = { + 'propertyVersion': new_version_number, + 'network': 'PRODUCTION', + 'note': 'Updating configuration for property %s: %s' % ( + self.property_id, update_detail), + 'notifyEmails': notify_email_list, + } + resp = self.akamai_driver.akamai_papi_api_client.post( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s/activations' % + self.property_id), + data=json.dumps(data), + headers={'Content-type': 'application/json'} + ) + # Here activation API call will return a 400, + # with all the messageIds need to handle that not as + # an exception + if resp.status_code != 201 and resp.status_code != 400: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + # else extract out all the warnings + # acknowledgementWarning + if resp.status_code == 400: + LOG.info("response text: %s" % resp.text) + warnings = [warning['messageId'] for warning in + json.loads(resp.text)['warnings']] + data['acknowledgeWarnings'] = warnings + resp = self.akamai_driver.akamai_papi_api_client.post( + self.akamai_driver.akamai_papi_api_base_url.format( + middle_part='properties/%s/activations/' % + self.property_id), + data=json.dumps(data), + headers={'Content-type': 'application/json'} + ) + if resp.status_code != 201: + raise RuntimeError('PAPI API request failed.' + 'Exception: %s' % resp.text) + + # first get the activation id + # activation id is inside of activation link itself + activation_link = json.loads(resp.text)['activationLink'] + LOG.info("Activation link: %s" % activation_link) + + return { + "activation_link": activation_link + } + + def revert(self, property_spec, new_version_number, **kwargs): + LOG.info('retrying task: %s ...' % self.name) diff --git a/poppy/provider/akamai/driver.py b/poppy/provider/akamai/driver.py index 1022498d..e3a2573e 100644 --- a/poppy/provider/akamai/driver.py +++ b/poppy/provider/akamai/driver.py @@ -102,14 +102,18 @@ AKAMAI_OPTIONS = [ cfg.StrOpt( 'group_id', help='Operator groupID'), - cfg.StrOpt( - 'property_id', - help='Operator propertyID') ] AKAMAI_GROUP = 'drivers:provider:akamai' +VALID_PROPERTY_SPEC = [ + "akamai_http_config_number", + "akamai_https_shared_config_number", + "akamai_https_san_config_numbers", + "akamai_https_custom_config_numbers"] + + class CDNProvider(base.Driver): def __init__(self, conf): @@ -164,11 +168,20 @@ class CDNProvider(base.Driver): ) ]) - self.akamai_sps_api_client = self.akamai_policy_api_client + self.akamai_papi_api_base_url = ''.join([ + str(self.akamai_conf.policy_api_base_url), + 'papi/v0/{middle_part}/' + '?contractId=ctr_%s&groupId=grp_%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.akamai_papi_api_client = self.akamai_policy_api_client + self.mod_san_queue = ( zookeeper_queue.ZookeeperModSanQueue(self._conf)) @@ -229,6 +242,16 @@ class CDNProvider(base.Driver): def papi_api_client(self): return self.akamai_papi_api_client + def papi_property_id(self, property_spec): + if property_spec not in VALID_PROPERTY_SPEC: + raise ValueError('No a valid property spec: %s' + ', valid property specs are: %s' + % (property_spec, VALID_PROPERTY_SPEC)) + prp_number = self.akamai_conf[property_spec] + if isinstance(prp_number, list): + prp_number = prp_number[0] + return 'prp_%s' % self.akamai_conf[property_spec][0] + @property def service_controller(self): """Returns the driver's hostname controller.""" diff --git a/poppy/storage/cassandra/services.py b/poppy/storage/cassandra/services.py index d7c988d2..519e314b 100644 --- a/poppy/storage/cassandra/services.py +++ b/poppy/storage/cassandra/services.py @@ -892,10 +892,13 @@ class ServicesController(base.ServicesController): CQL_SEARCH_BY_DOMAIN, consistency_level=self._driver.consistency_level) results = self.session.execute(stmt, args) + # If there is not service with this domain + # return None + details = None for r in results: proj_id = r.get('project_id') service = r.get('service_id') - details = self.get(proj_id, service) + details = self.get(proj_id, service) return details def update_provider_details(self, project_id, service_id, diff --git a/poppy/storage/mockdb/services.py b/poppy/storage/mockdb/services.py index 4ce46c54..ed18b5eb 100644 --- a/poppy/storage/mockdb/services.py +++ b/poppy/storage/mockdb/services.py @@ -14,11 +14,13 @@ # limitations under the License. import json +import random from poppy.model.helpers import domain from poppy.model.helpers import origin from poppy.model.helpers import provider_details from poppy.model import service +from poppy.model import ssl_certificate from poppy.storage import base @@ -166,6 +168,9 @@ class ServicesController(base.ServicesController): if key in self.certs: self.certs[key].cert_details = cert_details + def get_service_details_by_domain_name(self, domain_name): + pass + def create_cert(self, project_id, cert_obj): key = (cert_obj.flavor_id, cert_obj.domain_name, cert_obj.cert_type) if key not in self.certs: @@ -173,12 +178,33 @@ class ServicesController(base.ServicesController): else: raise ValueError - def get_certs_by_domain(self, domain_name, project_id=None): + def get_certs_by_domain(self, domain_name, project_id=None, flavor_id=None, + cert_type=None): certs = [] for cert in self.certs: if domain_name in cert: certs.append(self.certs[cert]) if project_id: + if flavor_id is not None and cert_type is not None: + return ssl_certificate.SSLCertificate( + "premium", + "blog.testabcd.com", + "san", + project_id=project_id, + cert_details={ + 'Akamai': { + u'cert_domain': u'secure2.san1.test_123.com', + u'extra_info': { + u'action': u'Waiting for customer domain ' + 'validation for blog.testabc.com', + u'akamai_spsId': str(random.randint(1, 100000) + ), + u'create_at': u'2015-09-29 16:09:12.429147', + u'san cert': u'secure2.san1.test_123.com', + u'status': u'create_in_progress'} + } + } + ) return [cert for cert in certs if cert.project_id == project_id] else: return certs diff --git a/poppy/transport/pecan/controllers/v1/admin.py b/poppy/transport/pecan/controllers/v1/admin.py index 3350b508..4152fbdd 100644 --- a/poppy/transport/pecan/controllers/v1/admin.py +++ b/poppy/transport/pecan/controllers/v1/admin.py @@ -22,6 +22,7 @@ from poppy.transport.pecan.controllers import base from poppy.transport.pecan import hooks as poppy_hooks from poppy.transport.pecan.models.response import service as resp_service_model from poppy.transport.validators import helpers +from poppy.transport.validators.schemas import background_jobs from poppy.transport.validators.schemas import domain_migration from poppy.transport.validators.schemas import service_action from poppy.transport.validators.schemas import service_limit @@ -73,10 +74,40 @@ class DomainMigrationController(base.Controller, hooks.HookController): return pecan.Response(None, 202) +class BackgroundJobController(base.Controller, hooks.HookController): + __hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()] + + def __init__(self, driver): + super(BackgroundJobController, self).__init__(driver) + + @pecan.expose('json') + @decorators.validate( + request=rule.Rule( + helpers.json_matches_service_schema( + background_jobs.BackgroundJobSchema.get_schema( + "background_jobs", "POST")), + helpers.abort_with_message, + stoplight_helpers.pecan_getter)) + def post(self): + request_json = json.loads(pecan.request.body.decode('utf-8')) + job_type = request_json.pop('job_type') + + try: + self._driver.manager.background_job_controller.post_job( + job_type, + request_json + ) + except NotImplementedError as e: + pecan.abort(404, str(e)) + + return pecan.Response(None, 202) + + class AkamaiController(base.Controller, hooks.HookController): def __init__(self, driver): super(AkamaiController, self).__init__(driver) self.__class__.service = DomainMigrationController(driver) + self.__class__.background_job = BackgroundJobController(driver) class ProviderController(base.Controller, hooks.HookController): diff --git a/poppy/transport/pecan/models/request/ssl_certificate.py b/poppy/transport/pecan/models/request/ssl_certificate.py index 6debabef..4fc0e0c0 100644 --- a/poppy/transport/pecan/models/request/ssl_certificate.py +++ b/poppy/transport/pecan/models/request/ssl_certificate.py @@ -20,5 +20,8 @@ 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") + project_id = json_data.get("project_id") + cert_details = json_data.get("cert_details", {}) - return ssl_certificate.SSLCertificate(flavor_id, domain_name, cert_type) + return ssl_certificate.SSLCertificate(flavor_id, domain_name, + cert_type, project_id, cert_details) diff --git a/poppy/transport/pecan/models/response/service.py b/poppy/transport/pecan/models/response/service.py index 0fa92457..e4b70704 100644 --- a/poppy/transport/pecan/models/response/service.py +++ b/poppy/transport/pecan/models/response/service.py @@ -108,6 +108,7 @@ class Model(collections.OrderedDict): continue except StopIteration: pass + if 'operator_url' in access_url: self['links'].append(link.Model( access_url['operator_url'], diff --git a/poppy/transport/validators/schemas/background_jobs.py b/poppy/transport/validators/schemas/background_jobs.py new file mode 100644 index 00000000..e30352fa --- /dev/null +++ b/poppy/transport/validators/schemas/background_jobs.py @@ -0,0 +1,88 @@ +# 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 BackgroundJobSchema(schema_base.SchemaBase): + + '''JSON Schmema validation for /admin/provider/akamai/background_jobs''' + + schema = { + 'background_jobs': { + 'POST': { + 'type': [{ + 'additionalProperties': False, + 'properties': { + 'job_type': { + 'type': 'string', + 'required': True, + 'enum': ['akamai_check_and_update_cert_status'] + }, + 'domain_name': { + 'type': 'string', + 'required': True + }, + 'project_id': { + 'type': 'string', + 'required': True + }, + 'cert_type': { + 'type': 'string', + 'required': True, + 'enum': ['san'] + }, + 'flavor_id': { + 'type': 'string', + 'required': True + } + } + }, + { + 'additionalProperties': False, + 'properties': { + 'job_type': { + 'type': 'string', + 'required': True, + 'enum': ['akamai_update_papi_property_for_mod_san'] + }, + 'domain_name': { + 'type': 'string', + 'required': True + }, + 'san_cert_name': { + 'type': 'string', + 'required': True + }, + 'update_type': { + 'type': 'string', + 'enum': ['hostsnames'] + }, + 'action': { + 'type': 'string', + 'enum': ['add', 'remove'] + }, + 'property_spec': { + 'type': 'string', + 'enum': ['akamai_https_san_config_numbers'] + }, + 'san_cert_domain_suffix': { + 'type': 'string' + } + } + }] + } + } + } diff --git a/setup.cfg b/setup.cfg index 20123684..8e3d6082 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,6 +34,8 @@ upload-dir = doc/build/html console_scripts = poppy-server = poppy.cmd.server:run poppy-worker = poppy.cmd.task_flow_worker:run + akamai-cert-status-check-and-update = poppy.cmd.akamai_check_and_update_cert_status:run + akamai-property-udpate-mod-san = poppy.cmd.akamai_update_papi_property_for_mod_san:run poppy.transport = pecan = poppy.transport.pecan:Driver diff --git a/tests/etc/default_functional.conf b/tests/etc/default_functional.conf index ba550084..a79ab339 100644 --- a/tests/etc/default_functional.conf +++ b/tests/etc/default_functional.conf @@ -1,9 +1,10 @@ [drivers] -providers = mock,maxcdn,cloudfront,fastly +providers = mock,maxcdn,akamai,fastly transport = pecan manager = default storage = mockdb dns = default +notifications = mailgun [drivers:storage:cassandra] cluster = "192.168.59.103" @@ -18,9 +19,15 @@ alias = "MYALIAS" consumer_secret = "MYCONSUMER_SECRET" consumer_key = "MYCONSUMERKEY" +[drivers:provider:akamai] +akamai_https_access_url_suffix = "my_https_url_suffix" + [drivers:provider:cloudfront] aws_access_key_id = "MY_AWS_ACCESS_KEY_ID" aws_secret_access_key = "MY_AWS_SECRET_ACCESS_KEY" +[drivers:notification:mailgun] +recipients = "myrecipients@abc.com" + [drivers:transport:limits] max_services_per_page = 20 diff --git a/tests/functional/transport/pecan/controllers/data_post_background_jobs.json b/tests/functional/transport/pecan/controllers/data_post_background_jobs.json new file mode 100644 index 00000000..8ce8228e --- /dev/null +++ b/tests/functional/transport/pecan/controllers/data_post_background_jobs.json @@ -0,0 +1,14 @@ +{ + "akamai_check_and_update_cert_status": { + "job_type": "akamai_check_and_update_cert_status", + "domain_name": "www.abc.com", + "flavor_id": "mock", + "cert_type": "san", + "project_id": "000" + }, + "akamai_update_papi_property_for_mod_san": { + "job_type": "akamai_update_papi_property_for_mod_san", + "domain_name": "www.abc.com", + "san_cert_name": "secure1.test_san.com" + } +} \ No newline at end of file diff --git a/tests/functional/transport/pecan/controllers/data_post_background_jobs_bad_input.json b/tests/functional/transport/pecan/controllers/data_post_background_jobs_bad_input.json new file mode 100644 index 00000000..e0aa72a0 --- /dev/null +++ b/tests/functional/transport/pecan/controllers/data_post_background_jobs_bad_input.json @@ -0,0 +1,22 @@ +{ + "invalid_job_type_name": { + "job_type": "nonsense", + "flavor_id": "mock" + }, + "missing_cert_type": { + "job_type": "akamai_check_and_update_cert_status", + "domain_name": "www.abc.com", + "flavor_id": "mock", + "project_id": "000" + }, + "missing_domain_name": { + "job_type": "akamai_check_and_update_cert_status", + "cert_type": "san", + "flavor_id": "mock", + "project_id": "000" + }, + "missing_san_cert_name": { + "job_type": "akamai_check_and_update_cert_status", + "domain_name": "www.abc.com" + } +} \ No newline at end of file diff --git a/tests/functional/transport/pecan/controllers/test_background_jobs.py b/tests/functional/transport/pecan/controllers/test_background_jobs.py new file mode 100644 index 00000000..4c6ea0c0 --- /dev/null +++ b/tests/functional/transport/pecan/controllers/test_background_jobs.py @@ -0,0 +1,51 @@ +# 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 json +import uuid + +import ddt + +from tests.functional.transport.pecan import base + + +@ddt.ddt +class BackgroundJobControllerTest(base.FunctionalTest): + + def setUp(self): + super(BackgroundJobControllerTest, self).setUp() + + self.project_id = str(uuid.uuid1()) + self.service_name = str(uuid.uuid1()) + self.flavor_id = str(uuid.uuid1()) + + @ddt.file_data("data_post_background_jobs_bad_input.json") + def test_post_background_job_negative(self, background_job_json): + response = self.app.post('/v1.0/admin/provider/akamai/background_job', + headers={'Content-Type': 'application/json', + 'X-Project-ID': self.project_id}, + params=json.dumps(background_job_json), + expect_errors=True) + + self.assertEqual(400, response.status_code) + + @ddt.file_data("data_post_background_jobs.json") + def test_post_background_job_positive(self, background_job_json): + response = self.app.post('/v1.0/admin/provider/akamai/background_job', + headers={'Content-Type': 'application/json', + 'X-Project-ID': self.project_id}, + params=json.dumps(background_job_json)) + + self.assertEqual(202, response.status_code) diff --git a/tests/functional/transport/pecan/controllers/test_ssl_certificate.py b/tests/functional/transport/pecan/controllers/test_ssl_certificate.py index d60857fe..5d1746b6 100644 --- a/tests/functional/transport/pecan/controllers/test_ssl_certificate.py +++ b/tests/functional/transport/pecan/controllers/test_ssl_certificate.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014 Rackspace, Inc. +# 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. diff --git a/tests/unit/provider/akamai/background_jobs/__init__.py b/tests/unit/provider/akamai/background_jobs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/unit/provider/akamai/background_jobs/akamai_mocks.py b/tests/unit/provider/akamai/background_jobs/akamai_mocks.py new file mode 100644 index 00000000..408bb013 --- /dev/null +++ b/tests/unit/provider/akamai/background_jobs/akamai_mocks.py @@ -0,0 +1,321 @@ +# 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 random +import uuid + +import json +import mock + +from poppy.model.helpers import domain +from poppy.model.helpers import origin +from poppy.model import service +from poppy.model import ssl_certificate + + +class MockBootStrap(mock.Mock): + + def __init__(self, conf): + super(MockBootStrap, self).__init__() + + @property + def manager(self): + return MockManager() + + +class MockProviderDetails(mock.Mock): + + def __init__(self, service_id): + super(MockProviderDetails, self).__init__() + self.service_id = service_id + + def __getitem__(self, provider_name): + akamai_provider_detail_mock = mock.Mock() + akamai_provider_detail_mock.provider_service_id = ''.join([ + '[{"protocol":', + ' "https", "certificate": ', + '"san", "policy_name": "blog.testabc.com"}]']) + akamai_provider_detail_mock.status = 'deployed' + return akamai_provider_detail_mock + + +class MockManager(mock.Mock): + def __init__(self): + super(MockManager, self).__init__() + + @property + def providers(self): + akamai_mock_provider = mock.Mock() + akamai_mock_provider_obj = mock.Mock() + akamai_mock_provider_obj.service_controller = mock.Mock() + akamai_mock_provider_obj.akamai_conf = { + 'property_id': 'prp_12345', + 'contract_id': "B-ABCDE", + 'group_id': 12345 + } + akamai_mock_provider_obj.akamai_sps_api_client = MockSPSAPIClient() + akamai_mock_provider_obj.akamai_papi_api_client = MockPapiAPIClient() + akamai_mock_provider_obj.akamai_sps_api_base_url = ( + 'https://mybaseurl.net/config-secure-provisioning-service/' + 'v1/sps-requests/{spsId}?' + 'contractId=None&groupId=None') + akamai_mock_provider_obj.akamai_papi_api_base_url = ( + 'https://mybaseurl.net/papi/v0/{middle_part}/' + '?contractId=ctr_None&groupId=grp_None') + akamai_mock_provider.obj = akamai_mock_provider_obj + providers = { + 'akamai': akamai_mock_provider, + } + return providers + + @property + def services_controller(self): + sc = mock.Mock() + sc.storage_controller = MockStorageController() + return sc + + +class MockStorageController(mock.Mock): + + def get_certs_by_domain(self, domain_name, project_id=None, + flavor_id=None, + cert_type=None): + + return ssl_certificate.SSLCertificate( + "premium", + "blog.testabcd.com", + "san", + project_id=project_id, + cert_details={ + 'Akamai': { + u'cert_domain': u'secure2.san1.test_123.com', + u'extra_info': { + u'action': u'Waiting for customer domain ' + 'validation for blog.testabc.com', + u'akamai_spsId': str(random.randint(1, 100000)), + u'create_at': u'2015-09-29 16:09:12.429147', + u'san cert': u'secure2.san1.test_123.com', + u'status': u'create_in_progress'} + } + } + ) + + def get_service_details_by_domain_name(self, domain_name): + r = service.Service( + str(uuid.uuid4()), + str(uuid.uuid4()), + [domain.Domain('wiki.cc', 'https', 'shared')], + [origin.Origin('mysite.com')], + "strawberry") + r.provider_details = MockProviderDetails(r.service_id) + return r + + +class MockPapiAPIClient(mock.Mock): + def __init__(self): + super(MockPapiAPIClient, self).__init__() + self.response_200 = mock.Mock(status_code=200) + + def get(self, url): + if 'hostnames' in url: + self.response_200.text = json.dumps({ + "accountId": "act_1-ABCDE", + "contractId": "B-ABCDE", + "groupId": "grp_12345", + "propertyId": "prp_12345", + "propertyName": "ssl.san.test_123.com_pm", + "propertyVersion": 2, + "etag": str(uuid.uuid4()), + "hostnames": { + "items": [{ + "cnameType": "EDGE_HOSTNAME", + "edgeHostnameId": "ehn_1052022", + "cnameFrom": "www.testxxx.com", + "cnameTo": "ssl.test_123.com.edge_host_test.net" + }, { + "cnameType": "EDGE_HOSTNAME", + "edgeHostnameId": "ehn_1126816", + "cnameFrom": "secure.san2.test_789.com", + "cnameTo": "secure.test_456.com.edge_host_test.net" + }] + } + }) + if 'edgehostnames' in url: + self.response_200.text = json.dumps({ + "accountId": "act_1-ABCDE", + "contractId": "B-ABCDE", + "groupId": "grp_12345", + "edgeHostnames": { + "items": [{ + "cnameType": "EDGE_HOSTNAME", + "edgeHostnameId": "ehn_1052022", + "domainPrefix": "secure1.san1.test_123.com", + "domainSuffix": "edge_host_test.net", + "ipVersionBehavior": "IPV4", + "secure": True, + "edgeHostnameDomain": "secure1.san1.test_123.com" + ".edge_host_test.net" + }, { + "cnameType": "EDGE_HOSTNAME", + "edgeHostnameId": "ehn_1159587", + "domainPrefix": "secure2.san1.test_123.com", + "domainSuffix": "edge_host_test.net", + "ipVersionBehavior": "IPV4", + "secure": True, + "edgeHostnameDomain": "secure2.san1.test_123.com" + ".edge_host_test.net" + }] + } + }) + if 'activations' in url: + self.response_200.text = json.dumps({ + "activationId": "atv_2511473", + "status": "SUCCESS" + }) + if 'versions' in url: + self.response_200.text = json.dumps({ + "propertyId": "prp_12345", + "propertyName": "secure.test_123.com_pm", + "accountId": "act_1-ABCDE", + "contractId": "B-ABCDE", + "groupId": "grp_12345", + "versions": { + "items": [{ + "propertyVersion": 1, + "etag": str(uuid.uuid4()) + }] + } + }) + else: + self.response_200.text = json.dumps({ + "properties": { + "items": [{ + "accountId": "act_1-ABCDE", + "contractId": "B-ABCDE", + "groupId": "grp_12345", + "propertyId": "prp_12345", + "propertyName": "secure.test_123.com_pm", + "latestVersion": 2, + "stagingVersion": 2, + "productionVersion": 1 + }] + } + }) + self.response_200.status_code = 200 + return self.response_200 + + def post(self, url, data=None, headers=None): + if 'activations' in url: + self.response_200.status_code = 201 + self.response_200.text = json.dumps({ + "activationLink": "/papi/v0/properties/prp_227429/" + "activations/atv_2511473?contractId" + "=ctr_C-2M6JYA&groupId=grp_12345", + 'warnings': [] + }) + if 'versions' in url: + self.response_200.status_code = 201 + return self.response_200 + + def put(self, url, data=None, headers=None): + if 'hostnames' in url: + self.response_200.text = json.dumps({ + "accountId": "act_1-ABCDE", + "contractId": "B-ABCDE", + "groupId": "grp_12345", + "propertyId": "prp_12345", + "propertyName": "ssl.san.test_123.com_pm", + "propertyVersion": 2, + "etag": str(uuid.uuid4()), + "hostnames": { + "items": [{ + "cnameType": "EDGE_HOSTNAME", + "edgeHostnameId": "ehn_1052022", + "cnameFrom": "secure.san1.test_789.com", + "cnameTo": "ssl.test_123.com.edge_host_test.net" + }, { + "cnameType": "EDGE_HOSTNAME", + "edgeHostnameId": "ehn_1126816", + "cnameFrom": "secure.san2.test_789.com", + "cnameTo": "secure.test_456.com.edge_host_test.net" + }, { + 'cnameTo': 'secure.test_7891.com.edge_host_test.net', + 'cnameFrom': 'www.blogyyy.com', + 'edgeHostnameId': 'ehn_1126816', + 'cnameType': u'EDGE_HOSTNAME' + }, { + 'cnameTo': u'secure.test_7891.com.edge_host_test.net', + 'cnameFrom': u'www.testxxx.com', + 'edgeHostnameId': u'ehn_1126816', + 'cnameType': u'EDGE_HOSTNAME' + }] + } + }) + return self.response_200 + + +class MockSPSAPIClient(mock.Mock): + def __init__(self): + super(MockSPSAPIClient, self).__init__() + self.response_200 = mock.Mock(status_code=200) + + def get(self, url): + self.response_200.text = json.dumps({ + "requestList": + [{"resourceUrl": "/config-secure-provisioning-service/" + "v1/sps-requests/1849", + "parameters": [{ + "name": "cnameHostname", + "value": "secure.san3.test_123.com" + }, {"name": "createType", "value": "san"}, + {"name": "csr.cn", + "value": "secure.san3.test_123.com"}, + {"name": "csr.c", "value": "US"}, + {"name": "csr.st", "value": "TX"}, + {"name": "csr.l", "value": "San Antonio"}, + {"name": "csr.o", "value": "Rackspace US Inc."}, + {"name": "csr.ou", "value": "IT"}, + {"name": "csr.sans", + "value": "secure.san3.test_123.com"}, + {"name": "organization-information.organization-name", + "value": "Rackspace US Inc."}, + {"name": "organization-information.address-line-one", + "value": "1 Fanatical Place"}, + {"name": "organization-information.city", + "value": "San Antonio"}], + "lastStatusChange": "2015-03-19T21:47:10Z", + "spsId": random.randint(1, 10000), + "status": "SPS Request Complete", + "jobId": random.randint(1, 100000)}]}) + self.response_200.status_code = 200 + return self.response_200 + + def post(self, url, data=None, headers=None): + self.response_200.status_code = 202 + self.response_200.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} + }]}}) + return self.response_200 + + def put(self, url, data=None, headers=None): + return self.response_200 diff --git a/tests/unit/provider/akamai/background_jobs/test_flows.py b/tests/unit/provider/akamai/background_jobs/test_flows.py new file mode 100644 index 00000000..a79e46f5 --- /dev/null +++ b/tests/unit/provider/akamai/background_jobs/test_flows.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. +import json + +import mock +from taskflow import engines + +from poppy.provider.akamai.background_jobs.check_cert_status_and_update \ + import check_cert_status_and_update_flow +from poppy.provider.akamai.background_jobs.update_property import ( + update_property_flow) +from tests.unit import base +from tests.unit.provider.akamai.background_jobs import akamai_mocks + + +class TestAkamaiBJFlowRuns(base.TestCase): + + def setUp(self): + super(TestAkamaiBJFlowRuns, self).setUp() + + bootstrap_patcher = mock.patch( + 'poppy.bootstrap.Bootstrap', + new=akamai_mocks.MockBootStrap + ) + bootstrap_patcher.start() + self.addCleanup(bootstrap_patcher.stop) + + def test_check_cert_status_and_update_flow(self): + kwargs = { + 'domain_name': "blog.testabc.com", + 'cert_type': "san", + 'flavor_id': "premium", + 'project_id': "000" + } + engines.run(check_cert_status_and_update_flow. + check_cert_status_and_update_flow(), + store=kwargs) + + def test_update_papi_flow(self): + kwargs = { + "property_spec": "akamai_https_san_config_numbers", + "update_type": "hostnames", + "update_info_list": json.dumps([ + ( + "add", + { + "cnameFrome": "blog.testabc.com", + "cnameTo": 'secure2.san1.test_cdn.com', + "cnameType": "EDGE_HOSTNAME" + } + )]) + } + engines.run(update_property_flow.update_property_flow(), + store=kwargs) diff --git a/tests/unit/storage/cassandra/data_get_cert_by_domain.json b/tests/unit/storage/cassandra/data_get_certs_by_domain.json similarity index 100% rename from tests/unit/storage/cassandra/data_get_cert_by_domain.json rename to tests/unit/storage/cassandra/data_get_certs_by_domain.json diff --git a/tests/unit/storage/cassandra/test_services.py b/tests/unit/storage/cassandra/test_services.py index 302e9a57..18f9050a 100644 --- a/tests/unit/storage/cassandra/test_services.py +++ b/tests/unit/storage/cassandra/test_services.py @@ -180,11 +180,11 @@ class CassandraStorageServiceTests(base.TestCase): self.assertTrue("CloudFront" in actual_response) self.assertTrue("Fastly" in actual_response) - @ddt.file_data('data_get_cert_by_domain.json') + @ddt.file_data('data_get_certs_by_domain.json') @mock.patch.object(services.ServicesController, 'session') @mock.patch.object(cassandra.cluster.Session, 'execute') - def test_get_cert_by_domain(self, cert_details_json, - mock_session, mock_execute): + def test_get_certs_by_domain(self, cert_details_json, + mock_session, mock_execute): # mock the response from cassandra mock_execute.execute.return_value = cert_details_json[0] actual_response = self.sc.get_certs_by_domain(