Merge charm-storage-connector:defer-service-restarts-integration into charm-storage-connector:master
- Git
- lp:charm-storage-connector
- defer-service-restarts-integration
- Merge into master
Status: | Merged |
---|---|
Approved by: | Tianqi Xiao |
Approved revision: | 37b83ef6eab6b315e36e14bc53961e4af918d089 |
Merged at revision: | 0ac891951896d6133ed80e7f2759f7f403b99918 |
Proposed branch: | charm-storage-connector:defer-service-restarts-integration |
Merge into: | charm-storage-connector:master |
Diff against target: |
1433 lines (+768/-154) 11 files modified
README.md (+9/-10) actions.yaml (+32/-3) config.yaml (+25/-18) metadata.yaml (+2/-2) requirements.txt (+4/-2) src/charm.py (+232/-72) tests/functional/storage-connector-func-tests/setup.py (+48/-0) tests/functional/storage-connector-func-tests/tests.py (+98/-31) tests/functional/tests/tests.yaml (+1/-0) tests/unit/test_charm.py (+312/-11) tox.ini (+5/-5) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Andrea Ieri | Approve | ||
🤖 prod-jenkaas-bootstack (community) | continuous-integration | Approve | |
BootStack Reviewers | Pending | ||
Review via email:
|
Commit message
Add defer methods for better handling service restarts
Description of the change
This MP fixes LP#2003241. The following changes have been made:
- Make use of charmhelpers policy_rcd script to block automatic stop, restarts, try-restarts action for all charm managed services, including iscsid open-iscsi, and multipathd.
- Handles deferred restarts triggered by both charm and non-charm events
- Display the queue of deferred service restarts in charm status message
and periodic updates
- Add `enable-
- Add `restart-services` action to restart charm managed service. The
operator can choose to restart either all deferred services or a specific
set of services.
- Add `show-deferred-
with the timestamp and reason
- Add `iscsi-
run iscsiadm discovery and login when needed
- Remove `restart-
`restart-services` action
- Separate charm status logic from the state of its underlying services
- Add unittests and functional tests for the new and modified methods
- Rework functional tests logic to make each test case independent of each other.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:37b83ef6eab
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andrea Ieri (aieri) wrote : | # |
Can you please link to the original MPs that generated the integration branch you're trying to merge? I think it would be useful to note them for tracking purposes.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tianqi Xiao (txiao) wrote : | # |
@aieri Sure, thanks for the suggestion.
The original MPs that generated the integration branch are #438042 [1] and #438419 [2].
[1] https:/
[2] https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : | # |
Change successfully merged at revision 0ac891951896d61
Preview Diff
1 | diff --git a/README.md b/README.md |
2 | index 247b96d..2802aab 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -2,10 +2,10 @@ |
6 | |
7 | ## Overview |
8 | |
9 | -This charm configures a unit to connect to a storage endpoint, either iSCSI or Fibre Channel. |
10 | -It acts as a subordinate charm, which can be deployed on any bare metal or virtual machine, |
11 | -alongside a main charm. It is not supported in containers due to lack of ability for lxd |
12 | -containers to access iSCSI and Fibre Channel hardware of the underlying kernel. |
13 | +This charm configures a unit to connect to a storage endpoint, either iSCSI or Fibre Channel. |
14 | +It acts as a subordinate charm, which can be deployed on any bare metal or virtual machine, |
15 | +alongside a main charm. It is not supported in containers due to lack of ability for lxd |
16 | +containers to access iSCSI and Fibre Channel hardware of the underlying kernel. |
17 | |
18 | If you configure this charm for iSCSI, this charm will: |
19 | - Generate an iSCSI initiator name and put it in /etc/iscsi/initiatorname.iscsi |
20 | @@ -23,11 +23,11 @@ If you configure it for Fibre Channel, this charm will: |
21 | - Configure multipath under /etc/multipath/conf.d directory |
22 | - Reload and restart multipathd.service |
23 | |
24 | -Please note that the Fiber Channel option currently supports only one device. |
25 | +Please note that the Fiber Channel option currently supports only one device. |
26 | |
27 | If iSCSI, the user can input a initiator name dictionary in config.yaml if they wish to use a |
28 | specific IQN for a specific unit. Also, the target IP and port are needed to perform |
29 | -the discovery and login with iscsiadm. |
30 | +the discovery and login with iscsiadm. |
31 | |
32 | For Fibre Channel, the user can choose the device alias to be used when mapping the disk. |
33 | See the option "fc-lun-alias" for further details. |
34 | @@ -85,9 +85,9 @@ juju relate ubuntu storage-connector |
35 | |
36 | ## Scaling |
37 | |
38 | -This charm will scale with the units it is related to. For example, if you scale the |
39 | -ubuntu application, and the storage-connector is related to it, it will be deployed on each |
40 | -newly deployed ubuntu unit. |
41 | +This charm will scale with the units it is related to. For example, if you scale the |
42 | +ubuntu application, and the storage-connector is related to it, it will be deployed on each |
43 | +newly deployed ubuntu unit. |
44 | ``` |
45 | juju add-unit ubuntu |
46 | juju remove-unit ubuntu/1 |
47 | @@ -104,4 +104,3 @@ cause of the error. |
48 | - Author: Camille Rodriguez <camille.rodriguez@canonical.com> |
49 | - Maintainers: BootStack Charmers <bootstack-charmers@lists.canonical.com> |
50 | - Bug Tracker: [here](https://bugs.launchpad.net/charm-storage-connector) |
51 | - |
52 | \ No newline at end of file |
53 | diff --git a/actions.yaml b/actions.yaml |
54 | index 998a670..79eed26 100644 |
55 | --- a/actions.yaml |
56 | +++ b/actions.yaml |
57 | @@ -1,4 +1,33 @@ |
58 | -restart-iscsi-services: |
59 | - description: "Restart iscsid and open-iscsi services" |
60 | +restart-services: |
61 | + description: | |
62 | + Restart services managed by the charm. |
63 | + params: |
64 | + deferred-only: |
65 | + type: boolean |
66 | + default: false |
67 | + description: | |
68 | + Restart all deferred services. To check which services has been deferred, |
69 | + run the `show-deferred-events` action. |
70 | + NOTE: deferred-only and services params are mutually exclusive. Providing |
71 | + both options will cause the action to fail. |
72 | + services: |
73 | + type: string |
74 | + default: "" |
75 | + description: | |
76 | + List of services to restart. Supporting services includes iscsi, |
77 | + open-iscsi, and multipathd. |
78 | + This param accepts space-separated services in string format, |
79 | + e.g. "iscsid open-iscsi" |
80 | + NOTE: deferred-only and services params are mutually exclusive. Providing |
81 | + both options will cause the action to fail. |
82 | +show-deferred-restarts: |
83 | + descrpition: | |
84 | + Show the outstanding service restarts |
85 | reload-multipathd-service: |
86 | - description: "Reload multipathd service" |
87 | + description: | |
88 | + Reload multipathd service |
89 | +iscsi-discovery-and-login: |
90 | + description: | |
91 | + Run discovery and login against iscsi target. This action is needed when |
92 | + changes are made to iscsi configuration and the iscsi services |
93 | + are restarted to apply these changes. |
94 | diff --git a/config.yaml b/config.yaml |
95 | index 739ef42..c962a7f 100644 |
96 | --- a/config.yaml |
97 | +++ b/config.yaml |
98 | @@ -8,7 +8,7 @@ options: |
99 | type: string |
100 | default: data1 |
101 | description: | |
102 | - LUN alias to give to the WWID for the mapping of the device. It is used in the path of the new mounted device, i.e /dev/mapper/<FC-LUN-ALIAS>. |
103 | + LUN alias to give to the WWID for the mapping of the device. It is used in the path of the new mounted device, i.e /dev/mapper/<FC-LUN-ALIAS>. |
104 | iscsi-target: |
105 | type: string |
106 | default: |
107 | @@ -21,42 +21,49 @@ options: |
108 | type: boolean |
109 | default: True |
110 | description: | |
111 | - If set to True (default) and storage-type is iscsi, the charm will configure |
112 | - the iscsid and multipath configuration files, and then run an iscsiadm |
113 | - discovery and login against the target. If set to False, the charm will simply |
114 | - configure the unit but not run the discovery and login against the target. |
115 | + If set to True (default) and storage-type is iscsi, the charm will run an |
116 | + iscsiadm discovery and login against the target after charm-managed iscsi |
117 | + services restart (triggered by either config changes or by the restart-services |
118 | + action). If set to False, the charm will not automatically run the discovery |
119 | + and login against the target. |
120 | initiator-dictionary: |
121 | type: string |
122 | default: '{}' |
123 | description: | |
124 | - Dictionary of hostnames (FQDN) and initiator names, surrounded by |
125 | - single quotes. The charm compares the machine hostname to |
126 | - the list, and provide the correct initiator associated to |
127 | - it. If not defined, the initiator name will be provided |
128 | - randomly. Format : |
129 | + Dictionary of hostnames (FQDN) and initiator names, surrounded by |
130 | + single quotes. The charm compares the machine hostname to |
131 | + the list, and provide the correct initiator associated to |
132 | + it. If not defined, the initiator name will be provided |
133 | + randomly. Format : |
134 | '{"hostname1": "iqn.yyyy-mm.naming-authority:uniquename1", |
135 | "hostname2": "iqn.yyyy-mm.naming-authority:uniquename2}' |
136 | + enable-auto-restarts: |
137 | + type: boolean |
138 | + default: False |
139 | + description: | |
140 | + Allow the charm and packages to restart services automatically when |
141 | + required. |
142 | multipath-defaults: |
143 | type: string |
144 | default: '{"user_friendly_names": "yes"}' |
145 | description: | |
146 | In multipath config, sets the defaults configuration. String should be of JSON dictionary format. |
147 | Double quotes are essential to the correct format of this JSON string. |
148 | - Example: |
149 | + Example: |
150 | value : '{"user_friendly_names":"yes", "find_multipaths":"yes", "polling_interval":"10"}' |
151 | Which will produce this configuration: |
152 | - defaults { |
153 | - user_friendly_names yes |
154 | - find_multipaths yes |
155 | + defaults { |
156 | + user_friendly_names yes |
157 | + find_multipaths yes |
158 | polling_interval 10 |
159 | - } |
160 | + } |
161 | multipath-devices: |
162 | type: string |
163 | default: |
164 | description: | |
165 | In multipath config, add a device specific configuration. String should be of JSON dictionary format. |
166 | Double quotes are essential to the correct format of this JSON string. |
167 | - Example: |
168 | + Example: |
169 | value : '{"vendor":"PURE","product": "FlashArray","fast_io_fail_tmo": "10", "path_grouping_policy":"group_by_prio"}' |
170 | Which will produce this configuration: |
171 | device { |
172 | @@ -67,9 +74,9 @@ options: |
173 | } |
174 | multipath-blacklist: |
175 | type: string |
176 | - default: |
177 | + default: |
178 | description: | |
179 | - In multipath config, add a blacklist device section to exlude local disks from being handled by multipath-tools. It is possible |
180 | + In multipath config, add a blacklist device section to exclude local disks from being handled by multipath-tools. It is possible |
181 | to blacklist by vendor/product, by devnode or WWID. String should be of JSON dictionary format. Double quotes are essential to the correct format of this JSON string. |
182 | Example: |
183 | value: '{"vendor": "QEMU", "product": "*"}' |
184 | diff --git a/metadata.yaml b/metadata.yaml |
185 | index 81f963d..1ab3f3d 100644 |
186 | --- a/metadata.yaml |
187 | +++ b/metadata.yaml |
188 | @@ -5,8 +5,8 @@ maintainers: |
189 | description: | |
190 | The Storage Connector charm is an open-source charm that can be installed as a |
191 | subordinate to any VM or bare metal machine. It has two modes: iSCSI or Fibre Channel. |
192 | - In the iSCSI mode, it will connect the host to an iSCSI target and configure multipath |
193 | - accordingly. For Fibre Channel, this charm detects the WWID of the FC LUNs and |
194 | + In the iSCSI mode, it will connect the host to an iSCSI target and configure multipath |
195 | + accordingly. For Fibre Channel, this charm detects the WWID of the FC LUNs and |
196 | configures multipathd so that the device is ready to be used. |
197 | tags: |
198 | - iscsi |
199 | diff --git a/requirements.txt b/requirements.txt |
200 | index 814be4a..49af55c 100644 |
201 | --- a/requirements.txt |
202 | +++ b/requirements.txt |
203 | @@ -1,3 +1,5 @@ |
204 | jinja2 |
205 | -ops |
206 | -charmhelpers |
207 | \ No newline at end of file |
208 | +charmhelpers |
209 | + |
210 | +# Pin ops version to support bionic deployments |
211 | +ops < 2.0 |
212 | \ No newline at end of file |
213 | diff --git a/src/charm.py b/src/charm.py |
214 | index d921131..21ebdb2 100755 |
215 | --- a/src/charm.py |
216 | +++ b/src/charm.py |
217 | @@ -2,15 +2,24 @@ |
218 | |
219 | """Iscsi Connector Charm.""" |
220 | |
221 | + |
222 | import json |
223 | import logging |
224 | +import os |
225 | import re |
226 | import socket |
227 | import subprocess |
228 | +import time |
229 | +from datetime import datetime |
230 | +from functools import wraps |
231 | from pathlib import Path |
232 | +from typing import Any, Callable |
233 | |
234 | import apt |
235 | |
236 | +import charmhelpers.contrib.openstack.deferred_events as deferred_events |
237 | +import charmhelpers.contrib.openstack.policy_rcd as policy_rcd |
238 | + |
239 | from jinja2 import Environment, FileSystemLoader |
240 | |
241 | from ops.charm import CharmBase |
242 | @@ -23,9 +32,42 @@ from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider |
243 | from storage_connector import metrics_utils, nrpe_utils |
244 | import utils # noqa |
245 | |
246 | +import yaml |
247 | + |
248 | logger = logging.getLogger(__name__) |
249 | |
250 | |
251 | +def check_deferred_restarts_queue(func: Callable) -> Callable: |
252 | + """Check the queue of deferred restarts and set correct status message. |
253 | + |
254 | + This wrapper can be used to decorate a method (primarily an event handler) from |
255 | + `StorageConnectorCharm` class. After decorated function is executed, this |
256 | + wrapper will clear the expired deferred restarts and attach correct status |
257 | + message if the unit is active. |
258 | + """ |
259 | + |
260 | + @wraps(func) |
261 | + def wrapper(self: "StorageConnectorCharm", *args: Any, **kwargs: Any) -> Any: |
262 | + """Execute wrapped method and perform status assessment.""" |
263 | + result = func(self, *args, **kwargs) |
264 | + |
265 | + # Handle manual service restarts outside the charm (e.g. run |
266 | + # `systemctl restart {service}` in the unit). It compares |
267 | + # each service's start time with its deferred events timestamp |
268 | + # and clears any deferred events which happened prior to this time |
269 | + try: |
270 | + deferred_events.check_restart_timestamps() |
271 | + except ValueError as err: |
272 | + logging.error("Cannot retrieve services' start time: %s", err) |
273 | + |
274 | + if isinstance(self.unit.status, ActiveStatus): |
275 | + self.unit.status = ActiveStatus(self._get_status_message()) |
276 | + |
277 | + return result |
278 | + |
279 | + return wrapper |
280 | + |
281 | + |
282 | class StorageConnectorCharm(CharmBase): |
283 | """Class representing this Operator charm.""" |
284 | |
285 | @@ -41,6 +83,7 @@ class StorageConnectorCharm(CharmBase): |
286 | |
287 | ISCSI_SERVICES = ['iscsid', 'open-iscsi'] |
288 | MULTIPATHD_SERVICE = 'multipathd' |
289 | + DEFERRED_SERVICES = ISCSI_SERVICES + [MULTIPATHD_SERVICE] |
290 | |
291 | VALID_STORAGE_TYPES = ["fc", "iscsi"] |
292 | MANDATORY_CONFIG = { |
293 | @@ -62,15 +105,19 @@ class StorageConnectorCharm(CharmBase): |
294 | super().__init__(*args) |
295 | |
296 | # -- standard hook observation |
297 | - self.framework.observe(self.on.install, self.on_install) |
298 | - self.framework.observe(self.on.install, self.render_config) |
299 | - self.framework.observe(self.on.start, self.on_start) |
300 | - self.framework.observe(self.on.config_changed, self.render_config) |
301 | - self.framework.observe(self.on.config_changed, self.on_config_changed) |
302 | - self.framework.observe(self.on.restart_iscsi_services_action, |
303 | - self.on_restart_iscsi_services_action) |
304 | + self.framework.observe(self.on.install, self._on_install) |
305 | + self.framework.observe(self.on.start, self._on_start) |
306 | + self.framework.observe(self.on.config_changed, self._render_config) |
307 | + self.framework.observe(self.on.config_changed, self._on_config_changed) |
308 | + self.framework.observe(self.on.update_status, self._on_update_status) |
309 | + self.framework.observe(self.on.restart_services_action, |
310 | + self._on_restart_services_action) |
311 | + self.framework.observe(self.on.show_deferred_restarts_action, |
312 | + self._on_show_deferred_restarts_action) |
313 | self.framework.observe(self.on.reload_multipathd_service_action, |
314 | - self.on_reload_multipathd_service_action) |
315 | + self._on_reload_multipathd_service_action) |
316 | + self.framework.observe(self.on.iscsi_discovery_and_login_action, |
317 | + self._on_iscsi_discovery_and_login_action) |
318 | self.framework.observe(self.on.metrics_endpoint_relation_created, |
319 | self._on_metrics_endpoint_relation_created) |
320 | self.framework.observe(self.on.metrics_endpoint_relation_broken, |
321 | @@ -105,13 +152,14 @@ class StorageConnectorCharm(CharmBase): |
322 | ) |
323 | self.mp_path = self.MULTIPATH_CONF_PATH / self._stored.mp_conf_name |
324 | |
325 | - def on_install(self, event): |
326 | + def _on_install(self, _): |
327 | """Handle install state.""" |
328 | self.unit.status = MaintenanceStatus("Installing charm software") |
329 | if self._check_if_container(): |
330 | return |
331 | |
332 | - if not self._check_mandatory_config(): |
333 | + self._check_mandatory_config() |
334 | + if isinstance(self.unit.status, BlockedStatus): |
335 | return |
336 | |
337 | # install packages |
338 | @@ -137,21 +185,24 @@ class StorageConnectorCharm(CharmBase): |
339 | logging.info("Install of software complete") |
340 | self._stored.installed = True |
341 | |
342 | - def on_config_changed(self, event): |
343 | + def _on_config_changed(self, _): |
344 | """Config-changed event handler.""" |
345 | if self._stored.nrpe_related: |
346 | nrpe_utils.update_nrpe_config(self.model.config) |
347 | |
348 | - def render_config(self, event): |
349 | + def _render_config(self, _): |
350 | """Render configuration templates upon config change.""" |
351 | if self._check_if_container(): |
352 | return |
353 | |
354 | - if not self._check_mandatory_config(): |
355 | + self.unit.status = MaintenanceStatus("Validating charm configuration") |
356 | + self._check_mandatory_config() |
357 | + if isinstance(self.unit.status, BlockedStatus): |
358 | return |
359 | |
360 | if self._stored.storage_type == 'fc' and not self._stored.fc_scan_ran_once: |
361 | - if not self._fc_scan_host_successfully(): |
362 | + self._fc_scan_host() |
363 | + if isinstance(self.unit.status, BlockedStatus): |
364 | return |
365 | |
366 | self.unit.status = MaintenanceStatus("Rendering charm configuration") |
367 | @@ -159,19 +210,19 @@ class StorageConnectorCharm(CharmBase): |
368 | |
369 | tenv = Environment(loader=FileSystemLoader('templates')) |
370 | |
371 | - if self._stored.storage_type == 'iscsi': |
372 | - self._iscsi_initiator(tenv) |
373 | - self._iscsid_configuration(tenv) |
374 | - self._restart_iscsi_services() |
375 | + self._configure_deferred_restarts() |
376 | |
377 | - if self.model.config.get('iscsi-discovery-and-login'): |
378 | - logging.info('Launching iscsiadm discovery and login') |
379 | - self._iscsiadm_discovery() |
380 | - self._iscsiadm_login() |
381 | + if self._stored.storage_type == 'iscsi': |
382 | + self._configure_iscsi(tenv=tenv, event_name="config changed") |
383 | + if isinstance(self.unit.status, BlockedStatus): |
384 | + return |
385 | |
386 | - if not self._multipath_configuration(tenv): |
387 | + self._multipath_configuration(tenv) |
388 | + if isinstance(self.unit.status, BlockedStatus): |
389 | return |
390 | - if not self._validate_multipath_config(): |
391 | + |
392 | + self._validate_multipath_config() |
393 | + if isinstance(self.unit.status, BlockedStatus): |
394 | return |
395 | |
396 | self._reload_multipathd_service() |
397 | @@ -179,9 +230,9 @@ class StorageConnectorCharm(CharmBase): |
398 | logging.info("Setting started state") |
399 | self._stored.started = True |
400 | self._stored.configured = True |
401 | - self.unit.status = ActiveStatus("Unit is ready") |
402 | + self.unit.status = ActiveStatus(self._get_status_message()) |
403 | |
404 | - def on_start(self, event): |
405 | + def _on_start(self, event): |
406 | """Handle start state.""" |
407 | if not self._stored.configured: |
408 | logging.warning("Start called before configuration complete, " + |
409 | @@ -190,32 +241,124 @@ class StorageConnectorCharm(CharmBase): |
410 | return |
411 | self.unit.status = MaintenanceStatus("Starting charm software") |
412 | # Start software |
413 | - self.unit.status = ActiveStatus("Unit is ready") |
414 | + self.unit.status = ActiveStatus(self._get_status_message()) |
415 | self._stored.started = True |
416 | logging.info("Started") |
417 | |
418 | + @check_deferred_restarts_queue |
419 | + def _on_update_status(self, _): |
420 | + """Assess unit's status.""" |
421 | + |
422 | # Actions |
423 | - def on_restart_iscsi_services_action(self, event): |
424 | - """Restart iscsid and open-iscsi services.""" |
425 | - event.log('Restarting iscsi services') |
426 | - self._restart_iscsi_services() |
427 | + @check_deferred_restarts_queue |
428 | + def _on_restart_services_action(self, event): |
429 | + """Restart charm managed services.""" |
430 | + deferred_only = event.params["deferred-only"] |
431 | + services = event.params["services"].split() |
432 | + if deferred_only and services: |
433 | + event.set_results({ |
434 | + "failed": "deferred-only and services are mutually exclusive" |
435 | + }) |
436 | + return |
437 | + if not (deferred_only or services): |
438 | + event.set_results({ |
439 | + "failed": "Please specify either deferred-only or services" |
440 | + }) |
441 | + return |
442 | + if deferred_only: |
443 | + deferred_services = list( |
444 | + {deferred_event.service for deferred_event |
445 | + in deferred_events.get_deferred_restarts()}) |
446 | + if not deferred_services: |
447 | + event.set_results({"failed": "No deferred services to restart"}) |
448 | + return |
449 | + event.log('Restarting the following deferred services: {}'.format( |
450 | + ', '.join(deferred_services))) |
451 | + self._restart_services(services=deferred_services) |
452 | + else: |
453 | + specified_services = list( |
454 | + {service for service in services if service in |
455 | + self.DEFERRED_SERVICES}) |
456 | + if not specified_services: |
457 | + event.set_results({"failed": "No valid services are specified."}) |
458 | + return |
459 | + event.log('Restarting the following services: {}'.format( |
460 | + ', '.join(specified_services))) |
461 | + self._restart_services(services=specified_services) |
462 | + |
463 | event.set_results({"success": "True"}) |
464 | |
465 | - def on_reload_multipathd_service_action(self, event): |
466 | + def _on_show_deferred_restarts_action(self, event): |
467 | + """Get and display the list of service multipathd service.""" |
468 | + output = [] |
469 | + for deferred_event in deferred_events.get_deferred_restarts(): |
470 | + output.append('{} {} {} {}'.format( |
471 | + str(datetime.utcfromtimestamp(deferred_event.timestamp)), |
472 | + "+0000 UTC", |
473 | + deferred_event.service.ljust(40), |
474 | + deferred_event.reason)) |
475 | + output.sort() |
476 | + event.set_results({"deferred-restarts": "{}".format( |
477 | + yaml.dump(output, default_flow_style=False) |
478 | + )}) |
479 | + |
480 | + def _on_reload_multipathd_service_action(self, event): |
481 | """Reload multipathd service.""" |
482 | event.log('Reloading multipathd service') |
483 | self._reload_multipathd_service() |
484 | event.set_results({"success": "True"}) |
485 | |
486 | + def _on_iscsi_discovery_and_login_action(self, event): |
487 | + """Run discovery and login against iscsi target(s).""" |
488 | + self._iscsi_discovery_and_login() |
489 | + event.set_results({"success": "True"}) |
490 | + |
491 | # Additional functions |
492 | - def _restart_iscsi_services(self): |
493 | + def _get_status_message(self): |
494 | + """Set unit status to active with correct status message. |
495 | + |
496 | + Check if any services need restarts. If yes, append the queue of |
497 | + such services to the status message. |
498 | + """ |
499 | + status_message = "Unit is ready" |
500 | + if not self.model.config.get('enable-auto-restarts'): |
501 | + deferred_restarts = list( |
502 | + {event.service for event in deferred_events.get_deferred_restarts() |
503 | + if event.policy_requestor_name == self.app.name}) |
504 | + if deferred_restarts: |
505 | + svc_msg = "Services queued for restart: {}".format( |
506 | + ', '.join(sorted(deferred_restarts))) |
507 | + status_message = "{}. {}".format(status_message, svc_msg) |
508 | + |
509 | + return status_message |
510 | + |
511 | + def _defer_service_restart(self, services=None, reason=None): |
512 | + """Defer service restarts and record this event.""" |
513 | + for service in services: |
514 | + deferred_events.save_event(deferred_events.ServiceEvent( |
515 | + timestamp=round(time.time()), |
516 | + service=service, |
517 | + reason='Charm event: {}'.format(reason), |
518 | + action='restart')) |
519 | + |
520 | + def _restart_services(self, services=None): |
521 | """Restart iscsid and open-iscsi services.""" |
522 | - for service in self.ISCSI_SERVICES: |
523 | - logging.info('Restarting %s service', service) |
524 | - try: |
525 | - subprocess.check_call(['systemctl', 'restart', service]) |
526 | - except subprocess.CalledProcessError: |
527 | - logging.exception('An error occured while restarting %s.', service) |
528 | + if services: |
529 | + for service in services: |
530 | + logging.info('Restarting %s service', service) |
531 | + try: |
532 | + subprocess.check_call(['systemctl', 'restart', service]) |
533 | + except subprocess.CalledProcessError: |
534 | + logging.exception('An error occured while restarting %s.', service) |
535 | + |
536 | + # Clear deferred restart events |
537 | + deferred_events.clear_deferred_restarts(services) |
538 | + |
539 | + # If any iscsi services restarted and iscsi-discovery-and-login config is |
540 | + # set to true, run iscsiadm discovery and login. |
541 | + if (any(svc in self.ISCSI_SERVICES for svc in services) and |
542 | + self.model.config.get('iscsi-discovery-and-login')): |
543 | + self._iscsi_discovery_and_login() |
544 | |
545 | def _reload_multipathd_service(self): |
546 | """Reload multipathd service.""" |
547 | @@ -223,15 +366,25 @@ class StorageConnectorCharm(CharmBase): |
548 | try: |
549 | subprocess.check_call(['systemctl', 'reload', self.MULTIPATHD_SERVICE]) |
550 | except subprocess.CalledProcessError: |
551 | - self.unit.status = BlockedStatus( |
552 | - "An error occured while reloading the multipathd service." |
553 | - ) |
554 | logging.exception( |
555 | '%s', |
556 | "An error occured while reloading the multipathd service." |
557 | ) |
558 | |
559 | + def _configure_iscsi(self, tenv, event_name): |
560 | + self._iscsi_initiator(tenv) |
561 | + self._iscsid_configuration(tenv) |
562 | + |
563 | + charm_config = self.model.config |
564 | + if charm_config.get('enable-auto-restarts') or not self._stored.started: |
565 | + self._restart_services(services=self.ISCSI_SERVICES) |
566 | + else: |
567 | + self._defer_service_restart( |
568 | + services=self.ISCSI_SERVICES, |
569 | + reason=event_name) |
570 | + |
571 | def _check_mandatory_config(self): |
572 | + """Check whether mandatory configs are provided.""" |
573 | charm_config = self.model.config |
574 | |
575 | if charm_config['storage-type'] in self.VALID_STORAGE_TYPES: |
576 | @@ -246,12 +399,12 @@ class StorageConnectorCharm(CharmBase): |
577 | self.unit.status = BlockedStatus( |
578 | "Storage type cannot be changed after deployment." |
579 | ) |
580 | - return False |
581 | + return |
582 | else: |
583 | self.unit.status = BlockedStatus( |
584 | "Missing/Invalid storage type. Valid options are 'iscsi' or 'fc'." |
585 | ) |
586 | - return False |
587 | + return |
588 | |
589 | mandatory_config = self.MANDATORY_CONFIG[self._stored.storage_type] |
590 | missing_config = [] |
591 | @@ -261,8 +414,6 @@ class StorageConnectorCharm(CharmBase): |
592 | if missing_config: |
593 | self.unit.status = BlockedStatus("Missing mandatory configuration " + |
594 | "option(s) {}".format(missing_config)) |
595 | - return False |
596 | - return True |
597 | |
598 | def _defer_once(self, event): |
599 | """Defer the given event, but only once.""" |
600 | @@ -352,7 +503,7 @@ class StorageConnectorCharm(CharmBase): |
601 | "Exception occured during the multipath \ |
602 | configuration. Please check logs." |
603 | ) |
604 | - return False |
605 | + return |
606 | else: |
607 | logging.debug("multipath-" + section + " is empty.") |
608 | |
609 | @@ -362,7 +513,7 @@ class StorageConnectorCharm(CharmBase): |
610 | self.unit.status = BlockedStatus( |
611 | "No WWID was found. Please check multipath status and logs." |
612 | ) |
613 | - return False |
614 | + return |
615 | alias = charm_config.get('fc-lun-alias') |
616 | ctxt['multipaths'] = {'wwid': wwid, 'alias': alias} |
617 | |
618 | @@ -371,33 +522,29 @@ class StorageConnectorCharm(CharmBase): |
619 | rendered_content = template.render(ctxt) |
620 | self.mp_path.write_text(rendered_content) |
621 | self.mp_path.chmod(0o600) |
622 | - return True |
623 | |
624 | - def _iscsiadm_discovery(self): |
625 | + def _iscsi_discovery_and_login(self): |
626 | + """Run iscsiadm discovery and login against targets.""" |
627 | charm_config = self.model.config |
628 | target = charm_config.get('iscsi-target') |
629 | port = charm_config.get('iscsi-port') |
630 | - logging.info('Launching iscsiadm discovery against target') |
631 | + logging.info('Launching iscsiadm discovery and login against targets') |
632 | + |
633 | try: |
634 | subprocess.check_call(['iscsiadm', '-m', 'discovery', '-t', 'sendtargets', |
635 | '-p', target + ':' + port]) |
636 | except subprocess.CalledProcessError: |
637 | logging.exception('Iscsi discovery failed.') |
638 | - self.unit.status = BlockedStatus( |
639 | - 'Iscsi discovery failed against target' |
640 | - ) |
641 | + return |
642 | |
643 | - def _iscsiadm_login(self): |
644 | - # add check if already logged in, no error if it is. |
645 | try: |
646 | - subprocess.check_call(['iscsiadm', '-m', 'node', '--login']) |
647 | - except subprocess.CalledProcessError: |
648 | - logging.exception('Iscsi login failed.') |
649 | - self.unit.status = BlockedStatus( |
650 | - 'Iscsi login failed against target' |
651 | - ) |
652 | + subprocess.check_output( |
653 | + ['iscsiadm', '-m', 'node', '--login'], |
654 | + stderr=subprocess.STDOUT) |
655 | + except subprocess.CalledProcessError as err: |
656 | + logging.exception('Iscsi login failed. \n%s', err.output.decode("utf-8")) |
657 | |
658 | - def _fc_scan_host_successfully(self): |
659 | + def _fc_scan_host(self): |
660 | hba_adapters = subprocess.getoutput('ls /sys/class/scsi_host') |
661 | logging.debug('hba_adapters: {}'.format(hba_adapters)) |
662 | if not hba_adapters: |
663 | @@ -405,7 +552,7 @@ class StorageConnectorCharm(CharmBase): |
664 | self.unit.status = BlockedStatus( |
665 | "No scsi devices were found. Scan aborted" |
666 | ) |
667 | - return False |
668 | + return |
669 | |
670 | for adapter in hba_adapters.split('\n'): |
671 | try: |
672 | @@ -419,9 +566,8 @@ class StorageConnectorCharm(CharmBase): |
673 | self.unit.status = BlockedStatus( |
674 | 'Scan of the HBA adapters failed on the host.' |
675 | ) |
676 | - return False |
677 | + return |
678 | self._stored.fc_scan_ran_once = True |
679 | - return True |
680 | |
681 | def _retrieve_multipath_wwid(self): |
682 | logging.info('Retrive device WWID via multipath -ll') |
683 | @@ -440,8 +586,6 @@ class StorageConnectorCharm(CharmBase): |
684 | self.unit.status = BlockedStatus( |
685 | 'Multipath conf error: {}', error |
686 | ) |
687 | - return False |
688 | - return True |
689 | |
690 | def _check_if_container(self): |
691 | """Check if the charm is being deployed on a container host.""" |
692 | @@ -455,13 +599,29 @@ class StorageConnectorCharm(CharmBase): |
693 | return True |
694 | return False |
695 | |
696 | + def _configure_deferred_restarts(self): |
697 | + """Set up deferred restarts in policy-rc.d.""" |
698 | + policy_rcd.install_policy_rcd() |
699 | + os.chmod( |
700 | + '/var/lib/charm/{}/policy-rc.d'.format( |
701 | + self.app.name), |
702 | + 0o755) |
703 | + |
704 | + charm_config = self.model.config |
705 | + if charm_config.get('enable-auto-restarts'): |
706 | + policy_rcd.remove_policy_file() |
707 | + else: |
708 | + blocked_actions = ['stop', 'restart', 'try-restart'] |
709 | + for svc in self.DEFERRED_SERVICES: |
710 | + policy_rcd.add_policy_block(svc, blocked_actions) |
711 | + |
712 | def _on_metrics_endpoint_relation_created(self, event): |
713 | """Relation-created event handler for metrics-endpoint.""" |
714 | self.unit.status = MaintenanceStatus("Installing exporter") |
715 | metrics_utils.install_exporter(self.model.resources) |
716 | |
717 | self._stored.prometheus_related = True |
718 | - self.unit.status = ActiveStatus("Unit is ready") |
719 | + self.unit.status = ActiveStatus(self._get_status_message()) |
720 | |
721 | def _on_metrics_endpoint_relation_broken(self, event): |
722 | """Relation-broken event handler for metrics-endpoint.""" |
723 | @@ -470,7 +630,7 @@ class StorageConnectorCharm(CharmBase): |
724 | metrics_utils.uninstall_exporter() |
725 | |
726 | self._stored.prometheus_related = False |
727 | - self.unit.status = ActiveStatus("Unit is ready") |
728 | + self.unit.status = ActiveStatus(self._get_status_message()) |
729 | |
730 | def _on_nrpe_external_master_relation_created(self, event): |
731 | """Relation-created event handler for nrpe-external-master.""" |
732 | @@ -478,7 +638,7 @@ class StorageConnectorCharm(CharmBase): |
733 | metrics_utils.install_exporter(self.model.resources) |
734 | |
735 | self._stored.nrpe_related = True |
736 | - self.unit.status = ActiveStatus("Unit is ready") |
737 | + self.unit.status = ActiveStatus(self._get_status_message()) |
738 | |
739 | def _on_nrpe_external_master_relation_changed(self, event): |
740 | """Relation-changed event handler for nrpe-external-master.""" |
741 | @@ -494,7 +654,7 @@ class StorageConnectorCharm(CharmBase): |
742 | nrpe_utils.unsync_nrpe_files() |
743 | |
744 | self._stored.nrpe_related = False |
745 | - self.unit.status = ActiveStatus("Unit is ready") |
746 | + self.unit.status = ActiveStatus(self._get_status_message()) |
747 | |
748 | |
749 | if __name__ == "__main__": |
750 | diff --git a/tests/functional/storage-connector-func-tests/setup.py b/tests/functional/storage-connector-func-tests/setup.py |
751 | index 3f8d801..01e3eb8 100644 |
752 | --- a/tests/functional/storage-connector-func-tests/setup.py |
753 | +++ b/tests/functional/storage-connector-func-tests/setup.py |
754 | @@ -14,6 +14,7 @@ |
755 | |
756 | """Code for setting up storage-connector tests.""" |
757 | |
758 | +import json |
759 | import logging |
760 | |
761 | import zaza.model |
762 | @@ -58,3 +59,50 @@ def configure_iscsi_target(): |
763 | # Restart tgt to load new config |
764 | restart_tgt = "systemctl restart tgt" |
765 | zaza.model.run_on_unit('ubuntu-target/0', restart_tgt) |
766 | + |
767 | + |
768 | +def get_unit_full_hostname(unit_name): |
769 | + """Retrieve the full hostname of a unit.""" |
770 | + for unit in zaza.model.get_units(unit_name): |
771 | + result = zaza.model.run_on_unit(unit.entity_id, 'hostname -f') |
772 | + hostname = result['Stdout'].rstrip() |
773 | + return hostname |
774 | + |
775 | + |
776 | +def configure_iscsi_connector(): |
777 | + """Configure iscsi connector.""" |
778 | + iqn = 'iqn.2020-07.canonical.com:lun1' |
779 | + unit_fqdn = get_unit_full_hostname('ubuntu') |
780 | + iscsi_target = zaza.model.get_app_ips('ubuntu-target')[0] |
781 | + initiator_dictionary = json.dumps({unit_fqdn: iqn}) |
782 | + conf = { |
783 | + 'storage-type': 'iscsi', |
784 | + 'initiator-dictionary': initiator_dictionary, |
785 | + 'iscsi-target': iscsi_target, |
786 | + 'iscsi-port': '3260', |
787 | + 'iscsi-node-session-auth-authmethod': 'CHAP', |
788 | + 'iscsi-node-session-auth-username': 'iscsi-user', |
789 | + 'iscsi-node-session-auth-password': 'password123', |
790 | + 'iscsi-node-session-auth-username-in': 'iscsi-target', |
791 | + 'iscsi-node-session-auth-password-in': 'secretpass', |
792 | + 'multipath-devices': '{}', |
793 | + 'enable-auto-restarts': "false" |
794 | + } |
795 | + |
796 | + zaza.model.set_application_config('storage-connector', conf) |
797 | + zaza.model.wait_for_application_states( |
798 | + states={ |
799 | + "storage-connector": { |
800 | + "workload-status": "active", |
801 | + "workload-status-message-prefix": "Unit is ready" |
802 | + }, |
803 | + "ubuntu": { |
804 | + "workload-status": "active", |
805 | + "workload-status-message-regex": "^$" |
806 | + }, |
807 | + "ubuntu-target": { |
808 | + "workload-status": "active", |
809 | + "workload-status-message-regex": "^$" |
810 | + } |
811 | + } |
812 | + ) |
813 | diff --git a/tests/functional/storage-connector-func-tests/tests.py b/tests/functional/storage-connector-func-tests/tests.py |
814 | index bc6c7e9..21a7d25 100644 |
815 | --- a/tests/functional/storage-connector-func-tests/tests.py |
816 | +++ b/tests/functional/storage-connector-func-tests/tests.py |
817 | @@ -14,7 +14,6 @@ |
818 | |
819 | """Encapsulate storage-connector testing.""" |
820 | |
821 | -import json |
822 | import logging |
823 | |
824 | import zaza.model |
825 | @@ -29,42 +28,37 @@ class StorageConnectorTest(test_utils.BaseCharmTest): |
826 | """Run class setup for running glance tests.""" |
827 | super(StorageConnectorTest, cls).setUpClass() |
828 | |
829 | - def configure_iscsi_connector(self): |
830 | - """Configure iscsi connector.""" |
831 | - iqn = 'iqn.2020-07.canonical.com:lun1' |
832 | - unit_fqdn = self.get_unit_full_hostname('ubuntu') |
833 | - target_ip = zaza.model.get_app_ips('ubuntu-target')[0] |
834 | - initiator_dictionary = json.dumps({unit_fqdn: iqn}) |
835 | - conf = { |
836 | - 'storage-type': 'iscsi', |
837 | - 'initiator-dictionary': initiator_dictionary, |
838 | - 'iscsi-target': target_ip, |
839 | - 'iscsi-port': '3260', |
840 | - 'iscsi-node-session-auth-authmethod': 'CHAP', |
841 | - 'iscsi-node-session-auth-username': 'iscsi-user', |
842 | - 'iscsi-node-session-auth-password': 'password123', |
843 | - 'iscsi-node-session-auth-username-in': 'iscsi-target', |
844 | - 'iscsi-node-session-auth-password-in': 'secretpass', |
845 | - 'multipath-devices': '{}' |
846 | - } |
847 | + def test_iscsi_connector_config_changed(self): |
848 | + """Test iscsi configuration changes and wait for idle status.""" |
849 | + conf = zaza.model.get_application_config('storage-connector') |
850 | + conf["storage-type"] = "null" |
851 | zaza.model.set_application_config('storage-connector', conf) |
852 | + logging.info('Wait for block status...') |
853 | + zaza.model.wait_for_application_states( |
854 | + states={ |
855 | + "storage-connector": { |
856 | + "workload-status": "blocked", |
857 | + "workload-status-message-prefix": "Missing" |
858 | + }, |
859 | + "ubuntu": { |
860 | + "workload-status": "active", |
861 | + "workload-status-message-regex": "^$" |
862 | + }, |
863 | + "ubuntu-target": { |
864 | + "workload-status": "active", |
865 | + "workload-status-message-regex": "^$" |
866 | + } |
867 | + } |
868 | + ) |
869 | |
870 | - def get_unit_full_hostname(self, unit_name): |
871 | - """Retrieve the full hostname of a unit.""" |
872 | - for unit in zaza.model.get_units(unit_name): |
873 | - result = zaza.model.run_on_unit(unit.entity_id, 'hostname -f') |
874 | - hostname = result['Stdout'].rstrip() |
875 | - return hostname |
876 | - |
877 | - def test_iscsi_connector(self): |
878 | - """Test iscsi configuration and wait for idle status.""" |
879 | - self.configure_iscsi_connector() |
880 | + conf["storage-type"] = "iscsi" |
881 | + zaza.model.set_application_config('storage-connector', conf) |
882 | logging.info('Wait for idle/ready status...') |
883 | zaza.model.wait_for_application_states( |
884 | states={ |
885 | "storage-connector": { |
886 | "workload-status": "active", |
887 | - "workload-status-message-prefix": "Unit is ready" |
888 | + "workload-status-message-prefix": "" |
889 | }, |
890 | "ubuntu": { |
891 | "workload-status": "active", |
892 | @@ -79,7 +73,7 @@ class StorageConnectorTest(test_utils.BaseCharmTest): |
893 | |
894 | def test_validate_iscsi_session(self): |
895 | """Validate that the iscsi session is active.""" |
896 | - unit = zaza.model.get_units('ubuntu')[0] |
897 | + unit = zaza.model.get_units('storage-connector')[0] |
898 | logging.info('Checking if iscsi session is active.') |
899 | run = zaza.model.run_on_unit(unit.entity_id, 'iscsiadm -m session') |
900 | logging.info("""iscsiadm -m session: Stdout: {}, Stderr: {}, """ |
901 | @@ -87,3 +81,76 @@ class StorageConnectorTest(test_utils.BaseCharmTest): |
902 | run['Stderr'], |
903 | run['Code'])) |
904 | assert run['Code'] == '0' |
905 | + |
906 | + def test_defer_service_restarts(self): |
907 | + """Validate that the iscsi services are deferred on config-changed.""" |
908 | + unit = zaza.model.get_units('storage-connector')[0] |
909 | + |
910 | + active_time_pre_check = zaza.model.get_systemd_service_active_time( |
911 | + unit.entity_id, 'iscsid.service' |
912 | + ) |
913 | + logging.info("""Service start time before config change: {}""".format( |
914 | + active_time_pre_check)) |
915 | + |
916 | + # Modify the value of a random config option to trigger config-changed event |
917 | + conf = zaza.model.get_application_config('storage-connector') |
918 | + conf["iscsi-node-session-scan"] = "manual" |
919 | + zaza.model.set_application_config('storage-connector', conf) |
920 | + |
921 | + zaza.model.wait_for_application_states( |
922 | + states={ |
923 | + "storage-connector": { |
924 | + "workload-status": "active", |
925 | + "workload-status-message-regex": ".*Services queued for restart.*" |
926 | + }, |
927 | + "ubuntu": { |
928 | + "workload-status": "active", |
929 | + "workload-status-message-regex": "^$" |
930 | + }, |
931 | + "ubuntu-target": { |
932 | + "workload-status": "active", |
933 | + "workload-status-message-regex": "^$" |
934 | + } |
935 | + } |
936 | + ) |
937 | + |
938 | + active_time_post_check = zaza.model.get_systemd_service_active_time( |
939 | + unit.entity_id, 'iscsid.service' |
940 | + ) |
941 | + logging.info("""Service start time after config change: {}""".format( |
942 | + active_time_post_check)) |
943 | + |
944 | + # Check if service restart is blocked |
945 | + assert active_time_pre_check == active_time_post_check |
946 | + |
947 | + # Run restart-iscsi-services to restart the deferred services |
948 | + zaza.model.run_action( |
949 | + unit.entity_id, |
950 | + "restart-services", |
951 | + action_params={ |
952 | + 'deferred-only': True}) |
953 | + zaza.model.wait_for_application_states( |
954 | + states={ |
955 | + "storage-connector": { |
956 | + "workload-status": "active", |
957 | + "workload-status-message-prefix": "Unit is ready" |
958 | + }, |
959 | + "ubuntu": { |
960 | + "workload-status": "active", |
961 | + "workload-status-message-regex": "^$" |
962 | + }, |
963 | + "ubuntu-target": { |
964 | + "workload-status": "active", |
965 | + "workload-status-message-regex": "^$" |
966 | + } |
967 | + } |
968 | + ) |
969 | + |
970 | + active_time_after_restart = zaza.model.get_systemd_service_active_time( |
971 | + unit.entity_id, 'iscsid.service' |
972 | + ) |
973 | + logging.info("""Service start time after service restart action: {}""".format( |
974 | + active_time_after_restart)) |
975 | + |
976 | + # Check if service restart was successful |
977 | + assert active_time_post_check != active_time_after_restart |
978 | diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml |
979 | index 6a43b9b..57551f4 100644 |
980 | --- a/tests/functional/tests/tests.yaml |
981 | +++ b/tests/functional/tests/tests.yaml |
982 | @@ -11,6 +11,7 @@ gate_bundles: |
983 | configure: |
984 | - storage-connector-func-tests.setup.basic_target_setup |
985 | - storage-connector-func-tests.setup.configure_iscsi_target |
986 | + - storage-connector-func-tests.setup.configure_iscsi_connector |
987 | |
988 | tests: |
989 | - storage-connector-func-tests.tests.StorageConnectorTest |
990 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
991 | index 499e35d..18daa18 100644 |
992 | --- a/tests/unit/test_charm.py |
993 | +++ b/tests/unit/test_charm.py |
994 | @@ -10,7 +10,10 @@ from unittest.mock import PropertyMock, call, mock_open, patch |
995 | |
996 | import charm |
997 | |
998 | +import charmhelpers.contrib.openstack.deferred_events as deferred_events |
999 | + |
1000 | from ops.framework import EventBase |
1001 | +from ops.model import ActiveStatus |
1002 | from ops.testing import Harness |
1003 | |
1004 | |
1005 | @@ -40,6 +43,7 @@ class TestCharm(unittest.TestCase): |
1006 | |
1007 | @patch("storage_connector.nrpe_utils.update_nrpe_config") |
1008 | @patch("charm.subprocess.check_call") |
1009 | + @patch("charm.subprocess.check_output") |
1010 | @patch("charm.subprocess.getoutput") |
1011 | @patch("charm.utils.is_container") |
1012 | @patch("charm.StorageConnectorCharm.MULTIPATH_CONF_PATH", new_callable=PropertyMock) |
1013 | @@ -49,7 +53,9 @@ class TestCharm(unittest.TestCase): |
1014 | ) |
1015 | @patch("charm.StorageConnectorCharm.ISCSI_CONF", new_callable=PropertyMock) |
1016 | @patch("charm.StorageConnectorCharm.ISCSI_CONF_PATH", new_callable=PropertyMock) |
1017 | + @patch("charm.StorageConnectorCharm._configure_deferred_restarts") |
1018 | def test_on_iscsi_install(self, |
1019 | + m_configure_deferred_restarts, |
1020 | m_iscsi_conf_path, |
1021 | m_iscsi_conf, |
1022 | m_iscsi_initiator_name, |
1023 | @@ -57,6 +63,7 @@ class TestCharm(unittest.TestCase): |
1024 | m_multipath_conf_path, |
1025 | m_is_container, |
1026 | m_getoutput, |
1027 | + m_check_output, |
1028 | m_check_call, |
1029 | _): |
1030 | """Test installation.""" |
1031 | @@ -86,11 +93,17 @@ class TestCharm(unittest.TestCase): |
1032 | [ |
1033 | call("systemctl restart iscsid".split()), |
1034 | call("systemctl restart open-iscsi".split()), |
1035 | - call("iscsiadm -m discovery -t sendtargets -p abc:443".split()), |
1036 | - call("iscsiadm -m node --login".split()) |
1037 | + call("iscsiadm -m discovery -t sendtargets -p abc:443".split()) |
1038 | + ], |
1039 | + any_order=False |
1040 | + ) |
1041 | + m_check_output.assert_has_calls( |
1042 | + [ |
1043 | + call("iscsiadm -m node --login".split(), stderr=-2) |
1044 | ], |
1045 | any_order=False |
1046 | ) |
1047 | + m_configure_deferred_restarts.assert_called_once() |
1048 | self.assertTrue(self.harness.charm._stored.installed) |
1049 | |
1050 | @patch("storage_connector.nrpe_utils.update_nrpe_config") |
1051 | @@ -101,7 +114,9 @@ class TestCharm(unittest.TestCase): |
1052 | @patch("charm.StorageConnectorCharm.MULTIPATH_CONF_DIR", new_callable=PropertyMock) |
1053 | @patch("charm.StorageConnectorCharm.ISCSI_CONF", new_callable=PropertyMock) |
1054 | @patch("charm.StorageConnectorCharm.ISCSI_CONF_PATH", new_callable=PropertyMock) |
1055 | + @patch("charm.StorageConnectorCharm._configure_deferred_restarts") |
1056 | def test_on_fiberchannel_install(self, |
1057 | + m_configure_deferred_restarts, |
1058 | m_iscsi_conf_path, |
1059 | m_iscsi_conf, |
1060 | m_multipath_conf_dir, |
1061 | @@ -129,6 +144,7 @@ class TestCharm(unittest.TestCase): |
1062 | self.assertTrue(os.path.exists(self.harness.charm.MULTIPATH_CONF_PATH)) |
1063 | self.assertTrue(self.harness.charm._stored.installed) |
1064 | m_open.assert_called_once_with("/sys/class/scsi_host/host0/scan", "w") |
1065 | + m_configure_deferred_restarts.assert_called_once() |
1066 | self.assertTrue( |
1067 | call(self.harness.charm.ISCSI_CONF, "w") not in m_open.mock_calls |
1068 | ) |
1069 | @@ -144,29 +160,314 @@ class TestCharm(unittest.TestCase): |
1070 | self.harness.charm.on.start.emit() |
1071 | self.assertTrue(self.harness.charm._stored.started) |
1072 | |
1073 | + def test_on_restart_services_action_mutually_exclusive_params(self): |
1074 | + """Test on restart servcices action with both deferred-only and services.""" |
1075 | + action_event = FakeActionEvent(params={ |
1076 | + "deferred-only": True, "services": "test_service"}) |
1077 | + self.harness.charm._on_restart_services_action(action_event) |
1078 | + self.assertEqual( |
1079 | + action_event.results['failed'], |
1080 | + 'deferred-only and services are mutually exclusive') |
1081 | + |
1082 | + def test_on_restart_services_action_no_params(self): |
1083 | + """Test on restart servcices action with no param.""" |
1084 | + action_event = FakeActionEvent(params={ |
1085 | + "deferred-only": False, "services": ""}) |
1086 | + self.harness.charm._on_restart_services_action(action_event) |
1087 | + self.assertEqual( |
1088 | + action_event.results['failed'], |
1089 | + 'Please specify either deferred-only or services') |
1090 | + |
1091 | @patch("charm.subprocess.check_call") |
1092 | - def test_on_restart_iscsi_services_action(self, m_check_call): |
1093 | - """Test on restart action.""" |
1094 | - action_event = FakeActionEvent() |
1095 | - m_check_call.return_value = True |
1096 | - self.harness.charm.on_restart_iscsi_services_action(action_event) |
1097 | + @patch("charm.deferred_events.get_deferred_restarts") |
1098 | + @patch("charm.deferred_events.check_restart_timestamps") |
1099 | + def test_on_restart_services_action_deferred_only_failed(self, |
1100 | + m_check_rtimestamps, |
1101 | + m_get_deferred_restarts, |
1102 | + m_check_call): |
1103 | + """Test on restart servcices action empty list of deferred restarts.""" |
1104 | + m_get_deferred_restarts.return_value = [] |
1105 | + action_event = FakeActionEvent(params={ |
1106 | + "deferred-only": True, "services": ""}) |
1107 | + self.harness.charm._on_restart_services_action(action_event) |
1108 | + self.assertEqual( |
1109 | + action_event.results['failed'], 'No deferred services to restart') |
1110 | + |
1111 | + @patch("charm.subprocess.check_call") |
1112 | + @patch("charm.deferred_events.get_deferred_restarts") |
1113 | + @patch("charm.deferred_events.check_restart_timestamps") |
1114 | + def test_on_restart_services_action_deferred_only_success(self, |
1115 | + m_check_rtimestamps, |
1116 | + m_get_deferred_restarts, |
1117 | + m_check_call): |
1118 | + """Test on restart servcices action with deferred only param.""" |
1119 | + m_get_deferred_restarts.return_value = [deferred_events.ServiceEvent( |
1120 | + timestamp=123456, |
1121 | + service='svc', |
1122 | + reason='Reason', |
1123 | + action='restart', |
1124 | + policy_requestor_name='myapp', |
1125 | + policy_requestor_type='charm')] |
1126 | + action_event = FakeActionEvent(params={ |
1127 | + "deferred-only": True, "services": ""}) |
1128 | + self.harness.charm._on_restart_services_action(action_event) |
1129 | m_check_call.assert_has_calls( |
1130 | [ |
1131 | - call(["systemctl", "restart", "iscsid"]), |
1132 | - call(["systemctl", "restart", "open-iscsi"]) |
1133 | + call(["systemctl", "restart", "svc"]) |
1134 | ], |
1135 | any_order=False |
1136 | ) |
1137 | self.assertEqual(action_event.results['success'], 'True') |
1138 | |
1139 | + def test_on_restart_services_action_services_failed(self): |
1140 | + """Test on restart servcices action failed with invalid service input.""" |
1141 | + action_event = FakeActionEvent(params={ |
1142 | + "deferred-only": False, "services": "non_valid_service"}) |
1143 | + self.harness.charm._on_restart_services_action(action_event) |
1144 | + self.assertEqual( |
1145 | + action_event.results['failed'], 'No valid services are specified.') |
1146 | + |
1147 | + @patch("charm.subprocess.check_call") |
1148 | + @patch("charm.StorageConnectorCharm._iscsi_discovery_and_login") |
1149 | + def test_on_restart_services_action_services_success(self, |
1150 | + m_iscsi_discovery_and_login, |
1151 | + m_check_call): |
1152 | + """Test on restart servcices action successfully run with services param.""" |
1153 | + action_event = FakeActionEvent(params={ |
1154 | + "deferred-only": False, "services": "iscsid open-iscsi multipathd"}) |
1155 | + self.harness.charm._on_restart_services_action(action_event) |
1156 | + m_check_call.assert_has_calls( |
1157 | + [ |
1158 | + call(["systemctl", "restart", "iscsid"]), |
1159 | + call(["systemctl", "restart", "open-iscsi"]), |
1160 | + call(["systemctl", "restart", "multipathd"]) |
1161 | + ], |
1162 | + any_order=True |
1163 | + ) |
1164 | + self.assertEqual(action_event.results['success'], 'True') |
1165 | + m_iscsi_discovery_and_login.assert_called_once() |
1166 | + |
1167 | + @patch("charm.deferred_events.get_deferred_restarts") |
1168 | + def test_on_show_deferred_restarts_action(self, m_get_deferred_restarts): |
1169 | + """Test on show deferred restarts action.""" |
1170 | + m_get_deferred_restarts.return_value = [deferred_events.ServiceEvent( |
1171 | + timestamp=123456, |
1172 | + service='svc', |
1173 | + reason='Reason', |
1174 | + action='restart', |
1175 | + policy_requestor_name='myapp', |
1176 | + policy_requestor_type='charm')] |
1177 | + |
1178 | + action_event = FakeActionEvent() |
1179 | + self.harness.charm._on_show_deferred_restarts_action(action_event) |
1180 | + self.assertEqual( |
1181 | + action_event.results['deferred-restarts'], |
1182 | + '- 1970-01-02 10:17:36 +0000 UTC svc' + |
1183 | + ' Reason\n') |
1184 | + |
1185 | @patch("charm.subprocess.check_call") |
1186 | def test_on_reload_multipathd_service_action(self, m_check_call): |
1187 | - """Test on reload action.""" |
1188 | + """Test on reload multipathd action.""" |
1189 | action_event = FakeActionEvent() |
1190 | - self.harness.charm.on_reload_multipathd_service_action(action_event) |
1191 | + self.harness.charm._on_reload_multipathd_service_action(action_event) |
1192 | m_check_call.assert_called_once_with(["systemctl", "reload", "multipathd"]) |
1193 | self.assertEqual(action_event.results['success'], 'True') |
1194 | |
1195 | + @patch("charm.subprocess.check_call") |
1196 | + @patch("charm.subprocess.check_output") |
1197 | + def test_on_iscsi_discovery_and_login_action(self, m_check_output, m_check_call): |
1198 | + """Test on iscsi discovery and login action.""" |
1199 | + self.harness.update_config({ |
1200 | + "iscsi-target": "abc", |
1201 | + "iscsi-port": "443" |
1202 | + }) |
1203 | + action_event = FakeActionEvent() |
1204 | + self.harness.charm._on_iscsi_discovery_and_login_action(action_event) |
1205 | + |
1206 | + m_check_call.assert_has_calls( |
1207 | + [ |
1208 | + call(['iscsiadm', '-m', 'discovery', '-t', 'sendtargets', |
1209 | + '-p', "abc" + ':' + "443"]), |
1210 | + ], |
1211 | + any_order=False |
1212 | + ) |
1213 | + m_check_output.assert_has_calls( |
1214 | + [ |
1215 | + call(['iscsiadm', '-m', 'node', '--login'], stderr=-2) |
1216 | + ], |
1217 | + any_order=False |
1218 | + ) |
1219 | + self.assertEqual(action_event.results['success'], 'True') |
1220 | + |
1221 | + @patch("charm.deferred_events.get_deferred_restarts") |
1222 | + def test_get_status_message(self, m_get_deferred_restarts): |
1223 | + """Test on setting active status with correct status message.""" |
1224 | + m_get_deferred_restarts.return_value = [] |
1225 | + self.assertEqual(self.harness.charm._get_status_message(), "Unit is ready") |
1226 | + |
1227 | + m_get_deferred_restarts.return_value = [deferred_events.ServiceEvent( |
1228 | + timestamp=123456, |
1229 | + service='svc1', |
1230 | + reason='Reason1', |
1231 | + action='restart', |
1232 | + policy_requestor_name='other_app', |
1233 | + policy_requestor_type='charm'), deferred_events.ServiceEvent( |
1234 | + timestamp=234567, |
1235 | + service='svc2', |
1236 | + reason='Reason2', |
1237 | + action='restart', |
1238 | + policy_requestor_name='storage-connector', |
1239 | + policy_requestor_type='charm')] |
1240 | + self.assertEqual( |
1241 | + self.harness.charm._get_status_message(), |
1242 | + "Unit is ready. Services queued for restart: svc2" |
1243 | + ) |
1244 | + |
1245 | + @patch("charm.deferred_events.check_restart_timestamps") |
1246 | + @patch("charm.deferred_events.get_deferred_restarts") |
1247 | + def test_check_deferred_restarts_queue(self, |
1248 | + m_get_deferred_restarts, |
1249 | + m_check_restart_timestamps): |
1250 | + """Test check_deferred_restarts_queue decorator function.""" |
1251 | + self.harness.charm.unit.status = ActiveStatus("Unit is ready") |
1252 | + m_get_deferred_restarts.return_value = [deferred_events.ServiceEvent( |
1253 | + timestamp=234567, |
1254 | + service='svc', |
1255 | + reason='Reason', |
1256 | + action='restart', |
1257 | + policy_requestor_name='storage-connector', |
1258 | + policy_requestor_type='charm')] |
1259 | + |
1260 | + # Trigger decorator function by emitting update_status event |
1261 | + self.harness.charm.on.update_status.emit() |
1262 | + |
1263 | + m_check_restart_timestamps.assert_called_once() |
1264 | + self.assertEqual( |
1265 | + self.harness.charm.unit.status, |
1266 | + ActiveStatus("Unit is ready. Services queued for restart: svc") |
1267 | + ) |
1268 | + |
1269 | + @patch("charm.policy_rcd.install_policy_rcd") |
1270 | + @patch("charm.policy_rcd.remove_policy_file") |
1271 | + @patch("charm.policy_rcd.add_policy_block") |
1272 | + @patch("charm.os.chmod") |
1273 | + def test_configure_deferred_restarts(self, |
1274 | + m_chmod, |
1275 | + m_add_policy_block, |
1276 | + m_remove_policy_file, |
1277 | + install_policy_rcd): |
1278 | + """Test on setting up deferred restarts in policy-rc.d.""" |
1279 | + self.harness.update_config({'enable-auto-restarts': True}) |
1280 | + self.harness.charm._configure_deferred_restarts() |
1281 | + install_policy_rcd.assert_called_once() |
1282 | + m_remove_policy_file.assert_called_once() |
1283 | + m_chmod.assert_called_once() |
1284 | + |
1285 | + self.harness.update_config({'enable-auto-restarts': False}) |
1286 | + self.harness.charm._configure_deferred_restarts() |
1287 | + m_add_policy_block.assert_has_calls( |
1288 | + [ |
1289 | + call('iscsid', ['stop', 'restart', 'try-restart']), |
1290 | + call('open-iscsi', ['stop', 'restart', 'try-restart']) |
1291 | + ], |
1292 | + any_order=False |
1293 | + ) |
1294 | + |
1295 | + @patch("charm.subprocess.check_call") |
1296 | + @patch("charm.StorageConnectorCharm._iscsi_discovery_and_login") |
1297 | + def test_on_restart_non_iscsi_services(self, |
1298 | + m_iscsi_discovery_and_login, |
1299 | + m_check_call): |
1300 | + """Test on restarting non-iscsi services.""" |
1301 | + self.harness.charm._restart_services(services=["multipathd"]) |
1302 | + m_check_call.assert_has_calls( |
1303 | + [ |
1304 | + call(["systemctl", "restart", "multipathd"]) |
1305 | + ], |
1306 | + any_order=True |
1307 | + ) |
1308 | + m_iscsi_discovery_and_login.assert_not_called() |
1309 | + |
1310 | + @patch("charm.subprocess.check_call") |
1311 | + @patch("charm.StorageConnectorCharm._iscsi_discovery_and_login") |
1312 | + def test_on_restart_iscsi_services_with_discovery_login( |
1313 | + self, |
1314 | + m_iscsi_discovery_and_login, |
1315 | + m_check_call |
1316 | + ): |
1317 | + """Test on restarting a iscsi service with discovery and login.""" |
1318 | + self.harness.update_config({'iscsi-discovery-and-login': True}) |
1319 | + self.harness.charm._restart_services(services=["iscsid"]) |
1320 | + m_check_call.assert_has_calls( |
1321 | + [ |
1322 | + call(["systemctl", "restart", "iscsid"]) |
1323 | + ], |
1324 | + any_order=True |
1325 | + ) |
1326 | + m_iscsi_discovery_and_login.assert_called_once() |
1327 | + |
1328 | + @patch("charm.subprocess.check_call") |
1329 | + @patch("charm.StorageConnectorCharm._iscsi_discovery_and_login") |
1330 | + def test_on_restart_iscsi_services_without_discovery_login( |
1331 | + self, |
1332 | + m_iscsi_discovery_and_login, |
1333 | + m_check_call |
1334 | + ): |
1335 | + """Test on restarting a iscsi service without running discovery and login.""" |
1336 | + self.harness.update_config({'iscsi-discovery-and-login': False}) |
1337 | + self.harness.charm._restart_services(services=["iscsid"]) |
1338 | + m_check_call.assert_has_calls( |
1339 | + [ |
1340 | + call(["systemctl", "restart", "iscsid"]) |
1341 | + ], |
1342 | + any_order=True |
1343 | + ) |
1344 | + m_iscsi_discovery_and_login.assert_not_called() |
1345 | + |
1346 | + @patch("charm.subprocess.check_output") |
1347 | + @patch("charm.subprocess.check_call") |
1348 | + @patch("charm.logging.exception") |
1349 | + def test_iscsiadm_discovery_failed(self, |
1350 | + m_log_exception, |
1351 | + m_check_call, |
1352 | + m_check_output): |
1353 | + """Test response to iscsiadm discovery failure.""" |
1354 | + self.harness.update_config({ |
1355 | + "storage-type": "iscsi", |
1356 | + "iscsi-target": "abc", |
1357 | + "iscsi-port": "443" |
1358 | + }) |
1359 | + self.harness.charm.unit.status = ActiveStatus("Unit is ready") |
1360 | + m_check_call.side_effect = charm.subprocess.CalledProcessError( |
1361 | + returncode=15, |
1362 | + cmd=['iscsiadm', '-m', 'discovery', '-t', 'sendtargets', |
1363 | + '-p', "abc" + ':' + "443"], |
1364 | + ) |
1365 | + self.harness.charm._iscsi_discovery_and_login() |
1366 | + m_log_exception.assert_called_once() |
1367 | + |
1368 | + @patch("charm.subprocess.check_output") |
1369 | + @patch("charm.subprocess.check_call") |
1370 | + @patch("charm.logging.exception") |
1371 | + def test_iscsiadm_login_failed(self, |
1372 | + m_log_exception, |
1373 | + m_check_call, |
1374 | + m_check_output): |
1375 | + """Test response to iscsiadm login failure.""" |
1376 | + self.harness.update_config({ |
1377 | + "storage-type": "iscsi", |
1378 | + "iscsi-target": "abc", |
1379 | + "iscsi-port": "443" |
1380 | + }) |
1381 | + self.harness.charm.unit.status = ActiveStatus("Unit is ready") |
1382 | + m_check_output.side_effect = charm.subprocess.CalledProcessError( |
1383 | + returncode=15, |
1384 | + cmd=['iscsiadm', '-m', 'node', '--login'], |
1385 | + output=b'iscsiadm: Could not log into all portals' |
1386 | + ) |
1387 | + self.harness.charm._iscsi_discovery_and_login() |
1388 | + m_log_exception.assert_called_once() |
1389 | + |
1390 | @patch("storage_connector.metrics_utils.uninstall_exporter") |
1391 | @patch("storage_connector.metrics_utils.install_exporter") |
1392 | def test_on_metrics_endpoint_handlers( |
1393 | diff --git a/tox.ini b/tox.ini |
1394 | index 5c8e284..a401667 100644 |
1395 | --- a/tox.ini |
1396 | +++ b/tox.ini |
1397 | @@ -8,14 +8,14 @@ skip_missing_interpreters = False |
1398 | basepython = python3 |
1399 | setenv = |
1400 | PYTHONPATH = {toxinidir}/lib/:{toxinidir} |
1401 | -passenv = |
1402 | +passenv = |
1403 | HOME |
1404 | CHARM_BUILD_DIR |
1405 | OS_* |
1406 | whitelist_externals = charmcraft |
1407 | |
1408 | [testenv:unit] |
1409 | -commands = |
1410 | +commands = |
1411 | coverage erase |
1412 | stestr run --slowest {posargs} |
1413 | coverage combine |
1414 | @@ -24,7 +24,7 @@ commands = |
1415 | coverage report -m --omit lib/charms/*,templates/* |
1416 | deps = -r{toxinidir}/tests/unit/requirements.txt |
1417 | -r{toxinidir}/requirements.txt |
1418 | -setenv = |
1419 | +setenv = |
1420 | {[testenv]setenv} |
1421 | PYTHON=coverage run |
1422 | |
1423 | @@ -32,9 +32,9 @@ setenv = |
1424 | branch = True |
1425 | concurrency = multiprocessing |
1426 | parallel = True |
1427 | -source = |
1428 | +source = |
1429 | . |
1430 | -omit = |
1431 | +omit = |
1432 | .tox/* |
1433 | tests/* |
1434 |
This merge proposal is being monitored by mergebot. Change the status to Approved to merge.