Merge ~txiao/charm-storage-connector:defer_restart into charm-storage-connector:defer-service-restarts-integration

Proposed by Tianqi Xiao
Status: Merged
Approved by: Tianqi Xiao
Approved revision: 6fb18349ca35661bb3dfa5c882323cc44faca0b6
Merged at revision: 6fb18349ca35661bb3dfa5c882323cc44faca0b6
Proposed branch: ~txiao/charm-storage-connector:defer_restart
Merge into: charm-storage-connector:defer-service-restarts-integration
Diff against target: 1378 lines (+732/-146)
10 files modified
README.md (+9/-10)
actions.yaml (+32/-3)
config.yaml (+24/-18)
metadata.yaml (+2/-2)
src/charm.py (+234/-66)
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 (+279/-11)
tox.ini (+5/-5)
Reviewer Review Type Date Requested Status
Andrea Ieri Approve
🤖 prod-jenkaas-bootstack (community) continuous-integration Approve
Paul Goins Approve
JamesLin Pending
Eric Chen Pending
Martin Kalcok Pending
BootStack Reviewers Pending
Review via email: mp+438042@code.launchpad.net

This proposal supersedes a proposal from 2023-02-02.

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
- Better handling different types of iscsi discovery and login failures.
If failed because session already present, ignore the error; otherwise
change charm status to blocked.
- 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
Martin Kalcok (martin-kalcok) : Posted in a previous version of this proposal
Revision history for this message
Erhan Sunar (esunar) wrote : Posted in a previous version of this proposal

How did you test this?

Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

@martin-kalcok Thanks for the review. I have applied the suggestions and replied to your inquiry in-line.

Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

> How did you test this?

I've deployed the jammy bundle used in functional tests, performed config changes and run restart actions to observe its behavior. I'm also working on writing the unittests and functional tests, which will be pushed upon completion.

Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote : Posted in a previous version of this proposal

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 : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Paul Goins (vultaire) wrote : Posted in a previous version of this proposal

I haven't reviewed the tests, but I did review the main implementation. It's quite interesting, and I see some things I like there.

I have a few areas I have some concerns, or where I think we may have some subtle bugs. Please see my comments.

Also (as I think we've also discussed internally), we probably want to block restarts of the multipathd service as well.

I am not sure if a policy-based block on restarting is the best solution here, but I simply haven't seen this done elsewhere. It's interesting. A local state flag may be another idea, just to determine whether we've already initialized the node once and to block subsequent restarts unless explicitly desired... But, that's *only* an idea; the policy-based solution may be better. I just don't know since it's not something I'm familiar with.

Anyway, please take a look at my comments. Thank you.

review: Needs Fixing
Revision history for this message
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal

Please inline comment below

review: Needs Information
Revision history for this message
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal

Please check inline comment.

review: Needs Information
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

Hi reviewers. I have made major changes to the code implementation to facilitate new functionalities as requested. Initially I wanted to open a separate MP for the new changes, but it feels wrong to get this merged and then overwrite the methods created here. I apologize for increasing the workload needed to review this MP.

In hope of better illustrating new changes, I have kept the commits separate. The latest commit (918505f) contains the updates, including addressing the previous comments.

Feedback and suggestions are highly appreciated :)

Revision history for this message
Andrea Ieri (aieri) wrote : Posted in a previous version of this proposal

Comments in-line. I'm actually not done with the review but I'm a bit out of day and I wanted to start sending these comments.

I have also dropped some pre-existing trailing whitespace in https://code.launchpad.net/~aieri/charm-storage-connector/+git/charm-storage-connector/+merge/437433

The lint target does not include black. Please align the tox and/or pyproject.toml configs to the one from bootstack-charms-spec (in a separate MP).

review: Needs Fixing
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

@aieri Thanks a lot for the review. I have addressed your suggestions in the latest commit and answered your questions in-line.

Revision history for this message
Andrea Ieri (aieri) wrote : Posted in a previous version of this proposal

Added replies to previous comments for commit 918505f. My review is not complete.

Revision history for this message
Andrea Ieri (aieri) wrote : Posted in a previous version of this proposal

Comments in-line

review: Needs Fixing
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Tianqi Xiao (txiao) : Posted in a previous version of this proposal
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

@aieri Thanks for your review. I have made changes addressing your comments.

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
JamesLin (jneo8) wrote : Posted in a previous version of this proposal

Only one unit tests request.
Others LGTM.

review: Needs Fixing
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

@jneo8 Thanks for the review. I've added the requested unit test case.

Revision history for this message
Andrea Ieri (aieri) wrote : Posted in a previous version of this proposal

saving some in-line comments

Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Paul Goins (vultaire) wrote : Posted in a previous version of this proposal

This took some time to review... but I think I'm *almost* a +1 now.

I did find one bug, verified locally on a VM, which requires attention. Please see my comment; this would have been a hard one to catch.

Once this is resolved, I'm ready to +1 this review.

review: Needs Fixing
Revision history for this message
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Paul Goins (vultaire) wrote : Posted in a previous version of this proposal

+1 from me. Not sure what is up with CI; the error there doesn't look like anything related to the actual tests.

review: Approve
Revision history for this message
JamesLin (jneo8) wrote : Posted in a previous version of this proposal

LGTM

review: Approve
Revision history for this message
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal

I re-run the CI pipeline and it has passed. Since it was not automatically attached to this MP, I'm putting the link here: https://jenkins.canonical.com/bootstack/job/lp-charm-test-functest/125/

Revision history for this message
Andrea Ieri (aieri) wrote (last edit ): Posted in a previous version of this proposal

Overall I'm +1 as well, but please don't merge until we have a chance to demo the functionality and discuss whether the iscsi login action is really desirable from a usability perspective.

Another option would be to merge into a separate integration branch and create a new MP if we decide to change how actions behave.

review: Needs Information
Revision history for this message
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal

I try to merge it this morning and fortunately I found Andrea's message in time and restore the status.
Therefore, I change the status to WIP until we decide to merge it.

Revision history for this message
Paul Goins (vultaire) wrote :

LGTM

review: Approve
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 :

LGTM

review: Approve

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

Subscribers

People subscribed via source and target branches

to all changes: