Adds support for operator status

This patch adds support of operator_status. This can be used for
enabling or disabling a service.

Implements: blueprint enable-disable-service

Change-Id: I458abb7b34b88a823118af5f5da6f82555d34a2d
This commit is contained in:
Obulpathi
2015-06-08 14:31:32 -04:00
parent 68b8d96a9c
commit ddeff007f1
17 changed files with 376 additions and 22 deletions

View File

@@ -106,6 +106,8 @@ shared_ssl_num_shards = 5
shared_ssl_shard_prefix = "scdn"
shared_ssl_domain_suffix = "secure.poppycdn.net"
url = "poppycdn.net"
# link to page to be directed to when a service is disabled
url_404 = notfound.com
# You email associated with DNS, for notifications
email = "your@email.com"
auth_endpoint = ""
@@ -149,4 +151,3 @@ san_cert_hostname_limit = "MY_SAN_HOSTNAMES_LMIT"
identity_url = OPENSTACK_IDENTITY_URL
preferred_dcs = DC1,DC2
container_name = .CDN_ACCESS_LOGS

View File

@@ -41,3 +41,13 @@ class ServiceStatusNeitherDeployedNorFailed(Exception):
class ServiceStatusNotDeployed(Exception):
"""Raised when a non-deployed service is purged."""
class ServiceStatusDisabled(Exception):
"""Raised when a disabled status is updated."""
class InvalidServiceState(Exception):
"""Raised when a operator state is updated with an invalid status."""

View File

@@ -0,0 +1,41 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from oslo.config import cfg
from taskflow.patterns import linear_flow
from poppy.distributed_task.taskflow.task import update_service_state_tasks
from poppy.openstack.common import log
LOG = log.getLogger(__name__)
conf = cfg.CONF
conf(project='poppy', prog='poppy', args=[])
def disable_service():
flow = linear_flow.Flow('Breaking CNAME chain').add(
update_service_state_tasks.BreakDNSChainTask()
)
return flow
def enable_service():
flow = linear_flow.Flow('Fixing CNAME chain').add(
update_service_state_tasks.FixDNSChainTask()
)
return flow

View File

@@ -0,0 +1,59 @@
# 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 service
LOG = log.getLogger(__name__)
conf = cfg.CONF
conf(project='poppy', prog='poppy', args=[])
class FixDNSChainTask(task.Task):
def execute(self, service_obj):
service_obj_json = json.loads(service_obj)
service_obj = service.load_from_json(service_obj_json)
service_controller, dns = \
memoized_controllers.task_controllers('poppy', 'dns')
LOG.info(u'Starting to enable service')
dns.enable(service_obj)
LOG.info(u'Enabled service')
return
class BreakDNSChainTask(task.Task):
def execute(self, service_obj):
service_obj_json = json.loads(service_obj)
service_obj = service.load_from_json(service_obj_json)
service_controller, dns = \
memoized_controllers.task_controllers('poppy', 'dns')
LOG.info(u'Starting to disable service')
dns.disable(service_obj)
LOG.info(u'Disabled service')
return

View File

@@ -49,6 +49,8 @@ RACKSPACE_OPTIONS = [
help='Authentication end point for DNS'),
cfg.IntOpt('timeout', default=30, help='DNS response timeout'),
cfg.IntOpt('delay', default=1, help='DNS retry delay'),
cfg.StrOpt('url_404', default='',
help='The url to CNAME to when a service is disabled'),
]
RACKSPACE_GROUP = 'drivers:dns:rackspace'

View File

