Fix flavors support when using spares pool

This patch validates that a flavor is compatible with using spares
pool amphora. It will also update the amphora-agent config after
a spares pool amphora has been allocated.

This patch enables the ability to update a running amphora's agent
configuration and have the mutatable options be adopted.

The following amphora agent configuration options can be updated:
heartbeat_key
controller_ip_port_list
heartbeat_interval
loadbalancer_topology

This patch adds the support to the amphora-agent and the amphora
driver. A follow on patch will expose this capabililty via the
amphora admin API.

Change-Id: I97bdf5188808193516509f20767e82c0f8d2f5a5
This commit is contained in:
Michael Johnson 2019-01-22 18:07:58 -08:00
parent ddcae3e229
commit 5d7f10f6b8
22 changed files with 435 additions and 60 deletions

View File

@ -29,9 +29,9 @@ communication is limited to fail-over protocols.)
Versioning
----------
All Octavia APIs (including internal APIs like this one) are versioned. For the
purposes of this document, the initial version of this API shall be v0.1. (So,
purposes of this document, the initial version of this API shall be v0.5. (So,
any reference to a *:version* variable should be replaced with the literal
string 'v0.1'.)
string 'v0.5'.)
Response codes
--------------
@ -408,7 +408,7 @@ Get interface
::
GET URL:
https://octavia-haproxy-img-00328.local/v0.1/interface/10.0.0.1
https://octavia-haproxy-img-00328.local/v0.5/interface/10.0.0.1
JSON Response:
{
@ -422,7 +422,7 @@ Get interface
::
GET URL:
https://octavia-haproxy-img-00328.local/v0.1/interface/10.5.0.1
https://octavia-haproxy-img-00328.local/v0.5/interface/10.5.0.1
JSON Response:
{
@ -435,7 +435,7 @@ Get interface
::
GET URL:
https://octavia-haproxy-img-00328.local/v0.1/interface/10.6.0.1.1
https://octavia-haproxy-img-00328.local/v0.5/interface/10.6.0.1.1
JSON Response:
{
@ -621,7 +621,7 @@ Start or Stop a listener
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/start
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/start
JSON Response:
{
@ -634,7 +634,7 @@ Start or Stop a listener
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/BAD_TEST_DATA
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/BAD_TEST_DATA
JSON Response:
{
@ -647,7 +647,7 @@ Start or Stop a listener
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop
JSON Response:
{
@ -660,7 +660,7 @@ Start or Stop a listener
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/stop
Response:
{
@ -724,7 +724,7 @@ Delete a listener
::
DELETE URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a
JSON Response:
{
@ -736,7 +736,7 @@ Delete a listener
::
DELETE URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a
JSON Response:
{
@ -812,7 +812,7 @@ explicitly restarted
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
(Put data should contain the certificate information, concatenated as
described above)
@ -826,7 +826,7 @@ explicitly restarted
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
(If PUT data does not contain a certificate)
JSON Response:
@ -839,7 +839,7 @@ explicitly restarted
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
(If PUT data does not contain an RSA key)
JSON Response:
@ -852,7 +852,7 @@ explicitly restarted
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
(If the first certificate and the RSA key do not have the same modulus.)
JSON Response:
@ -865,7 +865,7 @@ explicitly restarted
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
JSON Response:
{
@ -988,7 +988,7 @@ Delete SSL certificate PEM file
::
DELETE URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
JSON Response:
{
@ -1000,7 +1000,7 @@ Delete SSL certificate PEM file
::
DELETE URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
https://octavia-haproxy-img-00328.local/v0.5/listeners/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/certificates/www.example.com.pem
JSON Response:
{
@ -1071,7 +1071,7 @@ out of the haproxy daemon status interface for tracking health and stats).
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/d459b1c8-54b0-4030-9bec-4f449e73b1ef/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/haproxy
https://octavia-haproxy-img-00328.local/v0.5/listeners/d459b1c8-54b0-4030-9bec-4f449e73b1ef/04bff5c3-5862-4a13-b9e3-9b440d0ed50a/haproxy
(Upload PUT data should be a raw haproxy.conf file.)
JSON Response:
@ -1134,7 +1134,7 @@ Get listener haproxy configuration
::
GET URL:
https://octavia-haproxy-img-00328.local/v0.1/listeners/7e9f91eb-b3e6-4e3b-a1a7-d6f7fdc1de7c/haproxy
https://octavia-haproxy-img-00328.local/v0.5/listeners/7e9f91eb-b3e6-4e3b-a1a7-d6f7fdc1de7c/haproxy
Response is the raw haproxy.cfg:
@ -1210,7 +1210,7 @@ Plug VIP
::
POST URL:
https://octavia-haproxy-img-00328.local/v0.1/plug/vip/203.0.113.2
https://octavia-haproxy-img-00328.local/v0.5/plug/vip/203.0.113.2
JSON POST parameters:
{
@ -1292,7 +1292,7 @@ Plug Network
::
POST URL:
https://octavia-haproxy-img-00328.local/v0.1/plug/network/
https://octavia-haproxy-img-00328.local/v0.5/plug/network/
JSON POST parameters:
{
@ -1362,7 +1362,7 @@ not be available for some time.
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/certificate
https://octavia-haproxy-img-00328.local/v0.5/certificate
(Put data should contain the certificate information, concatenated as
described above)
@ -1376,7 +1376,7 @@ not be available for some time.
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/certificates
https://octavia-haproxy-img-00328.local/v0.5/certificates
(If PUT data does not contain a certificate)
JSON Response:
@ -1389,7 +1389,7 @@ not be available for some time.
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/certificate
https://octavia-haproxy-img-00328.local/v0.5/certificate
(If PUT data does not contain an RSA key)
JSON Response:
@ -1402,7 +1402,7 @@ not be available for some time.
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/certificate
https://octavia-haproxy-img-00328.local/v0.5/certificate
(If the first certificate and the RSA key do not have the same modulus.)
JSON Response:
@ -1441,7 +1441,7 @@ OK
::
PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/upload
https://octavia-haproxy-img-00328.local/v0.5/vrrp/upload
JSON Response:
{
@ -1489,7 +1489,7 @@ Start, Stop, or Reload keepalived
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/start
https://octavia-haproxy-img-00328.local/v0.5/vrrp/start
JSON Response:
{
@ -1502,7 +1502,7 @@ Start, Stop, or Reload keepalived
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/BAD_TEST_DATA
https://octavia-haproxy-img-00328.local/v0.5/vrrp/BAD_TEST_DATA
JSON Response:
{
@ -1515,7 +1515,7 @@ Start, Stop, or Reload keepalived
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/stop
https://octavia-haproxy-img-00328.local/v0.5/vrrp/stop
JSON Response:
{
@ -1523,4 +1523,58 @@ Start, Stop, or Reload keepalived
'details': 'keeepalived process with PID 3352 not found',
}
Update the amphora agent configuration
--------------------------------------
* **URL:** /*:version*/config
* **Method:** PUT
* **Data params:** A amphora-agent configuration file
* **Success Response:**
* Code: 202
* Content: OK
* **Error Response:**
* Code: 500
* message: Unable to update amphora-agent configuration.
* details: *(The exception details)*
* **Response:**
| OK
* **Implied actions:**
* The running amphora-agent configuration file is mutated.
**Notes:** Only options that are marked mutable in the oslo configuration
will be updated.
**Examples:**
* Success code 202:
::
PUT URL:
https://octavia-haproxy-img-00328.local/v0.5/config
(Upload PUT data should be a raw amphora-agent.conf file.)
JSON Response:
{
'message': 'OK'
}
* Error code 500:
::
JSON Response:
{
'message': 'Unable to update amphora-agent configuration.',
'details': *(The exception output)*,
}

View File

@ -12,8 +12,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import os
import stat
import flask
from oslo_config import cfg
from oslo_log import log as logging
import six
import webob
from werkzeug import exceptions
@ -28,7 +32,10 @@ from octavia.amphorae.backends.agent.api_server import plug
from octavia.amphorae.backends.agent.api_server import udp_listener_base
from octavia.amphorae.backends.agent.api_server import util
BUFFER = 1024
CONF = cfg.CONF
PATH_PREFIX = '/' + api_server.VERSION
LOG = logging.getLogger(__name__)
# make the error pages all json
@ -81,6 +88,9 @@ class Server(object):
self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/<listener_id>',
view_func=self.delete_listener,
methods=['DELETE'])
self.app.add_url_rule(rule=PATH_PREFIX + '/config',
view_func=self.upload_config,
methods=['PUT'])
self.app.add_url_rule(rule=PATH_PREFIX + '/details',
view_func=self.get_details,
methods=['GET'])
@ -217,3 +227,27 @@ class Server(object):
def get_interface(self, ip_addr):
return self._amphora_info.get_interface(ip_addr)
def upload_config(self):
try:
stream = flask.request.stream
file_path = cfg.find_config_files(project=CONF.project,
prog=CONF.prog)[0]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
# mode 00600
mode = stat.S_IRUSR | stat.S_IWUSR
with os.fdopen(os.open(file_path, flags, mode), 'wb') as cfg_file:
b = stream.read(BUFFER)
while b:
cfg_file.write(b)
b = stream.read(BUFFER)
CONF.mutate_config_files()
except Exception as e:
LOG.error("Unable to update amphora-agent configuration: "
"{}".format(str(e)))
return webob.Response(json=dict(
message="Unable to update amphora-agent configuration.",
details=str(e)), status=500)
return webob.Response(json={'message': 'OK'}, status=202)

View File

@ -33,18 +33,9 @@ def round_robin_addr(addrinfo_list):
class UDPStatusSender(object):
def __init__(self):
self.dests = []
for ipport in CONF.health_manager.controller_ip_port_list:
try:
ip, port = ipport.rsplit(':', 1)
except ValueError:
LOG.error("Invalid ip and port '%s' in health_manager "
"controller_ip_port_list", ipport)
break
self.update(ip, port)
self._update_dests()
self.v4sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.v6sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
self.key = str(CONF.health_manager.heartbeat_key)
def update(self, dest, port):
addrlist = socket.getaddrinfo(dest, port, 0, socket.SOCK_DGRAM)
@ -55,7 +46,9 @@ class UDPStatusSender(object):
break
def _send_msg(self, dest, msg):
envelope_str = status_message.wrap_envelope(msg, self.key)
# Note: heartbeat_key is mutable and must be looked up for each call
envelope_str = status_message.wrap_envelope(
msg, str(CONF.health_manager.heartbeat_key))
# dest = (family, socktype, proto, canonname, sockaddr)
# e.g. 0 = sock family, 4 = sockaddr - what we actually need
try:
@ -71,7 +64,25 @@ class UDPStatusSender(object):
# if the message isn't received
pass
# The controller_ip_port_list configuration has mutated, reload it.
def _update_dests(self):
self.dests = []
for ipport in CONF.health_manager.controller_ip_port_list:
try:
ip, port = ipport.rsplit(':', 1)
except ValueError:
LOG.error("Invalid ip and port '%s' in health_manager "
"controller_ip_port_list", ipport)
break
self.update(ip, port)
self.current_controller_ip_port_list = (
CONF.health_manager.controller_ip_port_list)
def dosend(self, obj):
# Check for controller_ip_port_list mutation
if not (self.current_controller_ip_port_list ==
CONF.health_manager.controller_ip_port_list):
self._update_dests()
dest = round_robin_addr(self.dests)
if dest is None:
LOG.error('No controller address found. Unable to send heartbeat.')

View File

@ -116,3 +116,8 @@ class HealthMonitorProvisioningError(ProvisioningErrors):
class NodeProvisioningError(ProvisioningErrors):
message = _('couldn\'t provision Node')
class AmpDriverNotImplementedError(AmphoraDriverError):
message = _('Amphora does not implement this feature.')

View File

@ -219,6 +219,16 @@ class AmphoraLoadBalancerDriver(object):
"""
pass
def update_agent_config(self, amphora, agent_config):
"""Upload and update the amphora agent configuration.
:param amphora: amphora object, needs id and network ip(s)
:type amphora: object
:param agent_config: The new amphora agent configuration file.
:type agent_config: string
"""
pass
@six.add_metaclass(abc.ABCMeta)
class HealthMixin(object):

View File

@ -289,6 +289,31 @@ class HaproxyAmphoraLoadBalancerDriver(
self.client.upload_cert_pem(
amp, listener_id, name, pem)
def update_amphora_agent_config(self, amphora, agent_config,
timeout_dict=None):
"""Update the amphora agent configuration file.
:param amphora: The amphora to update.
:type amphora: object
:param agent_config: The new amphora agent configuration.
:type agent_config: string
:param timeout_dict: Dictionary of timeout values for calls to the
amphora. May contain: req_conn_timeout,
req_read_timeout, conn_max_retries,
conn_retry_interval
:returns: None
Note: This will mutate the amphora agent config and adopt the
new values.
"""
try:
self.client.update_agent_config(amphora, agent_config,
timeout_dict=timeout_dict)
except exc.NotFound:
LOG.debug('Amphora {} does not support the update_agent_config '
'API.'.format(amphora.id))
raise driver_except.AmpDriverNotImplementedError()
# Check a custom hostname
class CustomHostNameCheckingAdapter(requests.adapters.HTTPAdapter):
@ -515,3 +540,7 @@ class AmphoraAPIClient(object):
amphora_id=amp.id, listener_id=listener_id), timeout_dict,
data=config)
return exc.check_exception(r)
def update_agent_config(self, amp, agent_config, timeout_dict=None):
r = self.put(amp, 'config', timeout_dict, data=agent_config)
return exc.check_exception(r)

View File

@ -104,11 +104,18 @@ class NoopManager(object):
load_balancer.id, amphorae_network_config, 'post_vip_plug')
def upload_cert_amp(self, amphora, pem_file):
LOG.debug("Amphora %s no-op, upload cert amphora %s,with pem fle %s",
LOG.debug("Amphora %s no-op, upload cert amphora %s,with pem file %s",
self.__class__.__name__, amphora.id, pem_file)
self.amphoraconfig[amphora.id, pem_file] = (amphora.id, pem_file,
'update_amp_cert_file')
def update_agent_config(self, amphora, agent_config):
LOG.debug("Amphora %s no-op, update agent config amphora "
"%s, with agent config %s",
self.__class__.__name__, amphora.id, agent_config)
self.amphoraconfig[amphora.id, agent_config] = (
amphora.id, agent_config, 'update_agent_config')
class NoopAmphoraLoadBalancerDriver(
driver_base.AmphoraLoadBalancerDriver,
@ -164,6 +171,9 @@ class NoopAmphoraLoadBalancerDriver(
self.driver.upload_cert_amp(amphora, pem_file)
def update_agent_config(self, amphora, agent_config):
self.driver.update_agent_config(amphora, agent_config)
def update_vrrp_conf(self, loadbalancer):
pass

View File

@ -174,6 +174,7 @@ healthmanager_opts = [
default=None,
help=_('Number of processes for amphora stats update.')),
cfg.StrOpt('heartbeat_key',
mutable=True,
help=_('key used to validate amphora sending '
'the message'), secret=True),
cfg.IntOpt('heartbeat_timeout',
@ -191,9 +192,11 @@ healthmanager_opts = [
help=_('List of controller ip and port pairs for the '
'heartbeat receivers. Example 127.0.0.1:5555, '
'192.168.0.1:5555'),
mutable=True,
default=[]),
cfg.IntOpt('heartbeat_interval',
default=10,
mutable=True,
help=_('Sleep time between sending heartbeats.')),
# Used for updating health and stats
@ -377,6 +380,7 @@ controller_worker_opts = [
cfg.StrOpt('loadbalancer_topology',
default=constants.TOPOLOGY_SINGLE,
choices=constants.SUPPORTED_LB_TOPOLOGIES,
mutable=True,
help=_('Load balancer topology configuration. '
'SINGLE - One amphora per load balancer. '
'ACTIVE_STANDBY - Two amphora per load balancer.')),

View File

@ -334,6 +334,7 @@ AMP_COMPUTE_CONNECTIVITY_WAIT = 'octavia-amp-compute-connectivity-wait'
AMP_LISTENER_UPDATE = 'octavia-amp-listeners-update'
GENERATE_SERVER_PEM_TASK = 'GenerateServerPEMTask'
AMPHORA_CONFIG_UPDATE_TASK = 'AmphoraConfigUpdateTask'
# Batch Member Update constants
UNORDERED_MEMBER_UPDATES_FLOW = 'octavia-unordered-member-updates-flow'

View File

@ -346,3 +346,11 @@ def ip_not_reserved(ip_address):
if ip_address in CONF.networking.reserved_ips:
raise exceptions.InvalidOption(value=ip_address,
option='member address')
def is_flavor_spares_compatible(flavor):
if flavor:
# If a compute flavor is specified, the flavor is not spares compatible
if flavor.get(constants.COMPUTE_FLAVOR, None):
return False
return True

View File

@ -108,18 +108,22 @@ class ControllerWorker(base_taskflow.BaseTaskFlowEngine):
:returns: amphora_id
"""
create_amp_tf = self._taskflow_load(
self._amphora_flows.get_create_amphora_flow(),
store={constants.BUILD_TYPE_PRIORITY:
constants.LB_CREATE_SPARES_POOL_PRIORITY}
)
with tf_logging.DynamicLoggingListener(
create_amp_tf, log=LOG,
hide_inputs_outputs_of=self._exclude_result_logging_tasks):
try:
create_amp_tf = self._taskflow_load(
self._amphora_flows.get_create_amphora_flow(),
store={constants.BUILD_TYPE_PRIORITY:
constants.LB_CREATE_SPARES_POOL_PRIORITY,
constants.FLAVOR: None}
)
with tf_logging.DynamicLoggingListener(
create_amp_tf, log=LOG,
hide_inputs_outputs_of=self._exclude_result_logging_tasks):
create_amp_tf.run()
create_amp_tf.run()
return create_amp_tf.storage.fetch('amphora')
return create_amp_tf.storage.fetch('amphora')
except Exception as e:
LOG.error('Failed to create an amphora due to: {}'.format(str(e)))
def delete_amphora(self, amphora_id):
"""Deletes an existing Amphora.

View File

@ -95,6 +95,10 @@ class AmphoraFlows(object):
requires=constants.AMPHORA_ID,
provides=constants.AMPHORA))
post_map_amp_to_lb.add(amphora_driver_tasks.AmphoraConfigUpdate(
name=sf_name + '-' + constants.AMPHORA_CONFIG_UPDATE_TASK,
requires=(constants.AMPHORA, constants.FLAVOR)))
if role == constants.ROLE_MASTER:
post_map_amp_to_lb.add(database_tasks.MarkAmphoraMasterInDB(
name=sf_name + '-' + constants.MARK_AMP_MASTER_INDB,
@ -252,7 +256,7 @@ class AmphoraFlows(object):
# Setup the task that maps an amphora to a load balancer
allocate_and_associate_amp = database_tasks.MapLoadbalancerToAmphora(
name=sf_name + '-' + constants.MAP_LOADBALANCER_TO_AMPHORA,
requires=constants.LOADBALANCER_ID,
requires=(constants.LOADBALANCER_ID, constants.FLAVOR),
provides=constants.AMPHORA_ID)
# Define a subflow for if we successfully map an amphora

View File

@ -20,6 +20,7 @@ from stevedore import driver as stevedore_driver
from taskflow import task
from taskflow.types import failure
from octavia.amphorae.backends.agent import agent_jinja_cfg
from octavia.amphorae.driver_exceptions import exceptions as driver_except
from octavia.common import constants
from octavia.controller.worker import task_utils as task_utilities
@ -357,7 +358,7 @@ class AmphoraVRRPStart(BaseAmphoraTask):
class AmphoraComputeConnectivityWait(BaseAmphoraTask):
""""Task to wait for the compute instance to be up."""
"""Task to wait for the compute instance to be up."""
def execute(self, amphora):
"""Execute get_info routine for an amphora until it responds."""
@ -373,3 +374,28 @@ class AmphoraComputeConnectivityWait(BaseAmphoraTask):
self.amphora_repo.update(db_apis.get_session(), amphora.id,
status=constants.ERROR)
raise
class AmphoraConfigUpdate(BaseAmphoraTask):
"""Task to push a new amphora agent configuration to the amphroa."""
def execute(self, amphora, flavor):
# Extract any flavor based settings
if flavor:
topology = flavor.get(constants.LOADBALANCER_TOPOLOGY,
CONF.controller_worker.loadbalancer_topology)
else:
topology = CONF.controller_worker.loadbalancer_topology
# Build the amphora agent config
agent_cfg_tmpl = agent_jinja_cfg.AgentJinjaTemplater()
agent_config = agent_cfg_tmpl.build_agent_config(amphora.id, topology)
# Push the new configuration to the amphroa
try:
self.amphora_driver.update_amphora_agent_config(amphora,
agent_config)
except driver_except.AmpDriverNotImplementedError:
LOG.error('Amphora {} does not support agent configuration '
'update. Please update the amphora image for this '
'amphora. Skipping.'.format(amphora.id))

View File

@ -27,6 +27,7 @@ from taskflow.types import failure
from octavia.common import constants
from octavia.common import data_models
import octavia.common.tls_utils.cert_parser as cert_parser
from octavia.common import validate
from octavia.controller.worker import task_utils as task_utilities
from octavia.db import api as db_apis
from octavia.db import repositories as repo
@ -479,7 +480,7 @@ class AssociateFailoverAmphoraWithLBID(BaseDatabaseTask):
class MapLoadbalancerToAmphora(BaseDatabaseTask):
"""Maps and assigns a load balancer to an amphora in the database."""
def execute(self, loadbalancer_id, server_group_id=None):
def execute(self, loadbalancer_id, server_group_id=None, flavor=None):
"""Allocates an Amphora for the load balancer in the database.
:param loadbalancer_id: The load balancer id to map to an amphora
@ -495,6 +496,13 @@ class MapLoadbalancerToAmphora(BaseDatabaseTask):
"pool allocation.")
return None
# Validate the flavor is spares compatible
if not validate.is_flavor_spares_compatible(flavor):
LOG.debug("Load balancer has a flavor that is not compatible with "
"using spares pool amphora. Skipping spares pool "
"allocation.")
return None
amp = self.amphora_repo.allocate_and_associate(
db_apis.get_session(),
loadbalancer_id)

View File

@ -19,6 +19,7 @@ import socket
import stat
import subprocess
import fixtures
import mock
import netifaces
from oslo_config import fixture as oslo_fixture
@ -36,6 +37,7 @@ from octavia.tests.common import utils as test_utils
import octavia.tests.unit.base as base
AMP_AGENT_CONF_PATH = '/etc/octavia/amphora-agent.conf'
RANDOM_ERROR = 'random error'
OK = dict(message='OK')
@ -49,6 +51,11 @@ class TestServerTestCase(base.TestCase):
self.conf.config(group="haproxy_amphora", base_path='/var/lib/octavia')
self.conf.config(group="controller_worker",
loadbalancer_topology=consts.TOPOLOGY_SINGLE)
self.conf.load_raw_values(project='fake_project')
self.conf.load_raw_values(prog='fake_prog')
self.useFixture(fixtures.MockPatch(
'oslo_config.cfg.find_config_files',
return_value=[AMP_AGENT_CONF_PATH]))
with mock.patch('distro.id',
return_value='ubuntu'):
self.ubuntu_test_server = server.Server()
@ -2650,3 +2657,38 @@ class TestServerTestCase(base.TestCase):
self.assertEqual(200, rv.status_code)
self.assertEqual(expected_dict,
json.loads(rv.data.decode('utf-8')))
def test_ubuntu_upload_config(self):
self._test_upload_config(consts.UBUNTU)
def test_centos_upload_config(self):
self._test_upload_config(consts.CENTOS)
@mock.patch('oslo_config.cfg.CONF.mutate_config_files')
def _test_upload_config(self, distro, mock_mutate):
server.BUFFER = 5 # test the while loop
m = self.useFixture(
test_utils.OpenFixture(AMP_AGENT_CONF_PATH)).mock_open
with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m):
if distro == consts.UBUNTU:
rv = self.ubuntu_app.put('/' + api_server.VERSION +
'/config', data='TestTest')
elif distro == consts.CENTOS:
rv = self.centos_app.put('/' + api_server.VERSION +
'/config', data='TestTest')
self.assertEqual(202, rv.status_code)
self.assertEqual(OK, json.loads(rv.data.decode('utf-8')))
handle = m()
handle.write.assert_any_call(six.b('TestT'))
handle.write.assert_any_call(six.b('est'))
mock_mutate.assert_called_once_with()
# Test the exception handling
mock_mutate.side_effect = Exception('boom')
if distro == consts.UBUNTU:
rv = self.ubuntu_app.put('/' + api_server.VERSION +
'/config', data='TestTest')
elif distro == consts.CENTOS:
rv = self.centos_app.put('/' + api_server.VERSION +
'/config', data='TestTest')
self.assertEqual(500, rv.status_code)

View File

@ -127,3 +127,37 @@ class TestHealthSender(base.TestCase):
# Should not raise an exception
sender.dosend(SAMPLE_MSG)
# Test an controller_ip_port_list update
sendto_mock.reset_mock()
mock_getaddrinfo.reset_mock()
self.conf.config(group="health_manager",
controller_ip_port_list=['192.0.2.20:80'])
mock_getaddrinfo.return_value = [(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP,
'',
('192.0.2.20', 80))]
sender = health_sender.UDPStatusSender()
sender.dosend(SAMPLE_MSG)
sendto_mock.assert_called_once_with(SAMPLE_MSG_BIN,
('192.0.2.20', 80))
mock_getaddrinfo.assert_called_once_with('192.0.2.20', '80',
0, socket.SOCK_DGRAM)
sendto_mock.reset_mock()
mock_getaddrinfo.reset_mock()
self.conf.config(group="health_manager",
controller_ip_port_list=['192.0.2.21:81'])
mock_getaddrinfo.return_value = [(socket.AF_INET,
socket.SOCK_DGRAM,
socket.IPPROTO_UDP,
'',
('192.0.2.21', 81))]
sender.dosend(SAMPLE_MSG)
mock_getaddrinfo.assert_called_once_with('192.0.2.21', '81',
0, socket.SOCK_DGRAM)
sendto_mock.assert_called_once_with(SAMPLE_MSG_BIN,
('192.0.2.21', 81))
sendto_mock.reset_mock()
mock_getaddrinfo.reset_mock()

View File

@ -375,6 +375,11 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.client.get_info.assert_called_once_with(self.amp)
self.assertEqual(ref_versions, result)
def test_update_amphora_agent_config(self):
self.driver.update_amphora_agent_config(self.amp, six.b('test'))
self.driver.client.update_agent_config.assert_called_once_with(
self.amp, six.b('test'), timeout_dict=None)
class TestAmphoraAPIClientTest(base.TestCase):
@ -1067,3 +1072,16 @@ class TestAmphoraAPIClientTest(base.TestCase):
self.assertRaises(exc.InternalServerError,
self.driver.get_interface,
self.amp, ip_addr)
@requests_mock.mock()
def test_update_agent_config(self, m):
m.put("{base}/config".format(base=self.base_url))
resp_body = self.driver.update_agent_config(self.amp, "some_file")
self.assertEqual(200, resp_body.status_code)
@requests_mock.mock()
def test_update_agent_config_error(self, m):
m.put("{base}/config".format(base=self.base_url), status_code=500)
self.assertRaises(exc.InternalServerError,
self.driver.update_agent_config, self.amp,
"some_file")

View File

@ -63,6 +63,7 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase):
vip_subnet=network_models.Subnet(id=self.FAKE_UUID_1))
}
self.pem_file = 'test_pem_file'
self.agent_config = 'test agent config'
self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1,
constants.REQ_READ_TIMEOUT: 2,
constants.CONN_MAX_RETRIES: 3,
@ -147,3 +148,10 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase):
(self.amphora.id, self.pem_file, 'update_amp_cert_file'),
self.driver.driver.amphoraconfig[(
self.amphora.id, self.pem_file)])
def test_update_agent_config(self):
self.driver.update_agent_config(self.amphora, self.agent_config)
self.assertEqual(
(self.amphora.id, self.agent_config, 'update_agent_config'),
self.driver.driver.amphoraconfig[(
self.amphora.id, self.agent_config)])

View File

@ -442,3 +442,13 @@ class TestValidations(base.TestCase):
self.assertRaises(exceptions.InvalidOption,
validate.ip_not_reserved,
'2001:0DB8::5')
def test_is_flavor_spares_compatible(self):
not_compat_flavor = {constants.COMPUTE_FLAVOR: 'chocolate'}
compat_flavor = {constants.LOADBALANCER_TOPOLOGY:
constants.TOPOLOGY_SINGLE}
self.assertTrue(validate.is_flavor_spares_compatible(None))
self.assertTrue(validate.is_flavor_spares_compatible(compat_flavor))
self.assertFalse(
validate.is_flavor_spares_compatible(not_compat_flavor))

View File

@ -366,41 +366,45 @@ class TestAmphoraFlows(base.TestCase):
self.assertIsInstance(amp_flow, flow.Flow)
self.assertIn(constants.FLAVOR, amp_flow.requires)
self.assertIn(constants.AMPHORA_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(amp_flow.provides))
self.assertEqual(1, len(amp_flow.requires))
self.assertEqual(2, len(amp_flow.requires))
amp_flow = self.AmpFlow._get_post_map_lb_subflow(
'SOMEPREFIX', constants.ROLE_BACKUP)
self.assertIsInstance(amp_flow, flow.Flow)
self.assertIn(constants.FLAVOR, amp_flow.requires)
self.assertIn(constants.AMPHORA_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(amp_flow.provides))
self.assertEqual(1, len(amp_flow.requires))
self.assertEqual(2, len(amp_flow.requires))
amp_flow = self.AmpFlow._get_post_map_lb_subflow(
'SOMEPREFIX', constants.ROLE_STANDALONE)
self.assertIsInstance(amp_flow, flow.Flow)
self.assertIn(constants.FLAVOR, amp_flow.requires)
self.assertIn(constants.AMPHORA_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(amp_flow.provides))
self.assertEqual(1, len(amp_flow.requires))
self.assertEqual(2, len(amp_flow.requires))
amp_flow = self.AmpFlow._get_post_map_lb_subflow(
'SOMEPREFIX', 'BOGUS_ROLE')
self.assertIsInstance(amp_flow, flow.Flow)
self.assertIn(constants.FLAVOR, amp_flow.requires)
self.assertIn(constants.AMPHORA_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(amp_flow.provides))
self.assertEqual(1, len(amp_flow.requires))
self.assertEqual(2, len(amp_flow.requires))

View File

@ -33,6 +33,7 @@ LISTENER_ID = uuidutils.generate_uuid()
LB_ID = uuidutils.generate_uuid()
CONN_MAX_RETRIES = 10
CONN_RETRY_INTERVAL = 6
FAKE_CONFIG_FILE = 'fake config file'
_amphora_mock = mock.MagicMock()
_amphora_mock.id = AMP_ID
@ -71,6 +72,8 @@ class TestAmphoraDriverTasks(base.TestCase):
active_connection_max_retries=CONN_MAX_RETRIES)
conf.config(group="haproxy_amphora",
active_connection_rety_interval=CONN_RETRY_INTERVAL)
conf.config(group="controller_worker",
loadbalancer_topology=constants.TOPOLOGY_SINGLE)
super(TestAmphoraDriverTasks, self).setUp()
def test_amp_listener_update(self,
@ -615,3 +618,50 @@ class TestAmphoraDriverTasks(base.TestCase):
amp_compute_conn_wait_obj.execute, _amphora_mock)
mock_amphora_repo_update.assert_called_once_with(
_session_mock, AMP_ID, status=constants.ERROR)
@mock.patch('octavia.amphorae.backends.agent.agent_jinja_cfg.'
'AgentJinjaTemplater.build_agent_config')
def test_amphora_config_update(self,
mock_build_config,
mock_driver,
mock_generate_uuid,
mock_log,
mock_get_session,
mock_listener_repo_get,
mock_listener_repo_update,
mock_amphora_repo_update):
mock_build_config.return_value = FAKE_CONFIG_FILE
amp_config_update_obj = amphora_driver_tasks.AmphoraConfigUpdate()
mock_driver.update_amphora_agent_config.side_effect = [
None, None, driver_except.AmpDriverNotImplementedError,
driver_except.TimeOutException]
# With Flavor
flavor = {constants.LOADBALANCER_TOPOLOGY:
constants.TOPOLOGY_ACTIVE_STANDBY}
amp_config_update_obj.execute(_amphora_mock, flavor)
mock_build_config.assert_called_once_with(
_amphora_mock.id, constants.TOPOLOGY_ACTIVE_STANDBY)
mock_driver.update_amphora_agent_config.assert_called_once_with(
_amphora_mock, FAKE_CONFIG_FILE)
# With no Flavor
mock_driver.reset_mock()
mock_build_config.reset_mock()
amp_config_update_obj.execute(_amphora_mock, None)
mock_build_config.assert_called_once_with(
_amphora_mock.id, constants.TOPOLOGY_SINGLE)
mock_driver.update_amphora_agent_config.assert_called_once_with(
_amphora_mock, FAKE_CONFIG_FILE)
# With amphora that does not support config update
mock_driver.reset_mock()
mock_build_config.reset_mock()
amp_config_update_obj.execute(_amphora_mock, flavor)
mock_build_config.assert_called_once_with(
_amphora_mock.id, constants.TOPOLOGY_ACTIVE_STANDBY)
mock_driver.update_amphora_agent_config.assert_called_once_with(
_amphora_mock, FAKE_CONFIG_FILE)
# With an unknown exception
mock_driver.reset_mock()
mock_build_config.reset_mock()
self.assertRaises(driver_except.TimeOutException,
amp_config_update_obj.execute,
_amphora_mock, flavor)

View File

@ -141,7 +141,8 @@ class TestControllerWorker(base.TestCase):
assert_called_once_with(
'TEST',
store={constants.BUILD_TYPE_PRIORITY:
constants.LB_CREATE_SPARES_POOL_PRIORITY}))
constants.LB_CREATE_SPARES_POOL_PRIORITY,
constants.FLAVOR: None}))
_flow_mock.run.assert_called_once_with()