Browse Source

Merge "Add long-running provider agent support"

tags/5.0.0.0rc1
Zuul 1 month ago
parent
commit
ff483f3457

+ 1
- 0
.pylintrc View File

@@ -9,6 +9,7 @@ ignore=.git,tests
9 9
 disable=
10 10
 # "F" Fatal errors that prevent further processing
11 11
 # "I" Informational noise
12
+ c-extension-no-member,
12 13
  locally-disabled,
13 14
 # "E" Error for important programming issues (likely bugs)
14 15
  import-error,

+ 90
- 0
doc/source/contributor/guides/providers.rst View File

@@ -33,6 +33,10 @@ Octavia API functions not listed here will continue to be handled by the
33 33
 Octavia API and will not call into the driver. Examples would be show, list,
34 34
 and quota requests.
35 35
 
36
+In addition, drivers may provide a provider agent that the Octavia driver-agent
37
+will launch at start up. This is a long-running process that is intended to
38
+support the provider driver.
39
+
36 40
 Driver Entry Points
37 41
 -------------------
38 42
 
@@ -48,6 +52,18 @@ for the octavia reference driver would be:
48 52
 
49 53
   amphora = octavia.api.drivers.amphora_driver.driver:AmphoraProviderDriver
50 54
 
55
+In addition, provider drivers may provide a provider agent also defined by a
56
+setup tools entry point. The provider agent namespace is
57
+"octavia.driver_agent.provider_agents". This will be called once, at Octavia
58
+driver-agent start up, to launch a long-running process. Provider agents must
59
+be enabled in the Octavia configuration file. An example provider agent
60
+entry point would be:
61
+
62
+.. code-block:: python
63
+
64
+  amphora_agent = octavia.api.drivers.amphora_driver.agent:AmphoraProviderAgent
65
+
66
+
51 67
 Stable Provider Driver Interface
52 68
 ================================
53 69
 
@@ -1992,6 +2008,80 @@ references to the failed record if available.
1992 2008
         super(DriverAgentTimeout, self).__init__(self.fault_string,
1993 2009
                                                  *args, **kwargs)
1994 2010
 
2011
+Provider Agents
2012
+===============
2013
+
2014
+Provider agents are long-running processes started by the Octavia driver-agent
2015
+process at start up. They are intended to allow provider drivers a long running
2016
+process that can handle periodic jobs for the provider driver or receive events
2017
+from another provider agent. Provider agents are optional and not required for
2018
+a successful Octavia provider driver.
2019
+
2020
+Provider Agents have access to the same `Stable Provider Driver Interface`_
2021
+as the provider driver. A provider agent must not access any other Octavia
2022
+code.
2023
+
2024
+.. warning::
2025
+
2026
+  The methods listed in the `Driver Support Library`_ section are the only
2027
+  Octavia callable methods for provider agents.
2028
+  All other interfaces are not considered stable or safe for provider agents to
2029
+  access. See `Stable Provider Driver Interface`_ for a list of acceptable
2030
+  APIs for provider agents use.
2031
+
2032
+Declaring Your Provider Agent
2033
+-----------------------------
2034
+
2035
+The Octavia driver-agent will use
2036
+`stevedore <https://docs.openstack.org/stevedore/latest/>`_ to load enabled
2037
+provider agents at start up. Provider agents are enabled in the Octavia
2038
+configuration file. Provider agents that are installed, but not enabled, will
2039
+not be loaded. An example configuration file entry for a provider agent is:
2040
+
2041
+.. code-block:: INI
2042
+
2043
+  [driver_agent]
2044
+  enabled_provider_agents = amphora_agent, noop_agent
2045
+
2046
+The provider agent name must match the provider agent name declared in your
2047
+python setup tools entry point. For example:
2048
+
2049
+.. code-block:: python
2050
+
2051
+  octavia.driver_agent.provider_agents =
2052
+      amphora_agent = octavia.api.drivers.amphora_driver.agent:AmphoraProviderAgent
2053
+      noop_agent = octavia.api.drivers.noop_driver.agent:noop_provider_agent
2054
+
2055
+Provider Agent Method Invocation
2056
+--------------------------------
2057
+
2058
+On start up of the Octavia driver-agent, the method defined in the entry point
2059
+will be launched in its own `multiprocessing Process <https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Process>`_.
2060
+
2061
+Your provider agent method will be passed a `multiprocessing Event <https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Event>`_ that will
2062
+be used to signal that the provider agent should shutdown. When this event
2063
+is "set", the provider agent should gracefully shutdown. If the provider agent
2064
+fails to exit within the Octavia configuration file setting
2065
+"provider_agent_shutdown_timeout" period, the driver-agent will forcefully
2066
+shutdown the provider agent with a SIGKILL signal.
2067
+
2068
+Example Provider Agent Method
2069
+-----------------------------
2070
+
2071
+If, for example, you declared a provider agent as "my_agent":
2072
+
2073
+.. code-block:: python
2074
+
2075
+  octavia.driver_agent.provider_agents =
2076
+      my_agent = example_inc.drivers.my_driver.agent:my_provider_agent
2077
+
2078
+The signature of your "my_provider_agent" method would be:
2079
+
2080
+.. code-block:: python
2081
+
2082
+  def my_provider_agent(exit_event):
2083
+
2084
+
1995 2085
 Documenting the Driver
