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 Versioning
---------- ----------
All Octavia APIs (including internal APIs like this one) are versioned. For the 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 any reference to a *:version* variable should be replaced with the literal
string 'v0.1'.) string 'v0.5'.)
Response codes Response codes
-------------- --------------
@ -408,7 +408,7 @@ Get interface
:: ::
GET URL: 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: JSON Response:
{ {
@ -422,7 +422,7 @@ Get interface
:: ::
GET URL: 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: JSON Response:
{ {
@ -435,7 +435,7 @@ Get interface
:: ::
GET URL: 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: JSON Response:
{ {
@ -621,7 +621,7 @@ Start or Stop a listener
:: ::
PUT URL: 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: JSON Response:
{ {
@ -634,7 +634,7 @@ Start or Stop a listener
:: ::
PUT URL: 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: JSON Response:
{ {
@ -647,7 +647,7 @@ Start or Stop a listener
:: ::
PUT URL: 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: JSON Response:
{ {
@ -660,7 +660,7 @@ Start or Stop a listener
:: ::
PUT URL: 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: Response:
{ {
@ -724,7 +724,7 @@ Delete a listener
:: ::
DELETE URL: 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: JSON Response:
{ {
@ -736,7 +736,7 @@ Delete a listener
:: ::
DELETE URL: 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: JSON Response:
{ {
@ -812,7 +812,7 @@ explicitly restarted
:: ::
PUT URI: 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 (Put data should contain the certificate information, concatenated as
described above) described above)
@ -826,7 +826,7 @@ explicitly restarted
:: ::
PUT URI: 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) (If PUT data does not contain a certificate)
JSON Response: JSON Response:
@ -839,7 +839,7 @@ explicitly restarted
:: ::
PUT URI: 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) (If PUT data does not contain an RSA key)
JSON Response: JSON Response:
@ -852,7 +852,7 @@ explicitly restarted
:: ::
PUT URI: 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.) (If the first certificate and the RSA key do not have the same modulus.)
JSON Response: JSON Response:
@ -865,7 +865,7 @@ explicitly restarted
:: ::
PUT URI: 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: JSON Response:
{ {
@ -988,7 +988,7 @@ Delete SSL certificate PEM file
:: ::
DELETE URL: 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: JSON Response:
{ {
@ -1000,7 +1000,7 @@ Delete SSL certificate PEM file
:: ::
DELETE URL: 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: JSON Response:
{ {
@ -1071,7 +1071,7 @@ out of the haproxy daemon status interface for tracking health and stats).
:: ::
PUT URL: 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.) (Upload PUT data should be a raw haproxy.conf file.)
JSON Response: JSON Response:
@ -1134,7 +1134,7 @@ Get listener haproxy configuration
:: ::
GET URL: 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: Response is the raw haproxy.cfg:
@ -1210,7 +1210,7 @@ Plug VIP
:: ::
POST URL: 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: JSON POST parameters:
{ {
@ -1292,7 +1292,7 @@ Plug Network
:: ::
POST URL: 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: JSON POST parameters:
{ {
@ -1362,7 +1362,7 @@ not be available for some time.
:: ::
PUT URI: 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 (Put data should contain the certificate information, concatenated as
described above) described above)
@ -1376,7 +1376,7 @@ not be available for some time.
:: ::
PUT URI: 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) (If PUT data does not contain a certificate)
JSON Response: JSON Response:
@ -1389,7 +1389,7 @@ not be available for some time.
:: ::
PUT URI: 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) (If PUT data does not contain an RSA key)
JSON Response: JSON Response:
@ -1402,7 +1402,7 @@ not be available for some time.
:: ::
PUT URI: 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.) (If the first certificate and the RSA key do not have the same modulus.)
JSON Response: JSON Response:
@ -1441,7 +1441,7 @@ OK
:: ::
PUT URI: PUT URI:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/upload https://octavia-haproxy-img-00328.local/v0.5/vrrp/upload
JSON Response: JSON Response:
{ {
@ -1489,7 +1489,7 @@ Start, Stop, or Reload keepalived
:: ::
PUT URL: PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/start https://octavia-haproxy-img-00328.local/v0.5/vrrp/start
JSON Response: JSON Response:
{ {
@ -1502,7 +1502,7 @@ Start, Stop, or Reload keepalived
:: ::
PUT URL: 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: JSON Response:
{ {
@ -1515,7 +1515,7 @@ Start, Stop, or Reload keepalived
:: ::
PUT URL: PUT URL:
https://octavia-haproxy-img-00328.local/v0.1/vrrp/stop https://octavia-haproxy-img-00328.local/v0.5/vrrp/stop
JSON Response: JSON Response:
{ {
@ -1523,4 +1523,58 @@ Start, Stop, or Reload keepalived
'details': 'keeepalived process with PID 3352 not found', '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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import stat
import flask import flask
from oslo_config import cfg
from oslo_log import log as logging
import six import six
import webob import webob
from werkzeug import exceptions 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 udp_listener_base
from octavia.amphorae.backends.agent.api_server import util from octavia.amphorae.backends.agent.api_server import util
BUFFER = 1024
CONF = cfg.CONF
PATH_PREFIX = '/' + api_server.VERSION PATH_PREFIX = '/' + api_server.VERSION
LOG = logging.getLogger(__name__)
# make the error pages all json # make the error pages all json
@ -81,6 +88,9 @@ class Server(object):
self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/<listener_id>', self.app.add_url_rule(rule=PATH_PREFIX + '/listeners/<listener_id>',
view_func=self.delete_listener, view_func=self.delete_listener,
methods=['DELETE']) 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', self.app.add_url_rule(rule=PATH_PREFIX + '/details',
view_func=self.get_details, view_func=self.get_details,
methods=['GET']) methods=['GET'])
@ -217,3 +227,27 @@ class Server(object):
def get_interface(self, ip_addr): def get_interface(self, ip_addr):
return self._amphora_info.get_interface(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): class UDPStatusSender(object):
def __init__(self): def __init__(self):
self.dests = [] self._update_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.v4sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.v4sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.v6sock = socket.socket(socket.AF_INET6, 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): def update(self, dest, port):
addrlist = socket.getaddrinfo(dest, port, 0, socket.SOCK_DGRAM) addrlist = socket.getaddrinfo(dest, port, 0, socket.SOCK_DGRAM)
@ -55,7 +46,9 @@ class UDPStatusSender(object):
break break
def _send_msg(self, dest, msg): 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) # dest = (family, socktype, proto, canonname, sockaddr)
# e.g. 0 = sock family, 4 = sockaddr - what we actually need # e.g. 0 = sock family, 4 = sockaddr - what we actually need
try: try:
@ -71,7 +64,25 @@ class UDPStatusSender(object):
# if the message isn't received # if the message isn't received
pass 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): 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) dest = round_robin_addr(self.dests)
if dest is None: if dest is None:
LOG.error('No controller address found. Unable to send heartbeat.') LOG.error('No controller address found. Unable to send heartbeat.')

View File

@ -116,3 +116,8 @@ class HealthMonitorProvisioningError(ProvisioningErrors):
class NodeProvisioningError(ProvisioningErrors): class NodeProvisioningError(ProvisioningErrors):
message = _('couldn\'t provision Node') 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 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) @six.add_metaclass(abc.ABCMeta)
class HealthMixin(object): class HealthMixin(object):

View File

@ -289,6 +289,31 @@ class HaproxyAmphoraLoadBalancerDriver(
self.client.upload_cert_pem( self.client.upload_cert_pem(
amp, listener_id, name, 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 # Check a custom hostname
class CustomHostNameCheckingAdapter(requests.adapters.HTTPAdapter): class CustomHostNameCheckingAdapter(requests.adapters.HTTPAdapter):
@ -515,3 +540,7 @@ class AmphoraAPIClient(object):
amphora_id=amp.id, listener_id=listener_id), timeout_dict, amphora_id=amp.id, listener_id=listener_id), timeout_dict,
data=config) data=config)
return exc.check_exception(r) 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') load_balancer.id, amphorae_network_config, 'post_vip_plug')
def upload_cert_amp(self, amphora, pem_file): 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.__class__.__name__, amphora.id, pem_file)
self.amphoraconfig[amphora.id, pem_file] = (amphora.id, pem_file, self.amphoraconfig[amphora.id, pem_file] = (amphora.id, pem_file,
'update_amp_cert_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( class NoopAmphoraLoadBalancerDriver(
driver_base.AmphoraLoadBalancerDriver, driver_base.AmphoraLoadBalancerDriver,
@ -164,6 +171,9 @@ class NoopAmphoraLoadBalancerDriver(
self.driver.upload_cert_amp(amphora, pem_file) 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): def update_vrrp_conf(self, loadbalancer):
pass pass

View File

@ -174,6 +174,7 @@ healthmanager_opts = [
default=None, default=None,
help=_('Number of processes for amphora stats update.')), help=_('Number of processes for amphora stats update.')),
cfg.StrOpt('heartbeat_key', cfg.StrOpt('heartbeat_key',
mutable=True,
help=_('key used to validate amphora sending ' help=_('key used to validate amphora sending '
'the message'), secret=True), 'the message'), secret=True),
cfg.IntOpt('heartbeat_timeout', cfg.IntOpt('heartbeat_timeout',
@ -191,9 +192,11 @@ healthmanager_opts = [
help=_('List of controller ip and port pairs for the ' help=_('List of controller ip and port pairs for the '
'heartbeat receivers. Example 127.0.0.1:5555, ' 'heartbeat receivers. Example 127.0.0.1:5555, '
'192.168.0.1:5555'), '192.168.0.1:5555'),
mutable=True,
default=[]), default=[]),
cfg.IntOpt('heartbeat_interval', cfg.IntOpt('heartbeat_interval',
default=10, default=10,
mutable=True,
help=_('Sleep time between sending heartbeats.')), help=_('Sleep time between sending heartbeats.')),
# Used for updating health and stats # Used for updating health and stats
@ -377,6 +380,7 @@ controller_worker_opts = [
cfg.StrOpt('loadbalancer_topology', cfg.StrOpt('loadbalancer_topology',
default=constants.TOPOLOGY_SINGLE, default=constants.TOPOLOGY_SINGLE,
choices=constants.SUPPORTED_LB_TOPOLOGIES, choices=constants.SUPPORTED_LB_TOPOLOGIES,
mutable=True,
help=_('Load balancer topology configuration. ' help=_('Load balancer topology configuration. '
'SINGLE - One amphora per load balancer. ' 'SINGLE - One amphora per load balancer. '
'ACTIVE_STANDBY - Two 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' AMP_LISTENER_UPDATE = 'octavia-amp-listeners-update'
GENERATE_SERVER_PEM_TASK = 'GenerateServerPEMTask' GENERATE_SERVER_PEM_TASK = 'GenerateServerPEMTask'
AMPHORA_CONFIG_UPDATE_TASK = 'AmphoraConfigUpdateTask'
# Batch Member Update constants # Batch Member Update constants
UNORDERED_MEMBER_UPDATES_FLOW = 'octavia-unordered-member-updates-flow' 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: if ip_address in CONF.networking.reserved_ips:
raise exceptions.InvalidOption(value=ip_address, raise exceptions.InvalidOption(value=ip_address,
option='member 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 :returns: amphora_id
""" """
create_amp_tf = self._taskflow_load( try:
self._amphora_flows.get_create_amphora_flow(), create_amp_tf = self._taskflow_load(
store={constants.BUILD_TYPE_PRIORITY: self._amphora_flows.get_create_amphora_flow(),
constants.LB_CREATE_SPARES_POOL_PRIORITY} store={constants.BUILD_TYPE_PRIORITY:
) constants.LB_CREATE_SPARES_POOL_PRIORITY,
with tf_logging.DynamicLoggingListener( constants.FLAVOR: None}
create_amp_tf, log=LOG, )
hide_inputs_outputs_of=self._exclude_result_logging_tasks): 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): def delete_amphora(self, amphora_id):
"""Deletes an existing Amphora. """Deletes an existing Amphora.

View File

@ -95,6 +95,10 @@ class AmphoraFlows(object):
requires=constants.AMPHORA_ID, requires=constants.AMPHORA_ID,
provides=constants.AMPHORA)) 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: if role == constants.ROLE_MASTER:
post_map_amp_to_lb.add(database_tasks.MarkAmphoraMasterInDB( post_map_amp_to_lb.add(database_tasks.MarkAmphoraMasterInDB(
name=sf_name + '-' + constants.MARK_AMP_MASTER_INDB, 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 # Setup the task that maps an amphora to a load balancer
allocate_and_associate_amp = database_tasks.MapLoadbalancerToAmphora( allocate_and_associate_amp = database_tasks.MapLoadbalancerToAmphora(
name=sf_name + '-' + constants.MAP_LOADBALANCER_TO_AMPHORA, name=sf_name + '-' + constants.MAP_LOADBALANCER_TO_AMPHORA,
requires=constants.LOADBALANCER_ID, requires=(constants.LOADBALANCER_ID, constants.FLAVOR),
provides=constants.AMPHORA_ID) provides=constants.AMPHORA_ID)
# Define a subflow for if we successfully map an amphora # 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 import task
from taskflow.types import failure 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.amphorae.driver_exceptions import exceptions as driver_except
from octavia.common import constants from octavia.common import constants
from octavia.controller.worker import task_utils as task_utilities from octavia.controller.worker import task_utils as task_utilities
@ -357,7 +358,7 @@ class AmphoraVRRPStart(BaseAmphoraTask):
class AmphoraComputeConnectivityWait(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): def execute(self, amphora):
"""Execute get_info routine for an amphora until it responds.""" """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, self.amphora_repo.update(db_apis.get_session(), amphora.id,
status=constants.ERROR) status=constants.ERROR)
raise 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 constants
from octavia.common import data_models from octavia.common import data_models
import octavia.common.tls_utils.cert_parser as cert_parser 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.controller.worker import task_utils as task_utilities
from octavia.db import api as db_apis from octavia.db import api as db_apis
from octavia.db import repositories as repo from octavia.db import repositories as repo
@ -479,7 +480,7 @@ class AssociateFailoverAmphoraWithLBID(BaseDatabaseTask):
class MapLoadbalancerToAmphora(BaseDatabaseTask): class MapLoadbalancerToAmphora(BaseDatabaseTask):
"""Maps and assigns a load balancer to an amphora in the database.""" """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. """Allocates an Amphora for the load balancer in the database.
:param loadbalancer_id: The load balancer id to map to an amphora :param loadbalancer_id: The load balancer id to map to an amphora
@ -495,6 +496,13 @@ class MapLoadbalancerToAmphora(BaseDatabaseTask):
"pool allocation.") "pool allocation.")
return None 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( amp = self.amphora_repo.allocate_and_associate(
db_apis.get_session(), db_apis.get_session(),
loadbalancer_id) loadbalancer_id)

View File

@ -19,6 +19,7 @@ import socket
import stat import stat
import subprocess import subprocess
import fixtures
import mock import mock
import netifaces import netifaces
from oslo_config import fixture as oslo_fixture 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 import octavia.tests.unit.base as base
AMP_AGENT_CONF_PATH = '/etc/octavia/amphora-agent.conf'
RANDOM_ERROR = 'random error' RANDOM_ERROR = 'random error'
OK = dict(message='OK') 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="haproxy_amphora", base_path='/var/lib/octavia')
self.conf.config(group="controller_worker", self.conf.config(group="controller_worker",
loadbalancer_topology=consts.TOPOLOGY_SINGLE) 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', with mock.patch('distro.id',
return_value='ubuntu'): return_value='ubuntu'):
self.ubuntu_test_server = server.Server() self.ubuntu_test_server = server.Server()
@ -2650,3 +2657,38 @@ class TestServerTestCase(base.TestCase):
self.assertEqual(200, rv.status_code) self.assertEqual(200, rv.status_code)
self.assertEqual(expected_dict, self.assertEqual(expected_dict,
json.loads(rv.data.decode('utf-8'))) 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 # Should not raise an exception
sender.dosend(SAMPLE_MSG) 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.driver.client.get_info.assert_called_once_with(self.amp)
self.assertEqual(ref_versions, result) 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): class TestAmphoraAPIClientTest(base.TestCase):
@ -1067,3 +1072,16 @@ class TestAmphoraAPIClientTest(base.TestCase):
self.assertRaises(exc.InternalServerError, self.assertRaises(exc.InternalServerError,
self.driver.get_interface, self.driver.get_interface,
self.amp, ip_addr) 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)) vip_subnet=network_models.Subnet(id=self.FAKE_UUID_1))
} }
self.pem_file = 'test_pem_file' self.pem_file = 'test_pem_file'
self.agent_config = 'test agent config'
self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1, self.timeout_dict = {constants.REQ_CONN_TIMEOUT: 1,
constants.REQ_READ_TIMEOUT: 2, constants.REQ_READ_TIMEOUT: 2,
constants.CONN_MAX_RETRIES: 3, constants.CONN_MAX_RETRIES: 3,
@ -147,3 +148,10 @@ class TestNoopAmphoraLoadBalancerDriver(base.TestCase):
(self.amphora.id, self.pem_file, 'update_amp_cert_file'), (self.amphora.id, self.pem_file, 'update_amp_cert_file'),
self.driver.driver.amphoraconfig[( self.driver.driver.amphoraconfig[(
self.amphora.id, self.pem_file)]) 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, self.assertRaises(exceptions.InvalidOption,
validate.ip_not_reserved, validate.ip_not_reserved,
'2001:0DB8::5') '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.assertIsInstance(amp_flow, flow.Flow)
self.assertIn(constants.FLAVOR, amp_flow.requires)
self.assertIn(constants.AMPHORA_ID, amp_flow.requires) self.assertIn(constants.AMPHORA_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(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( amp_flow = self.AmpFlow._get_post_map_lb_subflow(
'SOMEPREFIX', constants.ROLE_BACKUP) 'SOMEPREFIX', constants.ROLE_BACKUP)
self.assertIsInstance(amp_flow, flow.Flow) 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_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(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( amp_flow = self.AmpFlow._get_post_map_lb_subflow(
'SOMEPREFIX', constants.ROLE_STANDALONE) 'SOMEPREFIX', constants.ROLE_STANDALONE)
self.assertIsInstance(amp_flow, flow.Flow) 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_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(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( amp_flow = self.AmpFlow._get_post_map_lb_subflow(
'SOMEPREFIX', 'BOGUS_ROLE') 'SOMEPREFIX', 'BOGUS_ROLE')
self.assertIsInstance(amp_flow, flow.Flow) 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_ID, amp_flow.requires)
self.assertIn(constants.AMPHORA, amp_flow.provides) self.assertIn(constants.AMPHORA, amp_flow.provides)
self.assertEqual(1, len(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() LB_ID = uuidutils.generate_uuid()
CONN_MAX_RETRIES = 10 CONN_MAX_RETRIES = 10
CONN_RETRY_INTERVAL = 6 CONN_RETRY_INTERVAL = 6
FAKE_CONFIG_FILE = 'fake config file'
_amphora_mock = mock.MagicMock() _amphora_mock = mock.MagicMock()
_amphora_mock.id = AMP_ID _amphora_mock.id = AMP_ID
@ -71,6 +72,8 @@ class TestAmphoraDriverTasks(base.TestCase):
active_connection_max_retries=CONN_MAX_RETRIES) active_connection_max_retries=CONN_MAX_RETRIES)
conf.config(group="haproxy_amphora", conf.config(group="haproxy_amphora",
active_connection_rety_interval=CONN_RETRY_INTERVAL) active_connection_rety_interval=CONN_RETRY_INTERVAL)
conf.config(group="controller_worker",
loadbalancer_topology=constants.TOPOLOGY_SINGLE)
super(TestAmphoraDriverTasks, self).setUp() super(TestAmphoraDriverTasks, self).setUp()
def test_amp_listener_update(self, def test_amp_listener_update(self,
@ -615,3 +618,50 @@ class TestAmphoraDriverTasks(base.TestCase):
amp_compute_conn_wait_obj.execute, _amphora_mock) amp_compute_conn_wait_obj.execute, _amphora_mock)
mock_amphora_repo_update.assert_called_once_with( mock_amphora_repo_update.assert_called_once_with(
_session_mock, AMP_ID, status=constants.ERROR) _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( assert_called_once_with(
'TEST', 'TEST',
store={constants.BUILD_TYPE_PRIORITY: 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() _flow_mock.run.assert_called_once_with()