Merge ~txiao/charm-storage-connector:defer_restart into charm-storage-connector:defer-service-restarts-integration
- Git
- lp:~txiao/charm-storage-connector
- defer_restart
- Merge into defer-service-restarts-int...
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) |
Related bugs: |
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:
|
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-
- Add `restart-services` action to restart charm managed service. The
operator can choose to restart either all deferred services or a specific
set of services.
- Add `show-deferred-
with the timestamp and reason
- Add `iscsi-
run iscsiadm discovery and login when needed
- Remove `restart-
`restart-services` action
- 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Martin Kalcok (martin-kalcok) : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Erhan Sunar (esunar) wrote : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:60bb5f05f95
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
Please inline comment below
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Eric Chen (eric-chen) wrote : Posted in a previous version of this proposal | # |
Please check inline comment.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:7c3607952e0
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:918505fdff4
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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 :)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
The lint target does not include black. Please align the tox and/or pyproject.toml configs to the one from bootstack-
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:1540ff0713b
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:47e23872d87
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andrea Ieri (aieri) wrote : Posted in a previous version of this proposal | # |
Comments in-line
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:f3c025427b6
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tianqi Xiao (txiao) : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:a026647ae5f
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Tianqi Xiao (txiao) wrote : Posted in a previous version of this proposal | # |
@aieri Thanks for your review. I have made changes addressing your comments.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
PASSED: Continuous integration, rev:ae8936b5932
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
JamesLin (jneo8) wrote : Posted in a previous version of this proposal | # |
Only one unit tests request.
Others LGTM.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:a5df52ea49e
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Andrea Ieri (aieri) wrote : Posted in a previous version of this proposal | # |
saving some in-line comments
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:7d4ddba400f
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : Posted in a previous version of this proposal | # |
FAILED: Continuous integration, rev:7d221cbcbdd
https:/
Executed test runs:
FAILURE: https:/
None: https:/
Click here to trigger a rebuild:
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
JamesLin (jneo8) wrote : Posted in a previous version of this proposal | # |
LGTM
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 prod-jenkaas-bootstack (prod-jenkaas-bootstack) wrote : | # |
PASSED: Continuous integration, rev:784d3b30e70
https:/
Executed test runs:
SUCCESS: https:/
None: https:/
Click here to trigger a rebuild:
https:/
Preview Diff
1 | diff --git a/README.md b/README.md |
2 | index 247b96d..2802aab 100644 |
3 | --- a/README.md |
4 | +++ b/README.md |
5 | @@ -2,10 +2,10 @@ |
6 | |
7 | ## Overview |
8 | |
9 | -This charm configures a unit to connect to a storage endpoint, either iSCSI or Fibre Channel. |
10 | -It acts as a subordinate charm, which can be deployed on any bare metal or virtual machine, |
11 | -alongside a main charm. It is not supported in containers due to lack of ability for lxd |
12 | -containers to access iSCSI and Fibre Channel hardware of the underlying kernel. |
13 | +This charm configures a unit to connect to a storage endpoint, either iSCSI or Fibre Channel. |
14 | +It acts as a subordinate charm, which can be deployed on any bare metal or virtual machine, |
15 | +alongside a main charm. It is not supported in containers due to lack of ability for lxd |
16 | +containers to access iSCSI and Fibre Channel hardware of the underlying kernel. |
17 | |
18 | If you configure this charm for iSCSI, this charm will: |
19 | - Generate an iSCSI initiator name and put it in /etc/iscsi/initiatorname.iscsi |
20 | @@ -23,11 +23,11 @@ If you configure it for Fibre Channel, this charm will: |
21 | - Configure multipath under /etc/multipath/conf.d directory |
22 | - Reload and restart multipathd.service |
23 | |
24 | -Please note that the Fiber Channel option currently supports only one device. |
25 | +Please note that the Fiber Channel option currently supports only one device. |
26 | |
27 | If iSCSI, the user can input a initiator name dictionary in config.yaml if they wish to use a |
28 | specific IQN for a specific unit. Also, the target IP and port are needed to perform |
29 | -the discovery and login with iscsiadm. |
30 | +the discovery and login with iscsiadm. |
31 | |
32 | For Fibre Channel, the user can choose the device alias to be used when mapping the disk. |
33 | See the option "fc-lun-alias" for further details. |
34 | @@ -85,9 +85,9 @@ juju relate ubuntu storage-connector |
35 | |
36 | ## Scaling |
37 | |
38 | -This charm will scale with the units it is related to. For example, if you scale the |
39 | -ubuntu application, and the storage-connector is related to it, it will be deployed on each |
40 | -newly deployed ubuntu unit. |
41 | +This charm will scale with the units it is related to. For example, if you scale the |
42 | +ubuntu application, and the storage-connector is related to it, it will be deployed on each |
43 | +newly deployed ubuntu unit. |
44 | ``` |
45 | juju add-unit ubuntu |
46 | juju remove-unit ubuntu/1 |
47 | @@ -104,4 +104,3 @@ cause of the error. |
48 | - Author: Camille Rodriguez <camille.rodriguez@canonical.com> |
49 | - Maintainers: BootStack Charmers <bootstack-charmers@lists.canonical.com> |
50 | - Bug Tracker: [here](https://bugs.launchpad.net/charm-storage-connector) |
51 | - |
52 | \ No newline at end of file |
53 | diff --git a/actions.yaml b/actions.yaml |
54 | index 998a670..79eed26 100644 |
55 | --- a/actions.yaml |
56 | +++ b/actions.yaml |
57 | @@ -1,4 +1,33 @@ |
58 | -restart-iscsi-services: |
59 | - description: "Restart iscsid and open-iscsi services" |
60 | +restart-services: |
61 | + description: | |
62 | + Restart services managed by the charm. |
63 | + params: |
64 | + deferred-only: |
65 | + type: boolean |
66 | + default: false |
67 | + description: | |
68 | + Restart all deferred services. To check which services has been deferred, |
69 | + run the `show-deferred-events` action. |
70 | + NOTE: deferred-only and services params are mutually exclusive. Providing |
71 | + both options will cause the action to fail. |
72 | + services: |
73 | + type: string |
74 | + default: "" |
75 | + description: | |
76 | + List of services to restart. Supporting services includes iscsi, |
77 | + open-iscsi, and multipathd. |
78 | + This param accepts space-separated services in string format, |
79 | + e.g. "iscsid open-iscsi" |
80 | + NOTE: deferred-only and services params are mutually exclusive. Providing |
81 | + both options will cause the action to fail. |
82 | +show-deferred-restarts: |
83 | + descrpition: | |
84 | + Show the outstanding service restarts |
85 | reload-multipathd-service: |
86 | - description: "Reload multipathd service" |
87 | + description: | |
88 | + Reload multipathd service |
89 | +iscsi-discovery-and-login: |
90 | + description: | |
91 | + Run discovery and login against iscsi target. This action is needed when |
92 | + changes are made to iscsi configuration and the iscsi services |
93 | + are restarted to apply these changes. |
94 | diff --git a/config.yaml b/config.yaml |
95 | index 739ef42..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": "*"}' |
183 | diff --git a/metadata.yaml b/metadata.yaml |
184 | index 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 |
198 | diff --git a/src/charm.py b/src/charm.py |
199 | index 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__": |
728 | diff --git a/tests/functional/storage-connector-func-tests/setup.py b/tests/functional/storage-connector-func-tests/setup.py |
729 | index 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 | + ) |
791 | diff --git a/tests/functional/storage-connector-func-tests/tests.py b/tests/functional/storage-connector-func-tests/tests.py |
792 | index 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 |
956 | diff --git a/tests/functional/tests/tests.yaml b/tests/functional/tests/tests.yaml |
957 | index 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 |
968 | diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py |
969 | index 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( |
1338 | diff --git a/tox.ini b/tox.ini |
1339 | index 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 |
How did you test this?