Merge charm-storage-connector:defer-service-restarts-integration into charm-storage-connector:master

Proposed by Tianqi Xiao
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)
Reviewer Review Type Date Requested Status
Andrea Ieri Approve
🤖 prod-jenkaas-bootstack (community) continuous-integration Approve
BootStack Reviewers Pending
Review via email: mp+438827@code.launchpad.net

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-auto-restarts` config option to give user option to allow automatic restarts action if needed (the default value is False)
- 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-restarts` action to show all deferred restart events
with the timestamp and reason
- Add `iscsi-discovery-and-login` action that allow operator to manually
run iscsiadm discovery and login when needed
- Remove `restart-iscsi-services` action in favor of the new
`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.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote :
review: Approve (continuous-integration)
Revision history for this message
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.

Revision history for this message
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://code.launchpad.net/~txiao/charm-storage-connector/+git/charm-storage-connector/+merge/438042
[2] https://code.launchpad.net/~txiao/charm-storage-connector/+git/charm-storage-connector/+merge/438419

Revision history for this message
Andrea Ieri (aieri) wrote :

lgtm, thanks

review: Approve
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 0ac891951896d6133ed80e7f2759f7f403b99918

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.md b/README.md
2index 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
53diff --git a/actions.yaml b/actions.yaml
54index 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.
94diff --git a/config.yaml b/config.yaml
95index 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": "*"}'
184diff --git a/metadata.yaml b/metadata.yaml
185index 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
199diff --git a/requirements.txt b/requirements.txt
200index 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
213diff --git a/src/charm.py b/src/charm.py
214index 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__":
750diff --git a/tests/functional/storage-connector-func-tests/setup.py b/tests/functional/storage-connector-func-tests/setup.py
751index 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+ )
813diff --git a/tests/functional/storage-connector-func-tests/tests.py b/tests/functional/storage-connector-func-tests/tests.py
814index 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
978diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml
979index 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
990diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py
991index 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(
1393diff --git a/tox.ini b/tox.ini
1394index 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

Subscribers

No one subscribed via source and target branches