@@ -103,11 +103,12 @@ class ServicesController(base.ServicesBase):
subdomain.add_records(cname_records)
return dns_links
def _delete_cname_record(self, access_url, shared_ssl_flag):
"""Delete a CNAME record
def _search_cname_record(self, access_url, shared_ssl_flag):
"""Search a CNAME record
:param access_url: DNS Access URL
:return error_msg: returns error message, if any
:param shared_ssl_flag: flag indicating if this is a shared ssl domain
:return records: returns records, if any
"""
# extract shard name
@@ -129,17 +130,51 @@ class ServicesController(base.ServicesBase):
name = access_url
record_type = 'CNAME'
records = self.client.search_records(subdomain, record_type, name)
return records
def _delete_cname_record(self, access_url, shared_ssl_flag):
"""Delete a CNAME record
:param access_url: DNS Access URL
:param shared_ssl_flag: flag indicating if this is a shared ssl domain
:return error_msg: returns error message, if any
"""
records = self._search_cname_record(access_url, shared_ssl_flag)
# delete the record
# we should get one record,
# or none if it has been deleted already
if not records:
LOG.info('DNS record already deleted: {0}'.format(access_url))
elif len(records) == 1:
LOG.info('Deleting DNS records for : {0}'.format(access_url))
records[0].delete()
elif len(records) > 1:
error_msg = 'Multiple DNS records found: {0}'.format(access_url)
return error_msg
elif len(records) == 1:
LOG.info('Deleting DNS records for : {0}'.format(access_url))
records[0].delete()
return
def _change_cname_record(self, access_url, target_url, shared_ssl_flag):
"""Change a CNAME record
:param access_url: DNS Access URL
:param target_url: Operator Access URL
:param shared_ssl_flag: flag indicating if this is a shared ssl domain
:return error_msg: returns error message, if any
"""
records = self._search_cname_record(access_url, shared_ssl_flag)
# we should get one record, or none if it has been deleted already
if not records:
LOG.info('DNS record not found for: {0}'.format(access_url))
elif len(records) > 1:
LOG.info('Multiple DNS records found: {0}'.format(access_url))
elif len(records) == 1:
LOG.info('Updating DNS record for : {0}'.format(access_url))
records[0].update(data=target_url)
LOG.info('Updated DNS record for : {0}'.format(access_url))
return
def _generate_sharded_domain_name(self, shard_prefix, num_shards, suffix):
@@ -498,3 +533,55 @@ class ServicesController(base.ServicesBase):
dns_details[provider_name] = {'access_urls': access_urls}
return self.responder.updated(dns_details)
def gather_cname_links(self, service_obj):
provider_details = service_obj.provider_details
dns_details = {}
for provider_name in provider_details:
access_urls = []
for link in provider_details[provider_name].access_urls:
# if this is a log delivery URL, don't add
if 'log_delivery' in link:
continue
access_url = {
'domain': link['domain'],
'provider_url': link['provider_url'],
'operator_url': link['operator_url']
}
# Need to indicate if this access_url is a shared ssl
# access url, since its has different shard_prefix and
# num_shard
if link.get('certificate', None) == 'shared':
access_url['shared_ssl_flag'] = True
else:
access_url['shared_ssl_flag'] = False
access_urls.append(access_url)
dns_details[provider_name] = {'access_urls': access_urls}
return dns_details
def enable(self, service_obj):
dns_details = self.gather_cname_links(service_obj)
for provider_name in dns_details:
access_urls = dns_details[provider_name]['access_urls']
for access_url in access_urls:
provider_url = access_url['provider_url']
operator_url = access_url['operator_url']
shared_ssl_flag = access_url['shared_ssl_flag']
self._change_cname_record(operator_url,
provider_url,
shared_ssl_flag)
def disable(self, service_obj):
dns_details = self.gather_cname_links(service_obj)
provider_url = self._driver.rackdns_conf.url_404
for provider_name in dns_details:
access_urls = dns_details[provider_name]['access_urls']
for access_url in access_urls:
operator_url = access_url['operator_url']
shared_ssl_flag = access_url['shared_ssl_flag']
self._change_cname_record(operator_url,
provider_url,
shared_ssl_flag)

View File

@@ -72,6 +72,16 @@ class ServicesControllerBase(controller.ManagerControllerBase):
"""
raise NotImplementedError
@abc.abstractmethod
def update_state(self, project_id, service_id, state):
"""update_state
:param project_id
:param service_id
:param state
:raises ValueError
"""
@abc.abstractmethod
def delete(self, project_id, service_id):
"""DELETE

View File

@@ -24,6 +24,7 @@ from poppy.distributed_task.taskflow.flow import create_service
from poppy.distributed_task.taskflow.flow import delete_service
from poppy.distributed_task.taskflow.flow import purge_service
from poppy.distributed_task.taskflow.flow import update_service
from poppy.distributed_task.taskflow.flow import update_service_state
from poppy.manager import base
from poppy.model.helpers import rule
from poppy.model import service
@@ -142,6 +143,7 @@ class DefaultServicesController(base.ServicesController):
:param service_obj
:raises LookupError, ValueError
"""
try:
flavor = self.flavor_controller.get(service_json.get('flavor_id'))
# raise a lookup error if the flavor is not found
@@ -203,6 +205,10 @@ class DefaultServicesController(base.ServicesController):
except ValueError:
raise errors.ServiceNotFound("Service not found")
if service_old.operator_status == u'disabled':
raise errors.ServiceStatusDisabled(
u'Service {0} is disabled'.format(service_id))
if service_old.status not in [u'deployed', u'failed']:
raise errors.ServiceStatusNeitherDeployedNorFailed(
u'Service {0} neither deployed nor failed'.format(service_id))
@@ -221,6 +227,7 @@ class DefaultServicesController(base.ServicesController):
# remove fields that cannot be part of PATCH
del service_old_json['service_id']
del service_old_json['status']
del service_old_json['operator_status']
del service_old_json['provider_details']
service_new_json = jsonpatch.apply_patch(
@@ -283,6 +290,34 @@ class DefaultServicesController(base.ServicesController):
return
def update_state(self, project_id, service_id, state):
"""update_state.
:param project_id
:param service_id
:param state
:raises ValueError
"""
# call storage and update service state
try:
service_obj = self.storage_controller.update_state(project_id,
service_id,
state)
except ValueError:
raise errors.ServiceNotFound("Service not found")
kwargs = {
'service_obj': json.dumps(service_obj.to_dict())
}
if state == 'enabled':
self.distributed_task_controller.submit_task(
update_service_state.enable_service, **kwargs)
elif state == 'disabled':
self.distributed_task_controller.submit_task(
update_service_state.disable_service, **kwargs)
def delete(self, project_id, service_id):
"""delete.

View File

@@ -39,7 +39,8 @@ class Service(common.DictSerializableModel):
flavor_id,
caching=[],
restrictions=[],
log_delivery=None):
log_delivery=None,
operator_status='enabled'):
self._service_id = str(service_id)
self._name = name
self._domains = domains
@@ -50,6 +51,7 @@ class Service(common.DictSerializableModel):
self._log_delivery = log_delivery or ld.LogDelivery(False)
self._status = 'create_in_progress'
self._provider_details = {}
self._operator_status = operator_status
@property
def service_id(self):
@@ -124,6 +126,16 @@ class Service(common.DictSerializableModel):
"""Set log_delivery."""
self._log_delivery = value
@property
def operator_status(self):
"""Get operator status."""
return self._operator_status
@operator_status.setter
def operator_status(self, value):
"""Set operator status."""
self._operator_status = value
@property
def status(self):
"""Get or set status.

View File

@@ -56,6 +56,21 @@ class ServicesControllerBase(controller.StorageControllerBase):
:param project_id
:param service_id
:param service_json
:returns service_obj
:raise NotImplementedError
"""
raise NotImplementedError
@abc.abstractmethod
def update_state(self, project_id, service_id, state):
"""update_state
Update service state
:param project_id
:param service_id
:param state
:raise NotImplementedError
"""
raise NotImplementedError

View File

@@ -0,0 +1,7 @@
ALTER TABLE services ADD operator_status varchar;
ALTER TABLE archives ADD operator_status varchar;
--//@UNDO
ALTER TABLE services DROP operator_status;
ALTER TABLE archives DROP operator_status;

View File

@@ -60,6 +60,7 @@ CQL_LIST_SERVICES = '''
caching_rules,
restrictions,
provider_details,
operator_status,
log_delivery
FROM services
WHERE project_id = %(project_id)s
@@ -78,6 +79,7 @@ CQL_GET_SERVICE = '''
caching_rules,
restrictions,
provider_details,
operator_status,
log_delivery
FROM services
WHERE project_id = %(project_id)s AND service_id = %(service_id)s
@@ -116,6 +118,7 @@ CQL_ARCHIVE_SERVICE = '''
caching_rules,
restrictions,
provider_details,
operator_status,
archived_time
)
VALUES (%(project_id)s,
@@ -127,6 +130,7 @@ CQL_ARCHIVE_SERVICE = '''
%(caching_rules)s,
%(restrictions)s,
%(provider_details)s,
%(operator_status)s,
%(archived_time)s)
DELETE FROM services
@@ -157,6 +161,7 @@ CQL_CREATE_SERVICE = '''
caching_rules,
restrictions,
provider_details,
operator_status,
log_delivery
)
VALUES (%(project_id)s,
@@ -168,6 +173,7 @@ CQL_CREATE_SERVICE = '''
%(caching_rules)s,
%(restrictions)s,
%(provider_details)s,
%(operator_status)s,
%(log_delivery)s)
'''
@@ -329,7 +335,8 @@ class ServicesController(base.ServicesController):
'caching_rules': caching_rules,
'restrictions': restrictions,
'log_delivery': log_delivery,
'provider_details': {}
'provider_details': {},
'operator_status': 'enabled'
}
LOG.debug("Creating New Service - {0} ({1})".format(service_id,
@@ -390,7 +397,8 @@ class ServicesController(base.ServicesController):
'caching_rules': caching_rules,
'restrictions': restrictions,
'provider_details': pds,
'log_delivery': log_delivery
'log_delivery': log_delivery,
'operator_status': service_obj.operator_status
}
stmt = query.SimpleStatement(
@@ -422,6 +430,24 @@ class ServicesController(base.ServicesController):
domain_args)
self.session.execute(batch_claim)
def update_state(self, project_id, service_id, state):
"""update_state
Update service state
:param project_id
:param service_id
:param state
:returns service_obj
"""
service_obj = self.get(project_id, service_id)
service_obj.operator_status = state
self.update(project_id, service_id, service_obj)
return service_obj
def delete(self, project_id, service_id):
"""delete.
@@ -455,6 +481,8 @@ class ServicesController(base.ServicesController):
'caching_rules': result.get('caching_rules', []),
'restrictions': result.get('restrictions', []),
'provider_details': pds,
'operator_status': result.get('operator_status',
'enabled'),
'archived_time': datetime.datetime.utcnow(),
'domains_list': query.ValueSequence(domains_list)
}
@@ -574,6 +602,7 @@ class ServicesController(base.ServicesController):
caching_rules = [json.loads(c) for c in result.get('caching_rules', [])
or []]
log_delivery = json.loads(result.get('log_delivery', '{}') or '{}')
operator_status = result.get('operator_status', 'enabled')
# create models for each item
origins = [
@@ -613,7 +642,8 @@ class ServicesController(base.ServicesController):
s = service.Service(service_id, name, domains, origins, flavor_id,
caching=caching_rules,
restrictions=restrictions,
log_delivery=log_delivery)
log_delivery=log_delivery,
operator_status=operator_status)
# format the provider details
provider_detail_results = result.get('provider_details') or {}