1996 2086
 ======================
1997 2087
 

+ 6
- 0
etc/octavia.conf View File

@@ -563,3 +563,9 @@
563 563
 # Percentage of max_processes (both status and stats) in use to start
564 564
 # logging warning messages about an overloaded driver-agent.
565 565
 # max_process_warning_percent = .75
566
+
567
+# How long in seconds to wait for provider agents to exit before killing them.
568
+# provider_agent_shutdown_timeout = 60
569
+
570
+# List of enabled provider agents.
571
+# enabled_provider_agents =

+ 0
- 14
octavia/api/drivers/driver_agent/driver_listener.py View File

@@ -15,7 +15,6 @@
15 15
 
16 16
 import errno
17 17
 import os
18
-import signal
19 18
 import threading
20 19
 
21 20
 import six.moves.socketserver as socketserver
@@ -103,10 +102,6 @@ class ForkingUDSServer(socketserver.ForkingMixIn,
103 102
     pass
104 103
 
105 104
 
106
-def _mutate_config(*args, **kwargs):
107
-    CONF.mutate_config_files()
108
-
109
-
110 105
 def _cleanup_socket_file(filename):
111 106
     # Remove the socket file if it already exists
112 107
     try:
@@ -117,9 +112,6 @@ def _cleanup_socket_file(filename):
117 112
 
118 113
 
119 114
 def status_listener(exit_event):
120
-    signal.signal(signal.SIGINT, signal.SIG_IGN)
121
-    signal.signal(signal.SIGHUP, _mutate_config)
122
-
123 115
     _cleanup_socket_file(CONF.driver_agent.status_socket_path)
124 116
 
125 117
     server = ForkingUDSServer(CONF.driver_agent.status_socket_path,
@@ -140,9 +132,6 @@ def status_listener(exit_event):
140 132
 
141 133
 
142 134
 def stats_listener(exit_event):
143
-    signal.signal(signal.SIGINT, signal.SIG_IGN)
144
-    signal.signal(signal.SIGHUP, _mutate_config)
145
-
146 135
     _cleanup_socket_file(CONF.driver_agent.stats_socket_path)
147 136
 
148 137
     server = ForkingUDSServer(CONF.driver_agent.stats_socket_path,
@@ -163,9 +152,6 @@ def stats_listener(exit_event):
163 152
 
164 153
 
165 154
 def get_listener(exit_event):
166
-    signal.signal(signal.SIGINT, signal.SIG_IGN)
167
-    signal.signal(signal.SIGHUP, _mutate_config)
168
-
169 155
     _cleanup_socket_file(CONF.driver_agent.get_socket_path)
170 156
 
171 157
     server = ForkingUDSServer(CONF.driver_agent.get_socket_path,

+ 26
- 0
octavia/api/drivers/noop_driver/agent.py View File

@@ -0,0 +1,26 @@
1
+#    Copyright 2019 Red Hat, Inc. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import time
16
+
17
+from oslo_log import log as logging
18
+
19
+LOG = logging.getLogger(__name__)
20
+
21
+
22
+def noop_provider_agent(exit_event):
23
+    LOG.info('No-Op provider agent has started.')
24
+    while not exit_event.is_set():
25
+        time.sleep(1)
26
+    LOG.info('No-Op provider agent is exiting.')

+ 83
- 8
octavia/cmd/driver_agent.py View File

@@ -17,18 +17,21 @@ import multiprocessing
17 17
 import os
18 18
 import signal
19 19
 import sys
20
+import time
20 21
 
21 22
 from oslo_config import cfg
22 23
 from oslo_log import log as logging
23 24
 from oslo_reports import guru_meditation_report as gmr
25
+import setproctitle
26
+from stevedore import enabled as stevedore_enabled
24 27
 
25 28
 from octavia.api.drivers.driver_agent import driver_listener
26 29
 from octavia.common import service
27 30
 from octavia import version
28 31
 
29
-
30 32
 CONF = cfg.CONF
31 33
 LOG = logging.getLogger(__name__)
34
+PROVIDER_AGENT_PROCESSES = []
32 35
 
33 36
 
34 37
 def _mutate_config(*args, **kwargs):
@@ -42,6 +45,53 @@ def _handle_mutate_config(status_proc_pid, stats_proc_pid, *args, **kwargs):
42 45
     os.kill(stats_proc_pid, signal.SIGHUP)
43 46
 
44 47
 
48
+def _check_if_provider_agent_enabled(extension):
49
+    if extension.name in CONF.driver_agent.enabled_provider_agents:
50
+        return True
51
+    return False
52
+
53
+
54
+def _process_wrapper(exit_event, proc_name, function, agent_name=None):
55
+    signal.signal(signal.SIGINT, signal.SIG_IGN)
56
+    signal.signal(signal.SIGHUP, _mutate_config)
57
+    if agent_name:
58
+        process_title = 'octavia-driver-agent - {} -- {}'.format(
59
+            proc_name, agent_name)
60
+    else:
61
+        process_title = 'octavia-driver-agent - {}'.format(proc_name)
62
+    setproctitle.setproctitle(process_title)
63
+    while not exit_event.is_set():
64
+        try:
65
+            function(exit_event)
66
+        except Exception as e:
67
+            if agent_name:
68
+                LOG.exception('Provider agent "%s" raised exception: %s. '
69
+                              'Restarting the "%s" provider agent.',
70
+                              agent_name, str(e), agent_name)
71
+            else:
72
+                LOG.exception('%s raised exception: %s. '
73
+                              'Restarting %s.',
74
+                              proc_name, str(e), proc_name)
75
+            time.sleep(1)
76
+            continue
77
+        break
78
+
79
+
80
+def _start_provider_agents(exit_event):
81
+    extensions = stevedore_enabled.EnabledExtensionManager(
82
+        namespace='octavia.driver_agent.provider_agents',
83
+        check_func=_check_if_provider_agent_enabled)
84
+    for ext in extensions:
85
+        ext_process = multiprocessing.Process(
86
+            name=ext.name, target=_process_wrapper,
87
+            args=(exit_event, 'provider_agent', ext.plugin),
88
+            kwargs={'agent_name': ext.name})
89
+        PROVIDER_AGENT_PROCESSES.append(ext_process)
90
+        ext_process.start()
91
+        LOG.info('Started enabled provider agent: "%s" with PID: %d.',
92
+                 ext.name, ext_process.pid)
93
+
94
+
45 95
 def main():
46 96
     service.prepare_service(sys.argv)
47 97
 
@@ -51,36 +101,61 @@ def main():
51 101
     exit_event = multiprocessing.Event()
52 102
 
53 103
     status_listener_proc = multiprocessing.Process(
54
-        name='status_listener', target=driver_listener.status_listener,
55
-        args=(exit_event,))
104
+        name='status_listener', target=_process_wrapper,
105
+        args=(exit_event, 'status_listener', driver_listener.status_listener))
56 106
     processes.append(status_listener_proc)
57 107
 
58 108
     LOG.info("Driver agent status listener process starts:")
59 109
     status_listener_proc.start()
60 110
 
61 111
     stats_listener_proc = multiprocessing.Process(
62
-        name='stats_listener', target=driver_listener.stats_listener,
63
-        args=(exit_event,))
112
+        name='stats_listener', target=_process_wrapper,
113
+        args=(exit_event, 'stats_listener', driver_listener.stats_listener))
64 114
     processes.append(stats_listener_proc)
65 115
 
66 116
     LOG.info("Driver agent statistics listener process starts:")
67 117
     stats_listener_proc.start()
68 118
 
69 119
     get_listener_proc = multiprocessing.Process(
70
-        name='get_listener', target=driver_listener.get_listener,
71
-        args=(exit_event,))
120
+        name='get_listener', target=_process_wrapper,
121
+        args=(exit_event, 'get_listener', driver_listener.get_listener))
72 122
     processes.append(get_listener_proc)
73 123
 
74 124
     LOG.info("Driver agent get listener process starts:")
75 125
     get_listener_proc.start()
76 126
 
127
+    _start_provider_agents(exit_event)
128
+
77 129
     def process_cleanup(*args, **kwargs):
78
-        LOG.info("Driver agent exiting due to signal")
130
+        LOG.info("Driver agent exiting due to signal.")
79 131
         exit_event.set()
80 132
         status_listener_proc.join()
81 133
         stats_listener_proc.join()
82 134
         get_listener_proc.join()
83 135
 
136
+        for proc in PROVIDER_AGENT_PROCESSES:
137
+            LOG.info('Waiting up to %s seconds for provider agent "%s" to '
138
+                     'shutdown.',
139
+                     CONF.driver_agent.provider_agent_shutdown_timeout,
140
+                     proc.name)
141
+            try:
142
+                proc.join(CONF.driver_agent.provider_agent_shutdown_timeout)
143
+                if proc.exitcode is None:
144
+                    # TODO(johnsom) Change to proc.kill() once
145
+                    #               python 3.7 or newer only
146
+                    os.kill(proc.pid, signal.SIGKILL)
147
+                    LOG.warning(
148
+                        'Forcefully killed "%s" provider agent because it '
149
+                        'failed to shutdown in %s seconds.', proc.name,
150
+                        CONF.driver_agent.provider_agent_shutdown_timeout)
151
+            except Exception as e:
152
+                LOG.warning('Unknown error "%s" while shutting down "%s", '
153
+                            'ignoring and continuing shutdown process.',
154
+                            str(e), proc.name)
155
+            else:
156
+                LOG.info('Provider agent "%s" has succesfully shutdown.',
157
+                         proc.name)
158
+
84 159
     signal.signal(signal.SIGTERM, process_cleanup)
85 160
     signal.signal(signal.SIGHUP, partial(
86 161
         _handle_mutate_config, status_listener_proc.pid,

+ 7
- 0
octavia/common/config.py View File

@@ -704,6 +704,13 @@ driver_agent_opts = [
704 704
                  help=_('Percentage of max_processes (both status and stats) '
705 705
                         'in use to start logging warning messages about an '
706 706
                         'overloaded driver-agent.')),
707
+    cfg.IntOpt('provider_agent_shutdown_timeout',
708
+               default=60,
709
+               help=_('The time, in seconds, to wait for provider agents '
710
+                      'to shutdown after the exit event has been set.')),
711
+    cfg.ListOpt('enabled_provider_agents', default='',
712
+                help=_('List of enabled provider agents. The driver-agent '
713
+                       'will launch these agents at startup.'))
707 714
 ]
708 715
 
709 716
 # Register the configuration options

+ 3
- 14
octavia/tests/unit/api/drivers/driver_agent/test_driver_listener.py View File

@@ -135,11 +135,6 @@ class TestDriverListener(base.TestCase):
135 135
         mock_send.assert_called_with(b'15\n')
136 136
         mock_sendall.assert_called_with(jsonutils.dump_as_bytes(TEST_OBJECT))
137 137
 
138
-    @mock.patch('octavia.api.drivers.driver_agent.driver_listener.CONF')
139
-    def test_mutate_config(self, mock_conf):
140
-        driver_listener._mutate_config()
141
-        mock_conf.mutate_config_files.assert_called_once()
142
-
143 138
     @mock.patch('os.remove')
144 139
     def test_cleanup_socket_file(self, mock_remove):
145 140
         mock_remove.side_effect = [mock.DEFAULT, OSError,
@@ -154,11 +149,9 @@ class TestDriverListener(base.TestCase):
154 149
 
155 150
     @mock.patch('octavia.api.drivers.driver_agent.driver_listener.'
156 151
                 '_cleanup_socket_file')
157
-    @mock.patch('octavia.api.drivers.driver_agent.driver_listener.signal')
158 152
     @mock.patch('octavia.api.drivers.driver_agent.driver_listener.'
159 153
                 'ForkingUDSServer')
160
-    def test_status_listener(self, mock_forking_server,
161
-                             mock_signal, mock_cleanup):
154
+    def test_status_listener(self, mock_forking_server, mock_cleanup):
162 155
         mock_server = mock.MagicMock()
163 156
         mock_active_children = mock.PropertyMock(
164 157
             side_effect=['a', 'a', 'a',
@@ -176,11 +169,9 @@ class TestDriverListener(base.TestCase):
176 169
 
177 170
     @mock.patch('octavia.api.drivers.driver_agent.driver_listener.'
178 171
                 '_cleanup_socket_file')
179
-    @mock.patch('octavia.api.drivers.driver_agent.driver_listener.signal')
180 172
     @mock.patch('octavia.api.drivers.driver_agent.driver_listener.'
181 173
                 'ForkingUDSServer')
182
-    def test_stats_listener(self, mock_forking_server,
183
-                            mock_signal, mock_cleanup):
174
+    def test_stats_listener(self, mock_forking_server, mock_cleanup):
184 175
         mock_server = mock.MagicMock()
185 176
         mock_active_children = mock.PropertyMock(
186 177
             side_effect=['a', 'a', 'a',
@@ -197,11 +188,9 @@ class TestDriverListener(base.TestCase):
197 188
 
198 189
     @mock.patch('octavia.api.drivers.driver_agent.driver_listener.'
199 190
                 '_cleanup_socket_file')
200
-    @mock.patch('octavia.api.drivers.driver_agent.driver_listener.signal')
201 191
     @mock.patch('octavia.api.drivers.driver_agent.driver_listener.'
202 192
                 'ForkingUDSServer')
203
-    def test_get_listener(self, mock_forking_server,
204
-                          mock_signal, mock_cleanup):
193
+    def test_get_listener(self, mock_forking_server, mock_cleanup):
205 194
         mock_server = mock.MagicMock()
206 195
         mock_active_children = mock.PropertyMock(
207 196
             side_effect=['a', 'a', 'a',

+ 33
- 0
octavia/tests/unit/api/drivers/test_provider_noop_agent.py View File

@@ -0,0 +1,33 @@
1
+#    Copyright 2019 Red Hat, Inc. All rights reserved.
2
+#
3
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
4
+#    not use this file except in compliance with the License. You may obtain
5
+#    a copy of the License at
6
+#
7
+#         http://www.apache.org/licenses/LICENSE-2.0
8
+#
9
+#    Unless required by applicable law or agreed to in writing, software
10
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12
+#    License for the specific language governing permissions and limitations
13
+#    under the License.
14
+
15
+import mock
16
+
17
+from octavia.api.drivers.noop_driver import agent
18
+import octavia.tests.unit.base as base
19
+
20
+
21
+class TestNoopProviderAgent(base.TestCase):
22
+
23
+    def setUp(self):
24
+        super(TestNoopProviderAgent, self).setUp()
25
+
26
+    @mock.patch('time.sleep')
27
+    def test_noop_provider_agent(self, mock_sleep):
28
+        mock_exit_event = mock.MagicMock()
29
+        mock_exit_event.is_set.side_effect = [False, True]
30
+
31
+        agent.noop_provider_agent(mock_exit_event)
32
+
33
+        mock_sleep.assert_called_once_with(1)

+ 143
- 7
octavia/tests/unit/cmd/test_driver_agent.py View File

@@ -15,16 +15,21 @@
15 15
 import signal
16 16
 
17 17
 import mock
18
+from oslo_config import cfg
19
+from oslo_config import fixture as oslo_fixture
18 20
 
19 21
 import octavia.api.drivers.driver_agent.driver_listener
20 22
 from octavia.cmd import driver_agent
21 23
 from octavia.tests.unit import base
22 24
 
25
+CONF = cfg.CONF
26
+
23 27
 
24 28
 class TestDriverAgentCMD(base.TestCase):
25 29
 
26 30
     def setUp(self):
27 31
         super(TestDriverAgentCMD, self).setUp()
32
+        self.CONF = self.useFixture(oslo_fixture.Config(cfg.CONF))
28 33
 
29 34
     @mock.patch('os.kill')
30 35
     @mock.patch('octavia.cmd.driver_agent.CONF')
@@ -34,24 +39,118 @@ class TestDriverAgentCMD(base.TestCase):
34 39
         os_calls = [mock.call(1, signal.SIGHUP), mock.call(2, signal.SIGHUP)]
35 40
         mock_os_kill.assert_has_calls(os_calls, any_order=True)
36 41
 
42
+    def test_check_if_provider_agent_enabled(self):
43
+        mock_extension = mock.MagicMock()
44
+        self.CONF.config(group="driver_agent",
45
+                         enabled_provider_agents=[
46
+                             'spiffy_agent', 'super_agent'])
47
+        mock_extension.name = 'super_agent'
48
+        self.assertTrue(
49
+            driver_agent._check_if_provider_agent_enabled(mock_extension))
50
+        mock_extension.name = 'bogus_agent'
51
+        self.assertFalse(
52
+            driver_agent._check_if_provider_agent_enabled(mock_extension))
53
+
54
+    @mock.patch('setproctitle.setproctitle')
37 55
     @mock.patch('signal.signal')
56
+    def test_process_wrapper(self, mock_signal, mock_setproctitle):
57
+        mock_exit_event = mock.MagicMock()
58
+        mock_function = mock.MagicMock()
59
+        mock_function.side_effect = [
60
+            mock.DEFAULT, Exception('boom'), mock.DEFAULT, Exception('boom'),
61
+            mock.DEFAULT]
62
+        mock_exit_event.is_set.side_effect = [False, False, True,
63
+                                              False, False, True]
64
+
65
+        signal_calls = [mock.call(signal.SIGINT, signal.SIG_IGN),
66
+                        mock.call(signal.SIGHUP, driver_agent._mutate_config)]
67
+        # With agent_name
68
+        driver_agent._process_wrapper(
69
+            mock_exit_event, 'test_proc_name', mock_function,
70
+            agent_name='test_agent_name')
71
+        mock_signal.assert_has_calls(signal_calls)
72
+        mock_setproctitle.assert_called_once_with(
73
+            'octavia-driver-agent - test_proc_name -- test_agent_name')
74
+        mock_function.assert_called_once_with(mock_exit_event)
75
+
76
+        # With agent_name - With function exception
77
+        mock_signal.reset_mock()
78
+        mock_setproctitle.reset_mock()
79
+        mock_function.reset_mock()
80
+        driver_agent._process_wrapper(
81
+            mock_exit_event, 'test_proc_name', mock_function,
82
+            agent_name='test_agent_name')
83
+        mock_signal.assert_has_calls(signal_calls)
84
+        mock_setproctitle.assert_called_once_with(
85
+            'octavia-driver-agent - test_proc_name -- test_agent_name')
86
+        mock_function.assert_called_once_with(mock_exit_event)
87
+
88
+        # Without agent_name
89
+        mock_signal.reset_mock()
90
+        mock_setproctitle.reset_mock()
91
+        mock_function.reset_mock()
92
+        driver_agent._process_wrapper(
93
+            mock_exit_event, 'test_proc_name', mock_function)
94
+        mock_signal.assert_has_calls(signal_calls)
95
+        mock_setproctitle.assert_called_once_with(
96
+            'octavia-driver-agent - test_proc_name')
97
+        mock_function.assert_called_once_with(mock_exit_event)
98
+
99
+        # Without agent_name - With function exception
100
+        mock_signal.reset_mock()
101
+        mock_setproctitle.reset_mock()
102
+        mock_function.reset_mock()
103
+        driver_agent._process_wrapper(
104
+            mock_exit_event, 'test_proc_name', mock_function)
105
+        mock_signal.assert_has_calls(signal_calls)
106
+        mock_setproctitle.assert_called_once_with(
107
+            'octavia-driver-agent - test_proc_name')
108
+        mock_function.assert_called_once_with(mock_exit_event)
109
+
110
+    @mock.patch('octavia.cmd.driver_agent.multiprocessing')
111
+    @mock.patch('stevedore.enabled.EnabledExtensionManager')
112
+    def test_start_provider_agents(self, mock_stevedore, mock_multiprocessing):
113
+        mock_extension = mock.MagicMock()
114
+        mock_extension.name = 'test_extension'
115
+        mock_exit_event = mock.MagicMock()
116
+        mock_stevedore.return_value = [mock_extension]
117
+        mock_ext_proc = mock.MagicMock()
118
+        mock_multiprocessing.Process.return_value = mock_ext_proc
119
+
120
+        driver_agent._start_provider_agents(mock_exit_event)
121
+
122
+        mock_stevedore.assert_called_once_with(
123
+            namespace='octavia.driver_agent.provider_agents',
124
+            check_func=driver_agent._check_if_provider_agent_enabled)
125
+        mock_multiprocessing.Process.assert_called_once_with(
126
+            name='test_extension', target=driver_agent._process_wrapper,
127
+            args=(mock_exit_event, 'provider_agent', mock_extension.plugin),
128
+            kwargs={'agent_name': 'test_extension'})
129
+        mock_ext_proc.start.assert_called_once_with()
130
+
131
+    @mock.patch('os.kill')
38 132
     @mock.patch('octavia.cmd.driver_agent.multiprocessing')
39 133
     @mock.patch('oslo_reports.guru_meditation_report.TextGuruMeditation.'
40 134
                 'setup_autorun')
41 135
     @mock.patch('octavia.common.service.prepare_service')
42 136
     def test_main(self, mock_prep_srvc, mock_gmr, mock_multiprocessing,
43
-                  mock_signal):
137
+                  mock_kill):
44 138
         mock_exit_event = mock.MagicMock()
45 139
         mock_multiprocessing.Event.return_value = mock_exit_event
46 140
         mock_status_listener_proc = mock.MagicMock()
47 141
         mock_stats_listener_proc = mock.MagicMock()
48 142
         mock_get_listener_proc = mock.MagicMock()
49
-        mock_multiprocessing.Process.side_effect = [mock_status_listener_proc,
50
-                                                    mock_stats_listener_proc,
51
-                                                    mock_get_listener_proc,
52
-                                                    mock_status_listener_proc,
53
-                                                    mock_stats_listener_proc,
54
-                                                    mock_get_listener_proc]
143
+        mock_multiprocessing.Process.side_effect = [
144
+            mock_status_listener_proc, mock_stats_listener_proc,
145
+            mock_get_listener_proc,
146
+            mock_status_listener_proc, mock_stats_listener_proc,
147
+            mock_get_listener_proc,
148
+            mock_status_listener_proc, mock_stats_listener_proc,
149
+            mock_get_listener_proc,
150
+            mock_status_listener_proc, mock_stats_listener_proc,
151
+            mock_get_listener_proc,
152
+            mock_status_listener_proc, mock_stats_listener_proc,
153
+            mock_get_listener_proc]
55 154
         driver_agent.main()
56 155
         mock_prep_srvc.assert_called_once()
57 156
         mock_gmr.assert_called_once()
@@ -76,3 +175,40 @@ class TestDriverAgentCMD(base.TestCase):
76 175
         mock_stats_listener_proc.join.side_effect = [KeyboardInterrupt, None]
77 176
         driver_agent.main()
78 177
         mock_exit_event.set.assert_called_once()
178
+
179
+        # Test keyboard interrupt with provider agents
180
+        mock_exit_event.reset_mock()
181
+        mock_stats_listener_proc.join.side_effect = [KeyboardInterrupt, None]
182
+        mock_provider_proc = mock.MagicMock()
183
+        mock_provider_proc.pid = 'not-valid-pid'
184
+        mock_provider_proc.exitcode = 1
185
+        driver_agent.PROVIDER_AGENT_PROCESSES = [mock_provider_proc]
186
+        driver_agent.main()
187
+        mock_exit_event.set.assert_called_once()
188
+        mock_provider_proc.join.assert_called_once_with(
189
+            CONF.driver_agent.provider_agent_shutdown_timeout)
190
+
191
+        # Test keyboard interrupt with provider agents fails to stop
192
+        mock_exit_event.reset_mock()
193
+        mock_stats_listener_proc.join.side_effect = [KeyboardInterrupt, None]
194
+        mock_provider_proc = mock.MagicMock()
195
+        mock_provider_proc.pid = 'not-valid-pid'
196
+        mock_provider_proc.exitcode = None
197
+        driver_agent.PROVIDER_AGENT_PROCESSES = [mock_provider_proc]
198
+        driver_agent.main()
199
+        mock_exit_event.set.assert_called_once()
200
+        mock_provider_proc.join.assert_called_once_with(
201
+            CONF.driver_agent.provider_agent_shutdown_timeout)
202
+        mock_kill.assert_called_once_with('not-valid-pid', signal.SIGKILL)
203
+
204
+        # Test keyboard interrupt with provider agents join exception
205
+        mock_exit_event.reset_mock()
206
+        mock_stats_listener_proc.join.side_effect = [KeyboardInterrupt, None]
207
+        mock_provider_proc = mock.MagicMock()
208
+        mock_provider_proc.pid = 'not-valid-pid'
209
+        mock_provider_proc.join.side_effect = Exception('boom')
210
+        driver_agent.PROVIDER_AGENT_PROCESSES = [mock_provider_proc]
211
+        driver_agent.main()
212
+        mock_exit_event.set.assert_called_once()
213
+        mock_provider_proc.join.assert_called_once_with(
214
+            CONF.driver_agent.provider_agent_shutdown_timeout)

+ 6
- 0
releasenotes/notes/Add-provider-agent-support-a735806c4da4c470.yaml View File

@@ -0,0 +1,6 @@
1
+---
2
+features:
3
+  - |
4
+    The Octavia driver-agent now supports starting provider driver agents.
5
+    Provider driver agents are long running agent processes supporting
6
+    provider drivers.

+ 1
- 0
requirements.txt View File

@@ -49,6 +49,7 @@ debtcollector>=1.19.0 # Apache-2.0
49 49
 octavia-lib>=1.3.1 # Apache-2.0
50 50
 netaddr>=0.7.19 # BSD
51 51
 simplejson>=3.13.2 # MIT
52
+setproctitle>=1.1.10 # BSD
52 53
 
53 54
 #for the amphora api
54 55
 Flask!=0.11,>=0.10 # BSD

+ 2
- 0
setup.cfg View File

@@ -75,6 +75,8 @@ octavia.amphora.udp_api_server =
75 75
 octavia.compute.drivers =
76 76
     compute_noop_driver = octavia.compute.drivers.noop_driver.driver:NoopComputeDriver
77 77
     compute_nova_driver = octavia.compute.drivers.nova_driver:VirtualMachineManager
78
+octavia.driver_agent.provider_agents =
79
+    noop_agent = octavia.api.drivers.noop_driver.agent:noop_provider_agent
78 80
 octavia.network.drivers =
79 81
     network_noop_driver = octavia.network.drivers.noop_driver.driver:NoopNetworkDriver
80 82
     allowed_address_pairs_driver = octavia.network.drivers.neutron.allowed_address_pairs:AllowedAddressPairsDriver

Loading…
Cancel
Save