Merge lp:~lihuiguo/landscape-charm/bug-1934816 into lp:~landscape/landscape-charm/trunk
- bug-1934816
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Simon Poirier |
Approved revision: | 409 |
Merged at revision: | 407 |
Proposed branch: | lp:~lihuiguo/landscape-charm/bug-1934816 |
Merge into: | lp:~landscape/landscape-charm/trunk |
Diff against target: |
3080 lines (+1978/-192) 29 files modified
charm-helpers.yaml (+1/-0) charmhelpers/__init__.py (+6/-4) charmhelpers/contrib/charmsupport/__init__.py (+13/-0) charmhelpers/contrib/charmsupport/nrpe.py (+522/-0) charmhelpers/contrib/hahelpers/apache.py (+5/-1) charmhelpers/contrib/hahelpers/cluster.py (+47/-2) charmhelpers/core/decorators.py (+38/-0) charmhelpers/core/hookenv.py (+184/-35) charmhelpers/core/host.py (+262/-60) charmhelpers/core/host_factory/ubuntu.py (+13/-5) charmhelpers/core/services/base.py (+7/-2) charmhelpers/core/strutils.py (+7/-4) charmhelpers/core/sysctl.py (+12/-2) charmhelpers/core/unitdata.py (+3/-3) charmhelpers/fetch/__init__.py (+7/-2) charmhelpers/fetch/python/packages.py (+6/-4) charmhelpers/fetch/snap.py (+3/-3) charmhelpers/fetch/ubuntu.py (+341/-59) charmhelpers/fetch/ubuntu_apt_pkg.py (+312/-0) charmhelpers/osplatform.py (+27/-3) config.yaml (+16/-0) hooks/nrpe-external-master-relation-changed (+9/-0) hooks/nrpe-external-master-relation-joined (+9/-0) lib/callbacks/nrpe.py (+51/-0) lib/callbacks/tests/test_nrpe.py (+36/-0) lib/services.py (+5/-1) lib/tests/stubs.py (+24/-0) lib/tests/test_services.py (+9/-2) metadata.yaml (+3/-0) |
To merge this branch: | bzr merge lp:~lihuiguo/landscape-charm/bug-1934816 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
🤖 Landscape Builder | test results | Approve | |
Simon Poirier (community) | Approve | ||
James Troup (community) | Approve | ||
Review via email:
|
Commit message
Sync charm-helpers
Add relation: nrpe-external-
Add nrpe checks check_systemd for landscape services
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Linda Guo (lihuiguo) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) wrote : | # |
Command: make ci-test
Result: Fail
Revno: 407
Branch: lp:~lihuiguo/landscape-charm/bug-1934816
Jenkins: https:/
- 408. By Linda Guo <email address hidden>
-
Add charmhelpers.
contrib. charmsupport dependency
to charm-helpers.yaml to get nrpe.py
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) wrote : | # |
Command: make ci-test
Result: Success
Revno: 408
Branch: lp:~lihuiguo/landscape-charm/bug-1934816
Jenkins: https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
James Troup (elmo) wrote : | # |
I didn't review the charmhelpers changes, and my only comments are nitpick of docstrings (see inline comments). Other than those, this LGTM.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Simon Poirier (simpoir) wrote : | # |
+1 with inline comment
The test cases are a bit thin for my taste (only adding checks is covered, while the hooks handle add/remove/change)
- 409. By Linda Guo <email address hidden>
-
Fixed docstring
Added unit test to check remove nrpe
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
🤖 Landscape Builder (landscape-builder) wrote : | # |
Command: make ci-test
Result: Success
Revno: 409
Branch: lp:~lihuiguo/landscape-charm/bug-1934816
Jenkins: https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Linda Guo (lihuiguo) wrote : | # |
> +1 with inline comment
>
> The test cases are a bit thin for my taste (only adding checks is covered,
> while the hooks handle add/remove/change)
I have added more test cases to cover the nrpe check remove
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Simon Poirier (simpoir) wrote : | # |
Thanks for that extra test.
Preview Diff
1 | === modified file 'charm-helpers.yaml' | |||
2 | --- charm-helpers.yaml 2017-03-04 02:41:39 +0000 | |||
3 | +++ charm-helpers.yaml 2021-11-10 05:36:20 +0000 | |||
4 | @@ -6,3 +6,4 @@ | |||
5 | 6 | - fetch | 6 | - fetch |
6 | 7 | - osplatform | 7 | - osplatform |
7 | 8 | - contrib.hahelpers | 8 | - contrib.hahelpers |
8 | 9 | - contrib.charmsupport.nrpe | ||
9 | 9 | 10 | ||
10 | === modified file 'charmhelpers/__init__.py' | |||
11 | --- charmhelpers/__init__.py 2019-05-24 12:41:48 +0000 | |||
12 | +++ charmhelpers/__init__.py 2021-11-10 05:36:20 +0000 | |||
13 | @@ -49,7 +49,8 @@ | |||
14 | 49 | 49 | ||
15 | 50 | def deprecate(warning, date=None, log=None): | 50 | def deprecate(warning, date=None, log=None): |
16 | 51 | """Add a deprecation warning the first time the function is used. | 51 | """Add a deprecation warning the first time the function is used. |
18 | 52 | The date, which is a string in semi-ISO8660 format indicate the year-month | 52 | |
19 | 53 | The date which is a string in semi-ISO8660 format indicates the year-month | ||
20 | 53 | that the function is officially going to be removed. | 54 | that the function is officially going to be removed. |
21 | 54 | 55 | ||
22 | 55 | usage: | 56 | usage: |
23 | @@ -62,10 +63,11 @@ | |||
24 | 62 | The reason for passing the logging function (log) is so that hookenv.log | 63 | The reason for passing the logging function (log) is so that hookenv.log |
25 | 63 | can be used for a charm if needed. | 64 | can be used for a charm if needed. |
26 | 64 | 65 | ||
29 | 65 | :param warning: String to indicat where it has moved ot. | 66 | :param warning: String to indicate what is to be used instead. |
30 | 66 | :param date: optional sting, in YYYY-MM format to indicate when the | 67 | :param date: Optional string in YYYY-MM format to indicate when the |
31 | 67 | function will definitely (probably) be removed. | 68 | function will definitely (probably) be removed. |
33 | 68 | :param log: The log function to call to log. If not, logs to stdout | 69 | :param log: The log function to call in order to log. If None, logs to |
34 | 70 | stdout | ||
35 | 69 | """ | 71 | """ |
36 | 70 | def wrap(f): | 72 | def wrap(f): |
37 | 71 | 73 | ||
38 | 72 | 74 | ||
39 | === added directory 'charmhelpers/contrib/charmsupport' | |||
40 | === added file 'charmhelpers/contrib/charmsupport/__init__.py' | |||
41 | --- charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000 | |||
42 | +++ charmhelpers/contrib/charmsupport/__init__.py 2021-11-10 05:36:20 +0000 | |||
43 | @@ -0,0 +1,13 @@ | |||
44 | 1 | # Copyright 2014-2015 Canonical Limited. | ||
45 | 2 | # | ||
46 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
47 | 4 | # you may not use this file except in compliance with the License. | ||
48 | 5 | # You may obtain a copy of the License at | ||
49 | 6 | # | ||
50 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
51 | 8 | # | ||
52 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
53 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
54 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
55 | 12 | # See the License for the specific language governing permissions and | ||
56 | 13 | # limitations under the License. | ||
57 | 0 | 14 | ||
58 | === added file 'charmhelpers/contrib/charmsupport/nrpe.py' | |||
59 | --- charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000 | |||
60 | +++ charmhelpers/contrib/charmsupport/nrpe.py 2021-11-10 05:36:20 +0000 | |||
61 | @@ -0,0 +1,522 @@ | |||
62 | 1 | # Copyright 2012-2021 Canonical Limited. | ||
63 | 2 | # | ||
64 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
65 | 4 | # you may not use this file except in compliance with the License. | ||
66 | 5 | # You may obtain a copy of the License at | ||
67 | 6 | # | ||
68 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
69 | 8 | # | ||
70 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
71 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
72 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
73 | 12 | # See the License for the specific language governing permissions and | ||
74 | 13 | # limitations under the License. | ||
75 | 14 | |||
76 | 15 | """Compatibility with the nrpe-external-master charm""" | ||
77 | 16 | # | ||
78 | 17 | # Authors: | ||
79 | 18 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | ||
80 | 19 | |||
81 | 20 | import glob | ||
82 | 21 | import grp | ||
83 | 22 | import os | ||
84 | 23 | import pwd | ||
85 | 24 | import re | ||
86 | 25 | import shlex | ||
87 | 26 | import shutil | ||
88 | 27 | import subprocess | ||
89 | 28 | import yaml | ||
90 | 29 | |||
91 | 30 | from charmhelpers.core.hookenv import ( | ||
92 | 31 | config, | ||
93 | 32 | hook_name, | ||
94 | 33 | local_unit, | ||
95 | 34 | log, | ||
96 | 35 | relation_get, | ||
97 | 36 | relation_ids, | ||
98 | 37 | relation_set, | ||
99 | 38 | relations_of_type, | ||
100 | 39 | ) | ||
101 | 40 | |||
102 | 41 | from charmhelpers.core.host import service | ||
103 | 42 | from charmhelpers.core import host | ||
104 | 43 | |||
105 | 44 | # This module adds compatibility with the nrpe-external-master and plain nrpe | ||
106 | 45 | # subordinate charms. To use it in your charm: | ||
107 | 46 | # | ||
108 | 47 | # 1. Update metadata.yaml | ||
109 | 48 | # | ||
110 | 49 | # provides: | ||
111 | 50 | # (...) | ||
112 | 51 | # nrpe-external-master: | ||
113 | 52 | # interface: nrpe-external-master | ||
114 | 53 | # scope: container | ||
115 | 54 | # | ||
116 | 55 | # and/or | ||
117 | 56 | # | ||
118 | 57 | # provides: | ||
119 | 58 | # (...) | ||
120 | 59 | # local-monitors: | ||
121 | 60 | # interface: local-monitors | ||
122 | 61 | # scope: container | ||
123 | 62 | |||
124 | 63 | # | ||
125 | 64 | # 2. Add the following to config.yaml | ||
126 | 65 | # | ||
127 | 66 | # nagios_context: | ||
128 | 67 | # default: "juju" | ||
129 | 68 | # type: string | ||
130 | 69 | # description: | | ||
131 | 70 | # Used by the nrpe subordinate charms. | ||
132 | 71 | # A string that will be prepended to instance name to set the host name | ||
133 | 72 | # in nagios. So for instance the hostname would be something like: | ||
134 | 73 | # juju-myservice-0 | ||
135 | 74 | # If you're running multiple environments with the same services in them | ||
136 | 75 | # this allows you to differentiate between them. | ||
137 | 76 | # nagios_servicegroups: | ||
138 | 77 | # default: "" | ||
139 | 78 | # type: string | ||
140 | 79 | # description: | | ||
141 | 80 | # A comma-separated list of nagios servicegroups. | ||
142 | 81 | # If left empty, the nagios_context will be used as the servicegroup | ||
143 | 82 | # | ||
144 | 83 | # 3. Add custom checks (Nagios plugins) to files/nrpe-external-master | ||
145 | 84 | # | ||
146 | 85 | # 4. Update your hooks.py with something like this: | ||
147 | 86 | # | ||
148 | 87 | # from charmsupport.nrpe import NRPE | ||
149 | 88 | # (...) | ||
150 | 89 | # def update_nrpe_config(): | ||
151 | 90 | # nrpe_compat = NRPE() | ||
152 | 91 | # nrpe_compat.add_check( | ||
153 | 92 | # shortname = "myservice", | ||
154 | 93 | # description = "Check MyService", | ||
155 | 94 | # check_cmd = "check_http -w 2 -c 10 http://localhost" | ||
156 | 95 | # ) | ||
157 | 96 | # nrpe_compat.add_check( | ||
158 | 97 | # "myservice_other", | ||
159 | 98 | # "Check for widget failures", | ||
160 | 99 | # check_cmd = "/srv/myapp/scripts/widget_check" | ||
161 | 100 | # ) | ||
162 | 101 | # nrpe_compat.write() | ||
163 | 102 | # | ||
164 | 103 | # def config_changed(): | ||
165 | 104 | # (...) | ||
166 | 105 | # update_nrpe_config() | ||
167 | 106 | # | ||
168 | 107 | # def nrpe_external_master_relation_changed(): | ||
169 | 108 | # update_nrpe_config() | ||
170 | 109 | # | ||
171 | 110 | # def local_monitors_relation_changed(): | ||
172 | 111 | # update_nrpe_config() | ||
173 | 112 | # | ||
174 | 113 | # 4.a If your charm is a subordinate charm set primary=False | ||
175 | 114 | # | ||
176 | 115 | # from charmsupport.nrpe import NRPE | ||
177 | 116 | # (...) | ||
178 | 117 | # def update_nrpe_config(): | ||
179 | 118 | # nrpe_compat = NRPE(primary=False) | ||
180 | 119 | # | ||
181 | 120 | # 5. ln -s hooks.py nrpe-external-master-relation-changed | ||
182 | 121 | # ln -s hooks.py local-monitors-relation-changed | ||
183 | 122 | |||
184 | 123 | |||
185 | 124 | class CheckException(Exception): | ||
186 | 125 | pass | ||
187 | 126 | |||
188 | 127 | |||
189 | 128 | class Check(object): | ||
190 | 129 | shortname_re = '[A-Za-z0-9-_.@]+$' | ||
191 | 130 | service_template = (""" | ||
192 | 131 | #--------------------------------------------------- | ||
193 | 132 | # This file is Juju managed | ||
194 | 133 | #--------------------------------------------------- | ||
195 | 134 | define service {{ | ||
196 | 135 | use active-service | ||
197 | 136 | host_name {nagios_hostname} | ||
198 | 137 | service_description {nagios_hostname}[{shortname}] """ | ||
199 | 138 | """{description} | ||
200 | 139 | check_command check_nrpe!{command} | ||
201 | 140 | servicegroups {nagios_servicegroup} | ||
202 | 141 | {service_config_overrides} | ||
203 | 142 | }} | ||
204 | 143 | """) | ||
205 | 144 | |||
206 | 145 | def __init__(self, shortname, description, check_cmd, max_check_attempts=None): | ||
207 | 146 | super(Check, self).__init__() | ||
208 | 147 | # XXX: could be better to calculate this from the service name | ||
209 | 148 | if not re.match(self.shortname_re, shortname): | ||
210 | 149 | raise CheckException("shortname must match {}".format( | ||
211 | 150 | Check.shortname_re)) | ||
212 | 151 | self.shortname = shortname | ||
213 | 152 | self.command = "check_{}".format(shortname) | ||
214 | 153 | # Note: a set of invalid characters is defined by the | ||
215 | 154 | # Nagios server config | ||
216 | 155 | # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()= | ||
217 | 156 | self.description = description | ||
218 | 157 | self.check_cmd = self._locate_cmd(check_cmd) | ||
219 | 158 | self.max_check_attempts = max_check_attempts | ||
220 | 159 | |||
221 | 160 | def _get_check_filename(self): | ||
222 | 161 | return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command)) | ||
223 | 162 | |||
224 | 163 | def _get_service_filename(self, hostname): | ||
225 | 164 | return os.path.join(NRPE.nagios_exportdir, | ||
226 | 165 | 'service__{}_{}.cfg'.format(hostname, self.command)) | ||
227 | 166 | |||
228 | 167 | def _locate_cmd(self, check_cmd): | ||
229 | 168 | search_path = ( | ||
230 | 169 | '/usr/lib/nagios/plugins', | ||
231 | 170 | '/usr/local/lib/nagios/plugins', | ||
232 | 171 | ) | ||
233 | 172 | parts = shlex.split(check_cmd) | ||
234 | 173 | for path in search_path: | ||
235 | 174 | if os.path.exists(os.path.join(path, parts[0])): | ||
236 | 175 | command = os.path.join(path, parts[0]) | ||
237 | 176 | if len(parts) > 1: | ||
238 | 177 | command += " " + " ".join(parts[1:]) | ||
239 | 178 | return command | ||
240 | 179 | log('Check command not found: {}'.format(parts[0])) | ||
241 | 180 | return '' | ||
242 | 181 | |||
243 | 182 | def _remove_service_files(self): | ||
244 | 183 | if not os.path.exists(NRPE.nagios_exportdir): | ||
245 | 184 | return | ||
246 | 185 | for f in os.listdir(NRPE.nagios_exportdir): | ||
247 | 186 | if f.endswith('_{}.cfg'.format(self.command)): | ||
248 | 187 | os.remove(os.path.join(NRPE.nagios_exportdir, f)) | ||
249 | 188 | |||
250 | 189 | def remove(self, hostname): | ||
251 | 190 | nrpe_check_file = self._get_check_filename() | ||
252 | 191 | if os.path.exists(nrpe_check_file): | ||
253 | 192 | os.remove(nrpe_check_file) | ||
254 | 193 | self._remove_service_files() | ||
255 | 194 | |||
256 | 195 | def write(self, nagios_context, hostname, nagios_servicegroups): | ||
257 | 196 | nrpe_check_file = self._get_check_filename() | ||
258 | 197 | with open(nrpe_check_file, 'w') as nrpe_check_config: | ||
259 | 198 | nrpe_check_config.write("# check {}\n".format(self.shortname)) | ||
260 | 199 | if nagios_servicegroups: | ||
261 | 200 | nrpe_check_config.write( | ||
262 | 201 | "# The following header was added automatically by juju\n") | ||
263 | 202 | nrpe_check_config.write( | ||
264 | 203 | "# Modifying it will affect nagios monitoring and alerting\n") | ||
265 | 204 | nrpe_check_config.write( | ||
266 | 205 | "# servicegroups: {}\n".format(nagios_servicegroups)) | ||
267 | 206 | nrpe_check_config.write("command[{}]={}\n".format( | ||
268 | 207 | self.command, self.check_cmd)) | ||
269 | 208 | |||
270 | 209 | if not os.path.exists(NRPE.nagios_exportdir): | ||
271 | 210 | log('Not writing service config as {} is not accessible'.format( | ||
272 | 211 | NRPE.nagios_exportdir)) | ||
273 | 212 | else: | ||
274 | 213 | self.write_service_config(nagios_context, hostname, | ||
275 | 214 | nagios_servicegroups) | ||
276 | 215 | |||
277 | 216 | def write_service_config(self, nagios_context, hostname, | ||
278 | 217 | nagios_servicegroups): | ||
279 | 218 | self._remove_service_files() | ||
280 | 219 | |||
281 | 220 | if self.max_check_attempts: | ||
282 | 221 | service_config_overrides = ' max_check_attempts {}'.format( | ||
283 | 222 | self.max_check_attempts | ||
284 | 223 | ) # Note indentation is here rather than in the template to avoid trailing spaces | ||
285 | 224 | else: | ||
286 | 225 | service_config_overrides = '' # empty string to avoid printing 'None' | ||
287 | 226 | templ_vars = { | ||
288 | 227 | 'nagios_hostname': hostname, | ||
289 | 228 | 'nagios_servicegroup': nagios_servicegroups, | ||
290 | 229 | 'description': self.description, | ||
291 | 230 | 'shortname': self.shortname, | ||
292 | 231 | 'command': self.command, | ||
293 | 232 | 'service_config_overrides': service_config_overrides, | ||
294 | 233 | } | ||
295 | 234 | nrpe_service_text = Check.service_template.format(**templ_vars) | ||
296 | 235 | nrpe_service_file = self._get_service_filename(hostname) | ||
297 | 236 | with open(nrpe_service_file, 'w') as nrpe_service_config: | ||
298 | 237 | nrpe_service_config.write(str(nrpe_service_text)) | ||
299 | 238 | |||
300 | 239 | def run(self): | ||
301 | 240 | subprocess.call(self.check_cmd) | ||
302 | 241 | |||
303 | 242 | |||
304 | 243 | class NRPE(object): | ||
305 | 244 | nagios_logdir = '/var/log/nagios' | ||
306 | 245 | nagios_exportdir = '/var/lib/nagios/export' | ||
307 | 246 | nrpe_confdir = '/etc/nagios/nrpe.d' | ||
308 | 247 | homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server | ||
309 | 248 | |||
310 | 249 | def __init__(self, hostname=None, primary=True): | ||
311 | 250 | super(NRPE, self).__init__() | ||
312 | 251 | self.config = config() | ||
313 | 252 | self.primary = primary | ||
314 | 253 | self.nagios_context = self.config['nagios_context'] | ||
315 | 254 | if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']: | ||
316 | 255 | self.nagios_servicegroups = self.config['nagios_servicegroups'] | ||
317 | 256 | else: | ||
318 | 257 | self.nagios_servicegroups = self.nagios_context | ||
319 | 258 | self.unit_name = local_unit().replace('/', '-') | ||
320 | 259 | if hostname: | ||
321 | 260 | self.hostname = hostname | ||
322 | 261 | else: | ||
323 | 262 | nagios_hostname = get_nagios_hostname() | ||
324 | 263 | if nagios_hostname: | ||
325 | 264 | self.hostname = nagios_hostname | ||
326 | 265 | else: | ||
327 | 266 | self.hostname = "{}-{}".format(self.nagios_context, self.unit_name) | ||
328 | 267 | self.checks = [] | ||
329 | 268 | # Iff in an nrpe-external-master relation hook, set primary status | ||
330 | 269 | relation = relation_ids('nrpe-external-master') | ||
331 | 270 | if relation: | ||
332 | 271 | log("Setting charm primary status {}".format(primary)) | ||
333 | 272 | for rid in relation: | ||
334 | 273 | relation_set(relation_id=rid, relation_settings={'primary': self.primary}) | ||
335 | 274 | self.remove_check_queue = set() | ||
336 | 275 | |||
337 | 276 | @classmethod | ||
338 | 277 | def does_nrpe_conf_dir_exist(cls): | ||
339 | 278 | """Return True if th nrpe_confdif directory exists.""" | ||
340 | 279 | return os.path.isdir(cls.nrpe_confdir) | ||
341 | 280 | |||
342 | 281 | def add_check(self, *args, **kwargs): | ||
343 | 282 | shortname = None | ||
344 | 283 | if kwargs.get('shortname') is None: | ||
345 | 284 | if len(args) > 0: | ||
346 | 285 | shortname = args[0] | ||
347 | 286 | else: | ||
348 | 287 | shortname = kwargs['shortname'] | ||
349 | 288 | |||
350 | 289 | self.checks.append(Check(*args, **kwargs)) | ||
351 | 290 | try: | ||
352 | 291 | self.remove_check_queue.remove(shortname) | ||
353 | 292 | except KeyError: | ||
354 | 293 | pass | ||
355 | 294 | |||
356 | 295 | def remove_check(self, *args, **kwargs): | ||
357 | 296 | if kwargs.get('shortname') is None: | ||
358 | 297 | raise ValueError('shortname of check must be specified') | ||
359 | 298 | |||
360 | 299 | # Use sensible defaults if they're not specified - these are not | ||
361 | 300 | # actually used during removal, but they're required for constructing | ||
362 | 301 | # the Check object; check_disk is chosen because it's part of the | ||
363 | 302 | # nagios-plugins-basic package. | ||
364 | 303 | if kwargs.get('check_cmd') is None: | ||
365 | 304 | kwargs['check_cmd'] = 'check_disk' | ||
366 | 305 | if kwargs.get('description') is None: | ||
367 | 306 | kwargs['description'] = '' | ||
368 | 307 | |||
369 | 308 | check = Check(*args, **kwargs) | ||
370 | 309 | check.remove(self.hostname) | ||
371 | 310 | self.remove_check_queue.add(kwargs['shortname']) | ||
372 | 311 | |||
373 | 312 | def write(self): | ||
374 | 313 | try: | ||
375 | 314 | nagios_uid = pwd.getpwnam('nagios').pw_uid | ||
376 | 315 | nagios_gid = grp.getgrnam('nagios').gr_gid | ||
377 | 316 | except Exception: | ||
378 | 317 | log("Nagios user not set up, nrpe checks not updated") | ||
379 | 318 | return | ||
380 | 319 | |||
381 | 320 | if not os.path.exists(NRPE.nagios_logdir): | ||
382 | 321 | os.mkdir(NRPE.nagios_logdir) | ||
383 | 322 | os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid) | ||
384 | 323 | |||
385 | 324 | nrpe_monitors = {} | ||
386 | 325 | monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}} | ||
387 | 326 | |||
388 | 327 | # check that the charm can write to the conf dir. If not, then nagios | ||
389 | 328 | # probably isn't installed, and we can defer. | ||
390 | 329 | if not self.does_nrpe_conf_dir_exist(): | ||
391 | 330 | return | ||
392 | 331 | |||
393 | 332 | for nrpecheck in self.checks: | ||
394 | 333 | nrpecheck.write(self.nagios_context, self.hostname, | ||
395 | 334 | self.nagios_servicegroups) | ||
396 | 335 | nrpe_monitors[nrpecheck.shortname] = { | ||
397 | 336 | "command": nrpecheck.command, | ||
398 | 337 | } | ||
399 | 338 | # If we were passed max_check_attempts, add that to the relation data | ||
400 | 339 | if nrpecheck.max_check_attempts is not None: | ||
401 | 340 | nrpe_monitors[nrpecheck.shortname]['max_check_attempts'] = nrpecheck.max_check_attempts | ||
402 | 341 | |||
403 | 342 | # update-status hooks are configured to firing every 5 minutes by | ||
404 | 343 | # default. When nagios-nrpe-server is restarted, the nagios server | ||
405 | 344 | # reports checks failing causing unnecessary alerts. Let's not restart | ||
406 | 345 | # on update-status hooks. | ||
407 | 346 | if not hook_name() == 'update-status': | ||
408 | 347 | service('restart', 'nagios-nrpe-server') | ||
409 | 348 | |||
410 | 349 | monitor_ids = relation_ids("local-monitors") + \ | ||
411 | 350 | relation_ids("nrpe-external-master") | ||
412 | 351 | for rid in monitor_ids: | ||
413 | 352 | reldata = relation_get(unit=local_unit(), rid=rid) | ||
414 | 353 | if 'monitors' in reldata: | ||
415 | 354 | # update the existing set of monitors with the new data | ||
416 | 355 | old_monitors = yaml.safe_load(reldata['monitors']) | ||
417 | 356 | old_nrpe_monitors = old_monitors['monitors']['remote']['nrpe'] | ||
418 | 357 | # remove keys that are in the remove_check_queue | ||
419 | 358 | old_nrpe_monitors = {k: v for k, v in old_nrpe_monitors.items() | ||
420 | 359 | if k not in self.remove_check_queue} | ||
421 | 360 | # update/add nrpe_monitors | ||
422 | 361 | old_nrpe_monitors.update(nrpe_monitors) | ||
423 | 362 | old_monitors['monitors']['remote']['nrpe'] = old_nrpe_monitors | ||
424 | 363 | # write back to the relation | ||
425 | 364 | relation_set(relation_id=rid, monitors=yaml.dump(old_monitors)) | ||
426 | 365 | else: | ||
427 | 366 | # write a brand new set of monitors, as no existing ones. | ||
428 | 367 | relation_set(relation_id=rid, monitors=yaml.dump(monitors)) | ||
429 | 368 | |||
430 | 369 | self.remove_check_queue.clear() | ||
431 | 370 | |||
432 | 371 | |||
433 | 372 | def get_nagios_hostcontext(relation_name='nrpe-external-master'): | ||
434 | 373 | """ | ||
435 | 374 | Query relation with nrpe subordinate, return the nagios_host_context | ||
436 | 375 | |||
437 | 376 | :param str relation_name: Name of relation nrpe sub joined to | ||
438 | 377 | """ | ||
439 | 378 | for rel in relations_of_type(relation_name): | ||
440 | 379 | if 'nagios_host_context' in rel: | ||
441 | 380 | return rel['nagios_host_context'] | ||
442 | 381 | |||
443 | 382 | |||
444 | 383 | def get_nagios_hostname(relation_name='nrpe-external-master'): | ||
445 | 384 | """ | ||
446 | 385 | Query relation with nrpe subordinate, return the nagios_hostname | ||
447 | 386 | |||
448 | 387 | :param str relation_name: Name of relation nrpe sub joined to | ||
449 | 388 | """ | ||
450 | 389 | for rel in relations_of_type(relation_name): | ||
451 | 390 | if 'nagios_hostname' in rel: | ||
452 | 391 | return rel['nagios_hostname'] | ||
453 | 392 | |||
454 | 393 | |||
455 | 394 | def get_nagios_unit_name(relation_name='nrpe-external-master'): | ||
456 | 395 | """ | ||
457 | 396 | Return the nagios unit name prepended with host_context if needed | ||
458 | 397 | |||
459 | 398 | :param str relation_name: Name of relation nrpe sub joined to | ||
460 | 399 | """ | ||
461 | 400 | host_context = get_nagios_hostcontext(relation_name) | ||
462 | 401 | if host_context: | ||
463 | 402 | unit = "%s:%s" % (host_context, local_unit()) | ||
464 | 403 | else: | ||
465 | 404 | unit = local_unit() | ||
466 | 405 | return unit | ||
467 | 406 | |||
468 | 407 | |||
469 | 408 | def add_init_service_checks(nrpe, services, unit_name, immediate_check=True): | ||
470 | 409 | """ | ||
471 | 410 | Add checks for each service in list | ||
472 | 411 | |||
473 | 412 | :param NRPE nrpe: NRPE object to add check to | ||
474 | 413 | :param list services: List of services to check | ||
475 | 414 | :param str unit_name: Unit name to use in check description | ||
476 | 415 | :param bool immediate_check: For sysv init, run the service check immediately | ||
477 | 416 | """ | ||
478 | 417 | for svc in services: | ||
479 | 418 | # Don't add a check for these services from neutron-gateway | ||
480 | 419 | if svc in ['ext-port', 'os-charm-phy-nic-mtu']: | ||
481 | 420 | next | ||
482 | 421 | |||
483 | 422 | upstart_init = '/etc/init/%s.conf' % svc | ||
484 | 423 | sysv_init = '/etc/init.d/%s' % svc | ||
485 | 424 | |||
486 | 425 | if host.init_is_systemd(service_name=svc): | ||
487 | 426 | nrpe.add_check( | ||
488 | 427 | shortname=svc, | ||
489 | 428 | description='process check {%s}' % unit_name, | ||
490 | 429 | check_cmd='check_systemd.py %s' % svc | ||
491 | 430 | ) | ||
492 | 431 | elif os.path.exists(upstart_init): | ||
493 | 432 | nrpe.add_check( | ||
494 | 433 | shortname=svc, | ||
495 | 434 | description='process check {%s}' % unit_name, | ||
496 | 435 | check_cmd='check_upstart_job %s' % svc | ||
497 | 436 | ) | ||
498 | 437 | elif os.path.exists(sysv_init): | ||
499 | 438 | cronpath = '/etc/cron.d/nagios-service-check-%s' % svc | ||
500 | 439 | checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc) | ||
501 | 440 | croncmd = ( | ||
502 | 441 | '/usr/local/lib/nagios/plugins/check_exit_status.pl ' | ||
503 | 442 | '-e -s /etc/init.d/%s status' % svc | ||
504 | 443 | ) | ||
505 | 444 | cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath) | ||
506 | 445 | f = open(cronpath, 'w') | ||
507 | 446 | f.write(cron_file) | ||
508 | 447 | f.close() | ||
509 | 448 | nrpe.add_check( | ||
510 | 449 | shortname=svc, | ||
511 | 450 | description='service check {%s}' % unit_name, | ||
512 | 451 | check_cmd='check_status_file.py -f %s' % checkpath, | ||
513 | 452 | ) | ||
514 | 453 | # if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail | ||
515 | 454 | # (LP: #1670223). | ||
516 | 455 | if immediate_check and os.path.isdir(nrpe.homedir): | ||
517 | 456 | f = open(checkpath, 'w') | ||
518 | 457 | subprocess.call( | ||
519 | 458 | croncmd.split(), | ||
520 | 459 | stdout=f, | ||
521 | 460 | stderr=subprocess.STDOUT | ||
522 | 461 | ) | ||
523 | 462 | f.close() | ||
524 | 463 | os.chmod(checkpath, 0o644) | ||
525 | 464 | |||
526 | 465 | |||
527 | 466 | def copy_nrpe_checks(nrpe_files_dir=None): | ||
528 | 467 | """ | ||
529 | 468 | Copy the nrpe checks into place | ||
530 | 469 | |||
531 | 470 | """ | ||
532 | 471 | NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins' | ||
533 | 472 | if nrpe_files_dir is None: | ||
534 | 473 | # determine if "charmhelpers" is in CHARMDIR or CHARMDIR/hooks | ||
535 | 474 | for segment in ['.', 'hooks']: | ||
536 | 475 | nrpe_files_dir = os.path.abspath(os.path.join( | ||
537 | 476 | os.getenv('CHARM_DIR'), | ||
538 | 477 | segment, | ||
539 | 478 | 'charmhelpers', | ||
540 | 479 | 'contrib', | ||
541 | 480 | 'openstack', | ||
542 | 481 | 'files')) | ||
543 | 482 | if os.path.isdir(nrpe_files_dir): | ||
544 | 483 | break | ||
545 | 484 | else: | ||
546 | 485 | raise RuntimeError("Couldn't find charmhelpers directory") | ||
547 | 486 | if not os.path.exists(NAGIOS_PLUGINS): | ||
548 | 487 | os.makedirs(NAGIOS_PLUGINS) | ||
549 | 488 | for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")): | ||
550 | 489 | if os.path.isfile(fname): | ||
551 | 490 | shutil.copy2(fname, | ||
552 | 491 | os.path.join(NAGIOS_PLUGINS, os.path.basename(fname))) | ||
553 | 492 | |||
554 | 493 | |||
555 | 494 | def add_haproxy_checks(nrpe, unit_name): | ||
556 | 495 | """ | ||
557 | 496 | Add checks for each service in list | ||
558 | 497 | |||
559 | 498 | :param NRPE nrpe: NRPE object to add check to | ||
560 | 499 | :param str unit_name: Unit name to use in check description | ||
561 | 500 | """ | ||
562 | 501 | nrpe.add_check( | ||
563 | 502 | shortname='haproxy_servers', | ||
564 | 503 | description='Check HAProxy {%s}' % unit_name, | ||
565 | 504 | check_cmd='check_haproxy.sh') | ||
566 | 505 | nrpe.add_check( | ||
567 | 506 | shortname='haproxy_queue', | ||
568 | 507 | description='Check HAProxy queue depth {%s}' % unit_name, | ||
569 | 508 | check_cmd='check_haproxy_queue_depth.sh') | ||
570 | 509 | |||
571 | 510 | |||
572 | 511 | def remove_deprecated_check(nrpe, deprecated_services): | ||
573 | 512 | """ | ||
574 | 513 | Remove checks for deprecated services in list | ||
575 | 514 | |||
576 | 515 | :param nrpe: NRPE object to remove check from | ||
577 | 516 | :type nrpe: NRPE | ||
578 | 517 | :param deprecated_services: List of deprecated services that are removed | ||
579 | 518 | :type deprecated_services: list | ||
580 | 519 | """ | ||
581 | 520 | for dep_svc in deprecated_services: | ||
582 | 521 | log('Deprecated service: {}'.format(dep_svc)) | ||
583 | 522 | nrpe.remove_check(shortname=dep_svc) | ||
584 | 0 | 523 | ||
585 | === modified file 'charmhelpers/contrib/hahelpers/apache.py' | |||
586 | --- charmhelpers/contrib/hahelpers/apache.py 2019-05-24 12:41:48 +0000 | |||
587 | +++ charmhelpers/contrib/hahelpers/apache.py 2021-11-10 05:36:20 +0000 | |||
588 | @@ -34,6 +34,10 @@ | |||
589 | 34 | INFO, | 34 | INFO, |
590 | 35 | ) | 35 | ) |
591 | 36 | 36 | ||
592 | 37 | # This file contains the CA cert from the charms ssl_ca configuration | ||
593 | 38 | # option, in future the file name should be updated reflect that. | ||
594 | 39 | CONFIG_CA_CERT_FILE = 'keystone_juju_ca_cert' | ||
595 | 40 | |||
596 | 37 | 41 | ||
597 | 38 | def get_cert(cn=None): | 42 | def get_cert(cn=None): |
598 | 39 | # TODO: deal with multiple https endpoints via charm config | 43 | # TODO: deal with multiple https endpoints via charm config |
599 | @@ -83,4 +87,4 @@ | |||
600 | 83 | 87 | ||
601 | 84 | 88 | ||
602 | 85 | def install_ca_cert(ca_cert): | 89 | def install_ca_cert(ca_cert): |
604 | 86 | host.install_ca_cert(ca_cert, 'keystone_juju_ca_cert') | 90 | host.install_ca_cert(ca_cert, CONFIG_CA_CERT_FILE) |
605 | 87 | 91 | ||
606 | === modified file 'charmhelpers/contrib/hahelpers/cluster.py' | |||
607 | --- charmhelpers/contrib/hahelpers/cluster.py 2019-05-24 12:41:48 +0000 | |||
608 | +++ charmhelpers/contrib/hahelpers/cluster.py 2021-11-10 05:36:20 +0000 | |||
609 | @@ -1,4 +1,4 @@ | |||
611 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2021 Canonical Limited. |
612 | 2 | # | 2 | # |
613 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
614 | 4 | # you may not use this file except in compliance with the License. | 4 | # you may not use this file except in compliance with the License. |
615 | @@ -25,6 +25,7 @@ | |||
616 | 25 | clustering-related helpers. | 25 | clustering-related helpers. |
617 | 26 | """ | 26 | """ |
618 | 27 | 27 | ||
619 | 28 | import functools | ||
620 | 28 | import subprocess | 29 | import subprocess |
621 | 29 | import os | 30 | import os |
622 | 30 | import time | 31 | import time |
623 | @@ -85,7 +86,7 @@ | |||
624 | 85 | 2. If the charm is part of a corosync cluster, call corosync to | 86 | 2. If the charm is part of a corosync cluster, call corosync to |
625 | 86 | determine leadership. | 87 | determine leadership. |
626 | 87 | 3. If the charm is not part of a corosync cluster, the leader is | 88 | 3. If the charm is not part of a corosync cluster, the leader is |
628 | 88 | determined as being "the alive unit with the lowest unit numer". In | 89 | determined as being "the alive unit with the lowest unit number". In |
629 | 89 | other words, the oldest surviving unit. | 90 | other words, the oldest surviving unit. |
630 | 90 | """ | 91 | """ |
631 | 91 | try: | 92 | try: |
632 | @@ -281,6 +282,10 @@ | |||
633 | 281 | return public_port - (i * 10) | 282 | return public_port - (i * 10) |
634 | 282 | 283 | ||
635 | 283 | 284 | ||
636 | 285 | determine_apache_port_single = functools.partial( | ||
637 | 286 | determine_apache_port, singlenode_mode=True) | ||
638 | 287 | |||
639 | 288 | |||
640 | 284 | def get_hacluster_config(exclude_keys=None): | 289 | def get_hacluster_config(exclude_keys=None): |
641 | 285 | ''' | 290 | ''' |
642 | 286 | Obtains all relevant configuration from charm configuration required | 291 | Obtains all relevant configuration from charm configuration required |
643 | @@ -404,3 +409,43 @@ | |||
644 | 404 | log(msg, DEBUG) | 409 | log(msg, DEBUG) |
645 | 405 | status_set('maintenance', msg) | 410 | status_set('maintenance', msg) |
646 | 406 | time.sleep(calculated_wait) | 411 | time.sleep(calculated_wait) |
647 | 412 | |||
648 | 413 | |||
649 | 414 | def get_managed_services_and_ports(services, external_ports, | ||
650 | 415 | external_services=None, | ||
651 | 416 | port_conv_f=determine_apache_port_single): | ||
652 | 417 | """Get the services and ports managed by this charm. | ||
653 | 418 | |||
654 | 419 | Return only the services and corresponding ports that are managed by this | ||
655 | 420 | charm. This excludes haproxy when there is a relation with hacluster. This | ||
656 | 421 | is because this charm passes responsibility for stopping and starting | ||
657 | 422 | haproxy to hacluster. | ||
658 | 423 | |||
659 | 424 | Similarly, if a relation with hacluster exists then the ports returned by | ||
660 | 425 | this method correspond to those managed by the apache server rather than | ||
661 | 426 | haproxy. | ||
662 | 427 | |||
663 | 428 | :param services: List of services. | ||
664 | 429 | :type services: List[str] | ||
665 | 430 | :param external_ports: List of ports managed by external services. | ||
666 | 431 | :type external_ports: List[int] | ||
667 | 432 | :param external_services: List of services to be removed if ha relation is | ||
668 | 433 | present. | ||
669 | 434 | :type external_services: List[str] | ||
670 | 435 | :param port_conv_f: Function to apply to ports to calculate the ports | ||
671 | 436 | managed by services controlled by this charm. | ||
672 | 437 | :type port_convert_func: f() | ||
673 | 438 | :returns: A tuple containing a list of services first followed by a list of | ||
674 | 439 | ports. | ||
675 | 440 | :rtype: Tuple[List[str], List[int]] | ||
676 | 441 | """ | ||
677 | 442 | if external_services is None: | ||
678 | 443 | external_services = ['haproxy'] | ||
679 | 444 | if relation_ids('ha'): | ||
680 | 445 | for svc in external_services: | ||
681 | 446 | try: | ||
682 | 447 | services.remove(svc) | ||
683 | 448 | except ValueError: | ||
684 | 449 | pass | ||
685 | 450 | external_ports = [port_conv_f(p) for p in external_ports] | ||
686 | 451 | return services, external_ports | ||
687 | 407 | 452 | ||
688 | === modified file 'charmhelpers/core/decorators.py' | |||
689 | --- charmhelpers/core/decorators.py 2017-03-03 21:03:14 +0000 | |||
690 | +++ charmhelpers/core/decorators.py 2021-11-10 05:36:20 +0000 | |||
691 | @@ -53,3 +53,41 @@ | |||
692 | 53 | return _retry_on_exception_inner_2 | 53 | return _retry_on_exception_inner_2 |
693 | 54 | 54 | ||
694 | 55 | return _retry_on_exception_inner_1 | 55 | return _retry_on_exception_inner_1 |
695 | 56 | |||
696 | 57 | |||
697 | 58 | def retry_on_predicate(num_retries, predicate_fun, base_delay=0): | ||
698 | 59 | """Retry based on return value | ||
699 | 60 | |||
700 | 61 | The return value of the decorated function is passed to the given predicate_fun. If the | ||
701 | 62 | result of the predicate is False, retry the decorated function up to num_retries times | ||
702 | 63 | |||
703 | 64 | An exponential backoff up to base_delay^num_retries seconds can be introduced by setting | ||
704 | 65 | base_delay to a nonzero value. The default is to run with a zero (i.e. no) delay | ||
705 | 66 | |||
706 | 67 | :param num_retries: Max. number of retries to perform | ||
707 | 68 | :type num_retries: int | ||
708 | 69 | :param predicate_fun: Predicate function to determine if a retry is necessary | ||
709 | 70 | :type predicate_fun: callable | ||
710 | 71 | :param base_delay: Starting value in seconds for exponential delay, defaults to 0 (no delay) | ||
711 | 72 | :type base_delay: float | ||
712 | 73 | """ | ||
713 | 74 | def _retry_on_pred_inner_1(f): | ||
714 | 75 | def _retry_on_pred_inner_2(*args, **kwargs): | ||
715 | 76 | retries = num_retries | ||
716 | 77 | multiplier = 1 | ||
717 | 78 | delay = base_delay | ||
718 | 79 | while True: | ||
719 | 80 | result = f(*args, **kwargs) | ||
720 | 81 | if predicate_fun(result) or retries <= 0: | ||
721 | 82 | return result | ||
722 | 83 | delay *= multiplier | ||
723 | 84 | multiplier += 1 | ||
724 | 85 | log("Result {}, retrying '{}' {} more times (delay={})".format( | ||
725 | 86 | result, f.__name__, retries, delay), level=INFO) | ||
726 | 87 | retries -= 1 | ||
727 | 88 | if delay: | ||
728 | 89 | time.sleep(delay) | ||
729 | 90 | |||
730 | 91 | return _retry_on_pred_inner_2 | ||
731 | 92 | |||
732 | 93 | return _retry_on_pred_inner_1 | ||
733 | 56 | 94 | ||
734 | === modified file 'charmhelpers/core/hookenv.py' | |||
735 | --- charmhelpers/core/hookenv.py 2019-05-24 12:41:48 +0000 | |||
736 | +++ charmhelpers/core/hookenv.py 2021-11-10 05:36:20 +0000 | |||
737 | @@ -1,4 +1,4 @@ | |||
739 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2013-2021 Canonical Limited. |
740 | 2 | # | 2 | # |
741 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
742 | 4 | # you may not use this file except in compliance with the License. | 4 | # you may not use this file except in compliance with the License. |
743 | @@ -13,7 +13,6 @@ | |||
744 | 13 | # limitations under the License. | 13 | # limitations under the License. |
745 | 14 | 14 | ||
746 | 15 | "Interactions with the Juju environment" | 15 | "Interactions with the Juju environment" |
747 | 16 | # Copyright 2013 Canonical Ltd. | ||
748 | 17 | # | 16 | # |
749 | 18 | # Authors: | 17 | # Authors: |
750 | 19 | # Charm Helpers Developers <juju@lists.ubuntu.com> | 18 | # Charm Helpers Developers <juju@lists.ubuntu.com> |
751 | @@ -21,6 +20,7 @@ | |||
752 | 21 | from __future__ import print_function | 20 | from __future__ import print_function |
753 | 22 | import copy | 21 | import copy |
754 | 23 | from distutils.version import LooseVersion | 22 | from distutils.version import LooseVersion |
755 | 23 | from enum import Enum | ||
756 | 24 | from functools import wraps | 24 | from functools import wraps |
757 | 25 | from collections import namedtuple | 25 | from collections import namedtuple |
758 | 26 | import glob | 26 | import glob |
759 | @@ -34,6 +34,8 @@ | |||
760 | 34 | import tempfile | 34 | import tempfile |
761 | 35 | from subprocess import CalledProcessError | 35 | from subprocess import CalledProcessError |
762 | 36 | 36 | ||
763 | 37 | from charmhelpers import deprecate | ||
764 | 38 | |||
765 | 37 | import six | 39 | import six |
766 | 38 | if not six.PY3: | 40 | if not six.PY3: |
767 | 39 | from UserDict import UserDict | 41 | from UserDict import UserDict |
768 | @@ -55,6 +57,14 @@ | |||
769 | 55 | 'This may not be compatible with software you are ' | 57 | 'This may not be compatible with software you are ' |
770 | 56 | 'running in your shell.') | 58 | 'running in your shell.') |
771 | 57 | 59 | ||
772 | 60 | |||
773 | 61 | class WORKLOAD_STATES(Enum): | ||
774 | 62 | ACTIVE = 'active' | ||
775 | 63 | BLOCKED = 'blocked' | ||
776 | 64 | MAINTENANCE = 'maintenance' | ||
777 | 65 | WAITING = 'waiting' | ||
778 | 66 | |||
779 | 67 | |||
780 | 58 | cache = {} | 68 | cache = {} |
781 | 59 | 69 | ||
782 | 60 | 70 | ||
783 | @@ -119,6 +129,24 @@ | |||
784 | 119 | raise | 129 | raise |
785 | 120 | 130 | ||
786 | 121 | 131 | ||
787 | 132 | def function_log(message): | ||
788 | 133 | """Write a function progress message""" | ||
789 | 134 | command = ['function-log'] | ||
790 | 135 | if not isinstance(message, six.string_types): | ||
791 | 136 | message = repr(message) | ||
792 | 137 | command += [message[:SH_MAX_ARG]] | ||
793 | 138 | # Missing function-log should not cause failures in unit tests | ||
794 | 139 | # Send function_log output to stderr | ||
795 | 140 | try: | ||
796 | 141 | subprocess.call(command) | ||
797 | 142 | except OSError as e: | ||
798 | 143 | if e.errno == errno.ENOENT: | ||
799 | 144 | message = "function-log: {}".format(message) | ||
800 | 145 | print(message, file=sys.stderr) | ||
801 | 146 | else: | ||
802 | 147 | raise | ||
803 | 148 | |||
804 | 149 | |||
805 | 122 | class Serializable(UserDict): | 150 | class Serializable(UserDict): |
806 | 123 | """Wrapper, an object that can be serialized to yaml or json""" | 151 | """Wrapper, an object that can be serialized to yaml or json""" |
807 | 124 | 152 | ||
808 | @@ -197,6 +225,17 @@ | |||
809 | 197 | raise ValueError('Must specify neither or both of relation_name and service_or_unit') | 225 | raise ValueError('Must specify neither or both of relation_name and service_or_unit') |
810 | 198 | 226 | ||
811 | 199 | 227 | ||
812 | 228 | def departing_unit(): | ||
813 | 229 | """The departing unit for the current relation hook. | ||
814 | 230 | |||
815 | 231 | Available since juju 2.8. | ||
816 | 232 | |||
817 | 233 | :returns: the departing unit, or None if the information isn't available. | ||
818 | 234 | :rtype: Optional[str] | ||
819 | 235 | """ | ||
820 | 236 | return os.environ.get('JUJU_DEPARTING_UNIT', None) | ||
821 | 237 | |||
822 | 238 | |||
823 | 200 | def local_unit(): | 239 | def local_unit(): |
824 | 201 | """Local unit ID""" | 240 | """Local unit ID""" |
825 | 202 | return os.environ['JUJU_UNIT_NAME'] | 241 | return os.environ['JUJU_UNIT_NAME'] |
826 | @@ -343,8 +382,10 @@ | |||
827 | 343 | try: | 382 | try: |
828 | 344 | self._prev_dict = json.load(f) | 383 | self._prev_dict = json.load(f) |
829 | 345 | except ValueError as e: | 384 | except ValueError as e: |
832 | 346 | log('Unable to parse previous config data - {}'.format(str(e)), | 385 | log('Found but was unable to parse previous config data, ' |
833 | 347 | level=ERROR) | 386 | 'ignoring which will report all values as changed - {}' |
834 | 387 | .format(str(e)), level=ERROR) | ||
835 | 388 | return | ||
836 | 348 | for k, v in copy.deepcopy(self._prev_dict).items(): | 389 | for k, v in copy.deepcopy(self._prev_dict).items(): |
837 | 349 | if k not in self: | 390 | if k not in self: |
838 | 350 | self[k] = v | 391 | self[k] = v |
839 | @@ -426,15 +467,20 @@ | |||
840 | 426 | 467 | ||
841 | 427 | 468 | ||
842 | 428 | @cached | 469 | @cached |
844 | 429 | def relation_get(attribute=None, unit=None, rid=None): | 470 | def relation_get(attribute=None, unit=None, rid=None, app=None): |
845 | 430 | """Get relation information""" | 471 | """Get relation information""" |
846 | 431 | _args = ['relation-get', '--format=json'] | 472 | _args = ['relation-get', '--format=json'] |
847 | 473 | if app is not None: | ||
848 | 474 | if unit is not None: | ||
849 | 475 | raise ValueError("Cannot use both 'unit' and 'app'") | ||
850 | 476 | _args.append('--app') | ||
851 | 432 | if rid: | 477 | if rid: |
852 | 433 | _args.append('-r') | 478 | _args.append('-r') |
853 | 434 | _args.append(rid) | 479 | _args.append(rid) |
854 | 435 | _args.append(attribute or '-') | 480 | _args.append(attribute or '-') |
857 | 436 | if unit: | 481 | # unit or application name |
858 | 437 | _args.append(unit) | 482 | if unit or app: |
859 | 483 | _args.append(unit or app) | ||
860 | 438 | try: | 484 | try: |
861 | 439 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) | 485 | return json.loads(subprocess.check_output(_args).decode('UTF-8')) |
862 | 440 | except ValueError: | 486 | except ValueError: |
863 | @@ -445,12 +491,14 @@ | |||
864 | 445 | raise | 491 | raise |
865 | 446 | 492 | ||
866 | 447 | 493 | ||
868 | 448 | def relation_set(relation_id=None, relation_settings=None, **kwargs): | 494 | def relation_set(relation_id=None, relation_settings=None, app=False, **kwargs): |
869 | 449 | """Set relation information for the current unit""" | 495 | """Set relation information for the current unit""" |
870 | 450 | relation_settings = relation_settings if relation_settings else {} | 496 | relation_settings = relation_settings if relation_settings else {} |
871 | 451 | relation_cmd_line = ['relation-set'] | 497 | relation_cmd_line = ['relation-set'] |
872 | 452 | accepts_file = "--file" in subprocess.check_output( | 498 | accepts_file = "--file" in subprocess.check_output( |
873 | 453 | relation_cmd_line + ["--help"], universal_newlines=True) | 499 | relation_cmd_line + ["--help"], universal_newlines=True) |
874 | 500 | if app: | ||
875 | 501 | relation_cmd_line.append('--app') | ||
876 | 454 | if relation_id is not None: | 502 | if relation_id is not None: |
877 | 455 | relation_cmd_line.extend(('-r', relation_id)) | 503 | relation_cmd_line.extend(('-r', relation_id)) |
878 | 456 | settings = relation_settings.copy() | 504 | settings = relation_settings.copy() |
879 | @@ -561,7 +609,7 @@ | |||
880 | 561 | relation_type())) | 609 | relation_type())) |
881 | 562 | 610 | ||
882 | 563 | :param reltype: Relation type to list data for, default is to list data for | 611 | :param reltype: Relation type to list data for, default is to list data for |
884 | 564 | the realtion type we are currently executing a hook for. | 612 | the relation type we are currently executing a hook for. |
885 | 565 | :type reltype: str | 613 | :type reltype: str |
886 | 566 | :returns: iterator | 614 | :returns: iterator |
887 | 567 | :rtype: types.GeneratorType | 615 | :rtype: types.GeneratorType |
888 | @@ -578,7 +626,7 @@ | |||
889 | 578 | 626 | ||
890 | 579 | @cached | 627 | @cached |
891 | 580 | def relation_for_unit(unit=None, rid=None): | 628 | def relation_for_unit(unit=None, rid=None): |
893 | 581 | """Get the json represenation of a unit's relation""" | 629 | """Get the json representation of a unit's relation""" |
894 | 582 | unit = unit or remote_unit() | 630 | unit = unit or remote_unit() |
895 | 583 | relation = relation_get(unit=unit, rid=rid) | 631 | relation = relation_get(unit=unit, rid=rid) |
896 | 584 | for key in relation: | 632 | for key in relation: |
897 | @@ -946,9 +994,23 @@ | |||
898 | 946 | return os.environ.get('CHARM_DIR') | 994 | return os.environ.get('CHARM_DIR') |
899 | 947 | 995 | ||
900 | 948 | 996 | ||
901 | 997 | def cmd_exists(cmd): | ||
902 | 998 | """Return True if the specified cmd exists in the path""" | ||
903 | 999 | return any( | ||
904 | 1000 | os.access(os.path.join(path, cmd), os.X_OK) | ||
905 | 1001 | for path in os.environ["PATH"].split(os.pathsep) | ||
906 | 1002 | ) | ||
907 | 1003 | |||
908 | 1004 | |||
909 | 949 | @cached | 1005 | @cached |
910 | 1006 | @deprecate("moved to function_get()", log=log) | ||
911 | 950 | def action_get(key=None): | 1007 | def action_get(key=None): |
913 | 951 | """Gets the value of an action parameter, or all key/value param pairs""" | 1008 | """ |
914 | 1009 | .. deprecated:: 0.20.7 | ||
915 | 1010 | Alias for :func:`function_get`. | ||
916 | 1011 | |||
917 | 1012 | Gets the value of an action parameter, or all key/value param pairs. | ||
918 | 1013 | """ | ||
919 | 952 | cmd = ['action-get'] | 1014 | cmd = ['action-get'] |
920 | 953 | if key is not None: | 1015 | if key is not None: |
921 | 954 | cmd.append(key) | 1016 | cmd.append(key) |
922 | @@ -957,52 +1019,130 @@ | |||
923 | 957 | return action_data | 1019 | return action_data |
924 | 958 | 1020 | ||
925 | 959 | 1021 | ||
926 | 1022 | @cached | ||
927 | 1023 | def function_get(key=None): | ||
928 | 1024 | """Gets the value of an action parameter, or all key/value param pairs""" | ||
929 | 1025 | cmd = ['function-get'] | ||
930 | 1026 | # Fallback for older charms. | ||
931 | 1027 | if not cmd_exists('function-get'): | ||
932 | 1028 | cmd = ['action-get'] | ||
933 | 1029 | |||
934 | 1030 | if key is not None: | ||
935 | 1031 | cmd.append(key) | ||
936 | 1032 | cmd.append('--format=json') | ||
937 | 1033 | function_data = json.loads(subprocess.check_output(cmd).decode('UTF-8')) | ||
938 | 1034 | return function_data | ||
939 | 1035 | |||
940 | 1036 | |||
941 | 1037 | @deprecate("moved to function_set()", log=log) | ||
942 | 960 | def action_set(values): | 1038 | def action_set(values): |
944 | 961 | """Sets the values to be returned after the action finishes""" | 1039 | """ |
945 | 1040 | .. deprecated:: 0.20.7 | ||
946 | 1041 | Alias for :func:`function_set`. | ||
947 | 1042 | |||
948 | 1043 | Sets the values to be returned after the action finishes. | ||
949 | 1044 | """ | ||
950 | 962 | cmd = ['action-set'] | 1045 | cmd = ['action-set'] |
951 | 963 | for k, v in list(values.items()): | 1046 | for k, v in list(values.items()): |
952 | 964 | cmd.append('{}={}'.format(k, v)) | 1047 | cmd.append('{}={}'.format(k, v)) |
953 | 965 | subprocess.check_call(cmd) | 1048 | subprocess.check_call(cmd) |
954 | 966 | 1049 | ||
955 | 967 | 1050 | ||
956 | 1051 | def function_set(values): | ||
957 | 1052 | """Sets the values to be returned after the function finishes""" | ||
958 | 1053 | cmd = ['function-set'] | ||
959 | 1054 | # Fallback for older charms. | ||
960 | 1055 | if not cmd_exists('function-get'): | ||
961 | 1056 | cmd = ['action-set'] | ||
962 | 1057 | |||
963 | 1058 | for k, v in list(values.items()): | ||
964 | 1059 | cmd.append('{}={}'.format(k, v)) | ||
965 | 1060 | subprocess.check_call(cmd) | ||
966 | 1061 | |||
967 | 1062 | |||
968 | 1063 | @deprecate("moved to function_fail()", log=log) | ||
969 | 968 | def action_fail(message): | 1064 | def action_fail(message): |
973 | 969 | """Sets the action status to failed and sets the error message. | 1065 | """ |
974 | 970 | 1066 | .. deprecated:: 0.20.7 | |
975 | 971 | The results set by action_set are preserved.""" | 1067 | Alias for :func:`function_fail`. |
976 | 1068 | |||
977 | 1069 | Sets the action status to failed and sets the error message. | ||
978 | 1070 | |||
979 | 1071 | The results set by action_set are preserved. | ||
980 | 1072 | """ | ||
981 | 972 | subprocess.check_call(['action-fail', message]) | 1073 | subprocess.check_call(['action-fail', message]) |
982 | 973 | 1074 | ||
983 | 974 | 1075 | ||
984 | 1076 | def function_fail(message): | ||
985 | 1077 | """Sets the function status to failed and sets the error message. | ||
986 | 1078 | |||
987 | 1079 | The results set by function_set are preserved.""" | ||
988 | 1080 | cmd = ['function-fail'] | ||
989 | 1081 | # Fallback for older charms. | ||
990 | 1082 | if not cmd_exists('function-fail'): | ||
991 | 1083 | cmd = ['action-fail'] | ||
992 | 1084 | cmd.append(message) | ||
993 | 1085 | |||
994 | 1086 | subprocess.check_call(cmd) | ||
995 | 1087 | |||
996 | 1088 | |||
997 | 975 | def action_name(): | 1089 | def action_name(): |
998 | 976 | """Get the name of the currently executing action.""" | 1090 | """Get the name of the currently executing action.""" |
999 | 977 | return os.environ.get('JUJU_ACTION_NAME') | 1091 | return os.environ.get('JUJU_ACTION_NAME') |
1000 | 978 | 1092 | ||
1001 | 979 | 1093 | ||
1002 | 1094 | def function_name(): | ||
1003 | 1095 | """Get the name of the currently executing function.""" | ||
1004 | 1096 | return os.environ.get('JUJU_FUNCTION_NAME') or action_name() | ||
1005 | 1097 | |||
1006 | 1098 | |||
1007 | 980 | def action_uuid(): | 1099 | def action_uuid(): |
1008 | 981 | """Get the UUID of the currently executing action.""" | 1100 | """Get the UUID of the currently executing action.""" |
1009 | 982 | return os.environ.get('JUJU_ACTION_UUID') | 1101 | return os.environ.get('JUJU_ACTION_UUID') |
1010 | 983 | 1102 | ||
1011 | 984 | 1103 | ||
1012 | 1104 | def function_id(): | ||
1013 | 1105 | """Get the ID of the currently executing function.""" | ||
1014 | 1106 | return os.environ.get('JUJU_FUNCTION_ID') or action_uuid() | ||
1015 | 1107 | |||
1016 | 1108 | |||
1017 | 985 | def action_tag(): | 1109 | def action_tag(): |
1018 | 986 | """Get the tag for the currently executing action.""" | 1110 | """Get the tag for the currently executing action.""" |
1019 | 987 | return os.environ.get('JUJU_ACTION_TAG') | 1111 | return os.environ.get('JUJU_ACTION_TAG') |
1020 | 988 | 1112 | ||
1021 | 989 | 1113 | ||
1023 | 990 | def status_set(workload_state, message): | 1114 | def function_tag(): |
1024 | 1115 | """Get the tag for the currently executing function.""" | ||
1025 | 1116 | return os.environ.get('JUJU_FUNCTION_TAG') or action_tag() | ||
1026 | 1117 | |||
1027 | 1118 | |||
1028 | 1119 | def status_set(workload_state, message, application=False): | ||
1029 | 991 | """Set the workload state with a message | 1120 | """Set the workload state with a message |
1030 | 992 | 1121 | ||
1031 | 993 | Use status-set to set the workload state with a message which is visible | 1122 | Use status-set to set the workload state with a message which is visible |
1032 | 994 | to the user via juju status. If the status-set command is not found then | 1123 | to the user via juju status. If the status-set command is not found then |
1034 | 995 | assume this is juju < 1.23 and juju-log the message unstead. | 1124 | assume this is juju < 1.23 and juju-log the message instead. |
1035 | 996 | 1125 | ||
1038 | 997 | workload_state -- valid juju workload state. | 1126 | workload_state -- valid juju workload state. str or WORKLOAD_STATES |
1039 | 998 | message -- status update message | 1127 | message -- status update message |
1040 | 1128 | application -- Whether this is an application state set | ||
1041 | 999 | """ | 1129 | """ |
1048 | 1000 | valid_states = ['maintenance', 'blocked', 'waiting', 'active'] | 1130 | bad_state_msg = '{!r} is not a valid workload state' |
1049 | 1001 | if workload_state not in valid_states: | 1131 | |
1050 | 1002 | raise ValueError( | 1132 | if isinstance(workload_state, str): |
1051 | 1003 | '{!r} is not a valid workload state'.format(workload_state) | 1133 | try: |
1052 | 1004 | ) | 1134 | # Convert string to enum. |
1053 | 1005 | cmd = ['status-set', workload_state, message] | 1135 | workload_state = WORKLOAD_STATES[workload_state.upper()] |
1054 | 1136 | except KeyError: | ||
1055 | 1137 | raise ValueError(bad_state_msg.format(workload_state)) | ||
1056 | 1138 | |||
1057 | 1139 | if workload_state not in WORKLOAD_STATES: | ||
1058 | 1140 | raise ValueError(bad_state_msg.format(workload_state)) | ||
1059 | 1141 | |||
1060 | 1142 | cmd = ['status-set'] | ||
1061 | 1143 | if application: | ||
1062 | 1144 | cmd.append('--application') | ||
1063 | 1145 | cmd.extend([workload_state.value, message]) | ||
1064 | 1006 | try: | 1146 | try: |
1065 | 1007 | ret = subprocess.call(cmd) | 1147 | ret = subprocess.call(cmd) |
1066 | 1008 | if ret == 0: | 1148 | if ret == 0: |
1067 | @@ -1010,7 +1150,7 @@ | |||
1068 | 1010 | except OSError as e: | 1150 | except OSError as e: |
1069 | 1011 | if e.errno != errno.ENOENT: | 1151 | if e.errno != errno.ENOENT: |
1070 | 1012 | raise | 1152 | raise |
1072 | 1013 | log_message = 'status-set failed: {} {}'.format(workload_state, | 1153 | log_message = 'status-set failed: {} {}'.format(workload_state.value, |
1073 | 1014 | message) | 1154 | message) |
1074 | 1015 | log(log_message, level='INFO') | 1155 | log(log_message, level='INFO') |
1075 | 1016 | 1156 | ||
1076 | @@ -1425,13 +1565,13 @@ | |||
1077 | 1425 | """Get proxy settings from process environment variables. | 1565 | """Get proxy settings from process environment variables. |
1078 | 1426 | 1566 | ||
1079 | 1427 | Get charm proxy settings from environment variables that correspond to | 1567 | Get charm proxy settings from environment variables that correspond to |
1087 | 1428 | juju-http-proxy, juju-https-proxy and juju-no-proxy (available as of 2.4.2, | 1568 | juju-http-proxy, juju-https-proxy juju-no-proxy (available as of 2.4.2, see |
1088 | 1429 | see lp:1782236) in a format suitable for passing to an application that | 1569 | lp:1782236) and juju-ftp-proxy in a format suitable for passing to an |
1089 | 1430 | reacts to proxy settings passed as environment variables. Some applications | 1570 | application that reacts to proxy settings passed as environment variables. |
1090 | 1431 | support lowercase or uppercase notation (e.g. curl), some support only | 1571 | Some applications support lowercase or uppercase notation (e.g. curl), some |
1091 | 1432 | lowercase (e.g. wget), there are also subjectively rare cases of only | 1572 | support only lowercase (e.g. wget), there are also subjectively rare cases |
1092 | 1433 | uppercase notation support. no_proxy CIDR and wildcard support also varies | 1573 | of only uppercase notation support. no_proxy CIDR and wildcard support also |
1093 | 1434 | between runtimes and applications as there is no enforced standard. | 1574 | varies between runtimes and applications as there is no enforced standard. |
1094 | 1435 | 1575 | ||
1095 | 1436 | Some applications may connect to multiple destinations and expose config | 1576 | Some applications may connect to multiple destinations and expose config |
1096 | 1437 | options that would affect only proxy settings for a specific destination | 1577 | options that would affect only proxy settings for a specific destination |
1097 | @@ -1473,11 +1613,11 @@ | |||
1098 | 1473 | def _contains_range(addresses): | 1613 | def _contains_range(addresses): |
1099 | 1474 | """Check for cidr or wildcard domain in a string. | 1614 | """Check for cidr or wildcard domain in a string. |
1100 | 1475 | 1615 | ||
1102 | 1476 | Given a string comprising a comma seperated list of ip addresses | 1616 | Given a string comprising a comma separated list of ip addresses |
1103 | 1477 | and domain names, determine whether the string contains IP ranges | 1617 | and domain names, determine whether the string contains IP ranges |
1104 | 1478 | or wildcard domains. | 1618 | or wildcard domains. |
1105 | 1479 | 1619 | ||
1107 | 1480 | :param addresses: comma seperated list of domains and ip addresses. | 1620 | :param addresses: comma separated list of domains and ip addresses. |
1108 | 1481 | :type addresses: str | 1621 | :type addresses: str |
1109 | 1482 | """ | 1622 | """ |
1110 | 1483 | return ( | 1623 | return ( |
1111 | @@ -1488,3 +1628,12 @@ | |||
1112 | 1488 | addresses.startswith(".") or | 1628 | addresses.startswith(".") or |
1113 | 1489 | ",." in addresses or | 1629 | ",." in addresses or |
1114 | 1490 | " ." in addresses) | 1630 | " ." in addresses) |
1115 | 1631 | |||
1116 | 1632 | |||
1117 | 1633 | def is_subordinate(): | ||
1118 | 1634 | """Check whether charm is subordinate in unit metadata. | ||
1119 | 1635 | |||
1120 | 1636 | :returns: True if unit is subordniate, False otherwise. | ||
1121 | 1637 | :rtype: bool | ||
1122 | 1638 | """ | ||
1123 | 1639 | return metadata().get('subordinate') is True | ||
1124 | 1491 | 1640 | ||
1125 | === modified file 'charmhelpers/core/host.py' | |||
1126 | --- charmhelpers/core/host.py 2019-05-24 12:41:48 +0000 | |||
1127 | +++ charmhelpers/core/host.py 2021-11-10 05:36:20 +0000 | |||
1128 | @@ -1,4 +1,4 @@ | |||
1130 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2021 Canonical Limited. |
1131 | 2 | # | 2 | # |
1132 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1133 | 4 | # you may not use this file except in compliance with the License. | 4 | # you may not use this file except in compliance with the License. |
1134 | @@ -19,6 +19,7 @@ | |||
1135 | 19 | # Nick Moffitt <nick.moffitt@canonical.com> | 19 | # Nick Moffitt <nick.moffitt@canonical.com> |
1136 | 20 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> | 20 | # Matthew Wedgwood <matthew.wedgwood@canonical.com> |
1137 | 21 | 21 | ||
1138 | 22 | import errno | ||
1139 | 22 | import os | 23 | import os |
1140 | 23 | import re | 24 | import re |
1141 | 24 | import pwd | 25 | import pwd |
1142 | @@ -33,7 +34,7 @@ | |||
1143 | 33 | import six | 34 | import six |
1144 | 34 | 35 | ||
1145 | 35 | from contextlib import contextmanager | 36 | from contextlib import contextmanager |
1147 | 36 | from collections import OrderedDict | 37 | from collections import OrderedDict, defaultdict |
1148 | 37 | from .hookenv import log, INFO, DEBUG, local_unit, charm_name | 38 | from .hookenv import log, INFO, DEBUG, local_unit, charm_name |
1149 | 38 | from .fstab import Fstab | 39 | from .fstab import Fstab |
1150 | 39 | from charmhelpers.osplatform import get_platform | 40 | from charmhelpers.osplatform import get_platform |
1151 | @@ -59,6 +60,7 @@ | |||
1152 | 59 | ) # flake8: noqa -- ignore F401 for this import | 60 | ) # flake8: noqa -- ignore F401 for this import |
1153 | 60 | 61 | ||
1154 | 61 | UPDATEDB_PATH = '/etc/updatedb.conf' | 62 | UPDATEDB_PATH = '/etc/updatedb.conf' |
1155 | 63 | CA_CERT_DIR = '/usr/local/share/ca-certificates' | ||
1156 | 62 | 64 | ||
1157 | 63 | 65 | ||
1158 | 64 | def service_start(service_name, **kwargs): | 66 | def service_start(service_name, **kwargs): |
1159 | @@ -193,7 +195,7 @@ | |||
1160 | 193 | stopped = service_stop(service_name, **kwargs) | 195 | stopped = service_stop(service_name, **kwargs) |
1161 | 194 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) | 196 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
1162 | 195 | sysv_file = os.path.join(initd_dir, service_name) | 197 | sysv_file = os.path.join(initd_dir, service_name) |
1164 | 196 | if init_is_systemd(): | 198 | if init_is_systemd(service_name=service_name): |
1165 | 197 | service('disable', service_name) | 199 | service('disable', service_name) |
1166 | 198 | service('mask', service_name) | 200 | service('mask', service_name) |
1167 | 199 | elif os.path.exists(upstart_file): | 201 | elif os.path.exists(upstart_file): |
1168 | @@ -215,7 +217,7 @@ | |||
1169 | 215 | initd_dir="/etc/init.d", **kwargs): | 217 | initd_dir="/etc/init.d", **kwargs): |
1170 | 216 | """Resume a system service. | 218 | """Resume a system service. |
1171 | 217 | 219 | ||
1173 | 218 | Reenable starting again at boot. Start the service. | 220 | Re-enable starting again at boot. Start the service. |
1174 | 219 | 221 | ||
1175 | 220 | :param service_name: the name of the service to resume | 222 | :param service_name: the name of the service to resume |
1176 | 221 | :param init_dir: the path to the init dir | 223 | :param init_dir: the path to the init dir |
1177 | @@ -227,7 +229,7 @@ | |||
1178 | 227 | """ | 229 | """ |
1179 | 228 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) | 230 | upstart_file = os.path.join(init_dir, "{}.conf".format(service_name)) |
1180 | 229 | sysv_file = os.path.join(initd_dir, service_name) | 231 | sysv_file = os.path.join(initd_dir, service_name) |
1182 | 230 | if init_is_systemd(): | 232 | if init_is_systemd(service_name=service_name): |
1183 | 231 | service('unmask', service_name) | 233 | service('unmask', service_name) |
1184 | 232 | service('enable', service_name) | 234 | service('enable', service_name) |
1185 | 233 | elif os.path.exists(upstart_file): | 235 | elif os.path.exists(upstart_file): |
1186 | @@ -257,7 +259,7 @@ | |||
1187 | 257 | :param **kwargs: additional params to be passed to the service command in | 259 | :param **kwargs: additional params to be passed to the service command in |
1188 | 258 | the form of key=value. | 260 | the form of key=value. |
1189 | 259 | """ | 261 | """ |
1191 | 260 | if init_is_systemd(): | 262 | if init_is_systemd(service_name=service_name): |
1192 | 261 | cmd = ['systemctl', action, service_name] | 263 | cmd = ['systemctl', action, service_name] |
1193 | 262 | else: | 264 | else: |
1194 | 263 | cmd = ['service', service_name, action] | 265 | cmd = ['service', service_name, action] |
1195 | @@ -281,7 +283,7 @@ | |||
1196 | 281 | units (e.g. service ceph-osd status id=2). The kwargs | 283 | units (e.g. service ceph-osd status id=2). The kwargs |
1197 | 282 | are ignored in systemd services. | 284 | are ignored in systemd services. |
1198 | 283 | """ | 285 | """ |
1200 | 284 | if init_is_systemd(): | 286 | if init_is_systemd(service_name=service_name): |
1201 | 285 | return service('is-active', service_name) | 287 | return service('is-active', service_name) |
1202 | 286 | else: | 288 | else: |
1203 | 287 | if os.path.exists(_UPSTART_CONF.format(service_name)): | 289 | if os.path.exists(_UPSTART_CONF.format(service_name)): |
1204 | @@ -311,8 +313,14 @@ | |||
1205 | 311 | SYSTEMD_SYSTEM = '/run/systemd/system' | 313 | SYSTEMD_SYSTEM = '/run/systemd/system' |
1206 | 312 | 314 | ||
1207 | 313 | 315 | ||
1210 | 314 | def init_is_systemd(): | 316 | def init_is_systemd(service_name=None): |
1211 | 315 | """Return True if the host system uses systemd, False otherwise.""" | 317 | """ |
1212 | 318 | Returns whether the host uses systemd for the specified service. | ||
1213 | 319 | |||
1214 | 320 | @param Optional[str] service_name: specific name of service | ||
1215 | 321 | """ | ||
1216 | 322 | if str(service_name).startswith("snap."): | ||
1217 | 323 | return True | ||
1218 | 316 | if lsb_release()['DISTRIB_CODENAME'] == 'trusty': | 324 | if lsb_release()['DISTRIB_CODENAME'] == 'trusty': |
1219 | 317 | return False | 325 | return False |
1220 | 318 | return os.path.isdir(SYSTEMD_SYSTEM) | 326 | return os.path.isdir(SYSTEMD_SYSTEM) |
1221 | @@ -671,7 +679,7 @@ | |||
1222 | 671 | 679 | ||
1223 | 672 | :param str checksum: Value of the checksum used to validate the file. | 680 | :param str checksum: Value of the checksum used to validate the file. |
1224 | 673 | :param str hash_type: Hash algorithm used to generate `checksum`. | 681 | :param str hash_type: Hash algorithm used to generate `checksum`. |
1226 | 674 | Can be any hash alrgorithm supported by :mod:`hashlib`, | 682 | Can be any hash algorithm supported by :mod:`hashlib`, |
1227 | 675 | such as md5, sha1, sha256, sha512, etc. | 683 | such as md5, sha1, sha256, sha512, etc. |
1228 | 676 | :raises ChecksumError: If the file fails the checksum | 684 | :raises ChecksumError: If the file fails the checksum |
1229 | 677 | 685 | ||
1230 | @@ -686,78 +694,227 @@ | |||
1231 | 686 | pass | 694 | pass |
1232 | 687 | 695 | ||
1233 | 688 | 696 | ||
1258 | 689 | def restart_on_change(restart_map, stopstart=False, restart_functions=None): | 697 | class restart_on_change(object): |
1259 | 690 | """Restart services based on configuration files changing | 698 | """Decorator and context manager to handle restarts. |
1260 | 691 | 699 | ||
1261 | 692 | This function is used a decorator, for example:: | 700 | Usage: |
1262 | 693 | 701 | ||
1263 | 694 | @restart_on_change({ | 702 | @restart_on_change(restart_map, ...) |
1264 | 695 | '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ] | 703 | def function_that_might_trigger_a_restart(...) |
1265 | 696 | '/etc/apache/sites-enabled/*': [ 'apache2' ] | 704 | ... |
1266 | 697 | }) | 705 | |
1267 | 698 | def config_changed(): | 706 | Or: |
1268 | 699 | pass # your code here | 707 | |
1269 | 700 | 708 | with restart_on_change(restart_map, ...): | |
1270 | 701 | In this example, the cinder-api and cinder-volume services | 709 | do_stuff_that_might_trigger_a_restart() |
1271 | 702 | would be restarted if /etc/ceph/ceph.conf is changed by the | 710 | ... |
1248 | 703 | ceph_client_changed function. The apache2 service would be | ||
1249 | 704 | restarted if any file matching the pattern got changed, created | ||
1250 | 705 | or removed. Standard wildcards are supported, see documentation | ||
1251 | 706 | for the 'glob' module for more information. | ||
1252 | 707 | |||
1253 | 708 | @param restart_map: {path_file_name: [service_name, ...] | ||
1254 | 709 | @param stopstart: DEFAULT false; whether to stop, start OR restart | ||
1255 | 710 | @param restart_functions: nonstandard functions to use to restart services | ||
1256 | 711 | {svc: func, ...} | ||
1257 | 712 | @returns result from decorated function | ||
1272 | 713 | """ | 711 | """ |
1274 | 714 | def wrap(f): | 712 | |
1275 | 713 | def __init__(self, restart_map, stopstart=False, restart_functions=None, | ||
1276 | 714 | can_restart_now_f=None, post_svc_restart_f=None, | ||
1277 | 715 | pre_restarts_wait_f=None): | ||
1278 | 716 | """ | ||
1279 | 717 | :param restart_map: {file: [service, ...]} | ||
1280 | 718 | :type restart_map: Dict[str, List[str,]] | ||
1281 | 719 | :param stopstart: whether to stop, start or restart a service | ||
1282 | 720 | :type stopstart: booleean | ||
1283 | 721 | :param restart_functions: nonstandard functions to use to restart | ||
1284 | 722 | services {svc: func, ...} | ||
1285 | 723 | :type restart_functions: Dict[str, Callable[[str], None]] | ||
1286 | 724 | :param can_restart_now_f: A function used to check if the restart is | ||
1287 | 725 | permitted. | ||
1288 | 726 | :type can_restart_now_f: Callable[[str, List[str]], boolean] | ||
1289 | 727 | :param post_svc_restart_f: A function run after a service has | ||
1290 | 728 | restarted. | ||
1291 | 729 | :type post_svc_restart_f: Callable[[str], None] | ||
1292 | 730 | :param pre_restarts_wait_f: A function called before any restarts. | ||
1293 | 731 | :type pre_restarts_wait_f: Callable[None, None] | ||
1294 | 732 | """ | ||
1295 | 733 | self.restart_map = restart_map | ||
1296 | 734 | self.stopstart = stopstart | ||
1297 | 735 | self.restart_functions = restart_functions | ||
1298 | 736 | self.can_restart_now_f = can_restart_now_f | ||
1299 | 737 | self.post_svc_restart_f = post_svc_restart_f | ||
1300 | 738 | self.pre_restarts_wait_f = pre_restarts_wait_f | ||
1301 | 739 | |||
1302 | 740 | def __call__(self, f): | ||
1303 | 741 | """Work like a decorator. | ||
1304 | 742 | |||
1305 | 743 | Returns a wrapped function that performs the restart if triggered. | ||
1306 | 744 | |||
1307 | 745 | :param f: The function that is being wrapped. | ||
1308 | 746 | :type f: Callable[[Any], Any] | ||
1309 | 747 | :returns: the wrapped function | ||
1310 | 748 | :rtype: Callable[[Any], Any] | ||
1311 | 749 | """ | ||
1312 | 715 | @functools.wraps(f) | 750 | @functools.wraps(f) |
1313 | 716 | def wrapped_f(*args, **kwargs): | 751 | def wrapped_f(*args, **kwargs): |
1314 | 717 | return restart_on_change_helper( | 752 | return restart_on_change_helper( |
1317 | 718 | (lambda: f(*args, **kwargs)), restart_map, stopstart, | 753 | (lambda: f(*args, **kwargs)), |
1318 | 719 | restart_functions) | 754 | self.restart_map, |
1319 | 755 | stopstart=self.stopstart, | ||
1320 | 756 | restart_functions=self.restart_functions, | ||
1321 | 757 | can_restart_now_f=self.can_restart_now_f, | ||
1322 | 758 | post_svc_restart_f=self.post_svc_restart_f, | ||
1323 | 759 | pre_restarts_wait_f=self.pre_restarts_wait_f) | ||
1324 | 720 | return wrapped_f | 760 | return wrapped_f |
1326 | 721 | return wrap | 761 | |
1327 | 762 | def __enter__(self): | ||
1328 | 763 | """Enter the runtime context related to this object. """ | ||
1329 | 764 | self.checksums = _pre_restart_on_change_helper(self.restart_map) | ||
1330 | 765 | |||
1331 | 766 | def __exit__(self, exc_type, exc_val, exc_tb): | ||
1332 | 767 | """Exit the runtime context related to this object. | ||
1333 | 768 | |||
1334 | 769 | The parameters describe the exception that caused the context to be | ||
1335 | 770 | exited. If the context was exited without an exception, all three | ||
1336 | 771 | arguments will be None. | ||
1337 | 772 | """ | ||
1338 | 773 | if exc_type is None: | ||
1339 | 774 | _post_restart_on_change_helper( | ||
1340 | 775 | self.checksums, | ||
1341 | 776 | self.restart_map, | ||
1342 | 777 | stopstart=self.stopstart, | ||
1343 | 778 | restart_functions=self.restart_functions, | ||
1344 | 779 | can_restart_now_f=self.can_restart_now_f, | ||
1345 | 780 | post_svc_restart_f=self.post_svc_restart_f, | ||
1346 | 781 | pre_restarts_wait_f=self.pre_restarts_wait_f) | ||
1347 | 782 | # All is good, so return False; any exceptions will propagate. | ||
1348 | 783 | return False | ||
1349 | 722 | 784 | ||
1350 | 723 | 785 | ||
1351 | 724 | def restart_on_change_helper(lambda_f, restart_map, stopstart=False, | 786 | def restart_on_change_helper(lambda_f, restart_map, stopstart=False, |
1353 | 725 | restart_functions=None): | 787 | restart_functions=None, |
1354 | 788 | can_restart_now_f=None, | ||
1355 | 789 | post_svc_restart_f=None, | ||
1356 | 790 | pre_restarts_wait_f=None): | ||
1357 | 726 | """Helper function to perform the restart_on_change function. | 791 | """Helper function to perform the restart_on_change function. |
1358 | 727 | 792 | ||
1359 | 728 | This is provided for decorators to restart services if files described | 793 | This is provided for decorators to restart services if files described |
1360 | 729 | in the restart_map have changed after an invocation of lambda_f(). | 794 | in the restart_map have changed after an invocation of lambda_f(). |
1361 | 730 | 795 | ||
1368 | 731 | @param lambda_f: function to call. | 796 | This functions allows for a number of helper functions to be passed. |
1369 | 732 | @param restart_map: {file: [service, ...]} | 797 | |
1370 | 733 | @param stopstart: whether to stop, start or restart a service | 798 | `restart_functions` is a map with a service as the key and the |
1371 | 734 | @param restart_functions: nonstandard functions to use to restart services | 799 | corresponding value being the function to call to restart the service. For |
1372 | 735 | {svc: func, ...} | 800 | example if `restart_functions={'some-service': my_restart_func}` then |
1373 | 736 | @returns result of lambda_f() | 801 | `my_restart_func` should a function which takes one argument which is the |
1374 | 802 | service name to be retstarted. | ||
1375 | 803 | |||
1376 | 804 | `can_restart_now_f` is a function which checks that a restart is permitted. | ||
1377 | 805 | It should return a bool which indicates if a restart is allowed and should | ||
1378 | 806 | take a service name (str) and a list of changed files (List[str]) as | ||
1379 | 807 | arguments. | ||
1380 | 808 | |||
1381 | 809 | `post_svc_restart_f` is a function which runs after a service has been | ||
1382 | 810 | restarted. It takes the service name that was restarted as an argument. | ||
1383 | 811 | |||
1384 | 812 | `pre_restarts_wait_f` is a function which is called before any restarts | ||
1385 | 813 | occur. The use case for this is an application which wants to try and | ||
1386 | 814 | stagger restarts between units. | ||
1387 | 815 | |||
1388 | 816 | :param lambda_f: function to call. | ||
1389 | 817 | :type lambda_f: Callable[[], ANY] | ||
1390 | 818 | :param restart_map: {file: [service, ...]} | ||
1391 | 819 | :type restart_map: Dict[str, List[str,]] | ||
1392 | 820 | :param stopstart: whether to stop, start or restart a service | ||
1393 | 821 | :type stopstart: booleean | ||
1394 | 822 | :param restart_functions: nonstandard functions to use to restart services | ||
1395 | 823 | {svc: func, ...} | ||
1396 | 824 | :type restart_functions: Dict[str, Callable[[str], None]] | ||
1397 | 825 | :param can_restart_now_f: A function used to check if the restart is | ||
1398 | 826 | permitted. | ||
1399 | 827 | :type can_restart_now_f: Callable[[str, List[str]], boolean] | ||
1400 | 828 | :param post_svc_restart_f: A function run after a service has | ||
1401 | 829 | restarted. | ||
1402 | 830 | :type post_svc_restart_f: Callable[[str], None] | ||
1403 | 831 | :param pre_restarts_wait_f: A function called before any restarts. | ||
1404 | 832 | :type pre_restarts_wait_f: Callable[None, None] | ||
1405 | 833 | :returns: result of lambda_f() | ||
1406 | 834 | :rtype: ANY | ||
1407 | 835 | """ | ||
1408 | 836 | checksums = _pre_restart_on_change_helper(restart_map) | ||
1409 | 837 | r = lambda_f() | ||
1410 | 838 | _post_restart_on_change_helper(checksums, | ||
1411 | 839 | restart_map, | ||
1412 | 840 | stopstart, | ||
1413 | 841 | restart_functions, | ||
1414 | 842 | can_restart_now_f, | ||
1415 | 843 | post_svc_restart_f, | ||
1416 | 844 | pre_restarts_wait_f) | ||
1417 | 845 | return r | ||
1418 | 846 | |||
1419 | 847 | |||
1420 | 848 | def _pre_restart_on_change_helper(restart_map): | ||
1421 | 849 | """Take a snapshot of file hashes. | ||
1422 | 850 | |||
1423 | 851 | :param restart_map: {file: [service, ...]} | ||
1424 | 852 | :type restart_map: Dict[str, List[str,]] | ||
1425 | 853 | :returns: Dictionary of file paths and the files checksum. | ||
1426 | 854 | :rtype: Dict[str, str] | ||
1427 | 855 | """ | ||
1428 | 856 | return {path: path_hash(path) for path in restart_map} | ||
1429 | 857 | |||
1430 | 858 | |||
1431 | 859 | def _post_restart_on_change_helper(checksums, | ||
1432 | 860 | restart_map, | ||
1433 | 861 | stopstart=False, | ||
1434 | 862 | restart_functions=None, | ||
1435 | 863 | can_restart_now_f=None, | ||
1436 | 864 | post_svc_restart_f=None, | ||
1437 | 865 | pre_restarts_wait_f=None): | ||
1438 | 866 | """Check whether files have changed. | ||
1439 | 867 | |||
1440 | 868 | :param checksums: Dictionary of file paths and the files checksum. | ||
1441 | 869 | :type checksums: Dict[str, str] | ||
1442 | 870 | :param restart_map: {file: [service, ...]} | ||
1443 | 871 | :type restart_map: Dict[str, List[str,]] | ||
1444 | 872 | :param stopstart: whether to stop, start or restart a service | ||
1445 | 873 | :type stopstart: booleean | ||
1446 | 874 | :param restart_functions: nonstandard functions to use to restart services | ||
1447 | 875 | {svc: func, ...} | ||
1448 | 876 | :type restart_functions: Dict[str, Callable[[str], None]] | ||
1449 | 877 | :param can_restart_now_f: A function used to check if the restart is | ||
1450 | 878 | permitted. | ||
1451 | 879 | :type can_restart_now_f: Callable[[str, List[str]], boolean] | ||
1452 | 880 | :param post_svc_restart_f: A function run after a service has | ||
1453 | 881 | restarted. | ||
1454 | 882 | :type post_svc_restart_f: Callable[[str], None] | ||
1455 | 883 | :param pre_restarts_wait_f: A function called before any restarts. | ||
1456 | 884 | :type pre_restarts_wait_f: Callable[None, None] | ||
1457 | 737 | """ | 885 | """ |
1458 | 738 | if restart_functions is None: | 886 | if restart_functions is None: |
1459 | 739 | restart_functions = {} | 887 | restart_functions = {} |
1462 | 740 | checksums = {path: path_hash(path) for path in restart_map} | 888 | changed_files = defaultdict(list) |
1463 | 741 | r = lambda_f() | 889 | restarts = [] |
1464 | 742 | # create a list of lists of the services to restart | 890 | # create a list of lists of the services to restart |
1468 | 743 | restarts = [restart_map[path] | 891 | for path, services in restart_map.items(): |
1469 | 744 | for path in restart_map | 892 | if path_hash(path) != checksums[path]: |
1470 | 745 | if path_hash(path) != checksums[path]] | 893 | restarts.append(services) |
1471 | 894 | for svc in services: | ||
1472 | 895 | changed_files[svc].append(path) | ||
1473 | 746 | # create a flat list of ordered services without duplicates from lists | 896 | # create a flat list of ordered services without duplicates from lists |
1474 | 747 | services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts))) | 897 | services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts))) |
1475 | 748 | if services_list: | 898 | if services_list: |
1476 | 899 | if pre_restarts_wait_f: | ||
1477 | 900 | pre_restarts_wait_f() | ||
1478 | 749 | actions = ('stop', 'start') if stopstart else ('restart',) | 901 | actions = ('stop', 'start') if stopstart else ('restart',) |
1479 | 750 | for service_name in services_list: | 902 | for service_name in services_list: |
1480 | 903 | if can_restart_now_f: | ||
1481 | 904 | if not can_restart_now_f(service_name, | ||
1482 | 905 | changed_files[service_name]): | ||
1483 | 906 | continue | ||
1484 | 751 | if service_name in restart_functions: | 907 | if service_name in restart_functions: |
1485 | 752 | restart_functions[service_name](service_name) | 908 | restart_functions[service_name](service_name) |
1486 | 753 | else: | 909 | else: |
1487 | 754 | for action in actions: | 910 | for action in actions: |
1488 | 755 | service(action, service_name) | 911 | service(action, service_name) |
1490 | 756 | return r | 912 | if post_svc_restart_f: |
1491 | 913 | post_svc_restart_f(service_name) | ||
1492 | 757 | 914 | ||
1493 | 758 | 915 | ||
1494 | 759 | def pwgen(length=None): | 916 | def pwgen(length=None): |
1496 | 760 | """Generate a random pasword.""" | 917 | """Generate a random password.""" |
1497 | 761 | if length is None: | 918 | if length is None: |
1498 | 762 | # A random length is ok to use a weak PRNG | 919 | # A random length is ok to use a weak PRNG |
1499 | 763 | length = random.choice(range(35, 45)) | 920 | length = random.choice(range(35, 45)) |
1500 | @@ -819,7 +976,8 @@ | |||
1501 | 819 | if nic_type: | 976 | if nic_type: |
1502 | 820 | for int_type in int_types: | 977 | for int_type in int_types: |
1503 | 821 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] | 978 | cmd = ['ip', 'addr', 'show', 'label', int_type + '*'] |
1505 | 822 | ip_output = subprocess.check_output(cmd).decode('UTF-8') | 979 | ip_output = subprocess.check_output( |
1506 | 980 | cmd).decode('UTF-8', errors='replace') | ||
1507 | 823 | ip_output = ip_output.split('\n') | 981 | ip_output = ip_output.split('\n') |
1508 | 824 | ip_output = (line for line in ip_output if line) | 982 | ip_output = (line for line in ip_output if line) |
1509 | 825 | for line in ip_output: | 983 | for line in ip_output: |
1510 | @@ -835,7 +993,8 @@ | |||
1511 | 835 | interfaces.append(iface) | 993 | interfaces.append(iface) |
1512 | 836 | else: | 994 | else: |
1513 | 837 | cmd = ['ip', 'a'] | 995 | cmd = ['ip', 'a'] |
1515 | 838 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') | 996 | ip_output = subprocess.check_output( |
1516 | 997 | cmd).decode('UTF-8', errors='replace').split('\n') | ||
1517 | 839 | ip_output = (line.strip() for line in ip_output if line) | 998 | ip_output = (line.strip() for line in ip_output if line) |
1518 | 840 | 999 | ||
1519 | 841 | key = re.compile(r'^[0-9]+:\s+(.+):') | 1000 | key = re.compile(r'^[0-9]+:\s+(.+):') |
1520 | @@ -859,7 +1018,8 @@ | |||
1521 | 859 | def get_nic_mtu(nic): | 1018 | def get_nic_mtu(nic): |
1522 | 860 | """Return the Maximum Transmission Unit (MTU) for a network interface.""" | 1019 | """Return the Maximum Transmission Unit (MTU) for a network interface.""" |
1523 | 861 | cmd = ['ip', 'addr', 'show', nic] | 1020 | cmd = ['ip', 'addr', 'show', nic] |
1525 | 862 | ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n') | 1021 | ip_output = subprocess.check_output( |
1526 | 1022 | cmd).decode('UTF-8', errors='replace').split('\n') | ||
1527 | 863 | mtu = "" | 1023 | mtu = "" |
1528 | 864 | for line in ip_output: | 1024 | for line in ip_output: |
1529 | 865 | words = line.split() | 1025 | words = line.split() |
1530 | @@ -871,7 +1031,7 @@ | |||
1531 | 871 | def get_nic_hwaddr(nic): | 1031 | def get_nic_hwaddr(nic): |
1532 | 872 | """Return the Media Access Control (MAC) for a network interface.""" | 1032 | """Return the Media Access Control (MAC) for a network interface.""" |
1533 | 873 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] | 1033 | cmd = ['ip', '-o', '-0', 'addr', 'show', nic] |
1535 | 874 | ip_output = subprocess.check_output(cmd).decode('UTF-8') | 1034 | ip_output = subprocess.check_output(cmd).decode('UTF-8', errors='replace') |
1536 | 875 | hwaddr = "" | 1035 | hwaddr = "" |
1537 | 876 | words = ip_output.split() | 1036 | words = ip_output.split() |
1538 | 877 | if 'link/ether' in words: | 1037 | if 'link/ether' in words: |
1539 | @@ -883,7 +1043,7 @@ | |||
1540 | 883 | def chdir(directory): | 1043 | def chdir(directory): |
1541 | 884 | """Change the current working directory to a different directory for a code | 1044 | """Change the current working directory to a different directory for a code |
1542 | 885 | block and return the previous directory after the block exits. Useful to | 1045 | block and return the previous directory after the block exits. Useful to |
1544 | 886 | run commands from a specificed directory. | 1046 | run commands from a specified directory. |
1545 | 887 | 1047 | ||
1546 | 888 | :param str directory: The directory path to change to for this context. | 1048 | :param str directory: The directory path to change to for this context. |
1547 | 889 | """ | 1049 | """ |
1548 | @@ -918,9 +1078,13 @@ | |||
1549 | 918 | for root, dirs, files in os.walk(path, followlinks=follow_links): | 1078 | for root, dirs, files in os.walk(path, followlinks=follow_links): |
1550 | 919 | for name in dirs + files: | 1079 | for name in dirs + files: |
1551 | 920 | full = os.path.join(root, name) | 1080 | full = os.path.join(root, name) |
1554 | 921 | broken_symlink = os.path.lexists(full) and not os.path.exists(full) | 1081 | try: |
1553 | 922 | if not broken_symlink: | ||
1555 | 923 | chown(full, uid, gid) | 1082 | chown(full, uid, gid) |
1556 | 1083 | except (IOError, OSError) as e: | ||
1557 | 1084 | # Intended to ignore "file not found". Catching both to be | ||
1558 | 1085 | # compatible with both Python 2.7 and 3.x. | ||
1559 | 1086 | if e.errno == errno.ENOENT: | ||
1560 | 1087 | pass | ||
1561 | 924 | 1088 | ||
1562 | 925 | 1089 | ||
1563 | 926 | def lchownr(path, owner, group): | 1090 | def lchownr(path, owner, group): |
1564 | @@ -1053,6 +1217,17 @@ | |||
1565 | 1053 | return calculated_wait_time | 1217 | return calculated_wait_time |
1566 | 1054 | 1218 | ||
1567 | 1055 | 1219 | ||
1568 | 1220 | def ca_cert_absolute_path(basename_without_extension): | ||
1569 | 1221 | """Returns absolute path to CA certificate. | ||
1570 | 1222 | |||
1571 | 1223 | :param basename_without_extension: Filename without extension | ||
1572 | 1224 | :type basename_without_extension: str | ||
1573 | 1225 | :returns: Absolute full path | ||
1574 | 1226 | :rtype: str | ||
1575 | 1227 | """ | ||
1576 | 1228 | return '{}/{}.crt'.format(CA_CERT_DIR, basename_without_extension) | ||
1577 | 1229 | |||
1578 | 1230 | |||
1579 | 1056 | def install_ca_cert(ca_cert, name=None): | 1231 | def install_ca_cert(ca_cert, name=None): |
1580 | 1057 | """ | 1232 | """ |
1581 | 1058 | Install the given cert as a trusted CA. | 1233 | Install the given cert as a trusted CA. |
1582 | @@ -1068,10 +1243,37 @@ | |||
1583 | 1068 | ca_cert = ca_cert.encode('utf8') | 1243 | ca_cert = ca_cert.encode('utf8') |
1584 | 1069 | if not name: | 1244 | if not name: |
1585 | 1070 | name = 'juju-{}'.format(charm_name()) | 1245 | name = 'juju-{}'.format(charm_name()) |
1587 | 1071 | cert_file = '/usr/local/share/ca-certificates/{}.crt'.format(name) | 1246 | cert_file = ca_cert_absolute_path(name) |
1588 | 1072 | new_hash = hashlib.md5(ca_cert).hexdigest() | 1247 | new_hash = hashlib.md5(ca_cert).hexdigest() |
1589 | 1073 | if file_hash(cert_file) == new_hash: | 1248 | if file_hash(cert_file) == new_hash: |
1590 | 1074 | return | 1249 | return |
1591 | 1075 | log("Installing new CA cert at: {}".format(cert_file), level=INFO) | 1250 | log("Installing new CA cert at: {}".format(cert_file), level=INFO) |
1592 | 1076 | write_file(cert_file, ca_cert) | 1251 | write_file(cert_file, ca_cert) |
1593 | 1077 | subprocess.check_call(['update-ca-certificates', '--fresh']) | 1252 | subprocess.check_call(['update-ca-certificates', '--fresh']) |
1594 | 1253 | |||
1595 | 1254 | |||
1596 | 1255 | def get_system_env(key, default=None): | ||
1597 | 1256 | """Get data from system environment as represented in ``/etc/environment``. | ||
1598 | 1257 | |||
1599 | 1258 | :param key: Key to look up | ||
1600 | 1259 | :type key: str | ||
1601 | 1260 | :param default: Value to return if key is not found | ||
1602 | 1261 | :type default: any | ||
1603 | 1262 | :returns: Value for key if found or contents of default parameter | ||
1604 | 1263 | :rtype: any | ||
1605 | 1264 | :raises: subprocess.CalledProcessError | ||
1606 | 1265 | """ | ||
1607 | 1266 | env_file = '/etc/environment' | ||
1608 | 1267 | # use the shell and env(1) to parse the global environments file. This is | ||
1609 | 1268 | # done to get the correct result even if the user has shell variable | ||
1610 | 1269 | # substitutions or other shell logic in that file. | ||
1611 | 1270 | output = subprocess.check_output( | ||
1612 | 1271 | ['env', '-i', '/bin/bash', '-c', | ||
1613 | 1272 | 'set -a && source {} && env'.format(env_file)], | ||
1614 | 1273 | universal_newlines=True) | ||
1615 | 1274 | for k, v in (line.split('=', 1) | ||
1616 | 1275 | for line in output.splitlines() if '=' in line): | ||
1617 | 1276 | if k == key: | ||
1618 | 1277 | return v | ||
1619 | 1278 | else: | ||
1620 | 1279 | return default | ||
1621 | 1078 | 1280 | ||
1622 | === modified file 'charmhelpers/core/host_factory/ubuntu.py' | |||
1623 | --- charmhelpers/core/host_factory/ubuntu.py 2019-05-24 12:41:48 +0000 | |||
1624 | +++ charmhelpers/core/host_factory/ubuntu.py 2021-11-10 05:36:20 +0000 | |||
1625 | @@ -24,6 +24,12 @@ | |||
1626 | 24 | 'bionic', | 24 | 'bionic', |
1627 | 25 | 'cosmic', | 25 | 'cosmic', |
1628 | 26 | 'disco', | 26 | 'disco', |
1629 | 27 | 'eoan', | ||
1630 | 28 | 'focal', | ||
1631 | 29 | 'groovy', | ||
1632 | 30 | 'hirsute', | ||
1633 | 31 | 'impish', | ||
1634 | 32 | 'jammy', | ||
1635 | 27 | ) | 33 | ) |
1636 | 28 | 34 | ||
1637 | 29 | 35 | ||
1638 | @@ -93,12 +99,14 @@ | |||
1639 | 93 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if | 99 | the pkgcache argument is None. Be sure to add charmhelpers.fetch if |
1640 | 94 | you call this function, or pass an apt_pkg.Cache() instance. | 100 | you call this function, or pass an apt_pkg.Cache() instance. |
1641 | 95 | """ | 101 | """ |
1643 | 96 | import apt_pkg | 102 | from charmhelpers.fetch import apt_pkg, get_installed_version |
1644 | 97 | if not pkgcache: | 103 | if not pkgcache: |
1649 | 98 | from charmhelpers.fetch import apt_cache | 104 | current_ver = get_installed_version(package) |
1650 | 99 | pkgcache = apt_cache() | 105 | else: |
1651 | 100 | pkg = pkgcache[package] | 106 | pkg = pkgcache[package] |
1652 | 101 | return apt_pkg.version_compare(pkg.current_ver.ver_str, revno) | 107 | current_ver = pkg.current_ver |
1653 | 108 | |||
1654 | 109 | return apt_pkg.version_compare(current_ver.ver_str, revno) | ||
1655 | 102 | 110 | ||
1656 | 103 | 111 | ||
1657 | 104 | @cached | 112 | @cached |
1658 | 105 | 113 | ||
1659 | === modified file 'charmhelpers/core/services/base.py' | |||
1660 | --- charmhelpers/core/services/base.py 2019-05-24 12:41:48 +0000 | |||
1661 | +++ charmhelpers/core/services/base.py 2021-11-10 05:36:20 +0000 | |||
1662 | @@ -14,9 +14,11 @@ | |||
1663 | 14 | 14 | ||
1664 | 15 | import os | 15 | import os |
1665 | 16 | import json | 16 | import json |
1667 | 17 | from inspect import getargspec | 17 | import inspect |
1668 | 18 | from collections import Iterable, OrderedDict | 18 | from collections import Iterable, OrderedDict |
1669 | 19 | 19 | ||
1670 | 20 | import six | ||
1671 | 21 | |||
1672 | 20 | from charmhelpers.core import host | 22 | from charmhelpers.core import host |
1673 | 21 | from charmhelpers.core import hookenv | 23 | from charmhelpers.core import hookenv |
1674 | 22 | 24 | ||
1675 | @@ -169,7 +171,10 @@ | |||
1676 | 169 | if not units: | 171 | if not units: |
1677 | 170 | continue | 172 | continue |
1678 | 171 | remote_service = units[0].split('/')[0] | 173 | remote_service = units[0].split('/')[0] |
1680 | 172 | argspec = getargspec(provider.provide_data) | 174 | if six.PY2: |
1681 | 175 | argspec = inspect.getargspec(provider.provide_data) | ||
1682 | 176 | else: | ||
1683 | 177 | argspec = inspect.getfullargspec(provider.provide_data) | ||
1684 | 173 | if len(argspec.args) > 1: | 178 | if len(argspec.args) > 1: |
1685 | 174 | data = provider.provide_data(remote_service, service_ready) | 179 | data = provider.provide_data(remote_service, service_ready) |
1686 | 175 | else: | 180 | else: |
1687 | 176 | 181 | ||
1688 | === modified file 'charmhelpers/core/strutils.py' | |||
1689 | --- charmhelpers/core/strutils.py 2019-05-24 12:41:48 +0000 | |||
1690 | +++ charmhelpers/core/strutils.py 2021-11-10 05:36:20 +0000 | |||
1691 | @@ -18,8 +18,11 @@ | |||
1692 | 18 | import six | 18 | import six |
1693 | 19 | import re | 19 | import re |
1694 | 20 | 20 | ||
1697 | 21 | 21 | TRUTHY_STRINGS = {'y', 'yes', 'true', 't', 'on'} | |
1698 | 22 | def bool_from_string(value): | 22 | FALSEY_STRINGS = {'n', 'no', 'false', 'f', 'off'} |
1699 | 23 | |||
1700 | 24 | |||
1701 | 25 | def bool_from_string(value, truthy_strings=TRUTHY_STRINGS, falsey_strings=FALSEY_STRINGS, assume_false=False): | ||
1702 | 23 | """Interpret string value as boolean. | 26 | """Interpret string value as boolean. |
1703 | 24 | 27 | ||
1704 | 25 | Returns True if value translates to True otherwise False. | 28 | Returns True if value translates to True otherwise False. |
1705 | @@ -32,9 +35,9 @@ | |||
1706 | 32 | 35 | ||
1707 | 33 | value = value.strip().lower() | 36 | value = value.strip().lower() |
1708 | 34 | 37 | ||
1710 | 35 | if value in ['y', 'yes', 'true', 't', 'on']: | 38 | if value in truthy_strings: |
1711 | 36 | return True | 39 | return True |
1713 | 37 | elif value in ['n', 'no', 'false', 'f', 'off']: | 40 | elif value in falsey_strings or assume_false: |
1714 | 38 | return False | 41 | return False |
1715 | 39 | 42 | ||
1716 | 40 | msg = "Unable to interpret string value '%s' as boolean" % (value) | 43 | msg = "Unable to interpret string value '%s' as boolean" % (value) |
1717 | 41 | 44 | ||
1718 | === modified file 'charmhelpers/core/sysctl.py' | |||
1719 | --- charmhelpers/core/sysctl.py 2019-05-24 12:41:48 +0000 | |||
1720 | +++ charmhelpers/core/sysctl.py 2021-11-10 05:36:20 +0000 | |||
1721 | @@ -17,14 +17,17 @@ | |||
1722 | 17 | 17 | ||
1723 | 18 | import yaml | 18 | import yaml |
1724 | 19 | 19 | ||
1726 | 20 | from subprocess import check_call | 20 | from subprocess import check_call, CalledProcessError |
1727 | 21 | 21 | ||
1728 | 22 | from charmhelpers.core.hookenv import ( | 22 | from charmhelpers.core.hookenv import ( |
1729 | 23 | log, | 23 | log, |
1730 | 24 | DEBUG, | 24 | DEBUG, |
1731 | 25 | ERROR, | 25 | ERROR, |
1732 | 26 | WARNING, | ||
1733 | 26 | ) | 27 | ) |
1734 | 27 | 28 | ||
1735 | 29 | from charmhelpers.core.host import is_container | ||
1736 | 30 | |||
1737 | 28 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' | 31 | __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>' |
1738 | 29 | 32 | ||
1739 | 30 | 33 | ||
1740 | @@ -62,4 +65,11 @@ | |||
1741 | 62 | if ignore: | 65 | if ignore: |
1742 | 63 | call.append("-e") | 66 | call.append("-e") |
1743 | 64 | 67 | ||
1745 | 65 | check_call(call) | 68 | try: |
1746 | 69 | check_call(call) | ||
1747 | 70 | except CalledProcessError as e: | ||
1748 | 71 | if is_container(): | ||
1749 | 72 | log("Error setting some sysctl keys in this container: {}".format(e.output), | ||
1750 | 73 | level=WARNING) | ||
1751 | 74 | else: | ||
1752 | 75 | raise e | ||
1753 | 66 | 76 | ||
1754 | === modified file 'charmhelpers/core/unitdata.py' | |||
1755 | --- charmhelpers/core/unitdata.py 2019-05-24 12:41:48 +0000 | |||
1756 | +++ charmhelpers/core/unitdata.py 2021-11-10 05:36:20 +0000 | |||
1757 | @@ -1,7 +1,7 @@ | |||
1758 | 1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
1759 | 2 | # -*- coding: utf-8 -*- | 2 | # -*- coding: utf-8 -*- |
1760 | 3 | # | 3 | # |
1762 | 4 | # Copyright 2014-2015 Canonical Limited. | 4 | # Copyright 2014-2021 Canonical Limited. |
1763 | 5 | # | 5 | # |
1764 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1765 | 7 | # you may not use this file except in compliance with the License. | 7 | # you may not use this file except in compliance with the License. |
1766 | @@ -61,7 +61,7 @@ | |||
1767 | 61 | 'previous value', prev, | 61 | 'previous value', prev, |
1768 | 62 | 'current value', cur) | 62 | 'current value', cur) |
1769 | 63 | 63 | ||
1771 | 64 | # Get some unit specific bookeeping | 64 | # Get some unit specific bookkeeping |
1772 | 65 | if not db.get('pkg_key'): | 65 | if not db.get('pkg_key'): |
1773 | 66 | key = urllib.urlopen('https://example.com/pkg_key').read() | 66 | key = urllib.urlopen('https://example.com/pkg_key').read() |
1774 | 67 | db.set('pkg_key', key) | 67 | db.set('pkg_key', key) |
1775 | @@ -449,7 +449,7 @@ | |||
1776 | 449 | 'previous value', prev, | 449 | 'previous value', prev, |
1777 | 450 | 'current value', cur) | 450 | 'current value', cur) |
1778 | 451 | 451 | ||
1780 | 452 | # Get some unit specific bookeeping | 452 | # Get some unit specific bookkeeping |
1781 | 453 | if not db.get('pkg_key'): | 453 | if not db.get('pkg_key'): |
1782 | 454 | key = urllib.urlopen('https://example.com/pkg_key').read() | 454 | key = urllib.urlopen('https://example.com/pkg_key').read() |
1783 | 455 | db.set('pkg_key', key) | 455 | db.set('pkg_key', key) |
1784 | 456 | 456 | ||
1785 | === modified file 'charmhelpers/fetch/__init__.py' | |||
1786 | --- charmhelpers/fetch/__init__.py 2019-05-24 12:41:48 +0000 | |||
1787 | +++ charmhelpers/fetch/__init__.py 2021-11-10 05:36:20 +0000 | |||
1788 | @@ -1,4 +1,4 @@ | |||
1790 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2021 Canonical Limited. |
1791 | 2 | # | 2 | # |
1792 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1793 | 4 | # you may not use this file except in compliance with the License. | 4 | # you may not use this file except in compliance with the License. |
1794 | @@ -103,6 +103,11 @@ | |||
1795 | 103 | apt_unhold = fetch.apt_unhold | 103 | apt_unhold = fetch.apt_unhold |
1796 | 104 | import_key = fetch.import_key | 104 | import_key = fetch.import_key |
1797 | 105 | get_upstream_version = fetch.get_upstream_version | 105 | get_upstream_version = fetch.get_upstream_version |
1798 | 106 | apt_pkg = fetch.ubuntu_apt_pkg | ||
1799 | 107 | get_apt_dpkg_env = fetch.get_apt_dpkg_env | ||
1800 | 108 | get_installed_version = fetch.get_installed_version | ||
1801 | 109 | OPENSTACK_RELEASES = fetch.OPENSTACK_RELEASES | ||
1802 | 110 | UBUNTU_OPENSTACK_RELEASE = fetch.UBUNTU_OPENSTACK_RELEASE | ||
1803 | 106 | elif __platform__ == "centos": | 111 | elif __platform__ == "centos": |
1804 | 107 | yum_search = fetch.yum_search | 112 | yum_search = fetch.yum_search |
1805 | 108 | 113 | ||
1806 | @@ -200,7 +205,7 @@ | |||
1807 | 200 | classname) | 205 | classname) |
1808 | 201 | plugin_list.append(handler_class()) | 206 | plugin_list.append(handler_class()) |
1809 | 202 | except NotImplementedError: | 207 | except NotImplementedError: |
1811 | 203 | # Skip missing plugins so that they can be ommitted from | 208 | # Skip missing plugins so that they can be omitted from |
1812 | 204 | # installation if desired | 209 | # installation if desired |
1813 | 205 | log("FetchHandler {} not found, skipping plugin".format( | 210 | log("FetchHandler {} not found, skipping plugin".format( |
1814 | 206 | handler_name)) | 211 | handler_name)) |
1815 | 207 | 212 | ||
1816 | === modified file 'charmhelpers/fetch/python/packages.py' | |||
1817 | --- charmhelpers/fetch/python/packages.py 2019-05-24 12:41:48 +0000 | |||
1818 | +++ charmhelpers/fetch/python/packages.py 2021-11-10 05:36:20 +0000 | |||
1819 | @@ -1,7 +1,7 @@ | |||
1820 | 1 | #!/usr/bin/env python | 1 | #!/usr/bin/env python |
1821 | 2 | # coding: utf-8 | 2 | # coding: utf-8 |
1822 | 3 | 3 | ||
1824 | 4 | # Copyright 2014-2015 Canonical Limited. | 4 | # Copyright 2014-2021 Canonical Limited. |
1825 | 5 | # | 5 | # |
1826 | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); | 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1827 | 7 | # you may not use this file except in compliance with the License. | 7 | # you may not use this file except in compliance with the License. |
1828 | @@ -27,7 +27,7 @@ | |||
1829 | 27 | 27 | ||
1830 | 28 | 28 | ||
1831 | 29 | def pip_execute(*args, **kwargs): | 29 | def pip_execute(*args, **kwargs): |
1833 | 30 | """Overriden pip_execute() to stop sys.path being changed. | 30 | """Overridden pip_execute() to stop sys.path being changed. |
1834 | 31 | 31 | ||
1835 | 32 | The act of importing main from the pip module seems to cause add wheels | 32 | The act of importing main from the pip module seems to cause add wheels |
1836 | 33 | from the /usr/share/python-wheels which are installed by various tools. | 33 | from the /usr/share/python-wheels which are installed by various tools. |
1837 | @@ -142,8 +142,10 @@ | |||
1838 | 142 | """Create an isolated Python environment.""" | 142 | """Create an isolated Python environment.""" |
1839 | 143 | if six.PY2: | 143 | if six.PY2: |
1840 | 144 | apt_install('python-virtualenv') | 144 | apt_install('python-virtualenv') |
1841 | 145 | extra_flags = [] | ||
1842 | 145 | else: | 146 | else: |
1844 | 146 | apt_install('python3-virtualenv') | 147 | apt_install(['python3-virtualenv', 'virtualenv']) |
1845 | 148 | extra_flags = ['--python=python3'] | ||
1846 | 147 | 149 | ||
1847 | 148 | if path: | 150 | if path: |
1848 | 149 | venv_path = path | 151 | venv_path = path |
1849 | @@ -151,4 +153,4 @@ | |||
1850 | 151 | venv_path = os.path.join(charm_dir(), 'venv') | 153 | venv_path = os.path.join(charm_dir(), 'venv') |
1851 | 152 | 154 | ||
1852 | 153 | if not os.path.exists(venv_path): | 155 | if not os.path.exists(venv_path): |
1854 | 154 | subprocess.check_call(['virtualenv', venv_path]) | 156 | subprocess.check_call(['virtualenv', venv_path] + extra_flags) |
1855 | 155 | 157 | ||
1856 | === modified file 'charmhelpers/fetch/snap.py' | |||
1857 | --- charmhelpers/fetch/snap.py 2019-05-24 12:41:48 +0000 | |||
1858 | +++ charmhelpers/fetch/snap.py 2021-11-10 05:36:20 +0000 | |||
1859 | @@ -1,4 +1,4 @@ | |||
1861 | 1 | # Copyright 2014-2017 Canonical Limited. | 1 | # Copyright 2014-2021 Canonical Limited. |
1862 | 2 | # | 2 | # |
1863 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1864 | 4 | # you may not use this file except in compliance with the License. | 4 | # you may not use this file except in compliance with the License. |
1865 | @@ -65,11 +65,11 @@ | |||
1866 | 65 | retry_count += + 1 | 65 | retry_count += + 1 |
1867 | 66 | if retry_count > SNAP_NO_LOCK_RETRY_COUNT: | 66 | if retry_count > SNAP_NO_LOCK_RETRY_COUNT: |
1868 | 67 | raise CouldNotAcquireLockException( | 67 | raise CouldNotAcquireLockException( |
1870 | 68 | 'Could not aquire lock after {} attempts' | 68 | 'Could not acquire lock after {} attempts' |
1871 | 69 | .format(SNAP_NO_LOCK_RETRY_COUNT)) | 69 | .format(SNAP_NO_LOCK_RETRY_COUNT)) |
1872 | 70 | return_code = e.returncode | 70 | return_code = e.returncode |
1873 | 71 | log('Snap failed to acquire lock, trying again in {} seconds.' | 71 | log('Snap failed to acquire lock, trying again in {} seconds.' |
1875 | 72 | .format(SNAP_NO_LOCK_RETRY_DELAY, level='WARN')) | 72 | .format(SNAP_NO_LOCK_RETRY_DELAY), level='WARN') |
1876 | 73 | sleep(SNAP_NO_LOCK_RETRY_DELAY) | 73 | sleep(SNAP_NO_LOCK_RETRY_DELAY) |
1877 | 74 | 74 | ||
1878 | 75 | return return_code | 75 | return return_code |
1879 | 76 | 76 | ||
1880 | === modified file 'charmhelpers/fetch/ubuntu.py' | |||
1881 | --- charmhelpers/fetch/ubuntu.py 2019-05-24 12:41:48 +0000 | |||
1882 | +++ charmhelpers/fetch/ubuntu.py 2021-11-10 05:36:20 +0000 | |||
1883 | @@ -1,4 +1,4 @@ | |||
1885 | 1 | # Copyright 2014-2015 Canonical Limited. | 1 | # Copyright 2014-2021 Canonical Limited. |
1886 | 2 | # | 2 | # |
1887 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
1888 | 4 | # you may not use this file except in compliance with the License. | 4 | # you may not use this file except in compliance with the License. |
1889 | @@ -17,10 +17,12 @@ | |||
1890 | 17 | import platform | 17 | import platform |
1891 | 18 | import re | 18 | import re |
1892 | 19 | import six | 19 | import six |
1893 | 20 | import subprocess | ||
1894 | 21 | import sys | ||
1895 | 20 | import time | 22 | import time |
1896 | 21 | import subprocess | ||
1897 | 22 | 23 | ||
1899 | 23 | from charmhelpers.core.host import get_distrib_codename | 24 | from charmhelpers import deprecate |
1900 | 25 | from charmhelpers.core.host import get_distrib_codename, get_system_env | ||
1901 | 24 | 26 | ||
1902 | 25 | from charmhelpers.core.hookenv import ( | 27 | from charmhelpers.core.hookenv import ( |
1903 | 26 | log, | 28 | log, |
1904 | @@ -29,6 +31,7 @@ | |||
1905 | 29 | env_proxy_settings, | 31 | env_proxy_settings, |
1906 | 30 | ) | 32 | ) |
1907 | 31 | from charmhelpers.fetch import SourceConfigError, GPGKeyError | 33 | from charmhelpers.fetch import SourceConfigError, GPGKeyError |
1908 | 34 | from charmhelpers.fetch import ubuntu_apt_pkg | ||
1909 | 32 | 35 | ||
1910 | 33 | PROPOSED_POCKET = ( | 36 | PROPOSED_POCKET = ( |
1911 | 34 | "# Proposed\n" | 37 | "# Proposed\n" |
1912 | @@ -173,12 +176,112 @@ | |||
1913 | 173 | 'stein/proposed': 'bionic-proposed/stein', | 176 | 'stein/proposed': 'bionic-proposed/stein', |
1914 | 174 | 'bionic-stein/proposed': 'bionic-proposed/stein', | 177 | 'bionic-stein/proposed': 'bionic-proposed/stein', |
1915 | 175 | 'bionic-proposed/stein': 'bionic-proposed/stein', | 178 | 'bionic-proposed/stein': 'bionic-proposed/stein', |
1916 | 179 | # Train | ||
1917 | 180 | 'train': 'bionic-updates/train', | ||
1918 | 181 | 'bionic-train': 'bionic-updates/train', | ||
1919 | 182 | 'bionic-train/updates': 'bionic-updates/train', | ||
1920 | 183 | 'bionic-updates/train': 'bionic-updates/train', | ||
1921 | 184 | 'train/proposed': 'bionic-proposed/train', | ||
1922 | 185 | 'bionic-train/proposed': 'bionic-proposed/train', | ||
1923 | 186 | 'bionic-proposed/train': 'bionic-proposed/train', | ||
1924 | 187 | # Ussuri | ||
1925 | 188 | 'ussuri': 'bionic-updates/ussuri', | ||
1926 | 189 | 'bionic-ussuri': 'bionic-updates/ussuri', | ||
1927 | 190 | 'bionic-ussuri/updates': 'bionic-updates/ussuri', | ||
1928 | 191 | 'bionic-updates/ussuri': 'bionic-updates/ussuri', | ||
1929 | 192 | 'ussuri/proposed': 'bionic-proposed/ussuri', | ||
1930 | 193 | 'bionic-ussuri/proposed': 'bionic-proposed/ussuri', | ||
1931 | 194 | 'bionic-proposed/ussuri': 'bionic-proposed/ussuri', | ||
1932 | 195 | # Victoria | ||
1933 | 196 | 'victoria': 'focal-updates/victoria', | ||
1934 | 197 | 'focal-victoria': 'focal-updates/victoria', | ||
1935 | 198 | 'focal-victoria/updates': 'focal-updates/victoria', | ||
1936 | 199 | 'focal-updates/victoria': 'focal-updates/victoria', | ||
1937 | 200 | 'victoria/proposed': 'focal-proposed/victoria', | ||
1938 | 201 | 'focal-victoria/proposed': 'focal-proposed/victoria', | ||
1939 | 202 | 'focal-proposed/victoria': 'focal-proposed/victoria', | ||
1940 | 203 | # Wallaby | ||
1941 | 204 | 'wallaby': 'focal-updates/wallaby', | ||
1942 | 205 | 'focal-wallaby': 'focal-updates/wallaby', | ||
1943 | 206 | 'focal-wallaby/updates': 'focal-updates/wallaby', | ||
1944 | 207 | 'focal-updates/wallaby': 'focal-updates/wallaby', | ||
1945 | 208 | 'wallaby/proposed': 'focal-proposed/wallaby', | ||
1946 | 209 | 'focal-wallaby/proposed': 'focal-proposed/wallaby', | ||
1947 | 210 | 'focal-proposed/wallaby': 'focal-proposed/wallaby', | ||
1948 | 211 | # Xena | ||
1949 | 212 | 'xena': 'focal-updates/xena', | ||
1950 | 213 | 'focal-xena': 'focal-updates/xena', | ||
1951 | 214 | 'focal-xena/updates': 'focal-updates/xena', | ||
1952 | 215 | 'focal-updates/xena': 'focal-updates/xena', | ||
1953 | 216 | 'xena/proposed': 'focal-proposed/xena', | ||
1954 | 217 | 'focal-xena/proposed': 'focal-proposed/xena', | ||
1955 | 218 | 'focal-proposed/xena': 'focal-proposed/xena', | ||
1956 | 219 | # Yoga | ||
1957 | 220 | 'yoga': 'focal-updates/yoga', | ||
1958 | 221 | 'focal-yoga': 'focal-updates/yoga', | ||
1959 | 222 | 'focal-yoga/updates': 'focal-updates/yoga', | ||
1960 | 223 | 'focal-updates/yoga': 'focal-updates/yoga', | ||
1961 | 224 | 'yoga/proposed': 'focal-proposed/yoga', | ||
1962 | 225 | 'focal-yoga/proposed': 'focal-proposed/yoga', | ||
1963 | 226 | 'focal-proposed/yoga': 'focal-proposed/yoga', | ||
1964 | 176 | } | 227 | } |
1965 | 177 | 228 | ||
1966 | 178 | 229 | ||
1967 | 230 | OPENSTACK_RELEASES = ( | ||
1968 | 231 | 'diablo', | ||
1969 | 232 | 'essex', | ||
1970 | 233 | 'folsom', | ||
1971 | 234 | 'grizzly', | ||
1972 | 235 | 'havana', | ||
1973 | 236 | 'icehouse', | ||
1974 | 237 | 'juno', | ||
1975 | 238 | 'kilo', | ||
1976 | 239 | 'liberty', | ||
1977 | 240 | 'mitaka', | ||
1978 | 241 | 'newton', | ||
1979 | 242 | 'ocata', | ||
1980 | 243 | 'pike', | ||
1981 | 244 | 'queens', | ||
1982 | 245 | 'rocky', | ||
1983 | 246 | 'stein', | ||
1984 | 247 | 'train', | ||
1985 | 248 | 'ussuri', | ||
1986 | 249 | 'victoria', | ||
1987 | 250 | 'wallaby', | ||
1988 | 251 | 'xena', | ||
1989 | 252 | 'yoga', | ||
1990 | 253 | ) | ||
1991 | 254 | |||
1992 | 255 | |||
1993 | 256 | UBUNTU_OPENSTACK_RELEASE = OrderedDict([ | ||
1994 | 257 | ('oneiric', 'diablo'), | ||
1995 | 258 | ('precise', 'essex'), | ||
1996 | 259 | ('quantal', 'folsom'), | ||
1997 | 260 | ('raring', 'grizzly'), | ||
1998 | 261 | ('saucy', 'havana'), | ||
1999 | 262 | ('trusty', 'icehouse'), | ||
2000 | 263 | ('utopic', 'juno'), | ||
2001 | 264 | ('vivid', 'kilo'), | ||
2002 | 265 | ('wily', 'liberty'), | ||
2003 | 266 | ('xenial', 'mitaka'), | ||
2004 | 267 | ('yakkety', 'newton'), | ||
2005 | 268 | ('zesty', 'ocata'), | ||
2006 | 269 | ('artful', 'pike'), | ||
2007 | 270 | ('bionic', 'queens'), | ||
2008 | 271 | ('cosmic', 'rocky'), | ||
2009 | 272 | ('disco', 'stein'), | ||
2010 | 273 | ('eoan', 'train'), | ||
2011 | 274 | ('focal', 'ussuri'), | ||
2012 | 275 | ('groovy', 'victoria'), | ||
2013 | 276 | ('hirsute', 'wallaby'), | ||
2014 | 277 | ('impish', 'xena'), | ||
2015 | 278 | ('jammy', 'yoga'), | ||
2016 | 279 | ]) | ||
2017 | 280 | |||
2018 | 281 | |||
2019 | 179 | APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. | 282 | APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT. |
2020 | 180 | CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries. | 283 | CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries. |
2022 | 181 | CMD_RETRY_COUNT = 3 # Retry a failing fatal command X times. | 284 | CMD_RETRY_COUNT = 10 # Retry a failing fatal command X times. |
2023 | 182 | 285 | ||
2024 | 183 | 286 | ||
2025 | 184 | def filter_installed_packages(packages): | 287 | def filter_installed_packages(packages): |
2026 | @@ -208,18 +311,50 @@ | |||
2027 | 208 | ) | 311 | ) |
2028 | 209 | 312 | ||
2029 | 210 | 313 | ||
2042 | 211 | def apt_cache(in_memory=True, progress=None): | 314 | def apt_cache(*_, **__): |
2043 | 212 | """Build and return an apt cache.""" | 315 | """Shim returning an object simulating the apt_pkg Cache. |
2044 | 213 | from apt import apt_pkg | 316 | |
2045 | 214 | apt_pkg.init() | 317 | :param _: Accept arguments for compatibility, not used. |
2046 | 215 | if in_memory: | 318 | :type _: any |
2047 | 216 | apt_pkg.config.set("Dir::Cache::pkgcache", "") | 319 | :param __: Accept keyword arguments for compatibility, not used. |
2048 | 217 | apt_pkg.config.set("Dir::Cache::srcpkgcache", "") | 320 | :type __: any |
2049 | 218 | return apt_pkg.Cache(progress) | 321 | :returns:Object used to interrogate the system apt and dpkg databases. |
2050 | 219 | 322 | :rtype:ubuntu_apt_pkg.Cache | |
2051 | 220 | 323 | """ | |
2052 | 221 | def apt_install(packages, options=None, fatal=False): | 324 | if 'apt_pkg' in sys.modules: |
2053 | 222 | """Install one or more packages.""" | 325 | # NOTE(fnordahl): When our consumer use the upstream ``apt_pkg`` module |
2054 | 326 | # in conjunction with the apt_cache helper function, they may expect us | ||
2055 | 327 | # to call ``apt_pkg.init()`` for them. | ||
2056 | 328 | # | ||
2057 | 329 | # Detect this situation, log a warning and make the call to | ||
2058 | 330 | # ``apt_pkg.init()`` to avoid the consumer Python interpreter from | ||
2059 | 331 | # crashing with a segmentation fault. | ||
2060 | 332 | @deprecate( | ||
2061 | 333 | 'Support for use of upstream ``apt_pkg`` module in conjunction' | ||
2062 | 334 | 'with charm-helpers is deprecated since 2019-06-25', | ||
2063 | 335 | date=None, log=lambda x: log(x, level=WARNING)) | ||
2064 | 336 | def one_shot_log(): | ||
2065 | 337 | pass | ||
2066 | 338 | |||
2067 | 339 | one_shot_log() | ||
2068 | 340 | sys.modules['apt_pkg'].init() | ||
2069 | 341 | return ubuntu_apt_pkg.Cache() | ||
2070 | 342 | |||
2071 | 343 | |||
2072 | 344 | def apt_install(packages, options=None, fatal=False, quiet=False): | ||
2073 | 345 | """Install one or more packages. | ||
2074 | 346 | |||
2075 | 347 | :param packages: Package(s) to install | ||
2076 | 348 | :type packages: Option[str, List[str]] | ||
2077 | 349 | :param options: Options to pass on to apt-get | ||
2078 | 350 | :type options: Option[None, List[str]] | ||
2079 | 351 | :param fatal: Whether the command's output should be checked and | ||
2080 | 352 | retried. | ||
2081 | 353 | :type fatal: bool | ||
2082 | 354 | :param quiet: if True (default), suppress log message to stdout/stderr | ||
2083 | 355 | :type quiet: bool | ||
2084 | 356 | :raises: subprocess.CalledProcessError | ||
2085 | 357 | """ | ||
2086 | 223 | if options is None: | 358 | if options is None: |
2087 | 224 | options = ['--option=Dpkg::Options::=--force-confold'] | 359 | options = ['--option=Dpkg::Options::=--force-confold'] |
2088 | 225 | 360 | ||
2089 | @@ -230,13 +365,24 @@ | |||
2090 | 230 | cmd.append(packages) | 365 | cmd.append(packages) |
2091 | 231 | else: | 366 | else: |
2092 | 232 | cmd.extend(packages) | 367 | cmd.extend(packages) |
2096 | 233 | log("Installing {} with options: {}".format(packages, | 368 | if not quiet: |
2097 | 234 | options)) | 369 | log("Installing {} with options: {}" |
2098 | 235 | _run_apt_command(cmd, fatal) | 370 | .format(packages, options)) |
2099 | 371 | _run_apt_command(cmd, fatal, quiet=quiet) | ||
2100 | 236 | 372 | ||
2101 | 237 | 373 | ||
2102 | 238 | def apt_upgrade(options=None, fatal=False, dist=False): | 374 | def apt_upgrade(options=None, fatal=False, dist=False): |
2104 | 239 | """Upgrade all packages.""" | 375 | """Upgrade all packages. |
2105 | 376 | |||
2106 | 377 | :param options: Options to pass on to apt-get | ||
2107 | 378 | :type options: Option[None, List[str]] | ||
2108 | 379 | :param fatal: Whether the command's output should be checked and | ||
2109 | 380 | retried. | ||
2110 | 381 | :type fatal: bool | ||
2111 | 382 | :param dist: Whether ``dist-upgrade`` should be used over ``upgrade`` | ||
2112 | 383 | :type dist: bool | ||
2113 | 384 | :raises: subprocess.CalledProcessError | ||
2114 | 385 | """ | ||
2115 | 240 | if options is None: | 386 | if options is None: |
2116 | 241 | options = ['--option=Dpkg::Options::=--force-confold'] | 387 | options = ['--option=Dpkg::Options::=--force-confold'] |
2117 | 242 | 388 | ||
2118 | @@ -257,7 +403,15 @@ | |||
2119 | 257 | 403 | ||
2120 | 258 | 404 | ||
2121 | 259 | def apt_purge(packages, fatal=False): | 405 | def apt_purge(packages, fatal=False): |
2123 | 260 | """Purge one or more packages.""" | 406 | """Purge one or more packages. |
2124 | 407 | |||
2125 | 408 | :param packages: Package(s) to install | ||
2126 | 409 | :type packages: Option[str, List[str]] | ||
2127 | 410 | :param fatal: Whether the command's output should be checked and | ||
2128 | 411 | retried. | ||
2129 | 412 | :type fatal: bool | ||
2130 | 413 | :raises: subprocess.CalledProcessError | ||
2131 | 414 | """ | ||
2132 | 261 | cmd = ['apt-get', '--assume-yes', 'purge'] | 415 | cmd = ['apt-get', '--assume-yes', 'purge'] |
2133 | 262 | if isinstance(packages, six.string_types): | 416 | if isinstance(packages, six.string_types): |
2134 | 263 | cmd.append(packages) | 417 | cmd.append(packages) |
2135 | @@ -268,7 +422,14 @@ | |||
2136 | 268 | 422 | ||
2137 | 269 | 423 | ||
2138 | 270 | def apt_autoremove(purge=True, fatal=False): | 424 | def apt_autoremove(purge=True, fatal=False): |
2140 | 271 | """Purge one or more packages.""" | 425 | """Purge one or more packages. |
2141 | 426 | :param purge: Whether the ``--purge`` option should be passed on or not. | ||
2142 | 427 | :type purge: bool | ||
2143 | 428 | :param fatal: Whether the command's output should be checked and | ||
2144 | 429 | retried. | ||
2145 | 430 | :type fatal: bool | ||
2146 | 431 | :raises: subprocess.CalledProcessError | ||
2147 | 432 | """ | ||
2148 | 272 | cmd = ['apt-get', '--assume-yes', 'autoremove'] | 433 | cmd = ['apt-get', '--assume-yes', 'autoremove'] |
2149 | 273 | if purge: | 434 | if purge: |
2150 | 274 | cmd.append('--purge') | 435 | cmd.append('--purge') |
2151 | @@ -304,7 +465,7 @@ | |||
2152 | 304 | A Radix64 format keyid is also supported for backwards | 465 | A Radix64 format keyid is also supported for backwards |
2153 | 305 | compatibility. In this case Ubuntu keyserver will be | 466 | compatibility. In this case Ubuntu keyserver will be |
2154 | 306 | queried for a key via HTTPS by its keyid. This method | 467 | queried for a key via HTTPS by its keyid. This method |
2156 | 307 | is less preferrable because https proxy servers may | 468 | is less preferable because https proxy servers may |
2157 | 308 | require traffic decryption which is equivalent to a | 469 | require traffic decryption which is equivalent to a |
2158 | 309 | man-in-the-middle attack (a proxy server impersonates | 470 | man-in-the-middle attack (a proxy server impersonates |
2159 | 310 | keyserver TLS certificates and has to be explicitly | 471 | keyserver TLS certificates and has to be explicitly |
2160 | @@ -481,6 +642,10 @@ | |||
2161 | 481 | with be used. If staging is NOT used then the cloud archive [3] will be | 642 | with be used. If staging is NOT used then the cloud archive [3] will be |
2162 | 482 | added, and the 'ubuntu-cloud-keyring' package will be added for the | 643 | added, and the 'ubuntu-cloud-keyring' package will be added for the |
2163 | 483 | current distro. | 644 | current distro. |
2164 | 645 | '<openstack-version>': translate to cloud:<release> based on the current | ||
2165 | 646 | distro version (i.e. for 'ussuri' this will either be 'bionic-ussuri' or | ||
2166 | 647 | 'distro'. | ||
2167 | 648 | '<openstack-version>/proposed': as above, but for proposed. | ||
2168 | 484 | 649 | ||
2169 | 485 | Otherwise the source is not recognised and this is logged to the juju log. | 650 | Otherwise the source is not recognised and this is logged to the juju log. |
2170 | 486 | However, no error is raised, unless sys_error_on_exit is True. | 651 | However, no error is raised, unless sys_error_on_exit is True. |
2171 | @@ -499,7 +664,7 @@ | |||
2172 | 499 | id may also be used, but be aware that only insecure protocols are | 664 | id may also be used, but be aware that only insecure protocols are |
2173 | 500 | available to retrieve the actual public key from a public keyserver | 665 | available to retrieve the actual public key from a public keyserver |
2174 | 501 | placing your Juju environment at risk. ppa and cloud archive keys | 666 | placing your Juju environment at risk. ppa and cloud archive keys |
2176 | 502 | are securely added automtically, so sould not be provided. | 667 | are securely added automatically, so should not be provided. |
2177 | 503 | 668 | ||
2178 | 504 | @param fail_invalid: (boolean) if True, then the function raises a | 669 | @param fail_invalid: (boolean) if True, then the function raises a |
2179 | 505 | SourceConfigError is there is no matching installation source. | 670 | SourceConfigError is there is no matching installation source. |
2180 | @@ -507,6 +672,12 @@ | |||
2181 | 507 | @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a | 672 | @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a |
2182 | 508 | valid pocket in CLOUD_ARCHIVE_POCKETS | 673 | valid pocket in CLOUD_ARCHIVE_POCKETS |
2183 | 509 | """ | 674 | """ |
2184 | 675 | # extract the OpenStack versions from the CLOUD_ARCHIVE_POCKETS; can't use | ||
2185 | 676 | # the list in contrib.openstack.utils as it might not be included in | ||
2186 | 677 | # classic charms and would break everything. Having OpenStack specific | ||
2187 | 678 | # code in this file is a bit of an antipattern, anyway. | ||
2188 | 679 | os_versions_regex = "({})".format("|".join(OPENSTACK_RELEASES)) | ||
2189 | 680 | |||
2190 | 510 | _mapping = OrderedDict([ | 681 | _mapping = OrderedDict([ |
2191 | 511 | (r"^distro$", lambda: None), # This is a NOP | 682 | (r"^distro$", lambda: None), # This is a NOP |
2192 | 512 | (r"^(?:proposed|distro-proposed)$", _add_proposed), | 683 | (r"^(?:proposed|distro-proposed)$", _add_proposed), |
2193 | @@ -516,6 +687,9 @@ | |||
2194 | 516 | (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check), | 687 | (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check), |
2195 | 517 | (r"^cloud:(.*)$", _add_cloud_pocket), | 688 | (r"^cloud:(.*)$", _add_cloud_pocket), |
2196 | 518 | (r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check), | 689 | (r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check), |
2197 | 690 | (r"^{}\/proposed$".format(os_versions_regex), | ||
2198 | 691 | _add_bare_openstack_proposed), | ||
2199 | 692 | (r"^{}$".format(os_versions_regex), _add_bare_openstack), | ||
2200 | 519 | ]) | 693 | ]) |
2201 | 520 | if source is None: | 694 | if source is None: |
2202 | 521 | source = '' | 695 | source = '' |
2203 | @@ -547,7 +721,7 @@ | |||
2204 | 547 | Uses get_distrib_codename to determine the correct stanza for | 721 | Uses get_distrib_codename to determine the correct stanza for |
2205 | 548 | the deb line. | 722 | the deb line. |
2206 | 549 | 723 | ||
2208 | 550 | For intel architecutres PROPOSED_POCKET is used for the release, but for | 724 | For Intel architectures PROPOSED_POCKET is used for the release, but for |
2209 | 551 | other architectures PROPOSED_PORTS_POCKET is used for the release. | 725 | other architectures PROPOSED_PORTS_POCKET is used for the release. |
2210 | 552 | """ | 726 | """ |
2211 | 553 | release = get_distrib_codename() | 727 | release = get_distrib_codename() |
2212 | @@ -568,11 +742,9 @@ | |||
2213 | 568 | if '{series}' in spec: | 742 | if '{series}' in spec: |
2214 | 569 | series = get_distrib_codename() | 743 | series = get_distrib_codename() |
2215 | 570 | spec = spec.replace('{series}', series) | 744 | spec = spec.replace('{series}', series) |
2216 | 571 | # software-properties package for bionic properly reacts to proxy settings | ||
2217 | 572 | # passed as environment variables (See lp:1433761). This is not the case | ||
2218 | 573 | # LTS and non-LTS releases below bionic. | ||
2219 | 574 | _run_with_retries(['add-apt-repository', '--yes', spec], | 745 | _run_with_retries(['add-apt-repository', '--yes', spec], |
2221 | 575 | cmd_env=env_proxy_settings(['https'])) | 746 | cmd_env=env_proxy_settings(['https', 'http', 'no_proxy']) |
2222 | 747 | ) | ||
2223 | 576 | 748 | ||
2224 | 577 | 749 | ||
2225 | 578 | def _add_cloud_pocket(pocket): | 750 | def _add_cloud_pocket(pocket): |
2226 | @@ -648,25 +820,102 @@ | |||
2227 | 648 | 'version ({})'.format(release, os_release, ubuntu_rel)) | 820 | 'version ({})'.format(release, os_release, ubuntu_rel)) |
2228 | 649 | 821 | ||
2229 | 650 | 822 | ||
2230 | 823 | def _add_bare_openstack(openstack_release): | ||
2231 | 824 | """Add cloud or distro based on the release given. | ||
2232 | 825 | |||
2233 | 826 | The spec given is, say, 'ussuri', but this could apply cloud:bionic-ussuri | ||
2234 | 827 | or 'distro' depending on whether the ubuntu release is bionic or focal. | ||
2235 | 828 | |||
2236 | 829 | :param openstack_release: the OpenStack codename to determine the release | ||
2237 | 830 | for. | ||
2238 | 831 | :type openstack_release: str | ||
2239 | 832 | :raises: SourceConfigError | ||
2240 | 833 | """ | ||
2241 | 834 | # TODO(ajkavanagh) - surely this means we should be removing cloud archives | ||
2242 | 835 | # if they exist? | ||
2243 | 836 | __add_bare_helper(openstack_release, "{}-{}", lambda: None) | ||
2244 | 837 | |||
2245 | 838 | |||
2246 | 839 | def _add_bare_openstack_proposed(openstack_release): | ||
2247 | 840 | """Add cloud of distro but with proposed. | ||
2248 | 841 | |||
2249 | 842 | The spec given is, say, 'ussuri' but this could apply | ||
2250 | 843 | cloud:bionic-ussuri/proposed or 'distro/proposed' depending on whether the | ||
2251 | 844 | ubuntu release is bionic or focal. | ||
2252 | 845 | |||
2253 | 846 | :param openstack_release: the OpenStack codename to determine the release | ||
2254 | 847 | for. | ||
2255 | 848 | :type openstack_release: str | ||
2256 | 849 | :raises: SourceConfigError | ||
2257 | 850 | """ | ||
2258 | 851 | __add_bare_helper(openstack_release, "{}-{}/proposed", _add_proposed) | ||
2259 | 852 | |||
2260 | 853 | |||
2261 | 854 | def __add_bare_helper(openstack_release, pocket_format, final_function): | ||
2262 | 855 | """Helper for _add_bare_openstack[_proposed] | ||
2263 | 856 | |||
2264 | 857 | The bulk of the work between the two functions is exactly the same except | ||
2265 | 858 | for the pocket format and the function that is run if it's the distro | ||
2266 | 859 | version. | ||
2267 | 860 | |||
2268 | 861 | :param openstack_release: the OpenStack codename. e.g. ussuri | ||
2269 | 862 | :type openstack_release: str | ||
2270 | 863 | :param pocket_format: the pocket formatter string to construct a pocket str | ||
2271 | 864 | from the openstack_release and the current ubuntu version. | ||
2272 | 865 | :type pocket_format: str | ||
2273 | 866 | :param final_function: the function to call if it is the distro version. | ||
2274 | 867 | :type final_function: Callable | ||
2275 | 868 | :raises SourceConfigError on error | ||
2276 | 869 | """ | ||
2277 | 870 | ubuntu_version = get_distrib_codename() | ||
2278 | 871 | possible_pocket = pocket_format.format(ubuntu_version, openstack_release) | ||
2279 | 872 | if possible_pocket in CLOUD_ARCHIVE_POCKETS: | ||
2280 | 873 | _add_cloud_pocket(possible_pocket) | ||
2281 | 874 | return | ||
2282 | 875 | # Otherwise it's almost certainly the distro version; verify that it | ||
2283 | 876 | # exists. | ||
2284 | 877 | try: | ||
2285 | 878 | assert UBUNTU_OPENSTACK_RELEASE[ubuntu_version] == openstack_release | ||
2286 | 879 | except KeyError: | ||
2287 | 880 | raise SourceConfigError( | ||
2288 | 881 | "Invalid ubuntu version {} isn't known to this library" | ||
2289 | 882 | .format(ubuntu_version)) | ||
2290 | 883 | except AssertionError: | ||
2291 | 884 | raise SourceConfigError( | ||
2292 | 885 | 'Invalid OpenStack release specified: {} for Ubuntu version {}' | ||
2293 | 886 | .format(openstack_release, ubuntu_version)) | ||
2294 | 887 | final_function() | ||
2295 | 888 | |||
2296 | 889 | |||
2297 | 651 | def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,), | 890 | def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,), |
2299 | 652 | retry_message="", cmd_env=None): | 891 | retry_message="", cmd_env=None, quiet=False): |
2300 | 653 | """Run a command and retry until success or max_retries is reached. | 892 | """Run a command and retry until success or max_retries is reached. |
2301 | 654 | 893 | ||
2309 | 655 | :param: cmd: str: The apt command to run. | 894 | :param cmd: The apt command to run. |
2310 | 656 | :param: max_retries: int: The number of retries to attempt on a fatal | 895 | :type cmd: str |
2311 | 657 | command. Defaults to CMD_RETRY_COUNT. | 896 | :param max_retries: The number of retries to attempt on a fatal |
2312 | 658 | :param: retry_exitcodes: tuple: Optional additional exit codes to retry. | 897 | command. Defaults to CMD_RETRY_COUNT. |
2313 | 659 | Defaults to retry on exit code 1. | 898 | :type max_retries: int |
2314 | 660 | :param: retry_message: str: Optional log prefix emitted during retries. | 899 | :param retry_exitcodes: Optional additional exit codes to retry. |
2315 | 661 | :param: cmd_env: dict: Environment variables to add to the command run. | 900 | Defaults to retry on exit code 1. |
2316 | 901 | :type retry_exitcodes: tuple | ||
2317 | 902 | :param retry_message: Optional log prefix emitted during retries. | ||
2318 | 903 | :type retry_message: str | ||
2319 | 904 | :param: cmd_env: Environment variables to add to the command run. | ||
2320 | 905 | :type cmd_env: Option[None, Dict[str, str]] | ||
2321 | 906 | :param quiet: if True, silence the output of the command from stdout and | ||
2322 | 907 | stderr | ||
2323 | 908 | :type quiet: bool | ||
2324 | 662 | """ | 909 | """ |
2325 | 910 | env = get_apt_dpkg_env() | ||
2326 | 911 | if cmd_env: | ||
2327 | 912 | env.update(cmd_env) | ||
2328 | 663 | 913 | ||
2329 | 664 | env = None | ||
2330 | 665 | kwargs = {} | 914 | kwargs = {} |
2335 | 666 | if cmd_env: | 915 | if quiet: |
2336 | 667 | env = os.environ.copy() | 916 | devnull = os.devnull if six.PY2 else subprocess.DEVNULL |
2337 | 668 | env.update(cmd_env) | 917 | kwargs['stdout'] = devnull |
2338 | 669 | kwargs['env'] = env | 918 | kwargs['stderr'] = devnull |
2339 | 670 | 919 | ||
2340 | 671 | if not retry_message: | 920 | if not retry_message: |
2341 | 672 | retry_message = "Failed executing '{}'".format(" ".join(cmd)) | 921 | retry_message = "Failed executing '{}'".format(" ".join(cmd)) |
2342 | @@ -678,8 +927,7 @@ | |||
2343 | 678 | retry_results = (None,) + retry_exitcodes | 927 | retry_results = (None,) + retry_exitcodes |
2344 | 679 | while result in retry_results: | 928 | while result in retry_results: |
2345 | 680 | try: | 929 | try: |
2348 | 681 | # result = subprocess.check_call(cmd, env=env) | 930 | result = subprocess.check_call(cmd, env=env, **kwargs) |
2347 | 682 | result = subprocess.check_call(cmd, **kwargs) | ||
2349 | 683 | except subprocess.CalledProcessError as e: | 931 | except subprocess.CalledProcessError as e: |
2350 | 684 | retry_count = retry_count + 1 | 932 | retry_count = retry_count + 1 |
2351 | 685 | if retry_count > max_retries: | 933 | if retry_count > max_retries: |
2352 | @@ -689,25 +937,30 @@ | |||
2353 | 689 | time.sleep(CMD_RETRY_DELAY) | 937 | time.sleep(CMD_RETRY_DELAY) |
2354 | 690 | 938 | ||
2355 | 691 | 939 | ||
2357 | 692 | def _run_apt_command(cmd, fatal=False): | 940 | def _run_apt_command(cmd, fatal=False, quiet=False): |
2358 | 693 | """Run an apt command with optional retries. | 941 | """Run an apt command with optional retries. |
2359 | 694 | 942 | ||
2363 | 695 | :param: cmd: str: The apt command to run. | 943 | :param cmd: The apt command to run. |
2364 | 696 | :param: fatal: bool: Whether the command's output should be checked and | 944 | :type cmd: str |
2365 | 697 | retried. | 945 | :param fatal: Whether the command's output should be checked and |
2366 | 946 | retried. | ||
2367 | 947 | :type fatal: bool | ||
2368 | 948 | :param quiet: if True, silence the output of the command from stdout and | ||
2369 | 949 | stderr | ||
2370 | 950 | :type quiet: bool | ||
2371 | 698 | """ | 951 | """ |
2372 | 699 | # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment. | ||
2373 | 700 | cmd_env = { | ||
2374 | 701 | 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')} | ||
2375 | 702 | |||
2376 | 703 | if fatal: | 952 | if fatal: |
2377 | 704 | _run_with_retries( | 953 | _run_with_retries( |
2380 | 705 | cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,), | 954 | cmd, retry_exitcodes=(1, APT_NO_LOCK,), |
2381 | 706 | retry_message="Couldn't acquire DPKG lock") | 955 | retry_message="Couldn't acquire DPKG lock", |
2382 | 956 | quiet=quiet) | ||
2383 | 707 | else: | 957 | else: |
2387 | 708 | env = os.environ.copy() | 958 | kwargs = {} |
2388 | 709 | env.update(cmd_env) | 959 | if quiet: |
2389 | 710 | subprocess.call(cmd, env=env) | 960 | devnull = os.devnull if six.PY2 else subprocess.DEVNULL |
2390 | 961 | kwargs['stdout'] = devnull | ||
2391 | 962 | kwargs['stderr'] = devnull | ||
2392 | 963 | subprocess.call(cmd, env=get_apt_dpkg_env(), **kwargs) | ||
2393 | 711 | 964 | ||
2394 | 712 | 965 | ||
2395 | 713 | def get_upstream_version(package): | 966 | def get_upstream_version(package): |
2396 | @@ -715,7 +968,6 @@ | |||
2397 | 715 | 968 | ||
2398 | 716 | @returns None (if not installed) or the upstream version | 969 | @returns None (if not installed) or the upstream version |
2399 | 717 | """ | 970 | """ |
2400 | 718 | import apt_pkg | ||
2401 | 719 | cache = apt_cache() | 971 | cache = apt_cache() |
2402 | 720 | try: | 972 | try: |
2403 | 721 | pkg = cache[package] | 973 | pkg = cache[package] |
2404 | @@ -727,4 +979,34 @@ | |||
2405 | 727 | # package is known, but no version is currently installed. | 979 | # package is known, but no version is currently installed. |
2406 | 728 | return None | 980 | return None |
2407 | 729 | 981 | ||
2409 | 730 | return apt_pkg.upstream_version(pkg.current_ver.ver_str) | 982 | return ubuntu_apt_pkg.upstream_version(pkg.current_ver.ver_str) |
2410 | 983 | |||
2411 | 984 | |||
2412 | 985 | def get_installed_version(package): | ||
2413 | 986 | """Determine installed version of a package | ||
2414 | 987 | |||
2415 | 988 | @returns None (if not installed) or the installed version as | ||
2416 | 989 | Version object | ||
2417 | 990 | """ | ||
2418 | 991 | cache = apt_cache() | ||
2419 | 992 | dpkg_result = cache._dpkg_list([package]).get(package, {}) | ||
2420 | 993 | current_ver = None | ||
2421 | 994 | installed_version = dpkg_result.get('version') | ||
2422 | 995 | |||
2423 | 996 | if installed_version: | ||
2424 | 997 | current_ver = ubuntu_apt_pkg.Version({'ver_str': installed_version}) | ||
2425 | 998 | return current_ver | ||
2426 | 999 | |||
2427 | 1000 | |||
2428 | 1001 | def get_apt_dpkg_env(): | ||
2429 | 1002 | """Get environment suitable for execution of APT and DPKG tools. | ||
2430 | 1003 | |||
2431 | 1004 | We keep this in a helper function instead of in a global constant to | ||
2432 | 1005 | avoid execution on import of the library. | ||
2433 | 1006 | :returns: Environment suitable for execution of APT and DPKG tools. | ||
2434 | 1007 | :rtype: Dict[str, str] | ||
2435 | 1008 | """ | ||
2436 | 1009 | # The fallback is used in the event of ``/etc/environment`` not containing | ||
2437 | 1010 | # avalid PATH variable. | ||
2438 | 1011 | return {'DEBIAN_FRONTEND': 'noninteractive', | ||
2439 | 1012 | 'PATH': get_system_env('PATH', '/usr/sbin:/usr/bin:/sbin:/bin')} | ||
2440 | 731 | 1013 | ||
2441 | === added file 'charmhelpers/fetch/ubuntu_apt_pkg.py' | |||
2442 | --- charmhelpers/fetch/ubuntu_apt_pkg.py 1970-01-01 00:00:00 +0000 | |||
2443 | +++ charmhelpers/fetch/ubuntu_apt_pkg.py 2021-11-10 05:36:20 +0000 | |||
2444 | @@ -0,0 +1,312 @@ | |||
2445 | 1 | # Copyright 2019-2021 Canonical Ltd | ||
2446 | 2 | # | ||
2447 | 3 | # Licensed under the Apache License, Version 2.0 (the "License"); | ||
2448 | 4 | # you may not use this file except in compliance with the License. | ||
2449 | 5 | # You may obtain a copy of the License at | ||
2450 | 6 | # | ||
2451 | 7 | # http://www.apache.org/licenses/LICENSE-2.0 | ||
2452 | 8 | # | ||
2453 | 9 | # Unless required by applicable law or agreed to in writing, software | ||
2454 | 10 | # distributed under the License is distributed on an "AS IS" BASIS, | ||
2455 | 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
2456 | 12 | # See the License for the specific language governing permissions and | ||
2457 | 13 | # limitations under the License. | ||
2458 | 14 | |||
2459 | 15 | """Provide a subset of the ``python-apt`` module API. | ||
2460 | 16 | |||
2461 | 17 | Data collection is done through subprocess calls to ``apt-cache`` and | ||
2462 | 18 | ``dpkg-query`` commands. | ||
2463 | 19 | |||
2464 | 20 | The main purpose for this module is to avoid dependency on the | ||
2465 | 21 | ``python-apt`` python module. | ||
2466 | 22 | |||
2467 | 23 | The indicated python module is a wrapper around the ``apt`` C++ library | ||
2468 | 24 | which is tightly connected to the version of the distribution it was | ||
2469 | 25 | shipped on. It is not developed in a backward/forward compatible manner. | ||
2470 | 26 | |||
2471 | 27 | This in turn makes it incredibly hard to distribute as a wheel for a piece | ||
2472 | 28 | of python software that supports a span of distro releases [0][1]. | ||
2473 | 29 | |||
2474 | 30 | Upstream feedback like [2] does not give confidence in this ever changing, | ||
2475 | 31 | so with this we get rid of the dependency. | ||
2476 | 32 | |||
2477 | 33 | 0: https://github.com/juju-solutions/layer-basic/pull/135 | ||
2478 | 34 | 1: https://bugs.launchpad.net/charm-octavia/+bug/1824112 | ||
2479 | 35 | 2: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=845330#10 | ||
2480 | 36 | """ | ||
2481 | 37 | |||
2482 | 38 | import locale | ||
2483 | 39 | import os | ||
2484 | 40 | import subprocess | ||
2485 | 41 | import sys | ||
2486 | 42 | |||
2487 | 43 | |||
2488 | 44 | class _container(dict): | ||
2489 | 45 | """Simple container for attributes.""" | ||
2490 | 46 | __getattr__ = dict.__getitem__ | ||
2491 | 47 | __setattr__ = dict.__setitem__ | ||
2492 | 48 | |||
2493 | 49 | |||
2494 | 50 | class Package(_container): | ||
2495 | 51 | """Simple container for package attributes.""" | ||
2496 | 52 | |||
2497 | 53 | |||
2498 | 54 | class Version(_container): | ||
2499 | 55 | """Simple container for version attributes.""" | ||
2500 | 56 | |||
2501 | 57 | |||
2502 | 58 | class Cache(object): | ||
2503 | 59 | """Simulation of ``apt_pkg`` Cache object.""" | ||
2504 | 60 | def __init__(self, progress=None): | ||
2505 | 61 | pass | ||
2506 | 62 | |||
2507 | 63 | def __contains__(self, package): | ||
2508 | 64 | try: | ||
2509 | 65 | pkg = self.__getitem__(package) | ||
2510 | 66 | return pkg is not None | ||
2511 | 67 | except KeyError: | ||
2512 | 68 | return False | ||
2513 | 69 | |||
2514 | 70 | def __getitem__(self, package): | ||
2515 | 71 | """Get information about a package from apt and dpkg databases. | ||
2516 | 72 | |||
2517 | 73 | :param package: Name of package | ||
2518 | 74 | :type package: str | ||
2519 | 75 | :returns: Package object | ||
2520 | 76 | :rtype: object | ||
2521 | 77 | :raises: KeyError, subprocess.CalledProcessError | ||
2522 | 78 | """ | ||
2523 | 79 | apt_result = self._apt_cache_show([package])[package] | ||
2524 | 80 | apt_result['name'] = apt_result.pop('package') | ||
2525 | 81 | pkg = Package(apt_result) | ||
2526 | 82 | dpkg_result = self._dpkg_list([package]).get(package, {}) | ||
2527 | 83 | current_ver = None | ||
2528 | 84 | installed_version = dpkg_result.get('version') | ||
2529 | 85 | if installed_version: | ||
2530 | 86 | current_ver = Version({'ver_str': installed_version}) | ||
2531 | 87 | pkg.current_ver = current_ver | ||
2532 | 88 | pkg.architecture = dpkg_result.get('architecture') | ||
2533 | 89 | return pkg | ||
2534 | 90 | |||
2535 | 91 | def _dpkg_list(self, packages): | ||
2536 | 92 | """Get data from system dpkg database for package. | ||
2537 | 93 | |||
2538 | 94 | :param packages: Packages to get data from | ||
2539 | 95 | :type packages: List[str] | ||
2540 | 96 | :returns: Structured data about installed packages, keys like | ||
2541 | 97 | ``dpkg-query --list`` | ||
2542 | 98 | :rtype: dict | ||
2543 | 99 | :raises: subprocess.CalledProcessError | ||
2544 | 100 | """ | ||
2545 | 101 | pkgs = {} | ||
2546 | 102 | cmd = ['dpkg-query', '--list'] | ||
2547 | 103 | cmd.extend(packages) | ||
2548 | 104 | if locale.getlocale() == (None, None): | ||
2549 | 105 | # subprocess calls out to locale.getpreferredencoding(False) to | ||
2550 | 106 | # determine encoding. Workaround for Trusty where the | ||
2551 | 107 | # environment appears to not be set up correctly. | ||
2552 | 108 | locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') | ||
2553 | 109 | try: | ||
2554 | 110 | output = subprocess.check_output(cmd, | ||
2555 | 111 | stderr=subprocess.STDOUT, | ||
2556 | 112 | universal_newlines=True) | ||
2557 | 113 | except subprocess.CalledProcessError as cp: | ||
2558 | 114 | # ``dpkg-query`` may return error and at the same time have | ||
2559 | 115 | # produced useful output, for example when asked for multiple | ||
2560 | 116 | # packages where some are not installed | ||
2561 | 117 | if cp.returncode != 1: | ||
2562 | 118 | raise | ||
2563 | 119 | output = cp.output | ||
2564 | 120 | headings = [] | ||
2565 | 121 | for line in output.splitlines(): | ||
2566 | 122 | if line.startswith('||/'): | ||
2567 | 123 | headings = line.split() | ||
2568 | 124 | headings.pop(0) | ||
2569 | 125 | continue | ||
2570 | 126 | elif (line.startswith('|') or line.startswith('+') or | ||
2571 | 127 | line.startswith('dpkg-query:')): | ||
2572 | 128 | continue | ||
2573 | 129 | else: | ||
2574 | 130 | data = line.split(None, 4) | ||
2575 | 131 | status = data.pop(0) | ||
2576 | 132 | if status not in ('ii', 'hi'): | ||
2577 | 133 | continue | ||
2578 | 134 | pkg = {} | ||
2579 | 135 | pkg.update({k.lower(): v for k, v in zip(headings, data)}) | ||
2580 | 136 | if 'name' in pkg: | ||
2581 | 137 | pkgs.update({pkg['name']: pkg}) | ||
2582 | 138 | return pkgs | ||
2583 | 139 | |||
2584 | 140 | def _apt_cache_show(self, packages): | ||
2585 | 141 | """Get data from system apt cache for package. | ||
2586 | 142 | |||
2587 | 143 | :param packages: Packages to get data from | ||
2588 | 144 | :type packages: List[str] | ||
2589 | 145 | :returns: Structured data about package, keys like | ||
2590 | 146 | ``apt-cache show`` | ||
2591 | 147 | :rtype: dict | ||
2592 | 148 | :raises: subprocess.CalledProcessError | ||
2593 | 149 | """ | ||
2594 | 150 | pkgs = {} | ||
2595 | 151 | cmd = ['apt-cache', 'show', '--no-all-versions'] | ||
2596 | 152 | cmd.extend(packages) | ||
2597 | 153 | if locale.getlocale() == (None, None): | ||
2598 | 154 | # subprocess calls out to locale.getpreferredencoding(False) to | ||
2599 | 155 | # determine encoding. Workaround for Trusty where the | ||
2600 | 156 | # environment appears to not be set up correctly. | ||
2601 | 157 | locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') | ||
2602 | 158 | try: | ||
2603 | 159 | output = subprocess.check_output(cmd, | ||
2604 | 160 | stderr=subprocess.STDOUT, | ||
2605 | 161 | universal_newlines=True) | ||
2606 | 162 | previous = None | ||
2607 | 163 | pkg = {} | ||
2608 | 164 | for line in output.splitlines(): | ||
2609 | 165 | if not line: | ||
2610 | 166 | if 'package' in pkg: | ||
2611 | 167 | pkgs.update({pkg['package']: pkg}) | ||
2612 | 168 | pkg = {} | ||
2613 | 169 | continue | ||
2614 | 170 | if line.startswith(' '): | ||
2615 | 171 | if previous and previous in pkg: | ||
2616 | 172 | pkg[previous] += os.linesep + line.lstrip() | ||
2617 | 173 | continue | ||
2618 | 174 | if ':' in line: | ||
2619 | 175 | kv = line.split(':', 1) | ||
2620 | 176 | key = kv[0].lower() | ||
2621 | 177 | if key == 'n': | ||
2622 | 178 | continue | ||
2623 | 179 | previous = key | ||
2624 | 180 | pkg.update({key: kv[1].lstrip()}) | ||
2625 | 181 | except subprocess.CalledProcessError as cp: | ||
2626 | 182 | # ``apt-cache`` returns 100 if none of the packages asked for | ||
2627 | 183 | # exist in the apt cache. | ||
2628 | 184 | if cp.returncode != 100: | ||
2629 | 185 | raise | ||
2630 | 186 | return pkgs | ||
2631 | 187 | |||
2632 | 188 | |||
2633 | 189 | class Config(_container): | ||
2634 | 190 | def __init__(self): | ||
2635 | 191 | super(Config, self).__init__(self._populate()) | ||
2636 | 192 | |||
2637 | 193 | def _populate(self): | ||
2638 | 194 | cfgs = {} | ||
2639 | 195 | cmd = ['apt-config', 'dump'] | ||
2640 | 196 | output = subprocess.check_output(cmd, | ||
2641 | 197 | stderr=subprocess.STDOUT, | ||
2642 | 198 | universal_newlines=True) | ||
2643 | 199 | for line in output.splitlines(): | ||
2644 | 200 | if not line.startswith("CommandLine"): | ||
2645 | 201 | k, v = line.split(" ", 1) | ||
2646 | 202 | cfgs[k] = v.strip(";").strip("\"") | ||
2647 | 203 | |||
2648 | 204 | return cfgs | ||
2649 | 205 | |||
2650 | 206 | |||
2651 | 207 | # Backwards compatibility with old apt_pkg module | ||
2652 | 208 | sys.modules[__name__].config = Config() | ||
2653 | 209 | |||
2654 | 210 | |||
2655 | 211 | def init(): | ||
2656 | 212 | """Compatibility shim that does nothing.""" | ||
2657 | 213 | pass | ||
2658 | 214 | |||
2659 | 215 | |||
2660 | 216 | def upstream_version(version): | ||
2661 | 217 | """Extracts upstream version from a version string. | ||
2662 | 218 | |||
2663 | 219 | Upstream reference: https://salsa.debian.org/apt-team/apt/blob/master/ | ||
2664 | 220 | apt-pkg/deb/debversion.cc#L259 | ||
2665 | 221 | |||
2666 | 222 | :param version: Version string | ||
2667 | 223 | :type version: str | ||
2668 | 224 | :returns: Upstream version | ||
2669 | 225 | :rtype: str | ||
2670 | 226 | """ | ||
2671 | 227 | if version: | ||
2672 | 228 | version = version.split(':')[-1] | ||
2673 | 229 | version = version.split('-')[0] | ||
2674 | 230 | return version | ||
2675 | 231 | |||
2676 | 232 | |||
2677 | 233 | def version_compare(a, b): | ||
2678 | 234 | """Compare the given versions. | ||
2679 | 235 | |||
2680 | 236 | Call out to ``dpkg`` to make sure the code doing the comparison is | ||
2681 | 237 | compatible with what the ``apt`` library would do. Mimic the return | ||
2682 | 238 | values. | ||
2683 | 239 | |||
2684 | 240 | Upstream reference: | ||
2685 | 241 | https://apt-team.pages.debian.net/python-apt/library/apt_pkg.html | ||
2686 | 242 | ?highlight=version_compare#apt_pkg.version_compare | ||
2687 | 243 | |||
2688 | 244 | :param a: version string | ||
2689 | 245 | :type a: str | ||
2690 | 246 | :param b: version string | ||
2691 | 247 | :type b: str | ||
2692 | 248 | :returns: >0 if ``a`` is greater than ``b``, 0 if a equals b, | ||
2693 | 249 | <0 if ``a`` is smaller than ``b`` | ||
2694 | 250 | :rtype: int | ||
2695 | 251 | :raises: subprocess.CalledProcessError, RuntimeError | ||
2696 | 252 | """ | ||
2697 | 253 | for op in ('gt', 1), ('eq', 0), ('lt', -1): | ||
2698 | 254 | try: | ||
2699 | 255 | subprocess.check_call(['dpkg', '--compare-versions', | ||
2700 | 256 | a, op[0], b], | ||
2701 | 257 | stderr=subprocess.STDOUT, | ||
2702 | 258 | universal_newlines=True) | ||
2703 | 259 | return op[1] | ||
2704 | 260 | except subprocess.CalledProcessError as cp: | ||
2705 | 261 | if cp.returncode == 1: | ||
2706 | 262 | continue | ||
2707 | 263 | raise | ||
2708 | 264 | else: | ||
2709 | 265 | raise RuntimeError('Unable to compare "{}" and "{}", according to ' | ||
2710 | 266 | 'our logic they are neither greater, equal nor ' | ||
2711 | 267 | 'less than each other.'.format(a, b)) | ||
2712 | 268 | |||
2713 | 269 | |||
2714 | 270 | class PkgVersion(): | ||
2715 | 271 | """Allow package versions to be compared. | ||
2716 | 272 | |||
2717 | 273 | For example:: | ||
2718 | 274 | |||
2719 | 275 | >>> import charmhelpers.fetch as fetch | ||
2720 | 276 | >>> (fetch.apt_pkg.PkgVersion('2:20.4.0') < | ||
2721 | 277 | ... fetch.apt_pkg.PkgVersion('2:20.5.0')) | ||
2722 | 278 | True | ||
2723 | 279 | >>> pkgs = [fetch.apt_pkg.PkgVersion('2:20.4.0'), | ||
2724 | 280 | ... fetch.apt_pkg.PkgVersion('2:21.4.0'), | ||
2725 | 281 | ... fetch.apt_pkg.PkgVersion('2:17.4.0')] | ||
2726 | 282 | >>> pkgs.sort() | ||
2727 | 283 | >>> pkgs | ||
2728 | 284 | [2:17.4.0, 2:20.4.0, 2:21.4.0] | ||
2729 | 285 | """ | ||
2730 | 286 | |||
2731 | 287 | def __init__(self, version): | ||
2732 | 288 | self.version = version | ||
2733 | 289 | |||
2734 | 290 | def __lt__(self, other): | ||
2735 | 291 | return version_compare(self.version, other.version) == -1 | ||
2736 | 292 | |||
2737 | 293 | def __le__(self, other): | ||
2738 | 294 | return self.__lt__(other) or self.__eq__(other) | ||
2739 | 295 | |||
2740 | 296 | def __gt__(self, other): | ||
2741 | 297 | return version_compare(self.version, other.version) == 1 | ||
2742 | 298 | |||
2743 | 299 | def __ge__(self, other): | ||
2744 | 300 | return self.__gt__(other) or self.__eq__(other) | ||
2745 | 301 | |||
2746 | 302 | def __eq__(self, other): | ||
2747 | 303 | return version_compare(self.version, other.version) == 0 | ||
2748 | 304 | |||
2749 | 305 | def __ne__(self, other): | ||
2750 | 306 | return not self.__eq__(other) | ||
2751 | 307 | |||
2752 | 308 | def __repr__(self): | ||
2753 | 309 | return self.version | ||
2754 | 310 | |||
2755 | 311 | def __hash__(self): | ||
2756 | 312 | return hash(repr(self)) | ||
2757 | 0 | 313 | ||
2758 | === modified file 'charmhelpers/osplatform.py' | |||
2759 | --- charmhelpers/osplatform.py 2017-03-04 02:42:23 +0000 | |||
2760 | +++ charmhelpers/osplatform.py 2021-11-10 05:36:20 +0000 | |||
2761 | @@ -1,4 +1,5 @@ | |||
2762 | 1 | import platform | 1 | import platform |
2763 | 2 | import os | ||
2764 | 2 | 3 | ||
2765 | 3 | 4 | ||
2766 | 4 | def get_platform(): | 5 | def get_platform(): |
2767 | @@ -9,9 +10,13 @@ | |||
2768 | 9 | This string is used to decide which platform module should be imported. | 10 | This string is used to decide which platform module should be imported. |
2769 | 10 | """ | 11 | """ |
2770 | 11 | # linux_distribution is deprecated and will be removed in Python 3.7 | 12 | # linux_distribution is deprecated and will be removed in Python 3.7 |
2774 | 12 | # Warings *not* disabled, as we certainly need to fix this. | 13 | # Warnings *not* disabled, as we certainly need to fix this. |
2775 | 13 | tuple_platform = platform.linux_distribution() | 14 | if hasattr(platform, 'linux_distribution'): |
2776 | 14 | current_platform = tuple_platform[0] | 15 | tuple_platform = platform.linux_distribution() |
2777 | 16 | current_platform = tuple_platform[0] | ||
2778 | 17 | else: | ||
2779 | 18 | current_platform = _get_platform_from_fs() | ||
2780 | 19 | |||
2781 | 15 | if "Ubuntu" in current_platform: | 20 | if "Ubuntu" in current_platform: |
2782 | 16 | return "ubuntu" | 21 | return "ubuntu" |
2783 | 17 | elif "CentOS" in current_platform: | 22 | elif "CentOS" in current_platform: |
2784 | @@ -20,6 +25,25 @@ | |||
2785 | 20 | # Stock Python does not detect Ubuntu and instead returns debian. | 25 | # Stock Python does not detect Ubuntu and instead returns debian. |
2786 | 21 | # Or at least it does in some build environments like Travis CI | 26 | # Or at least it does in some build environments like Travis CI |
2787 | 22 | return "ubuntu" | 27 | return "ubuntu" |
2788 | 28 | elif "elementary" in current_platform: | ||
2789 | 29 | # ElementaryOS fails to run tests locally without this. | ||
2790 | 30 | return "ubuntu" | ||
2791 | 31 | elif "Pop!_OS" in current_platform: | ||
2792 | 32 | # Pop!_OS also fails to run tests locally without this. | ||
2793 | 33 | return "ubuntu" | ||
2794 | 23 | else: | 34 | else: |
2795 | 24 | raise RuntimeError("This module is not supported on {}." | 35 | raise RuntimeError("This module is not supported on {}." |
2796 | 25 | .format(current_platform)) | 36 | .format(current_platform)) |
2797 | 37 | |||
2798 | 38 | |||
2799 | 39 | def _get_platform_from_fs(): | ||
2800 | 40 | """Get Platform from /etc/os-release.""" | ||
2801 | 41 | with open(os.path.join(os.sep, 'etc', 'os-release')) as fin: | ||
2802 | 42 | content = dict( | ||
2803 | 43 | line.split('=', 1) | ||
2804 | 44 | for line in fin.read().splitlines() | ||
2805 | 45 | if '=' in line | ||
2806 | 46 | ) | ||
2807 | 47 | for k, v in content.items(): | ||
2808 | 48 | content[k] = v.strip('"') | ||
2809 | 49 | return content["NAME"] | ||
2810 | 26 | 50 | ||
2811 | === modified file 'config.yaml' | |||
2812 | --- config.yaml 2021-10-07 00:38:56 +0000 | |||
2813 | +++ config.yaml 2021-11-10 05:36:20 +0000 | |||
2814 | @@ -121,3 +121,19 @@ | |||
2815 | 121 | type: string | 121 | type: string |
2816 | 122 | default: '' | 122 | default: '' |
2817 | 123 | description: An unique site name for Landscape deployment | 123 | description: An unique site name for Landscape deployment |
2818 | 124 | nagios_context: | ||
2819 | 125 | default: "juju" | ||
2820 | 126 | type: string | ||
2821 | 127 | description: | | ||
2822 | 128 | Used by the nrpe subordinate charms. | ||
2823 | 129 | A string that will be prepended to instance name to set the host name | ||
2824 | 130 | in nagios. So for instance the hostname would be something like: | ||
2825 | 131 | juju-myservice-0 | ||
2826 | 132 | If you're running multiple environments with the same services in them | ||
2827 | 133 | this allows you to differentiate between them. | ||
2828 | 134 | nagios_servicegroups: | ||
2829 | 135 | default: "" | ||
2830 | 136 | type: string | ||
2831 | 137 | description: | | ||
2832 | 138 | A comma-separated list of nagios servicegroups. | ||
2833 | 139 | If left empty, the nagios_context will be used as the servicegroup | ||
2834 | 124 | 140 | ||
2835 | === added file 'hooks/nrpe-external-master-relation-changed' | |||
2836 | --- hooks/nrpe-external-master-relation-changed 1970-01-01 00:00:00 +0000 | |||
2837 | +++ hooks/nrpe-external-master-relation-changed 2021-11-10 05:36:20 +0000 | |||
2838 | @@ -0,0 +1,9 @@ | |||
2839 | 1 | #!/usr/bin/python | ||
2840 | 2 | import sys | ||
2841 | 3 | |||
2842 | 4 | from lib.services import ServicesHook | ||
2843 | 5 | |||
2844 | 6 | |||
2845 | 7 | if __name__ == "__main__": | ||
2846 | 8 | hook = ServicesHook() | ||
2847 | 9 | sys.exit(hook()) | ||
2848 | 0 | 10 | ||
2849 | === added file 'hooks/nrpe-external-master-relation-joined' | |||
2850 | --- hooks/nrpe-external-master-relation-joined 1970-01-01 00:00:00 +0000 | |||
2851 | +++ hooks/nrpe-external-master-relation-joined 2021-11-10 05:36:20 +0000 | |||
2852 | @@ -0,0 +1,9 @@ | |||
2853 | 1 | #!/usr/bin/python | ||
2854 | 2 | import sys | ||
2855 | 3 | |||
2856 | 4 | from lib.services import ServicesHook | ||
2857 | 5 | |||
2858 | 6 | |||
2859 | 7 | if __name__ == "__main__": | ||
2860 | 8 | hook = ServicesHook() | ||
2861 | 9 | sys.exit(hook()) | ||
2862 | 0 | 10 | ||
2863 | === added file 'lib/callbacks/nrpe.py' | |||
2864 | --- lib/callbacks/nrpe.py 1970-01-01 00:00:00 +0000 | |||
2865 | +++ lib/callbacks/nrpe.py 2021-11-10 05:36:20 +0000 | |||
2866 | @@ -0,0 +1,51 @@ | |||
2867 | 1 | from charmhelpers.core import hookenv | ||
2868 | 2 | from charmhelpers.core.services.base import ManagerCallback | ||
2869 | 3 | from charmhelpers.contrib.charmsupport import nrpe | ||
2870 | 4 | |||
2871 | 5 | |||
2872 | 6 | # services running on all nodes | ||
2873 | 7 | DEFAULT_SERVICES = ['landscape-api', 'landscape-appserver', | ||
2874 | 8 | 'landscape-async-frontend', 'landscape-job-handler', | ||
2875 | 9 | 'landscape-msgserver', 'landscape-pingserver'] | ||
2876 | 10 | |||
2877 | 11 | # services running only on the leader | ||
2878 | 12 | LEADER_SERVICES = ['landscape-package-search', 'landscape-package-upload'] | ||
2879 | 13 | |||
2880 | 14 | |||
2881 | 15 | class ConfigureNRPE(ManagerCallback): | ||
2882 | 16 | """Configure service checks if nrpe-external-master relation exists""" | ||
2883 | 17 | |||
2884 | 18 | def __init__(self, hookenv=hookenv, nrpe_config=None): | ||
2885 | 19 | self._hookenv = hookenv | ||
2886 | 20 | self._unit = self._hookenv.local_unit() | ||
2887 | 21 | if nrpe_config: | ||
2888 | 22 | self._nrpe_config = nrpe_config | ||
2889 | 23 | else: | ||
2890 | 24 | self._nrpe_config = nrpe.NRPE() | ||
2891 | 25 | |||
2892 | 26 | def __call__(self, manager, service_name, event_name): | ||
2893 | 27 | self._hookenv.log('Configuring NRPE checks') | ||
2894 | 28 | if self._hookenv.relations_of_type('nrpe-external-master'): | ||
2895 | 29 | if self._hookenv.is_leader(): | ||
2896 | 30 | self._add_checks(DEFAULT_SERVICES + LEADER_SERVICES) | ||
2897 | 31 | else: | ||
2898 | 32 | self._add_checks(DEFAULT_SERVICES) | ||
2899 | 33 | self._remove_checks(LEADER_SERVICES) | ||
2900 | 34 | else: | ||
2901 | 35 | self._remove_checks(DEFAULT_SERVICES + LEADER_SERVICES) | ||
2902 | 36 | self._nrpe_config.write() | ||
2903 | 37 | |||
2904 | 38 | def _add_checks(self, services): | ||
2905 | 39 | """ Add a service check """ | ||
2906 | 40 | for service in services: | ||
2907 | 41 | hookenv.log('Adding nrpe check: %s' % service, hookenv.DEBUG) | ||
2908 | 42 | self._nrpe_config.add_check( | ||
2909 | 43 | shortname='%s' % service, | ||
2910 | 44 | description='process check {%s}' % self._unit, | ||
2911 | 45 | check_cmd='check_systemd.py %s' % service) | ||
2912 | 46 | |||
2913 | 47 | def _remove_checks(self, services): | ||
2914 | 48 | """ Remove a service check """ | ||
2915 | 49 | for service in services: | ||
2916 | 50 | hookenv.log('Removing nrpe check: %s' % service, hookenv.DEBUG) | ||
2917 | 51 | self._nrpe_config.remove_check(shortname=service) | ||
2918 | 0 | 52 | ||
2919 | === added file 'lib/callbacks/tests/test_nrpe.py' | |||
2920 | --- lib/callbacks/tests/test_nrpe.py 1970-01-01 00:00:00 +0000 | |||
2921 | +++ lib/callbacks/tests/test_nrpe.py 2021-11-10 05:36:20 +0000 | |||
2922 | @@ -0,0 +1,36 @@ | |||
2923 | 1 | from charmhelpers.core.services.base import ServiceManager | ||
2924 | 2 | |||
2925 | 3 | from lib.tests.helpers import HookenvTest | ||
2926 | 4 | from lib.tests.stubs import NrpeConfigStub | ||
2927 | 5 | from lib.callbacks.nrpe import ( | ||
2928 | 6 | ConfigureNRPE, | ||
2929 | 7 | DEFAULT_SERVICES, | ||
2930 | 8 | LEADER_SERVICES) | ||
2931 | 9 | |||
2932 | 10 | |||
2933 | 11 | class ConfigureNRPETest(HookenvTest): | ||
2934 | 12 | def setUp(self): | ||
2935 | 13 | super(ConfigureNRPETest, self).setUp() | ||
2936 | 14 | self.manager = ServiceManager([]) | ||
2937 | 15 | self.fake_nrpe = NrpeConfigStub() | ||
2938 | 16 | self.callback = ConfigureNRPE(hookenv=self.hookenv, | ||
2939 | 17 | nrpe_config=self.fake_nrpe) | ||
2940 | 18 | |||
2941 | 19 | def test_add_nrpe_check(self): | ||
2942 | 20 | """Test adding NRPE checks.""" | ||
2943 | 21 | config = self.hookenv.config() | ||
2944 | 22 | config["nagios_context"] = "juju" | ||
2945 | 23 | self.hookenv.relations['nrpe-external-master'] = {"id": "1"} | ||
2946 | 24 | self.callback(self.manager, None, None) | ||
2947 | 25 | nrpe_checks = self.fake_nrpe.get_nrpe_checks() | ||
2948 | 26 | for svc in DEFAULT_SERVICES: | ||
2949 | 27 | self.assertIn(svc, nrpe_checks) | ||
2950 | 28 | for svc in LEADER_SERVICES: | ||
2951 | 29 | self.assertIn(svc, nrpe_checks) | ||
2952 | 30 | |||
2953 | 31 | def test_remove_nrpe_check(self): | ||
2954 | 32 | config = self.hookenv.config() | ||
2955 | 33 | config["nagios_context"] = "juju" | ||
2956 | 34 | self.callback(self.manager, None, None) | ||
2957 | 35 | nrpe_checks = self.fake_nrpe.get_nrpe_checks() | ||
2958 | 36 | self.assertTrue(len(nrpe_checks) == 0) | ||
2959 | 0 | 37 | ||
2960 | === modified file 'lib/services.py' | |||
2961 | --- lib/services.py 2021-10-29 00:55:43 +0000 | |||
2962 | +++ lib/services.py 2021-11-10 05:36:20 +0000 | |||
2963 | @@ -21,6 +21,7 @@ | |||
2964 | 21 | from lib.callbacks.filesystem import ( | 21 | from lib.callbacks.filesystem import ( |
2965 | 22 | EnsureConfigDir, WriteCustomSSLCertificate, WriteLicenseFile) | 22 | EnsureConfigDir, WriteCustomSSLCertificate, WriteLicenseFile) |
2966 | 23 | from lib.callbacks.apt import SetAPTSources | 23 | from lib.callbacks.apt import SetAPTSources |
2967 | 24 | from lib.callbacks.nrpe import ConfigureNRPE | ||
2968 | 24 | 25 | ||
2969 | 25 | 26 | ||
2970 | 26 | class ServicesHook(Hook): | 27 | class ServicesHook(Hook): |
2971 | @@ -32,7 +33,7 @@ | |||
2972 | 32 | """ | 33 | """ |
2973 | 33 | def __init__(self, hookenv=hookenv, host=host, | 34 | def __init__(self, hookenv=hookenv, host=host, |
2974 | 34 | subprocess=subprocess, paths=default_paths, fetch=fetch, | 35 | subprocess=subprocess, paths=default_paths, fetch=fetch, |
2976 | 35 | psutil=psutil): | 36 | psutil=psutil, nrpe_config=None): |
2977 | 36 | super(ServicesHook, self).__init__(hookenv=hookenv) | 37 | super(ServicesHook, self).__init__(hookenv=hookenv) |
2978 | 37 | self._hookenv = hookenv | 38 | self._hookenv = hookenv |
2979 | 38 | self._host = host | 39 | self._host = host |
2980 | @@ -40,6 +41,7 @@ | |||
2981 | 40 | self._psutil = psutil | 41 | self._psutil = psutil |
2982 | 41 | self._subprocess = subprocess | 42 | self._subprocess = subprocess |
2983 | 42 | self._fetch = fetch | 43 | self._fetch = fetch |
2984 | 44 | self._nrpe_config = nrpe_config | ||
2985 | 43 | 45 | ||
2986 | 44 | def _run(self): | 46 | def _run(self): |
2987 | 45 | 47 | ||
2988 | @@ -88,6 +90,8 @@ | |||
2989 | 88 | WriteLicenseFile(host=self._host, paths=self._paths), | 90 | WriteLicenseFile(host=self._host, paths=self._paths), |
2990 | 89 | ConfigureSMTP( | 91 | ConfigureSMTP( |
2991 | 90 | hookenv=self._hookenv, subprocess=self._subprocess), | 92 | hookenv=self._hookenv, subprocess=self._subprocess), |
2992 | 93 | ConfigureNRPE(hookenv=self._hookenv, | ||
2993 | 94 | nrpe_config=self._nrpe_config), | ||
2994 | 91 | ], | 95 | ], |
2995 | 92 | "start": LSCtl(subprocess=self._subprocess, hookenv=self._hookenv), | 96 | "start": LSCtl(subprocess=self._subprocess, hookenv=self._hookenv), |
2996 | 93 | }]) | 97 | }]) |
2997 | 94 | 98 | ||
2998 | === modified file 'lib/tests/stubs.py' | |||
2999 | --- lib/tests/stubs.py 2019-07-15 20:01:06 +0000 | |||
3000 | +++ lib/tests/stubs.py 2021-11-10 05:36:20 +0000 | |||
3001 | @@ -33,6 +33,9 @@ | |||
3002 | 33 | def config(self): | 33 | def config(self): |
3003 | 34 | return self._config | 34 | return self._config |
3004 | 35 | 35 | ||
3005 | 36 | def relations_of_type(self, reltype): | ||
3006 | 37 | return self.relations.get(reltype, None) | ||
3007 | 38 | |||
3008 | 36 | def log(self, message, level=None): | 39 | def log(self, message, level=None): |
3009 | 37 | self.messages.append((message, level)) | 40 | self.messages.append((message, level)) |
3010 | 38 | 41 | ||
3011 | @@ -264,3 +267,24 @@ | |||
3012 | 264 | 267 | ||
3013 | 265 | def virtual_memory(self): | 268 | def virtual_memory(self): |
3014 | 266 | return PsutilUsageStub(self._physical_memory) | 269 | return PsutilUsageStub(self._physical_memory) |
3015 | 270 | |||
3016 | 271 | |||
3017 | 272 | class NrpeConfigStub(object): | ||
3018 | 273 | def __init__(self): | ||
3019 | 274 | self._nrpe_checks = {} | ||
3020 | 275 | |||
3021 | 276 | def write(self): | ||
3022 | 277 | pass | ||
3023 | 278 | |||
3024 | 279 | def add_check(self, shortname, description, check_cmd): | ||
3025 | 280 | self._nrpe_checks[shortname] = { | ||
3026 | 281 | "description": description, | ||
3027 | 282 | "command": check_cmd, | ||
3028 | 283 | } | ||
3029 | 284 | |||
3030 | 285 | def remove_check(self, shortname): | ||
3031 | 286 | if self._nrpe_checks.get(shortname): | ||
3032 | 287 | del self._nrpe_checks[shortname] | ||
3033 | 288 | |||
3034 | 289 | def get_nrpe_checks(self): | ||
3035 | 290 | return self._nrpe_checks | ||
3036 | 267 | 291 | ||
3037 | === modified file 'lib/tests/test_services.py' | |||
3038 | --- lib/tests/test_services.py 2021-04-13 19:03:22 +0000 | |||
3039 | +++ lib/tests/test_services.py 2021-11-10 05:36:20 +0000 | |||
3040 | @@ -5,7 +5,12 @@ | |||
3041 | 5 | from charmhelpers.core import templating | 5 | from charmhelpers.core import templating |
3042 | 6 | 6 | ||
3043 | 7 | from lib.tests.helpers import HookenvTest | 7 | from lib.tests.helpers import HookenvTest |
3045 | 8 | from lib.tests.stubs import HostStub, PsutilStub, SubprocessStub, FetchStub | 8 | from lib.tests.stubs import ( |
3046 | 9 | HostStub, | ||
3047 | 10 | PsutilStub, | ||
3048 | 11 | SubprocessStub, | ||
3049 | 12 | FetchStub, | ||
3050 | 13 | NrpeConfigStub) | ||
3051 | 9 | from lib.tests.sample import ( | 14 | from lib.tests.sample import ( |
3052 | 10 | SAMPLE_DB_UNIT_DATA, SAMPLE_LEADER_DATA, SAMPLE_AMQP_UNIT_DATA, | 15 | SAMPLE_DB_UNIT_DATA, SAMPLE_LEADER_DATA, SAMPLE_AMQP_UNIT_DATA, |
3053 | 11 | SAMPLE_CONFIG_LICENSE_DATA, SAMPLE_CONFIG_OPENID_DATA, | 16 | SAMPLE_CONFIG_LICENSE_DATA, SAMPLE_CONFIG_OPENID_DATA, |
3054 | @@ -34,9 +39,11 @@ | |||
3055 | 34 | self.root_dir = self.useFixture(RootDir()) | 39 | self.root_dir = self.useFixture(RootDir()) |
3056 | 35 | self.fetch = FetchStub(self.hookenv.config) | 40 | self.fetch = FetchStub(self.hookenv.config) |
3057 | 36 | self.psutil = PsutilStub(num_cpus=2, physical_memory=1*1024**3) | 41 | self.psutil = PsutilStub(num_cpus=2, physical_memory=1*1024**3) |
3058 | 42 | self.nrpe_config = NrpeConfigStub() | ||
3059 | 37 | self.hook = ServicesHook( | 43 | self.hook = ServicesHook( |
3060 | 38 | hookenv=self.hookenv, host=self.host, subprocess=self.subprocess, | 44 | hookenv=self.hookenv, host=self.host, subprocess=self.subprocess, |
3062 | 39 | paths=self.paths, fetch=self.fetch, psutil=self.psutil) | 45 | paths=self.paths, fetch=self.fetch, psutil=self.psutil, |
3063 | 46 | nrpe_config=self.nrpe_config) | ||
3064 | 40 | 47 | ||
3065 | 41 | # XXX Monkey patch the templating API, charmhelpers doesn't sport | 48 | # XXX Monkey patch the templating API, charmhelpers doesn't sport |
3066 | 42 | # any dependency injection here as well. | 49 | # any dependency injection here as well. |
3067 | 43 | 50 | ||
3068 | === modified file 'metadata.yaml' | |||
3069 | --- metadata.yaml 2021-10-05 08:24:58 +0000 | |||
3070 | +++ metadata.yaml 2021-11-10 05:36:20 +0000 | |||
3071 | @@ -21,6 +21,9 @@ | |||
3072 | 21 | hosted: | 21 | hosted: |
3073 | 22 | interface: landscape-hosted | 22 | interface: landscape-hosted |
3074 | 23 | scope: container | 23 | scope: container |
3075 | 24 | nrpe-external-master: | ||
3076 | 25 | interface: nrpe-external-master | ||
3077 | 26 | scope: container | ||
3078 | 24 | series: | 27 | series: |
3079 | 25 | - bionic | 28 | - bionic |
3080 | 26 | - xenial | 29 | - xenial |
This merge proposal is to supersede this patch
https:/ /code.launchpad .net/~stephanpa mpel/landscape- charm/systemd- monitoring/ +merge/ 411083