View File

@@ -151,6 +151,24 @@ class ServicesController(base.ServicesController):
# update configuration in storage
return ''
def update_state(self, project_id, service_id, state):
"""update_state
Update service state
:param project_id
:param service_id
:param state
:returns service_obj
"""
service_obj = self.get(project_id, service_id)
service_obj.operator_status = state
self.update(project_id, service_id, service_obj)
return self.get(project_id, service_id)
def delete(self, project_id, service_id):
if (service_id in self.created_service_ids):
self.created_service_ids.remove(service_id)

View File

@@ -75,6 +75,38 @@ class ServiceAssetsController(base.Controller, hooks.HookController):
return pecan.Response(None, 202)
class OperatorStateController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
def __init__(self, driver):
super(OperatorStateController, self).__init__(driver)
@pecan.expose('json')
def post(self, service_id):
service_state_json = json.loads(pecan.request.body.decode('utf-8'))
service_state = service_state_json.get('state', None)
services_controller = self._driver.manager.services_controller
if not service_state:
pecan.abort(400, detail='Invalid JSON, state is a required field')
if service_state not in [u'enabled', u'disabled']:
pecan.abort(400, detail=u'Service state {0} is invalid'
.format(service_state))
try:
services_controller.update_state(self.project_id,
service_id,
service_state)
except ValueError:
pecan.abort(404, detail='Service {0} could not be found'.format(
service_id))
return pecan.Response(None, 202)
class ServicesController(base.Controller, hooks.HookController):
__hooks__ = [poppy_hooks.Context(), poppy_hooks.Error()]
@@ -91,6 +123,7 @@ class ServicesController(base.Controller, hooks.HookController):
# so added it in __init__ method.
# see more in: http://pecan.readthedocs.org/en/latest/rest.html
self.__class__.assets = ServiceAssetsController(driver)
self.__class__.state = OperatorStateController(driver)
@pecan.expose('json')
def get_all(self):

