Adds rest driver methods Adds rest driver tests Add cert task for generating server certs Modified compute task/flow Fixed local certificate stuff Refactored to use requests-mock inetad of responses Added a "conditiobal flow" for REST Cleaned up and changed the code to work with https://review.openstack.org/#/c/160034/ Replaces: https://review.openstack.org/#/c/144348/ https://review.openstack.org/#/c/145637/14 Change-Id: Ibcbf0717b785aab4c604deef1061e8b2fa41006c Co-Authored-By: Phillip Toohill <phillip.toohill@rackspace.com> Co-Authored-By: German Eichberger <german.eichberger@hp.com> Co-Authored-By: Stephen Balukoff <sbalukoff@bluebox.net> Implements: bp/haproxy-amphora-driverchanges/72/171172/15
parent
792a116523
commit
0abcbc4f7d
@ -0,0 +1,110 @@
|
||||
# Copyright 2014 Rackspace
|
||||
#
|
||||
# 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 octavia.common.data_models as models
|
||||
|
||||
|
||||
class Topology(models.BaseDataModel):
|
||||
|
||||
def __init__(self, hostname=None, uuid=None, topology=None, role=None,
|
||||
ip=None, ha_ip=None):
|
||||
self.hostname = hostname
|
||||
self.uuid = uuid
|
||||
self.topology = topology
|
||||
self.role = role
|
||||
self.ip = ip
|
||||
self.ha_ip = ha_ip
|
||||
|
||||
|
||||
class Info(models.BaseDataModel):
|
||||
|
||||
def __init__(self, hostname=None, uuid=None, version=None,
|
||||
api_version=None):
|
||||
self.hostname = hostname
|
||||
self.uuid = uuid
|
||||
self.version = version
|
||||
self.api_version = api_version
|
||||
|
||||
|
||||
class Details(models.BaseDataModel):
|
||||
|
||||
def __init__(self, hostname=None, uuid=None, version=None,
|
||||
api_version=None, network_tx=None, network_rx=None,
|
||||
active=None, haproxy_count=None, cpu=None, memory=None,
|
||||
disk=None, load=None, listeners=None, packages=None):
|
||||
self.hostname = hostname
|
||||
self.uuid = uuid,
|
||||
self.version = version
|
||||
self.api_version = api_version
|
||||
self.network_tx = network_tx
|
||||
self.network_rx = network_rx
|
||||
self.active = active
|
||||
self.haproxy_count = haproxy_count
|
||||
self.cpu = cpu
|
||||
self.memory = memory
|
||||
self.disk = disk
|
||||
self.load = load or []
|
||||
self.listeners = listeners or []
|
||||
self.packages = packages or []
|
||||
|
||||
|
||||
class CPU(models.BaseDataModel):
|
||||
|
||||
def __init__(self, total=None, user=None, system=None, soft_irq=None):
|
||||
self.total = total
|
||||
self.user = user
|
||||
self.system = system
|
||||
self.soft_irq = soft_irq
|
||||
|
||||
|
||||
class Memory(models.BaseDataModel):
|
||||
|
||||
def __init__(self, total=None, free=None, available=None, buffers=None,
|
||||
cached=None, swap_used=None, shared=None, slab=None,
|
||||
committed_as=None):
|
||||
self.total = total
|
||||
self.free = free
|
||||
self.available = available
|
||||
self.buffers = buffers
|
||||
self.cached = cached
|
||||
self.swap_used = swap_used
|
||||
self.shared = shared
|
||||
self.slab = slab
|
||||
self.committed_as = committed_as
|
||||
|
||||
|
||||
class Disk(models.BaseDataModel):
|
||||
|
||||
def __init__(self, used=None, available=None):
|
||||
self.used = used
|
||||
self.available = available
|
||||
|
||||
|
||||
class ListenerStatus(models.BaseDataModel):
|
||||
|
||||
def __init__(self, status=None, uuid=None, provisioning_status=None,
|
||||
type=None, pools=None):
|
||||
self.status = status
|
||||
self.uuid = uuid
|
||||
self.provisioning_status = provisioning_status
|
||||
self.type = type
|
||||
self.pools = pools or []
|
||||
|
||||
|
||||
class Pool(models.BaseDataModel):
|
||||
|
||||
def __init__(self, uuid=None, status=None, members=None):
|
||||
self.uuid = uuid
|
||||
self.status = status
|
||||
self.members = members or []
|
@ -0,0 +1,72 @@
|
||||
# Copyright 2014 Rackspace
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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 webob import exc
|
||||
|
||||
|
||||
def check_exception(response):
|
||||
status_code = response.status_code
|
||||
responses = {
|
||||
401: Unauthorized,
|
||||
403: InvalidRequest,
|
||||
404: NotFound,
|
||||
405: InvalidRequest,
|
||||
409: Conflict,
|
||||
500: InternalServerError,
|
||||
503: ServiceUnavailable
|
||||
}
|
||||
if status_code in responses:
|
||||
raise responses[status_code]()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class APIException(exc.HTTPClientError):
|
||||
msg = "Something unknown went wrong"
|
||||
code = 500
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.msg = self.msg % kwargs
|
||||
super(APIException, self).__init__(detail=self.msg)
|
||||
|
||||
|
||||
class Unauthorized(APIException):
|
||||
msg = "Unauthorized"
|
||||
code = 401
|
||||
|
||||
|
||||
class InvalidRequest(APIException):
|
||||
msg = "Invalid request"
|
||||
code = 403
|
||||
|
||||
|
||||
class NotFound(APIException):
|
||||
msg = "Not Found"
|
||||
code = 404
|
||||
|
||||
|
||||
class Conflict(APIException):
|
||||
msg = "Conflict"
|
||||
code = 409
|
||||
|
||||
|
||||
class InternalServerError(APIException):
|
||||
msg = "Internal Server Error"
|
||||
code = 500
|
||||
|
||||
|
||||
class ServiceUnavailable(APIException):
|
||||
msg = "Service Unavailable"
|
||||
code = 503
|
@ -0,0 +1,299 @@
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2015 Rackspace
|
||||
#
|
||||
# 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 functools
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from oslo_log import log as logging
|
||||
import requests
|
||||
from stevedore import driver as stevedore_driver
|
||||
|
||||
from octavia.amphorae.drivers import driver_base as driver_base
|
||||
from octavia.amphorae.drivers.haproxy import exceptions as exc
|
||||
from octavia.amphorae.drivers.haproxy.jinja import jinja_cfg
|
||||
from octavia.common.config import cfg
|
||||
from octavia.common import data_models as data_models
|
||||
from octavia.common.tls_utils import cert_parser
|
||||
from octavia.i18n import _LW
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
API_VERSION = '0.5'
|
||||
OCTAVIA_API_CLIENT = (
|
||||
"Octavia HaProxy Rest Client/{version} "
|
||||
"(https://wiki.openstack.org/wiki/Octavia)").format(version=API_VERSION)
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group('haproxy_amphora', 'octavia.common.config')
|
||||
|
||||
|
||||
class HaproxyAmphoraLoadBalancerDriver(driver_base.AmphoraLoadBalancerDriver):
|
||||
def __init__(self):
|
||||
super(HaproxyAmphoraLoadBalancerDriver, self).__init__()
|
||||
self.client = AmphoraAPIClient()
|
||||
self.cert_manager = stevedore_driver.DriverManager(
|
||||
namespace='octavia.cert_manager',
|
||||
name=CONF.haproxy_amphora.cert_manager,
|
||||
invoke_on_load=True,
|
||||
).driver
|
||||
self.jinja = jinja_cfg.JinjaTemplater(
|
||||
base_amp_path=CONF.haproxy_amphora.base_path,
|
||||
base_crt_dir=CONF.haproxy_amphora.base_cert_dir,
|
||||
haproxy_template=CONF.haproxy_amphora.haproxy_template)
|
||||
|
||||
def update(self, listener, vip):
|
||||
LOG.debug("Amphora %s haproxy, updating listener %s, vip %s",
|
||||
self.__class__.__name__, listener.protocol_port,
|
||||
vip.ip_address)
|
||||
|
||||
# Process listener certificate info
|
||||
certs = self._process_tls_certificates(listener)
|
||||
|
||||
# Generate HaProxy configuration from listener object
|
||||
config = self.jinja.build_config(listener, certs['tls_cert'],
|
||||
certs['sni_certs'])
|
||||
|
||||
for amp in listener.load_balancer.amphorae:
|
||||
self.client.upload_config(amp, listener.id, config)
|
||||
# todo (german): add a method to REST interface to reload or start
|
||||
# without having to check
|
||||
# Is that listener running?
|
||||
r = self.client.get_listener_status(amp,
|
||||
listener.id)
|
||||
if r['status'] == 'ACTIVE':
|
||||
self.client.reload_listener(amp, listener.id)
|
||||
else:
|
||||
self.client.start_listener(amp, listener.id)
|
||||
|
||||
def _apply(self, func, listener=None, *args):
|
||||
for amp in listener.load_balancer.amphorae:
|
||||
func(amp, listener.id, *args)
|
||||
|
||||
def stop(self, listener, vip):
|
||||
self._apply(self.client.stop_listener, listener)
|
||||
|
||||
def start(self, listener, vip):
|
||||
self._apply(self.client.start_listener, listener)
|
||||
|
||||
def delete(self, listener, vip):
|
||||
self._apply(self.client.delete_listener, listener)
|
||||
|
||||
def get_info(self, amphora):
|
||||
self.driver.get_info(amphora.lb_network_ip)
|
||||
|
||||
def get_diagnostics(self, amphora):
|
||||
self.driver.get_diagnostics(amphora.lb_network_ip)
|
||||
|
||||
def finalize_amphora(self, amphora):
|
||||
pass
|
||||
|
||||
def post_vip_plug(self, load_balancer):
|
||||
for amp in load_balancer.amphorae:
|
||||
self.client.plug_vip(amp, load_balancer.vip.ip_address)
|
||||
|
||||
def post_network_plug(self, amphora):
|
||||
self.client.plug_network(amphora)
|
||||
|
||||
def _process_tls_certificates(self, listener):
|
||||
"""Processes TLS data from the listener.
|
||||
|
||||
Converts and uploads PEM data to the Amphora API
|
||||
|
||||
return TLS_CERT and SNI_CERTS
|
||||
"""
|
||||
tls_cert = None
|
||||
sni_certs = []
|
||||
|
||||
certs = []
|
||||
|
||||
if listener.tls_certificate_id:
|
||||
tls_cert = self._map_cert_tls_container(
|
||||
self.cert_manager.get_cert(listener.tls_certificate_id))
|
||||
certs.append(tls_cert)
|
||||
if listener.sni_containers:
|
||||
for sni_cont in listener.sni_containers:
|
||||
bbq_container = self._map_cert_tls_container(
|
||||
self.cert_manager.get_cert(sni_cont.tls_container.id))
|
||||
sni_certs.append(bbq_container)
|
||||
certs.append(bbq_container)
|
||||
|
||||
for cert in certs:
|
||||
pem = self._build_pem(cert)
|
||||
md5 = hashlib.md5(pem).hexdigest()
|
||||
name = '{cn}.pem'.format(cn=cert.primary_cn)
|
||||
self._apply(self._upload_cert, listener, pem, md5, name)
|
||||
|
||||
return {'tls_cert': tls_cert, 'sni_certs': sni_certs}
|
||||
|
||||
def _upload_cert(self, amp, listener_id, pem, md5, name):
|
||||
try:
|
||||
if self.client.get_cert_md5sum(amp, listener_id, name) == md5:
|
||||
return
|
||||
except exc.NotFound:
|
||||
pass
|
||||
|
||||
self.client.upload_cert_pem(
|
||||
amp, listener_id, name, pem)
|
||||
|
||||
def _get_primary_cn(self, tls_cert):
|
||||
"""Returns primary CN for Certificate."""
|
||||
return cert_parser.get_host_names(tls_cert.get_certificate())['cn']
|
||||
|
||||
def _map_cert_tls_container(self, cert):
|
||||
return data_models.TLSContainer(
|
||||
primary_cn=self._get_primary_cn(cert),
|
||||
private_key=cert.get_private_key(),
|
||||
certificate=cert.get_certificate(),
|
||||
intermediates=cert.get_intermediates())
|
||||
|
||||
def _build_pem(self, tls_cert):
|
||||
"""Concatenate TLS Certificate fields to create a PEM
|
||||
|
||||
encoded certificate file
|
||||
"""
|
||||
# TODO(ptoohill): Maybe this should be part of utils or manager?
|
||||
pem = tls_cert.intermediates[:]
|
||||
pem.extend([tls_cert.certificate, tls_cert.private_key])
|
||||
|
||||
return "\n".join(pem)
|
||||
|
||||
|
||||
# Check a custom hostname
|
||||
class CustomHostNameCheckingAdapter(requests.adapters.HTTPAdapter):
|
||||
def cert_verify(self, conn, url, verify, cert):
|
||||
conn.assert_hostname = self.uuid
|
||||
return super(CustomHostNameCheckingAdapter,
|
||||
self).cert_verify(conn, url, verify, cert)
|
||||
|
||||
|
||||
class AmphoraAPIClient(object):
|
||||
def __init__(self):
|
||||
super(AmphoraAPIClient, self).__init__()
|
||||
self.secure = False
|
||||
|
||||
self.get = functools.partial(self.request, 'get')
|
||||
self.post = functools.partial(self.request, 'post')
|
||||
self.put = functools.partial(self.request, 'put')
|
||||
self.delete = functools.partial(self.request, 'delete')
|
||||
self.head = functools.partial(self.request, 'head')
|
||||
|
||||
self.start_listener = functools.partial(self._action, 'start')
|
||||
self.stop_listener = functools.partial(self._action, 'stop')
|
||||
self.reload_listener = functools.partial(self._action, 'reload')
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.cert = CONF.haproxy_amphora.client_cert
|
||||
self.ssl_adapter = CustomHostNameCheckingAdapter()
|
||||
self.session.mount('https://', self.ssl_adapter)
|
||||
|
||||
def _base_url(self, ip):
|
||||
return "https://{ip}:{port}/{version}/".format(
|
||||
ip=ip,
|
||||
port=CONF.haproxy_amphora.bind_port,
|
||||
version=API_VERSION)
|
||||
|
||||
def request(self, method, amp, path='/', **kwargs):
|
||||
LOG.debug("request url " + path)
|
||||
_request = getattr(self.session, method.lower())
|
||||
_url = self._base_url(amp.lb_network_ip) + path
|
||||
|
||||
reqargs = {
|
||||
'verify': CONF.haproxy_amphora.server_ca,
|
||||
'url': _url, }
|
||||
reqargs.update(kwargs)
|
||||
headers = reqargs.setdefault('headers', {})
|
||||
|
||||
headers['User-Agent'] = OCTAVIA_API_CLIENT
|
||||
self.ssl_adapter.uuid = amp.id
|
||||
# Keep retrying
|
||||
for attempts in xrange(CONF.haproxy_amphora.connection_max_retries):
|
||||
try:
|
||||
r = _request(**reqargs)
|
||||
except requests.ConnectionError:
|
||||
LOG.warn(_LW("Could not talk to instance"))
|
||||
time.sleep(CONF.haproxy_amphora.connection_retry_interval)
|
||||
if attempts >= CONF.haproxy_amphora.connection_max_retries:
|
||||
raise exc.TimeOutException()
|
||||
else:
|
||||
return r
|
||||
raise exc.UnavailableException()
|
||||
|
||||
def upload_config(self, amp, listener_id, config):
|
||||
r = self.put(
|
||||
amp,
|
||||
'listeners/{listener_id}/haproxy'.format(listener_id=listener_id),
|
||||
data=config)
|
||||
return exc.check_exception(r)
|
||||
|
||||
def get_listener_status(self, amp, listener_id):
|
||||
r = self.get(
|
||||
amp,
|
||||
'listeners/{listener_id}'.format(listener_id=listener_id))
|
||||
if exc.check_exception(r):
|
||||
return r.json()
|
||||
|
||||
def _action(self, action, amp, listener_id):
|
||||
r = self.put(amp, 'listeners/{listener_id}/{action}'.format(
|
||||
listener_id=listener_id, action=action))
|
||||
return exc.check_exception(r)
|
||||
|
||||
def upload_cert_pem(self, amp, listener_id, pem_filename, pem_file):
|
||||
r = self.put(
|
||||
amp,
|
||||
'listeners/{listener_id}/certificates/{filename}'.format(
|
||||
listener_id=listener_id, filename=pem_filename),
|
||||
data=pem_file)
|
||||
return exc.check_exception(r)
|
||||
|
||||
def get_cert_md5sum(self, amp, listener_id, pem_filename):
|
||||
r = self.get(amp,
|
||||
'listeners/{listener_id}/certificates/{filename}'.format(
|
||||
listener_id=listener_id, filename=pem_filename))
|
||||
if exc.check_exception(r):
|
||||
return r.json().get("md5sum")
|
||||
|
||||
def delete_listener(self, amp, listener_id):
|
||||
r = self.delete(
|
||||
amp, 'listeners/{listener_id}'.format(listener_id=listener_id))
|
||||
return exc.check_exception(r)
|
||||
|
||||
def get_info(self, amp):
|
||||
r = self.get(amp, "info")
|
||||
if exc.check_exception(r):
|
||||
return r.json()
|
||||
|
||||
def get_details(self, amp):
|
||||
r = self.get(amp, "details")
|
||||
if exc.check_exception(r):
|
||||
return r.json()
|
||||
|
||||
def get_all_listeners(self, amp):
|
||||
r = self.get(amp, "listeners")
|
||||
if exc.check_exception(r):
|
||||
return r.json()
|
||||
|
||||
def delete_cert_pem(self, amp, listener_id, pem_filename):
|
||||
r = self.delete(
|
||||
amp,
|
||||
'listeners/{listener_id}/certificates/{filename}'.format(
|
||||
listener_id=listener_id, filename=pem_filename))
|
||||
return exc.check_exception(r)
|
||||
|
||||
def plug_network(self, amp):
|
||||
r = self.post(amp, 'plug/network')
|
||||
return exc.check_exception(r)
|
||||
|
||||
def plug_vip(self, amp, vip):
|
||||
r = self.post(amp, 'plug/vip/{vip}'.format(vip=vip))
|
||||
return exc.check_exception(r)
|
@ -0,0 +1,52 @@
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 logging
|
||||
|
||||
from oslo.config import cfg
|
||||
from stevedore import driver as stevedore_driver
|
||||
from taskflow import task
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.import_group('controller_worker', 'octavia.common.config')
|
||||
LOG = logging.getLogger(__name__)
|
||||
CERT_VALIDITY = 365
|
||||
|
||||
|
||||
class BaseCertTask(task.Task):
|
||||
"""Base task to load drivers common to the tasks."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(BaseCertTask, self).__init__(**kwargs)
|
||||
self.cert_generator = stevedore_driver.DriverManager(
|
||||
namespace='octavia.cert_generator',
|
||||
name=CONF.controller_worker.cert_generator,
|
||||
invoke_on_load=True,
|
||||
).driver
|
||||
|
||||
|
||||
class GenerateServerPEMTask(BaseCertTask):
|
||||
"""Create the server certs for the agent comm
|
||||
|
||||
Use the amphora_id for the CN
|
||||
"""
|
||||
|
||||
def execute(self, amphora_id):
|
||||
cert = self.cert_generator.generate_cert_key_pair(
|
||||
cn=amphora_id,
|
||||
validity=CERT_VALIDITY)
|
||||
|
||||
return cert.certificate + cert.private_key
|
@ -0,0 +1,601 @@
|
||||
# Copyright 2015 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2015 Rackspace
|
||||
#
|
||||
# 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_utils import uuidutils
|
||||
import requests_mock
|
||||
|
||||
from octavia.amphorae.drivers.haproxy import exceptions as exc
|
||||
from octavia.amphorae.drivers.haproxy import rest_api_driver as driver
|
||||
from octavia.db import models
|
||||
from octavia.tests.unit import base as base
|
||||
from octavia.tests.unit.common.sample_configs import sample_configs
|
||||
|
||||
|
||||
class HaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
FAKE_UUID_1 = uuidutils.generate_uuid()
|
||||
|
||||
def setUp(self):
|
||||
super(HaproxyAmphoraLoadBalancerDriverTest, self).setUp()
|
||||
self.driver = driver.HaproxyAmphoraLoadBalancerDriver()
|
||||
|
||||
self.driver.cert_manager = mock.MagicMock()
|
||||
self.driver.client = mock.MagicMock()
|
||||
self.driver.jinja = mock.MagicMock()
|
||||
|
||||
# Build sample Listener and VIP configs
|
||||
self.sl = sample_configs.sample_listener_tuple(tls=True, sni=True)
|
||||
self.amp = self.sl.load_balancer.amphorae[0]
|
||||
self.sv = sample_configs.sample_vip_tuple()
|
||||
self.lb = self.sl.load_balancer
|
||||
|
||||
@mock.patch('octavia.common.tls_utils.cert_parser.get_host_names')
|
||||
def test_update(self, mock_cert):
|
||||
mock_cert.return_value = {'cn': 'fakeCN'}
|
||||
self.driver.client.get_cert_md5sum.side_effect = [
|
||||
exc.NotFound, 'Fake_MD5', 'd41d8cd98f00b204e9800998ecf8427e']
|
||||
self.driver.jinja.build_config.side_effect = ['fake_config']
|
||||
self.driver.client.get_listener_status.side_effect = [
|
||||
dict(status='ACTIVE')]
|
||||
|
||||
# Execute driver method
|
||||
self.driver.update(self.sl, self.sv)
|
||||
|
||||
# verify result
|
||||
# this is called 3 times
|
||||
self.driver.client.get_cert_md5sum.assert_called_with(
|
||||
self.amp, self.sl.id, 'fakeCN.pem')
|
||||
# this is called twice (last MD5 matches)
|
||||
self.driver.client.upload_cert_pem.assert_called_with(
|
||||
self.amp, self.sl.id, 'fakeCN.pem', '')
|
||||
self.assertEqual(2, self.driver.client.upload_cert_pem.call_count)
|
||||
# upload only one config file
|
||||
self.driver.client.upload_config.assert_called_once_with(
|
||||
self.amp, self.sl.id, 'fake_config')
|
||||
# start should be called once
|
||||
self.driver.client.reload_listener.assert_called_once_with(
|
||||
self.amp, self.sl.id)
|
||||
|
||||
# listener down
|
||||
self.driver.client.get_cert_md5sum.side_effect = [
|
||||
'd41d8cd98f00b204e9800998ecf8427e'] * 3
|
||||
self.driver.jinja.build_config.side_effect = ['fake_config']
|
||||
self.driver.client.get_listener_status.side_effect = [
|
||||
dict(status='BLAH')]
|
||||
|
||||
self.driver.update(self.sl, self.sv)
|
||||
|
||||
self.driver.client.start_listener.assert_called_once_with(
|
||||
self.amp, self.sl.id)
|
||||
|
||||
def test_stop(self):
|
||||
# Execute driver method
|
||||
self.driver.stop(self.sl, self.sv)
|
||||
self.driver.client.stop_listener.assert_called_once_with(
|
||||
self.amp, self.sl.id)
|
||||
|
||||
def test_start(self):
|
||||
# Execute driver method
|
||||
self.driver.start(self.sl, self.sv)
|
||||
self.driver.client.start_listener.assert_called_once_with(
|
||||
self.amp, self.sl.id)
|
||||
|
||||
def test_delete(self):
|
||||
# Execute driver method
|
||||
self.driver.delete(self.sl, self.sv)
|
||||
self.driver.client.delete_listener.assert_called_once_with(
|
||||
self.amp, self.sl.id)
|
||||
|
||||
def test_get_info(self):
|
||||
pass
|
||||
|
||||
def test_get_diagnostics(self):
|
||||
pass
|
||||
|
||||
def test_finalize_amphora(self):
|
||||
pass
|
||||
|
||||
def test_post_vip_plug(self):
|
||||
self.driver.post_vip_plug(self.lb)
|
||||
self.driver.client.plug_vip.assert_called_once_with(
|
||||
self.amp, self.lb.vip.ip_address)
|
||||
|
||||
def test_post_network_plug(self):
|
||||
self.driver.post_network_plug(self.amp)
|
||||
self.driver.client.plug_network.assert_called_once_with(self.amp)
|
||||
|
||||
|
||||
class AmphoraAPIClientTest(base.TestCase):
|
||||
FAKE_UUID_1 = uuidutils.generate_uuid()
|
||||
FAKE_PEM_FILENAME = "file_name"
|
||||
|
||||
def setUp(self):
|
||||
super(AmphoraAPIClientTest, self).setUp()
|
||||
self.driver = driver.AmphoraAPIClient()
|
||||
self.base_url = "https://127.0.0.1:8443/0.5"
|
||||
self.amp = models.Amphora(lb_network_ip='127.0.0.1', compute_id='123')
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_info(self, m):
|
||||
info = {"hostname": "some_hostname", "version": "some_version",
|
||||
"api_version": "0.5", "uuid": self.FAKE_UUID_1}
|
||||
m.get("{base}/info".format(base=self.base_url),
|
||||
json=info)
|
||||
information = self.driver.get_info(self.amp)
|
||||
self.assertEqual(info, information)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_info_unauthorized(self, m):
|
||||
m.get("{base}/info".format(base=self.base_url),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.get_info, self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_info_missing(self, m):
|
||||
m.get("{base}/info".format(base=self.base_url),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.get_info, self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_info_server_error(self, m):
|
||||
m.get("{base}/info".format(base=self.base_url),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.get_info,
|
||||
self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_info_service_unavailable(self, m):
|
||||
m.get("{base}/info".format(base=self.base_url),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.get_info,
|
||||
self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_details(self, m):
|
||||
details = {"hostname": "some_hostname", "version": "some_version",
|
||||
"api_version": "0.5", "uuid": self.FAKE_UUID_1,
|
||||
"network_tx": "some_tx", "network_rx": "some_rx",
|
||||
"active": True, "haproxy_count": 10}
|
||||
m.get("{base}/details".format(base=self.base_url),
|
||||
json=details)
|
||||
amp_details = self.driver.get_details(self.amp)
|
||||
self.assertEqual(details, amp_details)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_details_unauthorized(self, m):
|
||||
m.get("{base}/details".format(base=self.base_url),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.get_details, self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_details_missing(self, m):
|
||||
m.get("{base}/details".format(base=self.base_url),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.get_details, self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_details_server_error(self, m):
|
||||
m.get("{base}/details".format(base=self.base_url),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.get_details,
|
||||
self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_details_service_unavailable(self, m):
|
||||
m.get("{base}/details".format(base=self.base_url),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.get_details,
|
||||
self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_all_listeners(self, m):
|
||||
listeners = [{"status": "ONLINE", "provisioning_status": "ACTIVE",
|
||||
"type": "PASSIVE", "uuid": self.FAKE_UUID_1}]
|
||||
m.get("{base}/listeners".format(base=self.base_url),
|
||||
json=listeners)
|
||||
all_listeners = self.driver.get_all_listeners(self.amp)
|
||||
self.assertEqual(listeners, all_listeners)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_all_listeners_unauthorized(self, m):
|
||||
m.get("{base}/listeners".format(base=self.base_url),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.get_all_listeners,
|
||||
self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_all_listeners_missing(self, m):
|
||||
m.get("{base}/listeners".format(base=self.base_url),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.get_all_listeners,
|
||||
self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_all_listeners_server_error(self, m):
|
||||
m.get("{base}/listeners".format(base=self.base_url),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError,
|
||||
self.driver.get_all_listeners, self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_all_listeners_service_unavailable(self, m):
|
||||
m.get("{base}/listeners".format(base=self.base_url),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable,
|
||||
self.driver.get_all_listeners, self.amp)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_listener_status(self, m):
|
||||
listener = {"status": "ONLINE", "provisioning_status": "ACTIVE",
|
||||
"type": "PASSIVE", "uuid": self.FAKE_UUID_1}
|
||||
m.get("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
json=listener)
|
||||
status = self.driver.get_listener_status(self.amp, self.FAKE_UUID_1)
|
||||
self.assertEqual(listener, status)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_listener_status_unauthorized(self, m):
|
||||
m.get("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized,
|
||||
self.driver.get_listener_status, self.amp,
|
||||
self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_listener_status_missing(self, m):
|
||||
m.get("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound,
|
||||
self.driver.get_listener_status, self.amp,
|
||||
self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_listener_status_server_error(self, m):
|
||||
m.get("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError,
|
||||
self.driver.get_listener_status, self.amp,
|
||||
self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_listener_status_service_unavailable(self, m):
|
||||
m.get("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable,
|
||||
self.driver.get_listener_status, self.amp,
|
||||
self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_start_listener(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/start".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1))
|
||||
self.driver.start_listener(self.amp, self.FAKE_UUID_1)
|
||||
self.assertTrue(m.called)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_start_listener_missing(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/start".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.start_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_start_listener_unauthorized(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/start".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.start_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_start_listener_server_error(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/start".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.start_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_start_listener_service_unavailable(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/start".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.start_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_stop_listener(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/stop".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1))
|
||||
self.driver.stop_listener(self.amp, self.FAKE_UUID_1)
|
||||
self.assertTrue(m.called)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_stop_listener_missing(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/stop".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.stop_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_stop_listener_unauthorized(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/stop".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.stop_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_stop_listener_server_error(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/stop".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.stop_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_stop_listener_service_unavailable(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/stop".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.stop_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_delete_listener(self, m):
|
||||
m.delete("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1), json={})
|
||||
self.driver.delete_listener(self.amp, self.FAKE_UUID_1)
|
||||
self.assertTrue(m.called)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_delete_listener_missing(self, m):
|
||||
m.delete("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.delete_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_delete_listener_unauthorized(self, m):
|
||||
m.delete("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.delete_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_delete_listener_server_error(self, m):
|
||||
m.delete("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.delete_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_delete_listener_service_unavailable(self, m):
|
||||
m.delete("{base}/listeners/{listener_id}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1),
|
||||
status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.delete_listener,
|
||||
self.amp, self.FAKE_UUID_1)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_upload_cert_pem(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME))
|
||||
self.driver.upload_cert_pem(self.amp, self.FAKE_UUID_1,
|
||||
self.FAKE_PEM_FILENAME,
|
||||
"some_file")
|
||||
self.assertTrue(m.called)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_upload_invalid_cert_pem(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=403)
|
||||
self.assertRaises(exc.InvalidRequest, self.driver.upload_cert_pem,
|
||||
self.amp, self.FAKE_UUID_1, self.FAKE_PEM_FILENAME,
|
||||
"some_file")
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_upload_cert_pem_unauthorized(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.upload_cert_pem,
|
||||
self.amp, self.FAKE_UUID_1, self.FAKE_PEM_FILENAME,
|
||||
"some_file")
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_upload_cert_pem_server_error(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.upload_cert_pem,
|
||||
self.amp, self.FAKE_UUID_1, self.FAKE_PEM_FILENAME,
|
||||
"some_file")
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_upload_cert_pem_service_unavailable(self, m):
|
||||
m.put("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=503)
|
||||
self.assertRaises(exc.ServiceUnavailable, self.driver.upload_cert_pem,
|
||||
self.amp, self.FAKE_UUID_1, self.FAKE_PEM_FILENAME,
|
||||
"some_file")
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_cert_5sum(self, m):
|
||||
md5sum = {"md5sum": "some_real_sum"}
|
||||
m.get("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), json=md5sum)
|
||||
sum_test = self.driver.get_cert_md5sum(self.amp, self.FAKE_UUID_1,
|
||||
self.FAKE_PEM_FILENAME)
|
||||
self.assertIsNotNone(sum_test)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_cert_5sum_missing(self, m):
|
||||
m.get("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=404)
|
||||
self.assertRaises(exc.NotFound, self.driver.get_cert_md5sum,
|
||||
self.amp, self.FAKE_UUID_1, self.FAKE_PEM_FILENAME)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_cert_5sum_unauthorized(self, m):
|
||||
m.get("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=401)
|
||||
self.assertRaises(exc.Unauthorized, self.driver.get_cert_md5sum,
|
||||
self.amp, self.FAKE_UUID_1, self.FAKE_PEM_FILENAME)
|
||||
|
||||
@requests_mock.mock()
|
||||
def test_get_cert_5sum_server_error(self, m):
|
||||
m.get("{base}/listeners/{listener_id}/certificates/{filename}".format(
|
||||
base=self.base_url, listener_id=self.FAKE_UUID_1,
|
||||
filename=self.FAKE_PEM_FILENAME), status_code=500)
|
||||
self.assertRaises(exc.InternalServerError, self.driver.get_cert_md5sum,
|
||||
self.amp, self.FAKE_UUID_1 |