View File

@@ -42,9 +42,11 @@ class Model(collections.OrderedDict):
service_obj.restrictions]
self["caching"] = [cachingrules.Model(c) for c in
service_obj.caching]
self["status"] = service_obj.status
self["flavor_id"] = service_obj.flavor_id
self["log_delivery"] = log_delivery.Model(service_obj.log_delivery)
self["status"] = service_obj.status
if service_obj.operator_status == "disabled":
self["status"] = service_obj.operator_status
self["errors"] = []

View File

@@ -338,7 +338,8 @@ class ServiceSchema(schema_base.SchemaBase):
}
}
}
}},
}
},
'PATCH': {
'type': 'array',
'properties': {

View File

@@ -474,10 +474,6 @@ class ServiceControllerTest1(base.FunctionalTest):
self.assertEqual(202, response.status_code)
self.assertTrue('Location' in response.headers)
self.service_url = urlparse.urlparse(response.headers["Location"]).path
# import pdb; pdb.set_trace()
# print '#############################################################'
# print self.service_url
# print '#############################################################'
def tearDown(self):
super(ServiceControllerTest1, self).tearDown()
@@ -492,15 +488,10 @@ class ServiceControllerTest1(base.FunctionalTest):
def test_update_with_good_input(self):
self.skipTest('Skip failing test')
# import pdb; pdb.set_trace()
# print '###################################'
# print self.service_url
response = self.app.get(
self.service_url,
headers={'X-Project-ID': self.project_id})
self.assertEqual(200, response.status_code)
# print '###################################'
# print self.service_url
# update with good data
response = self.app.patch(self.service_url,
params=json.dumps([