Merge lp:~lihuiguo/landscape-charm/bug-1934816 into lp:~landscape/landscape-charm/trunk

Proposed by Linda Guo
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
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Approve
Simon Poirier (community) Approve
James Troup (community) Approve
Review via email: mp+411583@code.launchpad.net

Commit message

Sync charm-helpers
Add relation: nrpe-external-master
Add nrpe checks check_systemd for landscape services

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
Linda Guo (lihuiguo) wrote :
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Needs Fixing (test results)
408. By Linda Guo <email address hidden>

Add charmhelpers.contrib.charmsupport dependency
to charm-helpers.yaml to get nrpe.py

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Approve (test results)
Revision history for this message
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.

review: Approve
Revision history for this message
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)

review: Approve
409. By Linda Guo <email address hidden>

Fixed docstring
Added unit test to check remove nrpe

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Approve (test results)
Revision history for this message
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

Revision history for this message
Simon Poirier (simpoir) wrote :

Thanks for that extra test.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'charm-helpers.yaml'
--- charm-helpers.yaml 2017-03-04 02:41:39 +0000
+++ charm-helpers.yaml 2021-11-10 05:36:20 +0000
@@ -6,3 +6,4 @@
6 - fetch6 - fetch
7 - osplatform7 - osplatform
8 - contrib.hahelpers8 - contrib.hahelpers
9 - contrib.charmsupport.nrpe
910
=== modified file 'charmhelpers/__init__.py'
--- charmhelpers/__init__.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/__init__.py 2021-11-10 05:36:20 +0000
@@ -49,7 +49,8 @@
4949
50def deprecate(warning, date=None, log=None):50def deprecate(warning, date=None, log=None):
51 """Add a deprecation warning the first time the function is used.51 """Add a deprecation warning the first time the function is used.
52 The date, which is a string in semi-ISO8660 format indicate the year-month52
53 The date which is a string in semi-ISO8660 format indicates the year-month
53 that the function is officially going to be removed.54 that the function is officially going to be removed.
5455
55 usage:56 usage:
@@ -62,10 +63,11 @@
62 The reason for passing the logging function (log) is so that hookenv.log63 The reason for passing the logging function (log) is so that hookenv.log
63 can be used for a charm if needed.64 can be used for a charm if needed.
6465
65 :param warning: String to indicat where it has moved ot.66 :param warning: String to indicate what is to be used instead.
66 :param date: optional sting, in YYYY-MM format to indicate when the67 :param date: Optional string in YYYY-MM format to indicate when the
67 function will definitely (probably) be removed.68 function will definitely (probably) be removed.
68 :param log: The log function to call to log. If not, logs to stdout69 :param log: The log function to call in order to log. If None, logs to
70 stdout
69 """71 """
70 def wrap(f):72 def wrap(f):
7173
7274
=== added directory 'charmhelpers/contrib/charmsupport'
=== added file 'charmhelpers/contrib/charmsupport/__init__.py'
--- charmhelpers/contrib/charmsupport/__init__.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/charmsupport/__init__.py 2021-11-10 05:36:20 +0000
@@ -0,0 +1,13 @@
1# Copyright 2014-2015 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
014
=== added file 'charmhelpers/contrib/charmsupport/nrpe.py'
--- charmhelpers/contrib/charmsupport/nrpe.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/contrib/charmsupport/nrpe.py 2021-11-10 05:36:20 +0000
@@ -0,0 +1,522 @@
1# Copyright 2012-2021 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Compatibility with the nrpe-external-master charm"""
16#
17# Authors:
18# Matthew Wedgwood <matthew.wedgwood@canonical.com>
19
20import glob
21import grp
22import os
23import pwd
24import re
25import shlex
26import shutil
27import subprocess
28import yaml
29
30from charmhelpers.core.hookenv import (
31 config,
32 hook_name,
33 local_unit,
34 log,
35 relation_get,
36 relation_ids,
37 relation_set,
38 relations_of_type,
39)
40
41from charmhelpers.core.host import service
42from charmhelpers.core import host
43
44# This module adds compatibility with the nrpe-external-master and plain nrpe
45# subordinate charms. To use it in your charm:
46#
47# 1. Update metadata.yaml
48#
49# provides:
50# (...)
51# nrpe-external-master:
52# interface: nrpe-external-master
53# scope: container
54#
55# and/or
56#
57# provides:
58# (...)
59# local-monitors:
60# interface: local-monitors
61# scope: container
62
63#
64# 2. Add the following to config.yaml
65#
66# nagios_context:
67# default: "juju"
68# type: string
69# description: |
70# Used by the nrpe subordinate charms.
71# A string that will be prepended to instance name to set the host name
72# in nagios. So for instance the hostname would be something like:
73# juju-myservice-0
74# If you're running multiple environments with the same services in them
75# this allows you to differentiate between them.
76# nagios_servicegroups:
77# default: ""
78# type: string
79# description: |
80# A comma-separated list of nagios servicegroups.
81# If left empty, the nagios_context will be used as the servicegroup
82#
83# 3. Add custom checks (Nagios plugins) to files/nrpe-external-master
84#
85# 4. Update your hooks.py with something like this:
86#
87# from charmsupport.nrpe import NRPE
88# (...)
89# def update_nrpe_config():
90# nrpe_compat = NRPE()
91# nrpe_compat.add_check(
92# shortname = "myservice",
93# description = "Check MyService",
94# check_cmd = "check_http -w 2 -c 10 http://localhost"
95# )
96# nrpe_compat.add_check(
97# "myservice_other",
98# "Check for widget failures",
99# check_cmd = "/srv/myapp/scripts/widget_check"
100# )
101# nrpe_compat.write()
102#
103# def config_changed():
104# (...)
105# update_nrpe_config()
106#
107# def nrpe_external_master_relation_changed():
108# update_nrpe_config()
109#
110# def local_monitors_relation_changed():
111# update_nrpe_config()
112#
113# 4.a If your charm is a subordinate charm set primary=False
114#
115# from charmsupport.nrpe import NRPE
116# (...)
117# def update_nrpe_config():
118# nrpe_compat = NRPE(primary=False)
119#
120# 5. ln -s hooks.py nrpe-external-master-relation-changed
121# ln -s hooks.py local-monitors-relation-changed
122
123
124class CheckException(Exception):
125 pass
126
127
128class Check(object):
129 shortname_re = '[A-Za-z0-9-_.@]+$'
130 service_template = ("""
131#---------------------------------------------------
132# This file is Juju managed
133#---------------------------------------------------
134define service {{
135 use active-service
136 host_name {nagios_hostname}
137 service_description {nagios_hostname}[{shortname}] """
138 """{description}
139 check_command check_nrpe!{command}
140 servicegroups {nagios_servicegroup}
141{service_config_overrides}
142}}
143""")
144
145 def __init__(self, shortname, description, check_cmd, max_check_attempts=None):
146 super(Check, self).__init__()
147 # XXX: could be better to calculate this from the service name
148 if not re.match(self.shortname_re, shortname):
149 raise CheckException("shortname must match {}".format(
150 Check.shortname_re))
151 self.shortname = shortname
152 self.command = "check_{}".format(shortname)
153 # Note: a set of invalid characters is defined by the
154 # Nagios server config
155 # The default is: illegal_object_name_chars=`~!$%^&*"|'<>?,()=
156 self.description = description
157 self.check_cmd = self._locate_cmd(check_cmd)
158 self.max_check_attempts = max_check_attempts
159
160 def _get_check_filename(self):
161 return os.path.join(NRPE.nrpe_confdir, '{}.cfg'.format(self.command))
162
163 def _get_service_filename(self, hostname):
164 return os.path.join(NRPE.nagios_exportdir,
165 'service__{}_{}.cfg'.format(hostname, self.command))
166
167 def _locate_cmd(self, check_cmd):
168 search_path = (
169 '/usr/lib/nagios/plugins',
170 '/usr/local/lib/nagios/plugins',
171 )
172 parts = shlex.split(check_cmd)
173 for path in search_path:
174 if os.path.exists(os.path.join(path, parts[0])):
175 command = os.path.join(path, parts[0])
176 if len(parts) > 1:
177 command += " " + " ".join(parts[1:])
178 return command
179 log('Check command not found: {}'.format(parts[0]))
180 return ''
181
182 def _remove_service_files(self):
183 if not os.path.exists(NRPE.nagios_exportdir):
184 return
185 for f in os.listdir(NRPE.nagios_exportdir):
186 if f.endswith('_{}.cfg'.format(self.command)):
187 os.remove(os.path.join(NRPE.nagios_exportdir, f))
188
189 def remove(self, hostname):
190 nrpe_check_file = self._get_check_filename()
191 if os.path.exists(nrpe_check_file):
192 os.remove(nrpe_check_file)
193 self._remove_service_files()
194
195 def write(self, nagios_context, hostname, nagios_servicegroups):
196 nrpe_check_file = self._get_check_filename()
197 with open(nrpe_check_file, 'w') as nrpe_check_config:
198 nrpe_check_config.write("# check {}\n".format(self.shortname))
199 if nagios_servicegroups:
200 nrpe_check_config.write(
201 "# The following header was added automatically by juju\n")
202 nrpe_check_config.write(
203 "# Modifying it will affect nagios monitoring and alerting\n")
204 nrpe_check_config.write(
205 "# servicegroups: {}\n".format(nagios_servicegroups))
206 nrpe_check_config.write("command[{}]={}\n".format(
207 self.command, self.check_cmd))
208
209 if not os.path.exists(NRPE.nagios_exportdir):
210 log('Not writing service config as {} is not accessible'.format(
211 NRPE.nagios_exportdir))
212 else:
213 self.write_service_config(nagios_context, hostname,
214 nagios_servicegroups)
215
216 def write_service_config(self, nagios_context, hostname,
217 nagios_servicegroups):
218 self._remove_service_files()
219
220 if self.max_check_attempts:
221 service_config_overrides = ' max_check_attempts {}'.format(
222 self.max_check_attempts
223 ) # Note indentation is here rather than in the template to avoid trailing spaces
224 else:
225 service_config_overrides = '' # empty string to avoid printing 'None'
226 templ_vars = {
227 'nagios_hostname': hostname,
228 'nagios_servicegroup': nagios_servicegroups,
229 'description': self.description,
230 'shortname': self.shortname,
231 'command': self.command,
232 'service_config_overrides': service_config_overrides,
233 }
234 nrpe_service_text = Check.service_template.format(**templ_vars)
235 nrpe_service_file = self._get_service_filename(hostname)
236 with open(nrpe_service_file, 'w') as nrpe_service_config:
237 nrpe_service_config.write(str(nrpe_service_text))
238
239 def run(self):
240 subprocess.call(self.check_cmd)
241
242
243class NRPE(object):
244 nagios_logdir = '/var/log/nagios'
245 nagios_exportdir = '/var/lib/nagios/export'
246 nrpe_confdir = '/etc/nagios/nrpe.d'
247 homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server
248
249 def __init__(self, hostname=None, primary=True):
250 super(NRPE, self).__init__()
251 self.config = config()
252 self.primary = primary
253 self.nagios_context = self.config['nagios_context']
254 if 'nagios_servicegroups' in self.config and self.config['nagios_servicegroups']:
255 self.nagios_servicegroups = self.config['nagios_servicegroups']
256 else:
257 self.nagios_servicegroups = self.nagios_context
258 self.unit_name = local_unit().replace('/', '-')
259 if hostname:
260 self.hostname = hostname
261 else:
262 nagios_hostname = get_nagios_hostname()
263 if nagios_hostname:
264 self.hostname = nagios_hostname
265 else:
266 self.hostname = "{}-{}".format(self.nagios_context, self.unit_name)
267 self.checks = []
268 # Iff in an nrpe-external-master relation hook, set primary status
269 relation = relation_ids('nrpe-external-master')
270 if relation:
271 log("Setting charm primary status {}".format(primary))
272 for rid in relation:
273 relation_set(relation_id=rid, relation_settings={'primary': self.primary})
274 self.remove_check_queue = set()
275
276 @classmethod
277 def does_nrpe_conf_dir_exist(cls):
278 """Return True if th nrpe_confdif directory exists."""
279 return os.path.isdir(cls.nrpe_confdir)
280
281 def add_check(self, *args, **kwargs):
282 shortname = None
283 if kwargs.get('shortname') is None:
284 if len(args) > 0:
285 shortname = args[0]
286 else:
287 shortname = kwargs['shortname']
288
289 self.checks.append(Check(*args, **kwargs))
290 try:
291 self.remove_check_queue.remove(shortname)
292 except KeyError:
293 pass
294
295 def remove_check(self, *args, **kwargs):
296 if kwargs.get('shortname') is None:
297 raise ValueError('shortname of check must be specified')
298
299 # Use sensible defaults if they're not specified - these are not
300 # actually used during removal, but they're required for constructing
301 # the Check object; check_disk is chosen because it's part of the
302 # nagios-plugins-basic package.
303 if kwargs.get('check_cmd') is None:
304 kwargs['check_cmd'] = 'check_disk'
305 if kwargs.get('description') is None:
306 kwargs['description'] = ''
307
308 check = Check(*args, **kwargs)
309 check.remove(self.hostname)
310 self.remove_check_queue.add(kwargs['shortname'])
311
312 def write(self):
313 try:
314 nagios_uid = pwd.getpwnam('nagios').pw_uid
315 nagios_gid = grp.getgrnam('nagios').gr_gid
316 except Exception:
317 log("Nagios user not set up, nrpe checks not updated")
318 return
319
320 if not os.path.exists(NRPE.nagios_logdir):
321 os.mkdir(NRPE.nagios_logdir)
322 os.chown(NRPE.nagios_logdir, nagios_uid, nagios_gid)
323
324 nrpe_monitors = {}
325 monitors = {"monitors": {"remote": {"nrpe": nrpe_monitors}}}
326
327 # check that the charm can write to the conf dir. If not, then nagios
328 # probably isn't installed, and we can defer.
329 if not self.does_nrpe_conf_dir_exist():
330 return
331
332 for nrpecheck in self.checks:
333 nrpecheck.write(self.nagios_context, self.hostname,
334 self.nagios_servicegroups)
335 nrpe_monitors[nrpecheck.shortname] = {
336 "command": nrpecheck.command,
337 }
338 # If we were passed max_check_attempts, add that to the relation data
339 if nrpecheck.max_check_attempts is not None:
340 nrpe_monitors[nrpecheck.shortname]['max_check_attempts'] = nrpecheck.max_check_attempts
341
342 # update-status hooks are configured to firing every 5 minutes by
343 # default. When nagios-nrpe-server is restarted, the nagios server
344 # reports checks failing causing unnecessary alerts. Let's not restart
345 # on update-status hooks.
346 if not hook_name() == 'update-status':
347 service('restart', 'nagios-nrpe-server')
348
349 monitor_ids = relation_ids("local-monitors") + \
350 relation_ids("nrpe-external-master")
351 for rid in monitor_ids:
352 reldata = relation_get(unit=local_unit(), rid=rid)
353 if 'monitors' in reldata:
354 # update the existing set of monitors with the new data
355 old_monitors = yaml.safe_load(reldata['monitors'])
356 old_nrpe_monitors = old_monitors['monitors']['remote']['nrpe']
357 # remove keys that are in the remove_check_queue
358 old_nrpe_monitors = {k: v for k, v in old_nrpe_monitors.items()
359 if k not in self.remove_check_queue}
360 # update/add nrpe_monitors
361 old_nrpe_monitors.update(nrpe_monitors)
362 old_monitors['monitors']['remote']['nrpe'] = old_nrpe_monitors
363 # write back to the relation
364 relation_set(relation_id=rid, monitors=yaml.dump(old_monitors))
365 else:
366 # write a brand new set of monitors, as no existing ones.
367 relation_set(relation_id=rid, monitors=yaml.dump(monitors))
368
369 self.remove_check_queue.clear()
370
371
372def get_nagios_hostcontext(relation_name='nrpe-external-master'):
373 """
374 Query relation with nrpe subordinate, return the nagios_host_context
375
376 :param str relation_name: Name of relation nrpe sub joined to
377 """
378 for rel in relations_of_type(relation_name):
379 if 'nagios_host_context' in rel:
380 return rel['nagios_host_context']
381
382
383def get_nagios_hostname(relation_name='nrpe-external-master'):
384 """
385 Query relation with nrpe subordinate, return the nagios_hostname
386
387 :param str relation_name: Name of relation nrpe sub joined to
388 """
389 for rel in relations_of_type(relation_name):
390 if 'nagios_hostname' in rel:
391 return rel['nagios_hostname']
392
393
394def get_nagios_unit_name(relation_name='nrpe-external-master'):
395 """
396 Return the nagios unit name prepended with host_context if needed
397
398 :param str relation_name: Name of relation nrpe sub joined to
399 """
400 host_context = get_nagios_hostcontext(relation_name)
401 if host_context:
402 unit = "%s:%s" % (host_context, local_unit())
403 else:
404 unit = local_unit()
405 return unit
406
407
408def add_init_service_checks(nrpe, services, unit_name, immediate_check=True):
409 """
410 Add checks for each service in list
411
412 :param NRPE nrpe: NRPE object to add check to
413 :param list services: List of services to check
414 :param str unit_name: Unit name to use in check description
415 :param bool immediate_check: For sysv init, run the service check immediately
416 """
417 for svc in services:
418 # Don't add a check for these services from neutron-gateway
419 if svc in ['ext-port', 'os-charm-phy-nic-mtu']:
420 next
421
422 upstart_init = '/etc/init/%s.conf' % svc
423 sysv_init = '/etc/init.d/%s' % svc
424
425 if host.init_is_systemd(service_name=svc):
426 nrpe.add_check(
427 shortname=svc,
428 description='process check {%s}' % unit_name,
429 check_cmd='check_systemd.py %s' % svc
430 )
431 elif os.path.exists(upstart_init):
432 nrpe.add_check(
433 shortname=svc,
434 description='process check {%s}' % unit_name,
435 check_cmd='check_upstart_job %s' % svc
436 )
437 elif os.path.exists(sysv_init):
438 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
439 checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc)
440 croncmd = (
441 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
442 '-e -s /etc/init.d/%s status' % svc
443 )
444 cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath)
445 f = open(cronpath, 'w')
446 f.write(cron_file)
447 f.close()
448 nrpe.add_check(
449 shortname=svc,
450 description='service check {%s}' % unit_name,
451 check_cmd='check_status_file.py -f %s' % checkpath,
452 )
453 # if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail
454 # (LP: #1670223).
455 if immediate_check and os.path.isdir(nrpe.homedir):
456 f = open(checkpath, 'w')
457 subprocess.call(
458 croncmd.split(),
459 stdout=f,
460 stderr=subprocess.STDOUT
461 )
462 f.close()
463 os.chmod(checkpath, 0o644)
464
465
466def copy_nrpe_checks(nrpe_files_dir=None):
467 """
468 Copy the nrpe checks into place
469
470 """
471 NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
472 if nrpe_files_dir is None:
473 # determine if "charmhelpers" is in CHARMDIR or CHARMDIR/hooks
474 for segment in ['.', 'hooks']:
475 nrpe_files_dir = os.path.abspath(os.path.join(
476 os.getenv('CHARM_DIR'),
477 segment,
478 'charmhelpers',
479 'contrib',
480 'openstack',
481 'files'))
482 if os.path.isdir(nrpe_files_dir):
483 break
484 else:
485 raise RuntimeError("Couldn't find charmhelpers directory")
486 if not os.path.exists(NAGIOS_PLUGINS):
487 os.makedirs(NAGIOS_PLUGINS)
488 for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
489 if os.path.isfile(fname):
490 shutil.copy2(fname,
491 os.path.join(NAGIOS_PLUGINS, os.path.basename(fname)))
492
493
494def add_haproxy_checks(nrpe, unit_name):
495 """
496 Add checks for each service in list
497
498 :param NRPE nrpe: NRPE object to add check to
499 :param str unit_name: Unit name to use in check description
500 """
501 nrpe.add_check(
502 shortname='haproxy_servers',
503 description='Check HAProxy {%s}' % unit_name,
504 check_cmd='check_haproxy.sh')
505 nrpe.add_check(
506 shortname='haproxy_queue',
507 description='Check HAProxy queue depth {%s}' % unit_name,
508 check_cmd='check_haproxy_queue_depth.sh')
509
510
511def remove_deprecated_check(nrpe, deprecated_services):
512 """
513 Remove checks for deprecated services in list
514
515 :param nrpe: NRPE object to remove check from
516 :type nrpe: NRPE
517 :param deprecated_services: List of deprecated services that are removed
518 :type deprecated_services: list
519 """
520 for dep_svc in deprecated_services:
521 log('Deprecated service: {}'.format(dep_svc))
522 nrpe.remove_check(shortname=dep_svc)
0523
=== modified file 'charmhelpers/contrib/hahelpers/apache.py'
--- charmhelpers/contrib/hahelpers/apache.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/contrib/hahelpers/apache.py 2021-11-10 05:36:20 +0000
@@ -34,6 +34,10 @@
34 INFO,34 INFO,
35)35)
3636
37# This file contains the CA cert from the charms ssl_ca configuration
38# option, in future the file name should be updated reflect that.
39CONFIG_CA_CERT_FILE = 'keystone_juju_ca_cert'
40
3741
38def get_cert(cn=None):42def get_cert(cn=None):
39 # TODO: deal with multiple https endpoints via charm config43 # TODO: deal with multiple https endpoints via charm config
@@ -83,4 +87,4 @@
8387
8488
85def install_ca_cert(ca_cert):89def install_ca_cert(ca_cert):
86 host.install_ca_cert(ca_cert, 'keystone_juju_ca_cert')90 host.install_ca_cert(ca_cert, CONFIG_CA_CERT_FILE)
8791
=== modified file 'charmhelpers/contrib/hahelpers/cluster.py'
--- charmhelpers/contrib/hahelpers/cluster.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/contrib/hahelpers/cluster.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2021 Canonical Limited.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -25,6 +25,7 @@
25clustering-related helpers.25clustering-related helpers.
26"""26"""
2727
28import functools
28import subprocess29import subprocess
29import os30import os
30import time31import time
@@ -85,7 +86,7 @@
85 2. If the charm is part of a corosync cluster, call corosync to86 2. If the charm is part of a corosync cluster, call corosync to
86 determine leadership.87 determine leadership.
87 3. If the charm is not part of a corosync cluster, the leader is88 3. If the charm is not part of a corosync cluster, the leader is
88 determined as being "the alive unit with the lowest unit numer". In89 determined as being "the alive unit with the lowest unit number". In
89 other words, the oldest surviving unit.90 other words, the oldest surviving unit.
90 """91 """
91 try:92 try:
@@ -281,6 +282,10 @@
281 return public_port - (i * 10)282 return public_port - (i * 10)
282283
283284
285determine_apache_port_single = functools.partial(
286 determine_apache_port, singlenode_mode=True)
287
288
284def get_hacluster_config(exclude_keys=None):289def get_hacluster_config(exclude_keys=None):
285 '''290 '''
286 Obtains all relevant configuration from charm configuration required291 Obtains all relevant configuration from charm configuration required
@@ -404,3 +409,43 @@
404 log(msg, DEBUG)409 log(msg, DEBUG)
405 status_set('maintenance', msg)410 status_set('maintenance', msg)
406 time.sleep(calculated_wait)411 time.sleep(calculated_wait)
412
413
414def get_managed_services_and_ports(services, external_ports,
415 external_services=None,
416 port_conv_f=determine_apache_port_single):
417 """Get the services and ports managed by this charm.
418
419 Return only the services and corresponding ports that are managed by this
420 charm. This excludes haproxy when there is a relation with hacluster. This
421 is because this charm passes responsibility for stopping and starting
422 haproxy to hacluster.
423
424 Similarly, if a relation with hacluster exists then the ports returned by
425 this method correspond to those managed by the apache server rather than
426 haproxy.
427
428 :param services: List of services.
429 :type services: List[str]
430 :param external_ports: List of ports managed by external services.
431 :type external_ports: List[int]
432 :param external_services: List of services to be removed if ha relation is
433 present.
434 :type external_services: List[str]
435 :param port_conv_f: Function to apply to ports to calculate the ports
436 managed by services controlled by this charm.
437 :type port_convert_func: f()
438 :returns: A tuple containing a list of services first followed by a list of
439 ports.
440 :rtype: Tuple[List[str], List[int]]
441 """
442 if external_services is None:
443 external_services = ['haproxy']
444 if relation_ids('ha'):
445 for svc in external_services:
446 try:
447 services.remove(svc)
448 except ValueError:
449 pass
450 external_ports = [port_conv_f(p) for p in external_ports]
451 return services, external_ports
407452
=== modified file 'charmhelpers/core/decorators.py'
--- charmhelpers/core/decorators.py 2017-03-03 21:03:14 +0000
+++ charmhelpers/core/decorators.py 2021-11-10 05:36:20 +0000
@@ -53,3 +53,41 @@
53 return _retry_on_exception_inner_253 return _retry_on_exception_inner_2
5454
55 return _retry_on_exception_inner_155 return _retry_on_exception_inner_1
56
57
58def retry_on_predicate(num_retries, predicate_fun, base_delay=0):
59 """Retry based on return value
60
61 The return value of the decorated function is passed to the given predicate_fun. If the
62 result of the predicate is False, retry the decorated function up to num_retries times
63
64 An exponential backoff up to base_delay^num_retries seconds can be introduced by setting
65 base_delay to a nonzero value. The default is to run with a zero (i.e. no) delay
66
67 :param num_retries: Max. number of retries to perform
68 :type num_retries: int
69 :param predicate_fun: Predicate function to determine if a retry is necessary
70 :type predicate_fun: callable
71 :param base_delay: Starting value in seconds for exponential delay, defaults to 0 (no delay)
72 :type base_delay: float
73 """
74 def _retry_on_pred_inner_1(f):
75 def _retry_on_pred_inner_2(*args, **kwargs):
76 retries = num_retries
77 multiplier = 1
78 delay = base_delay
79 while True:
80 result = f(*args, **kwargs)
81 if predicate_fun(result) or retries <= 0:
82 return result
83 delay *= multiplier
84 multiplier += 1
85 log("Result {}, retrying '{}' {} more times (delay={})".format(
86 result, f.__name__, retries, delay), level=INFO)
87 retries -= 1
88 if delay:
89 time.sleep(delay)
90
91 return _retry_on_pred_inner_2
92
93 return _retry_on_pred_inner_1
5694
=== modified file 'charmhelpers/core/hookenv.py'
--- charmhelpers/core/hookenv.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/hookenv.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2013-2021 Canonical Limited.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -13,7 +13,6 @@
13# limitations under the License.13# limitations under the License.
1414
15"Interactions with the Juju environment"15"Interactions with the Juju environment"
16# Copyright 2013 Canonical Ltd.
17#16#
18# Authors:17# Authors:
19# Charm Helpers Developers <juju@lists.ubuntu.com>18# Charm Helpers Developers <juju@lists.ubuntu.com>
@@ -21,6 +20,7 @@
21from __future__ import print_function20from __future__ import print_function
22import copy21import copy
23from distutils.version import LooseVersion22from distutils.version import LooseVersion
23from enum import Enum
24from functools import wraps24from functools import wraps
25from collections import namedtuple25from collections import namedtuple
26import glob26import glob
@@ -34,6 +34,8 @@
34import tempfile34import tempfile
35from subprocess import CalledProcessError35from subprocess import CalledProcessError
3636
37from charmhelpers import deprecate
38
37import six39import six
38if not six.PY3:40if not six.PY3:
39 from UserDict import UserDict41 from UserDict import UserDict
@@ -55,6 +57,14 @@
55 'This may not be compatible with software you are '57 'This may not be compatible with software you are '
56 'running in your shell.')58 'running in your shell.')
5759
60
61class WORKLOAD_STATES(Enum):
62 ACTIVE = 'active'
63 BLOCKED = 'blocked'
64 MAINTENANCE = 'maintenance'
65 WAITING = 'waiting'
66
67
58cache = {}68cache = {}
5969
6070
@@ -119,6 +129,24 @@
119 raise129 raise
120130
121131
132def function_log(message):
133 """Write a function progress message"""
134 command = ['function-log']
135 if not isinstance(message, six.string_types):
136 message = repr(message)
137 command += [message[:SH_MAX_ARG]]
138 # Missing function-log should not cause failures in unit tests
139 # Send function_log output to stderr
140 try:
141 subprocess.call(command)
142 except OSError as e:
143 if e.errno == errno.ENOENT:
144 message = "function-log: {}".format(message)
145 print(message, file=sys.stderr)
146 else:
147 raise
148
149
122class Serializable(UserDict):150class Serializable(UserDict):
123 """Wrapper, an object that can be serialized to yaml or json"""151 """Wrapper, an object that can be serialized to yaml or json"""
124152
@@ -197,6 +225,17 @@
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')
198226
199227
228def departing_unit():
229 """The departing unit for the current relation hook.
230
231 Available since juju 2.8.
232
233 :returns: the departing unit, or None if the information isn't available.
234 :rtype: Optional[str]
235 """
236 return os.environ.get('JUJU_DEPARTING_UNIT', None)
237
238
200def local_unit():239def local_unit():
201 """Local unit ID"""240 """Local unit ID"""
202 return os.environ['JUJU_UNIT_NAME']241 return os.environ['JUJU_UNIT_NAME']
@@ -343,8 +382,10 @@
343 try:382 try:
344 self._prev_dict = json.load(f)383 self._prev_dict = json.load(f)
345 except ValueError as e:384 except ValueError as e:
346 log('Unable to parse previous config data - {}'.format(str(e)),385 log('Found but was unable to parse previous config data, '
347 level=ERROR)386 'ignoring which will report all values as changed - {}'
387 .format(str(e)), level=ERROR)
388 return
348 for k, v in copy.deepcopy(self._prev_dict).items():389 for k, v in copy.deepcopy(self._prev_dict).items():
349 if k not in self:390 if k not in self:
350 self[k] = v391 self[k] = v
@@ -426,15 +467,20 @@
426467
427468
428@cached469@cached
429def relation_get(attribute=None, unit=None, rid=None):470def relation_get(attribute=None, unit=None, rid=None, app=None):
430 """Get relation information"""471 """Get relation information"""
431 _args = ['relation-get', '--format=json']472 _args = ['relation-get', '--format=json']
473 if app is not None:
474 if unit is not None:
475 raise ValueError("Cannot use both 'unit' and 'app'")
476 _args.append('--app')
432 if rid:477 if rid:
433 _args.append('-r')478 _args.append('-r')
434 _args.append(rid)479 _args.append(rid)
435 _args.append(attribute or '-')480 _args.append(attribute or '-')
436 if unit:481 # unit or application name
437 _args.append(unit)482 if unit or app:
483 _args.append(unit or app)
438 try:484 try:
439 return json.loads(subprocess.check_output(_args).decode('UTF-8'))485 return json.loads(subprocess.check_output(_args).decode('UTF-8'))
440 except ValueError:486 except ValueError:
@@ -445,12 +491,14 @@
445 raise491 raise
446492
447493
448def relation_set(relation_id=None, relation_settings=None, **kwargs):494def relation_set(relation_id=None, relation_settings=None, app=False, **kwargs):
449 """Set relation information for the current unit"""495 """Set relation information for the current unit"""
450 relation_settings = relation_settings if relation_settings else {}496 relation_settings = relation_settings if relation_settings else {}
451 relation_cmd_line = ['relation-set']497 relation_cmd_line = ['relation-set']
452 accepts_file = "--file" in subprocess.check_output(498 accepts_file = "--file" in subprocess.check_output(
453 relation_cmd_line + ["--help"], universal_newlines=True)499 relation_cmd_line + ["--help"], universal_newlines=True)
500 if app:
501 relation_cmd_line.append('--app')
454 if relation_id is not None:502 if relation_id is not None:
455 relation_cmd_line.extend(('-r', relation_id))503 relation_cmd_line.extend(('-r', relation_id))
456 settings = relation_settings.copy()504 settings = relation_settings.copy()
@@ -561,7 +609,7 @@
561 relation_type()))609 relation_type()))
562610
563 :param reltype: Relation type to list data for, default is to list data for611 :param reltype: Relation type to list data for, default is to list data for
564 the realtion type we are currently executing a hook for.612 the relation type we are currently executing a hook for.
565 :type reltype: str613 :type reltype: str
566 :returns: iterator614 :returns: iterator
567 :rtype: types.GeneratorType615 :rtype: types.GeneratorType
@@ -578,7 +626,7 @@
578626
579@cached627@cached
580def relation_for_unit(unit=None, rid=None):628def relation_for_unit(unit=None, rid=None):
581 """Get the json represenation of a unit's relation"""629 """Get the json representation of a unit's relation"""
582 unit = unit or remote_unit()630 unit = unit or remote_unit()
583 relation = relation_get(unit=unit, rid=rid)631 relation = relation_get(unit=unit, rid=rid)
584 for key in relation:632 for key in relation:
@@ -946,9 +994,23 @@
946 return os.environ.get('CHARM_DIR')994 return os.environ.get('CHARM_DIR')
947995
948996
997def cmd_exists(cmd):
998 """Return True if the specified cmd exists in the path"""
999 return any(
1000 os.access(os.path.join(path, cmd), os.X_OK)
1001 for path in os.environ["PATH"].split(os.pathsep)
1002 )
1003
1004
949@cached1005@cached
1006@deprecate("moved to function_get()", log=log)
950def action_get(key=None):1007def action_get(key=None):
951 """Gets the value of an action parameter, or all key/value param pairs"""1008 """
1009 .. deprecated:: 0.20.7
1010 Alias for :func:`function_get`.
1011
1012 Gets the value of an action parameter, or all key/value param pairs.
1013 """
952 cmd = ['action-get']1014 cmd = ['action-get']
953 if key is not None:1015 if key is not None:
954 cmd.append(key)1016 cmd.append(key)
@@ -957,52 +1019,130 @@
957 return action_data1019 return action_data
9581020
9591021
1022@cached
1023def function_get(key=None):
1024 """Gets the value of an action parameter, or all key/value param pairs"""
1025 cmd = ['function-get']
1026 # Fallback for older charms.
1027 if not cmd_exists('function-get'):
1028 cmd = ['action-get']
1029
1030 if key is not None:
1031 cmd.append(key)
1032 cmd.append('--format=json')
1033 function_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
1034 return function_data
1035
1036
1037@deprecate("moved to function_set()", log=log)
960def action_set(values):1038def action_set(values):
961 """Sets the values to be returned after the action finishes"""1039 """
1040 .. deprecated:: 0.20.7
1041 Alias for :func:`function_set`.
1042
1043 Sets the values to be returned after the action finishes.
1044 """
962 cmd = ['action-set']1045 cmd = ['action-set']
963 for k, v in list(values.items()):1046 for k, v in list(values.items()):
964 cmd.append('{}={}'.format(k, v))1047 cmd.append('{}={}'.format(k, v))
965 subprocess.check_call(cmd)1048 subprocess.check_call(cmd)
9661049
9671050
1051def function_set(values):
1052 """Sets the values to be returned after the function finishes"""
1053 cmd = ['function-set']
1054 # Fallback for older charms.
1055 if not cmd_exists('function-get'):
1056 cmd = ['action-set']
1057
1058 for k, v in list(values.items()):
1059 cmd.append('{}={}'.format(k, v))
1060 subprocess.check_call(cmd)
1061
1062
1063@deprecate("moved to function_fail()", log=log)
968def action_fail(message):1064def action_fail(message):
969 """Sets the action status to failed and sets the error message.1065 """
9701066 .. deprecated:: 0.20.7
971 The results set by action_set are preserved."""1067 Alias for :func:`function_fail`.
1068
1069 Sets the action status to failed and sets the error message.
1070
1071 The results set by action_set are preserved.
1072 """
972 subprocess.check_call(['action-fail', message])1073 subprocess.check_call(['action-fail', message])
9731074
9741075
1076def function_fail(message):
1077 """Sets the function status to failed and sets the error message.
1078
1079 The results set by function_set are preserved."""
1080 cmd = ['function-fail']
1081 # Fallback for older charms.
1082 if not cmd_exists('function-fail'):
1083 cmd = ['action-fail']
1084 cmd.append(message)
1085
1086 subprocess.check_call(cmd)
1087
1088
975def action_name():1089def action_name():
976 """Get the name of the currently executing action."""1090 """Get the name of the currently executing action."""
977 return os.environ.get('JUJU_ACTION_NAME')1091 return os.environ.get('JUJU_ACTION_NAME')
9781092
9791093
1094def function_name():
1095 """Get the name of the currently executing function."""
1096 return os.environ.get('JUJU_FUNCTION_NAME') or action_name()
1097
1098
980def action_uuid():1099def action_uuid():
981 """Get the UUID of the currently executing action."""1100 """Get the UUID of the currently executing action."""
982 return os.environ.get('JUJU_ACTION_UUID')1101 return os.environ.get('JUJU_ACTION_UUID')
9831102
9841103
1104def function_id():
1105 """Get the ID of the currently executing function."""
1106 return os.environ.get('JUJU_FUNCTION_ID') or action_uuid()
1107
1108
985def action_tag():1109def action_tag():
986 """Get the tag for the currently executing action."""1110 """Get the tag for the currently executing action."""
987 return os.environ.get('JUJU_ACTION_TAG')1111 return os.environ.get('JUJU_ACTION_TAG')
9881112
9891113
990def status_set(workload_state, message):1114def function_tag():
1115 """Get the tag for the currently executing function."""
1116 return os.environ.get('JUJU_FUNCTION_TAG') or action_tag()
1117
1118
1119def status_set(workload_state, message, application=False):
991 """Set the workload state with a message1120 """Set the workload state with a message
9921121
993 Use status-set to set the workload state with a message which is visible1122 Use status-set to set the workload state with a message which is visible
994 to the user via juju status. If the status-set command is not found then1123 to the user via juju status. If the status-set command is not found then
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.
9961125
997 workload_state -- valid juju workload state.1126 workload_state -- valid juju workload state. str or WORKLOAD_STATES
998 message -- status update message1127 message -- status update message
1128 application -- Whether this is an application state set
999 """1129 """
1000 valid_states = ['maintenance', 'blocked', 'waiting', 'active']1130 bad_state_msg = '{!r} is not a valid workload state'
1001 if workload_state not in valid_states:1131
1002 raise ValueError(1132 if isinstance(workload_state, str):
1003 '{!r} is not a valid workload state'.format(workload_state)1133 try:
1004 )1134 # Convert string to enum.
1005 cmd = ['status-set', workload_state, message]1135 workload_state = WORKLOAD_STATES[workload_state.upper()]
1136 except KeyError:
1137 raise ValueError(bad_state_msg.format(workload_state))
1138
1139 if workload_state not in WORKLOAD_STATES:
1140 raise ValueError(bad_state_msg.format(workload_state))
1141
1142 cmd = ['status-set']
1143 if application:
1144 cmd.append('--application')
1145 cmd.extend([workload_state.value, message])
1006 try:1146 try:
1007 ret = subprocess.call(cmd)1147 ret = subprocess.call(cmd)
1008 if ret == 0:1148 if ret == 0:
@@ -1010,7 +1150,7 @@
1010 except OSError as e:1150 except OSError as e:
1011 if e.errno != errno.ENOENT:1151 if e.errno != errno.ENOENT:
1012 raise1152 raise
1013 log_message = 'status-set failed: {} {}'.format(workload_state,1153 log_message = 'status-set failed: {} {}'.format(workload_state.value,
1014 message)1154 message)
1015 log(log_message, level='INFO')1155 log(log_message, level='INFO')
10161156
@@ -1425,13 +1565,13 @@
1425 """Get proxy settings from process environment variables.1565 """Get proxy settings from process environment variables.
14261566
1427 Get charm proxy settings from environment variables that correspond to1567 Get charm proxy settings from environment variables that correspond to
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
1429 see lp:1782236) in a format suitable for passing to an application that1569 lp:1782236) and juju-ftp-proxy in a format suitable for passing to an
1430 reacts to proxy settings passed as environment variables. Some applications1570 application that reacts to proxy settings passed as environment variables.
1431 support lowercase or uppercase notation (e.g. curl), some support only1571 Some applications support lowercase or uppercase notation (e.g. curl), some
1432 lowercase (e.g. wget), there are also subjectively rare cases of only1572 support only lowercase (e.g. wget), there are also subjectively rare cases
1433 uppercase notation support. no_proxy CIDR and wildcard support also varies1573 of only uppercase notation support. no_proxy CIDR and wildcard support also
1434 between runtimes and applications as there is no enforced standard.1574 varies between runtimes and applications as there is no enforced standard.
14351575
1436 Some applications may connect to multiple destinations and expose config1576 Some applications may connect to multiple destinations and expose config
1437 options that would affect only proxy settings for a specific destination1577 options that would affect only proxy settings for a specific destination
@@ -1473,11 +1613,11 @@
1473def _contains_range(addresses):1613def _contains_range(addresses):
1474 """Check for cidr or wildcard domain in a string.1614 """Check for cidr or wildcard domain in a string.
14751615
1476 Given a string comprising a comma seperated list of ip addresses1616 Given a string comprising a comma separated list of ip addresses
1477 and domain names, determine whether the string contains IP ranges1617 and domain names, determine whether the string contains IP ranges
1478 or wildcard domains.1618 or wildcard domains.
14791619
1480 :param addresses: comma seperated list of domains and ip addresses.1620 :param addresses: comma separated list of domains and ip addresses.
1481 :type addresses: str1621 :type addresses: str
1482 """1622 """
1483 return (1623 return (
@@ -1488,3 +1628,12 @@
1488 addresses.startswith(".") or1628 addresses.startswith(".") or
1489 ",." in addresses or1629 ",." in addresses or
1490 " ." in addresses)1630 " ." in addresses)
1631
1632
1633def is_subordinate():
1634 """Check whether charm is subordinate in unit metadata.
1635
1636 :returns: True if unit is subordniate, False otherwise.
1637 :rtype: bool
1638 """
1639 return metadata().get('subordinate') is True
14911640
=== modified file 'charmhelpers/core/host.py'
--- charmhelpers/core/host.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/host.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2021 Canonical Limited.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -19,6 +19,7 @@
19# Nick Moffitt <nick.moffitt@canonical.com>19# Nick Moffitt <nick.moffitt@canonical.com>
20# Matthew Wedgwood <matthew.wedgwood@canonical.com>20# Matthew Wedgwood <matthew.wedgwood@canonical.com>
2121
22import errno
22import os23import os
23import re24import re
24import pwd25import pwd
@@ -33,7 +34,7 @@
33import six34import six
3435
35from contextlib import contextmanager36from contextlib import contextmanager
36from collections import OrderedDict37from collections import OrderedDict, defaultdict
37from .hookenv import log, INFO, DEBUG, local_unit, charm_name38from .hookenv import log, INFO, DEBUG, local_unit, charm_name
38from .fstab import Fstab39from .fstab import Fstab
39from charmhelpers.osplatform import get_platform40from charmhelpers.osplatform import get_platform
@@ -59,6 +60,7 @@
59 ) # flake8: noqa -- ignore F401 for this import60 ) # flake8: noqa -- ignore F401 for this import
6061
61UPDATEDB_PATH = '/etc/updatedb.conf'62UPDATEDB_PATH = '/etc/updatedb.conf'
63CA_CERT_DIR = '/usr/local/share/ca-certificates'
6264
6365
64def service_start(service_name, **kwargs):66def service_start(service_name, **kwargs):
@@ -193,7 +195,7 @@
193 stopped = service_stop(service_name, **kwargs)195 stopped = service_stop(service_name, **kwargs)
194 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))196 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
195 sysv_file = os.path.join(initd_dir, service_name)197 sysv_file = os.path.join(initd_dir, service_name)
196 if init_is_systemd():198 if init_is_systemd(service_name=service_name):
197 service('disable', service_name)199 service('disable', service_name)
198 service('mask', service_name)200 service('mask', service_name)
199 elif os.path.exists(upstart_file):201 elif os.path.exists(upstart_file):
@@ -215,7 +217,7 @@
215 initd_dir="/etc/init.d", **kwargs):217 initd_dir="/etc/init.d", **kwargs):
216 """Resume a system service.218 """Resume a system service.
217219
218 Reenable starting again at boot. Start the service.220 Re-enable starting again at boot. Start the service.
219221
220 :param service_name: the name of the service to resume222 :param service_name: the name of the service to resume
221 :param init_dir: the path to the init dir223 :param init_dir: the path to the init dir
@@ -227,7 +229,7 @@
227 """229 """
228 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))230 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
229 sysv_file = os.path.join(initd_dir, service_name)231 sysv_file = os.path.join(initd_dir, service_name)
230 if init_is_systemd():232 if init_is_systemd(service_name=service_name):
231 service('unmask', service_name)233 service('unmask', service_name)
232 service('enable', service_name)234 service('enable', service_name)
233 elif os.path.exists(upstart_file):235 elif os.path.exists(upstart_file):
@@ -257,7 +259,7 @@
257 :param **kwargs: additional params to be passed to the service command in259 :param **kwargs: additional params to be passed to the service command in
258 the form of key=value.260 the form of key=value.
259 """261 """
260 if init_is_systemd():262 if init_is_systemd(service_name=service_name):
261 cmd = ['systemctl', action, service_name]263 cmd = ['systemctl', action, service_name]
262 else:264 else:
263 cmd = ['service', service_name, action]265 cmd = ['service', service_name, action]
@@ -281,7 +283,7 @@
281 units (e.g. service ceph-osd status id=2). The kwargs283 units (e.g. service ceph-osd status id=2). The kwargs
282 are ignored in systemd services.284 are ignored in systemd services.
283 """285 """
284 if init_is_systemd():286 if init_is_systemd(service_name=service_name):
285 return service('is-active', service_name)287 return service('is-active', service_name)
286 else:288 else:
287 if os.path.exists(_UPSTART_CONF.format(service_name)):289 if os.path.exists(_UPSTART_CONF.format(service_name)):
@@ -311,8 +313,14 @@
311SYSTEMD_SYSTEM = '/run/systemd/system'313SYSTEMD_SYSTEM = '/run/systemd/system'
312314
313315
314def init_is_systemd():316def init_is_systemd(service_name=None):
315 """Return True if the host system uses systemd, False otherwise."""317 """
318 Returns whether the host uses systemd for the specified service.
319
320 @param Optional[str] service_name: specific name of service
321 """
322 if str(service_name).startswith("snap."):
323 return True
316 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':324 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
317 return False325 return False
318 return os.path.isdir(SYSTEMD_SYSTEM)326 return os.path.isdir(SYSTEMD_SYSTEM)
@@ -671,7 +679,7 @@
671679
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.
673 :param str hash_type: Hash algorithm used to generate `checksum`.681 :param str hash_type: Hash algorithm used to generate `checksum`.
674 Can be any hash alrgorithm supported by :mod:`hashlib`,682 Can be any hash algorithm supported by :mod:`hashlib`,
675 such as md5, sha1, sha256, sha512, etc.683 such as md5, sha1, sha256, sha512, etc.
676 :raises ChecksumError: If the file fails the checksum684 :raises ChecksumError: If the file fails the checksum
677685
@@ -686,78 +694,227 @@
686 pass694 pass
687695
688696
689def restart_on_change(restart_map, stopstart=False, restart_functions=None):697class restart_on_change(object):
690 """Restart services based on configuration files changing698 """Decorator and context manager to handle restarts.
691699
692 This function is used a decorator, for example::700 Usage:
693701
694 @restart_on_change({702 @restart_on_change(restart_map, ...)
695 '/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]703 def function_that_might_trigger_a_restart(...)
696 '/etc/apache/sites-enabled/*': [ 'apache2' ]704 ...
697 })705
698 def config_changed():706 Or:
699 pass # your code here707
700708 with restart_on_change(restart_map, ...):
701 In this example, the cinder-api and cinder-volume services709 do_stuff_that_might_trigger_a_restart()
702 would be restarted if /etc/ceph/ceph.conf is changed by the710 ...
703 ceph_client_changed function. The apache2 service would be
704 restarted if any file matching the pattern got changed, created
705 or removed. Standard wildcards are supported, see documentation
706 for the 'glob' module for more information.
707
708 @param restart_map: {path_file_name: [service_name, ...]
709 @param stopstart: DEFAULT false; whether to stop, start OR restart
710 @param restart_functions: nonstandard functions to use to restart services
711 {svc: func, ...}
712 @returns result from decorated function
713 """711 """
714 def wrap(f):712
713 def __init__(self, restart_map, stopstart=False, restart_functions=None,
714 can_restart_now_f=None, post_svc_restart_f=None,
715 pre_restarts_wait_f=None):
716 """
717 :param restart_map: {file: [service, ...]}
718 :type restart_map: Dict[str, List[str,]]
719 :param stopstart: whether to stop, start or restart a service
720 :type stopstart: booleean
721 :param restart_functions: nonstandard functions to use to restart
722 services {svc: func, ...}
723 :type restart_functions: Dict[str, Callable[[str], None]]
724 :param can_restart_now_f: A function used to check if the restart is
725 permitted.
726 :type can_restart_now_f: Callable[[str, List[str]], boolean]
727 :param post_svc_restart_f: A function run after a service has
728 restarted.
729 :type post_svc_restart_f: Callable[[str], None]
730 :param pre_restarts_wait_f: A function called before any restarts.
731 :type pre_restarts_wait_f: Callable[None, None]
732 """
733 self.restart_map = restart_map
734 self.stopstart = stopstart
735 self.restart_functions = restart_functions
736 self.can_restart_now_f = can_restart_now_f
737 self.post_svc_restart_f = post_svc_restart_f
738 self.pre_restarts_wait_f = pre_restarts_wait_f
739
740 def __call__(self, f):
741 """Work like a decorator.
742
743 Returns a wrapped function that performs the restart if triggered.
744
745 :param f: The function that is being wrapped.
746 :type f: Callable[[Any], Any]
747 :returns: the wrapped function
748 :rtype: Callable[[Any], Any]
749 """
715 @functools.wraps(f)750 @functools.wraps(f)
716 def wrapped_f(*args, **kwargs):751 def wrapped_f(*args, **kwargs):
717 return restart_on_change_helper(752 return restart_on_change_helper(
718 (lambda: f(*args, **kwargs)), restart_map, stopstart,753 (lambda: f(*args, **kwargs)),
719 restart_functions)754 self.restart_map,
755 stopstart=self.stopstart,
756 restart_functions=self.restart_functions,
757 can_restart_now_f=self.can_restart_now_f,
758 post_svc_restart_f=self.post_svc_restart_f,
759 pre_restarts_wait_f=self.pre_restarts_wait_f)
720 return wrapped_f760 return wrapped_f
721 return wrap761
762 def __enter__(self):
763 """Enter the runtime context related to this object. """
764 self.checksums = _pre_restart_on_change_helper(self.restart_map)
765
766 def __exit__(self, exc_type, exc_val, exc_tb):
767 """Exit the runtime context related to this object.
768
769 The parameters describe the exception that caused the context to be
770 exited. If the context was exited without an exception, all three
771 arguments will be None.
772 """
773 if exc_type is None:
774 _post_restart_on_change_helper(
775 self.checksums,
776 self.restart_map,
777 stopstart=self.stopstart,
778 restart_functions=self.restart_functions,
779 can_restart_now_f=self.can_restart_now_f,
780 post_svc_restart_f=self.post_svc_restart_f,
781 pre_restarts_wait_f=self.pre_restarts_wait_f)
782 # All is good, so return False; any exceptions will propagate.
783 return False
722784
723785
724def restart_on_change_helper(lambda_f, restart_map, stopstart=False,786def restart_on_change_helper(lambda_f, restart_map, stopstart=False,
725 restart_functions=None):787 restart_functions=None,
788 can_restart_now_f=None,
789 post_svc_restart_f=None,
790 pre_restarts_wait_f=None):
726 """Helper function to perform the restart_on_change function.791 """Helper function to perform the restart_on_change function.
727792
728 This is provided for decorators to restart services if files described793 This is provided for decorators to restart services if files described
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().
730795
731 @param lambda_f: function to call.796 This functions allows for a number of helper functions to be passed.
732 @param restart_map: {file: [service, ...]}797
733 @param stopstart: whether to stop, start or restart a service798 `restart_functions` is a map with a service as the key and the
734 @param restart_functions: nonstandard functions to use to restart services799 corresponding value being the function to call to restart the service. For
735 {svc: func, ...}800 example if `restart_functions={'some-service': my_restart_func}` then
736 @returns result of lambda_f()801 `my_restart_func` should a function which takes one argument which is the
802 service name to be retstarted.
803
804 `can_restart_now_f` is a function which checks that a restart is permitted.
805 It should return a bool which indicates if a restart is allowed and should
806 take a service name (str) and a list of changed files (List[str]) as
807 arguments.
808
809 `post_svc_restart_f` is a function which runs after a service has been
810 restarted. It takes the service name that was restarted as an argument.
811
812 `pre_restarts_wait_f` is a function which is called before any restarts
813 occur. The use case for this is an application which wants to try and
814 stagger restarts between units.
815
816 :param lambda_f: function to call.
817 :type lambda_f: Callable[[], ANY]
818 :param restart_map: {file: [service, ...]}
819 :type restart_map: Dict[str, List[str,]]
820 :param stopstart: whether to stop, start or restart a service
821 :type stopstart: booleean
822 :param restart_functions: nonstandard functions to use to restart services
823 {svc: func, ...}
824 :type restart_functions: Dict[str, Callable[[str], None]]
825 :param can_restart_now_f: A function used to check if the restart is
826 permitted.
827 :type can_restart_now_f: Callable[[str, List[str]], boolean]
828 :param post_svc_restart_f: A function run after a service has
829 restarted.
830 :type post_svc_restart_f: Callable[[str], None]
831 :param pre_restarts_wait_f: A function called before any restarts.
832 :type pre_restarts_wait_f: Callable[None, None]
833 :returns: result of lambda_f()
834 :rtype: ANY
835 """
836 checksums = _pre_restart_on_change_helper(restart_map)
837 r = lambda_f()
838 _post_restart_on_change_helper(checksums,
839 restart_map,
840 stopstart,
841 restart_functions,
842 can_restart_now_f,
843 post_svc_restart_f,
844 pre_restarts_wait_f)
845 return r
846
847
848def _pre_restart_on_change_helper(restart_map):
849 """Take a snapshot of file hashes.
850
851 :param restart_map: {file: [service, ...]}
852 :type restart_map: Dict[str, List[str,]]
853 :returns: Dictionary of file paths and the files checksum.
854 :rtype: Dict[str, str]
855 """
856 return {path: path_hash(path) for path in restart_map}
857
858
859def _post_restart_on_change_helper(checksums,
860 restart_map,
861 stopstart=False,
862 restart_functions=None,
863 can_restart_now_f=None,
864 post_svc_restart_f=None,
865 pre_restarts_wait_f=None):
866 """Check whether files have changed.
867
868 :param checksums: Dictionary of file paths and the files checksum.
869 :type checksums: Dict[str, str]
870 :param restart_map: {file: [service, ...]}
871 :type restart_map: Dict[str, List[str,]]
872 :param stopstart: whether to stop, start or restart a service
873 :type stopstart: booleean
874 :param restart_functions: nonstandard functions to use to restart services
875 {svc: func, ...}
876 :type restart_functions: Dict[str, Callable[[str], None]]
877 :param can_restart_now_f: A function used to check if the restart is
878 permitted.
879 :type can_restart_now_f: Callable[[str, List[str]], boolean]
880 :param post_svc_restart_f: A function run after a service has
881 restarted.
882 :type post_svc_restart_f: Callable[[str], None]
883 :param pre_restarts_wait_f: A function called before any restarts.
884 :type pre_restarts_wait_f: Callable[None, None]
737 """885 """
738 if restart_functions is None:886 if restart_functions is None:
739 restart_functions = {}887 restart_functions = {}
740 checksums = {path: path_hash(path) for path in restart_map}888 changed_files = defaultdict(list)
741 r = lambda_f()889 restarts = []
742 # create a list of lists of the services to restart890 # create a list of lists of the services to restart
743 restarts = [restart_map[path]891 for path, services in restart_map.items():
744 for path in restart_map892 if path_hash(path) != checksums[path]:
745 if path_hash(path) != checksums[path]]893 restarts.append(services)
894 for svc in services:
895 changed_files[svc].append(path)
746 # create a flat list of ordered services without duplicates from lists896 # create a flat list of ordered services without duplicates from lists
747 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))897 services_list = list(OrderedDict.fromkeys(itertools.chain(*restarts)))
748 if services_list:898 if services_list:
899 if pre_restarts_wait_f:
900 pre_restarts_wait_f()
749 actions = ('stop', 'start') if stopstart else ('restart',)901 actions = ('stop', 'start') if stopstart else ('restart',)
750 for service_name in services_list:902 for service_name in services_list:
903 if can_restart_now_f:
904 if not can_restart_now_f(service_name,
905 changed_files[service_name]):
906 continue
751 if service_name in restart_functions:907 if service_name in restart_functions:
752 restart_functions[service_name](service_name)908 restart_functions[service_name](service_name)
753 else:909 else:
754 for action in actions:910 for action in actions:
755 service(action, service_name)911 service(action, service_name)
756 return r912 if post_svc_restart_f:
913 post_svc_restart_f(service_name)
757914
758915
759def pwgen(length=None):916def pwgen(length=None):
760 """Generate a random pasword."""917 """Generate a random password."""
761 if length is None:918 if length is None:
762 # A random length is ok to use a weak PRNG919 # A random length is ok to use a weak PRNG
763 length = random.choice(range(35, 45))920 length = random.choice(range(35, 45))
@@ -819,7 +976,8 @@
819 if nic_type:976 if nic_type:
820 for int_type in int_types:977 for int_type in int_types:
821 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']978 cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
822 ip_output = subprocess.check_output(cmd).decode('UTF-8')979 ip_output = subprocess.check_output(
980 cmd).decode('UTF-8', errors='replace')
823 ip_output = ip_output.split('\n')981 ip_output = ip_output.split('\n')
824 ip_output = (line for line in ip_output if line)982 ip_output = (line for line in ip_output if line)
825 for line in ip_output:983 for line in ip_output:
@@ -835,7 +993,8 @@
835 interfaces.append(iface)993 interfaces.append(iface)
836 else:994 else:
837 cmd = ['ip', 'a']995 cmd = ['ip', 'a']
838 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')996 ip_output = subprocess.check_output(
997 cmd).decode('UTF-8', errors='replace').split('\n')
839 ip_output = (line.strip() for line in ip_output if line)998 ip_output = (line.strip() for line in ip_output if line)
840999
841 key = re.compile(r'^[0-9]+:\s+(.+):')1000 key = re.compile(r'^[0-9]+:\s+(.+):')
@@ -859,7 +1018,8 @@
859def get_nic_mtu(nic):1018def get_nic_mtu(nic):
860 """Return the Maximum Transmission Unit (MTU) for a network interface."""1019 """Return the Maximum Transmission Unit (MTU) for a network interface."""
861 cmd = ['ip', 'addr', 'show', nic]1020 cmd = ['ip', 'addr', 'show', nic]
862 ip_output = subprocess.check_output(cmd).decode('UTF-8').split('\n')1021 ip_output = subprocess.check_output(
1022 cmd).decode('UTF-8', errors='replace').split('\n')
863 mtu = ""1023 mtu = ""
864 for line in ip_output:1024 for line in ip_output:
865 words = line.split()1025 words = line.split()
@@ -871,7 +1031,7 @@
871def get_nic_hwaddr(nic):1031def get_nic_hwaddr(nic):
872 """Return the Media Access Control (MAC) for a network interface."""1032 """Return the Media Access Control (MAC) for a network interface."""
873 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]1033 cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
874 ip_output = subprocess.check_output(cmd).decode('UTF-8')1034 ip_output = subprocess.check_output(cmd).decode('UTF-8', errors='replace')
875 hwaddr = ""1035 hwaddr = ""
876 words = ip_output.split()1036 words = ip_output.split()
877 if 'link/ether' in words:1037 if 'link/ether' in words:
@@ -883,7 +1043,7 @@
883def chdir(directory):1043def chdir(directory):
884 """Change the current working directory to a different directory for a code1044 """Change the current working directory to a different directory for a code
885 block and return the previous directory after the block exits. Useful to1045 block and return the previous directory after the block exits. Useful to
886 run commands from a specificed directory.1046 run commands from a specified directory.
8871047
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.
889 """1049 """
@@ -918,9 +1078,13 @@
918 for root, dirs, files in os.walk(path, followlinks=follow_links):1078 for root, dirs, files in os.walk(path, followlinks=follow_links):
919 for name in dirs + files:1079 for name in dirs + files:
920 full = os.path.join(root, name)1080 full = os.path.join(root, name)
921 broken_symlink = os.path.lexists(full) and not os.path.exists(full)1081 try:
922 if not broken_symlink:
923 chown(full, uid, gid)1082 chown(full, uid, gid)
1083 except (IOError, OSError) as e:
1084 # Intended to ignore "file not found". Catching both to be
1085 # compatible with both Python 2.7 and 3.x.
1086 if e.errno == errno.ENOENT:
1087 pass
9241088
9251089
926def lchownr(path, owner, group):1090def lchownr(path, owner, group):
@@ -1053,6 +1217,17 @@
1053 return calculated_wait_time1217 return calculated_wait_time
10541218
10551219
1220def ca_cert_absolute_path(basename_without_extension):
1221 """Returns absolute path to CA certificate.
1222
1223 :param basename_without_extension: Filename without extension
1224 :type basename_without_extension: str
1225 :returns: Absolute full path
1226 :rtype: str
1227 """
1228 return '{}/{}.crt'.format(CA_CERT_DIR, basename_without_extension)
1229
1230
1056def install_ca_cert(ca_cert, name=None):1231def install_ca_cert(ca_cert, name=None):
1057 """1232 """
1058 Install the given cert as a trusted CA.1233 Install the given cert as a trusted CA.
@@ -1068,10 +1243,37 @@
1068 ca_cert = ca_cert.encode('utf8')1243 ca_cert = ca_cert.encode('utf8')
1069 if not name:1244 if not name:
1070 name = 'juju-{}'.format(charm_name())1245 name = 'juju-{}'.format(charm_name())
1071 cert_file = '/usr/local/share/ca-certificates/{}.crt'.format(name)1246 cert_file = ca_cert_absolute_path(name)
1072 new_hash = hashlib.md5(ca_cert).hexdigest()1247 new_hash = hashlib.md5(ca_cert).hexdigest()
1073 if file_hash(cert_file) == new_hash:1248 if file_hash(cert_file) == new_hash:
1074 return1249 return
1075 log("Installing new CA cert at: {}".format(cert_file), level=INFO)1250 log("Installing new CA cert at: {}".format(cert_file), level=INFO)
1076 write_file(cert_file, ca_cert)1251 write_file(cert_file, ca_cert)
1077 subprocess.check_call(['update-ca-certificates', '--fresh'])1252 subprocess.check_call(['update-ca-certificates', '--fresh'])
1253
1254
1255def get_system_env(key, default=None):
1256 """Get data from system environment as represented in ``/etc/environment``.
1257
1258 :param key: Key to look up
1259 :type key: str
1260 :param default: Value to return if key is not found
1261 :type default: any
1262 :returns: Value for key if found or contents of default parameter
1263 :rtype: any
1264 :raises: subprocess.CalledProcessError
1265 """
1266 env_file = '/etc/environment'
1267 # use the shell and env(1) to parse the global environments file. This is
1268 # done to get the correct result even if the user has shell variable
1269 # substitutions or other shell logic in that file.
1270 output = subprocess.check_output(
1271 ['env', '-i', '/bin/bash', '-c',
1272 'set -a && source {} && env'.format(env_file)],
1273 universal_newlines=True)
1274 for k, v in (line.split('=', 1)
1275 for line in output.splitlines() if '=' in line):
1276 if k == key:
1277 return v
1278 else:
1279 return default
10781280
=== modified file 'charmhelpers/core/host_factory/ubuntu.py'
--- charmhelpers/core/host_factory/ubuntu.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/host_factory/ubuntu.py 2021-11-10 05:36:20 +0000
@@ -24,6 +24,12 @@
24 'bionic',24 'bionic',
25 'cosmic',25 'cosmic',
26 'disco',26 'disco',
27 'eoan',
28 'focal',
29 'groovy',
30 'hirsute',
31 'impish',
32 'jammy',
27)33)
2834
2935
@@ -93,12 +99,14 @@
93 the pkgcache argument is None. Be sure to add charmhelpers.fetch if99 the pkgcache argument is None. Be sure to add charmhelpers.fetch if
94 you call this function, or pass an apt_pkg.Cache() instance.100 you call this function, or pass an apt_pkg.Cache() instance.
95 """101 """
96 import apt_pkg102 from charmhelpers.fetch import apt_pkg, get_installed_version
97 if not pkgcache:103 if not pkgcache:
98 from charmhelpers.fetch import apt_cache104 current_ver = get_installed_version(package)
99 pkgcache = apt_cache()105 else:
100 pkg = pkgcache[package]106 pkg = pkgcache[package]
101 return apt_pkg.version_compare(pkg.current_ver.ver_str, revno)107 current_ver = pkg.current_ver
108
109 return apt_pkg.version_compare(current_ver.ver_str, revno)
102110
103111
104@cached112@cached
105113
=== modified file 'charmhelpers/core/services/base.py'
--- charmhelpers/core/services/base.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/services/base.py 2021-11-10 05:36:20 +0000
@@ -14,9 +14,11 @@
1414
15import os15import os
16import json16import json
17from inspect import getargspec17import inspect
18from collections import Iterable, OrderedDict18from collections import Iterable, OrderedDict
1919
20import six
21
20from charmhelpers.core import host22from charmhelpers.core import host
21from charmhelpers.core import hookenv23from charmhelpers.core import hookenv
2224
@@ -169,7 +171,10 @@
169 if not units:171 if not units:
170 continue172 continue
171 remote_service = units[0].split('/')[0]173 remote_service = units[0].split('/')[0]
172 argspec = getargspec(provider.provide_data)174 if six.PY2:
175 argspec = inspect.getargspec(provider.provide_data)
176 else:
177 argspec = inspect.getfullargspec(provider.provide_data)
173 if len(argspec.args) > 1:178 if len(argspec.args) > 1:
174 data = provider.provide_data(remote_service, service_ready)179 data = provider.provide_data(remote_service, service_ready)
175 else:180 else:
176181
=== modified file 'charmhelpers/core/strutils.py'
--- charmhelpers/core/strutils.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/strutils.py 2021-11-10 05:36:20 +0000
@@ -18,8 +18,11 @@
18import six18import six
19import re19import re
2020
2121TRUTHY_STRINGS = {'y', 'yes', 'true', 't', 'on'}
22def bool_from_string(value):22FALSEY_STRINGS = {'n', 'no', 'false', 'f', 'off'}
23
24
25def bool_from_string(value, truthy_strings=TRUTHY_STRINGS, falsey_strings=FALSEY_STRINGS, assume_false=False):
23 """Interpret string value as boolean.26 """Interpret string value as boolean.
2427
25 Returns True if value translates to True otherwise False.28 Returns True if value translates to True otherwise False.
@@ -32,9 +35,9 @@
3235
33 value = value.strip().lower()36 value = value.strip().lower()
3437
35 if value in ['y', 'yes', 'true', 't', 'on']:38 if value in truthy_strings:
36 return True39 return True
37 elif value in ['n', 'no', 'false', 'f', 'off']:40 elif value in falsey_strings or assume_false:
38 return False41 return False
3942
40 msg = "Unable to interpret string value '%s' as boolean" % (value)43 msg = "Unable to interpret string value '%s' as boolean" % (value)
4144
=== modified file 'charmhelpers/core/sysctl.py'
--- charmhelpers/core/sysctl.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/sysctl.py 2021-11-10 05:36:20 +0000
@@ -17,14 +17,17 @@
1717
18import yaml18import yaml
1919
20from subprocess import check_call20from subprocess import check_call, CalledProcessError
2121
22from charmhelpers.core.hookenv import (22from charmhelpers.core.hookenv import (
23 log,23 log,
24 DEBUG,24 DEBUG,
25 ERROR,25 ERROR,
26 WARNING,
26)27)
2728
29from charmhelpers.core.host import is_container
30
28__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'31__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
2932
3033
@@ -62,4 +65,11 @@
62 if ignore:65 if ignore:
63 call.append("-e")66 call.append("-e")
6467
65 check_call(call)68 try:
69 check_call(call)
70 except CalledProcessError as e:
71 if is_container():
72 log("Error setting some sysctl keys in this container: {}".format(e.output),
73 level=WARNING)
74 else:
75 raise e
6676
=== modified file 'charmhelpers/core/unitdata.py'
--- charmhelpers/core/unitdata.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/core/unitdata.py 2021-11-10 05:36:20 +0000
@@ -1,7 +1,7 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
3#3#
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2021 Canonical Limited.
5#5#
6# Licensed under the Apache License, Version 2.0 (the "License");6# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -61,7 +61,7 @@
61 'previous value', prev,61 'previous value', prev,
62 'current value', cur)62 'current value', cur)
6363
64 # Get some unit specific bookeeping64 # Get some unit specific bookkeeping
65 if not db.get('pkg_key'):65 if not db.get('pkg_key'):
66 key = urllib.urlopen('https://example.com/pkg_key').read()66 key = urllib.urlopen('https://example.com/pkg_key').read()
67 db.set('pkg_key', key)67 db.set('pkg_key', key)
@@ -449,7 +449,7 @@
449 'previous value', prev,449 'previous value', prev,
450 'current value', cur)450 'current value', cur)
451451
452 # Get some unit specific bookeeping452 # Get some unit specific bookkeeping
453 if not db.get('pkg_key'):453 if not db.get('pkg_key'):
454 key = urllib.urlopen('https://example.com/pkg_key').read()454 key = urllib.urlopen('https://example.com/pkg_key').read()
455 db.set('pkg_key', key)455 db.set('pkg_key', key)
456456
=== modified file 'charmhelpers/fetch/__init__.py'
--- charmhelpers/fetch/__init__.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/fetch/__init__.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2021 Canonical Limited.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -103,6 +103,11 @@
103 apt_unhold = fetch.apt_unhold103 apt_unhold = fetch.apt_unhold
104 import_key = fetch.import_key104 import_key = fetch.import_key
105 get_upstream_version = fetch.get_upstream_version105 get_upstream_version = fetch.get_upstream_version
106 apt_pkg = fetch.ubuntu_apt_pkg
107 get_apt_dpkg_env = fetch.get_apt_dpkg_env
108 get_installed_version = fetch.get_installed_version
109 OPENSTACK_RELEASES = fetch.OPENSTACK_RELEASES
110 UBUNTU_OPENSTACK_RELEASE = fetch.UBUNTU_OPENSTACK_RELEASE
106elif __platform__ == "centos":111elif __platform__ == "centos":
107 yum_search = fetch.yum_search112 yum_search = fetch.yum_search
108113
@@ -200,7 +205,7 @@
200 classname)205 classname)
201 plugin_list.append(handler_class())206 plugin_list.append(handler_class())
202 except NotImplementedError:207 except NotImplementedError:
203 # Skip missing plugins so that they can be ommitted from208 # Skip missing plugins so that they can be omitted from
204 # installation if desired209 # installation if desired
205 log("FetchHandler {} not found, skipping plugin".format(210 log("FetchHandler {} not found, skipping plugin".format(
206 handler_name))211 handler_name))
207212
=== modified file 'charmhelpers/fetch/python/packages.py'
--- charmhelpers/fetch/python/packages.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/fetch/python/packages.py 2021-11-10 05:36:20 +0000
@@ -1,7 +1,7 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# coding: utf-82# coding: utf-8
33
4# Copyright 2014-2015 Canonical Limited.4# Copyright 2014-2021 Canonical Limited.
5#5#
6# Licensed under the Apache License, Version 2.0 (the "License");6# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -27,7 +27,7 @@
2727
2828
29def pip_execute(*args, **kwargs):29def pip_execute(*args, **kwargs):
30 """Overriden pip_execute() to stop sys.path being changed.30 """Overridden pip_execute() to stop sys.path being changed.
3131
32 The act of importing main from the pip module seems to cause add wheels32 The act of importing main from the pip module seems to cause add wheels
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.
@@ -142,8 +142,10 @@
142 """Create an isolated Python environment."""142 """Create an isolated Python environment."""
143 if six.PY2:143 if six.PY2:
144 apt_install('python-virtualenv')144 apt_install('python-virtualenv')
145 extra_flags = []
145 else:146 else:
146 apt_install('python3-virtualenv')147 apt_install(['python3-virtualenv', 'virtualenv'])
148 extra_flags = ['--python=python3']
147149
148 if path:150 if path:
149 venv_path = path151 venv_path = path
@@ -151,4 +153,4 @@
151 venv_path = os.path.join(charm_dir(), 'venv')153 venv_path = os.path.join(charm_dir(), 'venv')
152154
153 if not os.path.exists(venv_path):155 if not os.path.exists(venv_path):
154 subprocess.check_call(['virtualenv', venv_path])156 subprocess.check_call(['virtualenv', venv_path] + extra_flags)
155157
=== modified file 'charmhelpers/fetch/snap.py'
--- charmhelpers/fetch/snap.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/fetch/snap.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2017 Canonical Limited.1# Copyright 2014-2021 Canonical Limited.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -65,11 +65,11 @@
65 retry_count += + 165 retry_count += + 1
66 if retry_count > SNAP_NO_LOCK_RETRY_COUNT:66 if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
67 raise CouldNotAcquireLockException(67 raise CouldNotAcquireLockException(
68 'Could not aquire lock after {} attempts'68 'Could not acquire lock after {} attempts'
69 .format(SNAP_NO_LOCK_RETRY_COUNT))69 .format(SNAP_NO_LOCK_RETRY_COUNT))
70 return_code = e.returncode70 return_code = e.returncode
71 log('Snap failed to acquire lock, trying again in {} seconds.'71 log('Snap failed to acquire lock, trying again in {} seconds.'
72 .format(SNAP_NO_LOCK_RETRY_DELAY, level='WARN'))72 .format(SNAP_NO_LOCK_RETRY_DELAY), level='WARN')
73 sleep(SNAP_NO_LOCK_RETRY_DELAY)73 sleep(SNAP_NO_LOCK_RETRY_DELAY)
7474
75 return return_code75 return return_code
7676
=== modified file 'charmhelpers/fetch/ubuntu.py'
--- charmhelpers/fetch/ubuntu.py 2019-05-24 12:41:48 +0000
+++ charmhelpers/fetch/ubuntu.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,4 @@
1# Copyright 2014-2015 Canonical Limited.1# Copyright 2014-2021 Canonical Limited.
2#2#
3# Licensed under the Apache License, Version 2.0 (the "License");3# Licensed under the Apache License, Version 2.0 (the "License");
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.
@@ -17,10 +17,12 @@
17import platform17import platform
18import re18import re
19import six19import six
20import subprocess
21import sys
20import time22import time
21import subprocess
2223
23from charmhelpers.core.host import get_distrib_codename24from charmhelpers import deprecate
25from charmhelpers.core.host import get_distrib_codename, get_system_env
2426
25from charmhelpers.core.hookenv import (27from charmhelpers.core.hookenv import (
26 log,28 log,
@@ -29,6 +31,7 @@
29 env_proxy_settings,31 env_proxy_settings,
30)32)
31from charmhelpers.fetch import SourceConfigError, GPGKeyError33from charmhelpers.fetch import SourceConfigError, GPGKeyError
34from charmhelpers.fetch import ubuntu_apt_pkg
3235
33PROPOSED_POCKET = (36PROPOSED_POCKET = (
34 "# Proposed\n"37 "# Proposed\n"
@@ -173,12 +176,112 @@
173 'stein/proposed': 'bionic-proposed/stein',176 'stein/proposed': 'bionic-proposed/stein',
174 'bionic-stein/proposed': 'bionic-proposed/stein',177 'bionic-stein/proposed': 'bionic-proposed/stein',
175 'bionic-proposed/stein': 'bionic-proposed/stein',178 'bionic-proposed/stein': 'bionic-proposed/stein',
179 # Train
180 'train': 'bionic-updates/train',
181 'bionic-train': 'bionic-updates/train',
182 'bionic-train/updates': 'bionic-updates/train',
183 'bionic-updates/train': 'bionic-updates/train',
184 'train/proposed': 'bionic-proposed/train',
185 'bionic-train/proposed': 'bionic-proposed/train',
186 'bionic-proposed/train': 'bionic-proposed/train',
187 # Ussuri
188 'ussuri': 'bionic-updates/ussuri',
189 'bionic-ussuri': 'bionic-updates/ussuri',
190 'bionic-ussuri/updates': 'bionic-updates/ussuri',
191 'bionic-updates/ussuri': 'bionic-updates/ussuri',
192 'ussuri/proposed': 'bionic-proposed/ussuri',
193 'bionic-ussuri/proposed': 'bionic-proposed/ussuri',
194 'bionic-proposed/ussuri': 'bionic-proposed/ussuri',
195 # Victoria
196 'victoria': 'focal-updates/victoria',
197 'focal-victoria': 'focal-updates/victoria',
198 'focal-victoria/updates': 'focal-updates/victoria',
199 'focal-updates/victoria': 'focal-updates/victoria',
200 'victoria/proposed': 'focal-proposed/victoria',
201 'focal-victoria/proposed': 'focal-proposed/victoria',
202 'focal-proposed/victoria': 'focal-proposed/victoria',
203 # Wallaby
204 'wallaby': 'focal-updates/wallaby',
205 'focal-wallaby': 'focal-updates/wallaby',
206 'focal-wallaby/updates': 'focal-updates/wallaby',
207 'focal-updates/wallaby': 'focal-updates/wallaby',
208 'wallaby/proposed': 'focal-proposed/wallaby',
209 'focal-wallaby/proposed': 'focal-proposed/wallaby',
210 'focal-proposed/wallaby': 'focal-proposed/wallaby',
211 # Xena
212 'xena': 'focal-updates/xena',
213 'focal-xena': 'focal-updates/xena',
214 'focal-xena/updates': 'focal-updates/xena',
215 'focal-updates/xena': 'focal-updates/xena',
216 'xena/proposed': 'focal-proposed/xena',
217 'focal-xena/proposed': 'focal-proposed/xena',
218 'focal-proposed/xena': 'focal-proposed/xena',
219 # Yoga
220 'yoga': 'focal-updates/yoga',
221 'focal-yoga': 'focal-updates/yoga',
222 'focal-yoga/updates': 'focal-updates/yoga',
223 'focal-updates/yoga': 'focal-updates/yoga',
224 'yoga/proposed': 'focal-proposed/yoga',
225 'focal-yoga/proposed': 'focal-proposed/yoga',
226 'focal-proposed/yoga': 'focal-proposed/yoga',
176}227}
177228
178229
230OPENSTACK_RELEASES = (
231 'diablo',
232 'essex',
233 'folsom',
234 'grizzly',
235 'havana',
236 'icehouse',
237 'juno',
238 'kilo',
239 'liberty',
240 'mitaka',
241 'newton',
242 'ocata',
243 'pike',
244 'queens',
245 'rocky',
246 'stein',
247 'train',
248 'ussuri',
249 'victoria',
250 'wallaby',
251 'xena',
252 'yoga',
253)
254
255
256UBUNTU_OPENSTACK_RELEASE = OrderedDict([
257 ('oneiric', 'diablo'),
258 ('precise', 'essex'),
259 ('quantal', 'folsom'),
260 ('raring', 'grizzly'),
261 ('saucy', 'havana'),
262 ('trusty', 'icehouse'),
263 ('utopic', 'juno'),
264 ('vivid', 'kilo'),
265 ('wily', 'liberty'),
266 ('xenial', 'mitaka'),
267 ('yakkety', 'newton'),
268 ('zesty', 'ocata'),
269 ('artful', 'pike'),
270 ('bionic', 'queens'),
271 ('cosmic', 'rocky'),
272 ('disco', 'stein'),
273 ('eoan', 'train'),
274 ('focal', 'ussuri'),
275 ('groovy', 'victoria'),
276 ('hirsute', 'wallaby'),
277 ('impish', 'xena'),
278 ('jammy', 'yoga'),
279])
280
281
179APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.282APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
180CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.283CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
181CMD_RETRY_COUNT = 3 # Retry a failing fatal command X times.284CMD_RETRY_COUNT = 10 # Retry a failing fatal command X times.
182285
183286
184def filter_installed_packages(packages):287def filter_installed_packages(packages):
@@ -208,18 +311,50 @@
208 )311 )
209312
210313
211def apt_cache(in_memory=True, progress=None):314def apt_cache(*_, **__):
212 """Build and return an apt cache."""315 """Shim returning an object simulating the apt_pkg Cache.
213 from apt import apt_pkg316
214 apt_pkg.init()317 :param _: Accept arguments for compatibility, not used.
215 if in_memory:318 :type _: any
216 apt_pkg.config.set("Dir::Cache::pkgcache", "")319 :param __: Accept keyword arguments for compatibility, not used.
217 apt_pkg.config.set("Dir::Cache::srcpkgcache", "")320 :type __: any
218 return apt_pkg.Cache(progress)321 :returns:Object used to interrogate the system apt and dpkg databases.
219322 :rtype:ubuntu_apt_pkg.Cache
220323 """
221def apt_install(packages, options=None, fatal=False):324 if 'apt_pkg' in sys.modules:
222 """Install one or more packages."""325 # NOTE(fnordahl): When our consumer use the upstream ``apt_pkg`` module
326 # in conjunction with the apt_cache helper function, they may expect us
327 # to call ``apt_pkg.init()`` for them.
328 #
329 # Detect this situation, log a warning and make the call to
330 # ``apt_pkg.init()`` to avoid the consumer Python interpreter from
331 # crashing with a segmentation fault.
332 @deprecate(
333 'Support for use of upstream ``apt_pkg`` module in conjunction'
334 'with charm-helpers is deprecated since 2019-06-25',
335 date=None, log=lambda x: log(x, level=WARNING))
336 def one_shot_log():
337 pass
338
339 one_shot_log()
340 sys.modules['apt_pkg'].init()
341 return ubuntu_apt_pkg.Cache()
342
343
344def apt_install(packages, options=None, fatal=False, quiet=False):
345 """Install one or more packages.
346
347 :param packages: Package(s) to install
348 :type packages: Option[str, List[str]]
349 :param options: Options to pass on to apt-get
350 :type options: Option[None, List[str]]
351 :param fatal: Whether the command's output should be checked and
352 retried.
353 :type fatal: bool
354 :param quiet: if True (default), suppress log message to stdout/stderr
355 :type quiet: bool
356 :raises: subprocess.CalledProcessError
357 """
223 if options is None:358 if options is None:
224 options = ['--option=Dpkg::Options::=--force-confold']359 options = ['--option=Dpkg::Options::=--force-confold']
225360
@@ -230,13 +365,24 @@
230 cmd.append(packages)365 cmd.append(packages)
231 else:366 else:
232 cmd.extend(packages)367 cmd.extend(packages)
233 log("Installing {} with options: {}".format(packages,368 if not quiet:
234 options))369 log("Installing {} with options: {}"
235 _run_apt_command(cmd, fatal)370 .format(packages, options))
371 _run_apt_command(cmd, fatal, quiet=quiet)
236372
237373
238def apt_upgrade(options=None, fatal=False, dist=False):374def apt_upgrade(options=None, fatal=False, dist=False):
239 """Upgrade all packages."""375 """Upgrade all packages.
376
377 :param options: Options to pass on to apt-get
378 :type options: Option[None, List[str]]
379 :param fatal: Whether the command's output should be checked and
380 retried.
381 :type fatal: bool
382 :param dist: Whether ``dist-upgrade`` should be used over ``upgrade``
383 :type dist: bool
384 :raises: subprocess.CalledProcessError
385 """
240 if options is None:386 if options is None:
241 options = ['--option=Dpkg::Options::=--force-confold']387 options = ['--option=Dpkg::Options::=--force-confold']
242388
@@ -257,7 +403,15 @@
257403
258404
259def apt_purge(packages, fatal=False):405def apt_purge(packages, fatal=False):
260 """Purge one or more packages."""406 """Purge one or more packages.
407
408 :param packages: Package(s) to install
409 :type packages: Option[str, List[str]]
410 :param fatal: Whether the command's output should be checked and
411 retried.
412 :type fatal: bool
413 :raises: subprocess.CalledProcessError
414 """
261 cmd = ['apt-get', '--assume-yes', 'purge']415 cmd = ['apt-get', '--assume-yes', 'purge']
262 if isinstance(packages, six.string_types):416 if isinstance(packages, six.string_types):
263 cmd.append(packages)417 cmd.append(packages)
@@ -268,7 +422,14 @@
268422
269423
270def apt_autoremove(purge=True, fatal=False):424def apt_autoremove(purge=True, fatal=False):
271 """Purge one or more packages."""425 """Purge one or more packages.
426 :param purge: Whether the ``--purge`` option should be passed on or not.
427 :type purge: bool
428 :param fatal: Whether the command's output should be checked and
429 retried.
430 :type fatal: bool
431 :raises: subprocess.CalledProcessError
432 """
272 cmd = ['apt-get', '--assume-yes', 'autoremove']433 cmd = ['apt-get', '--assume-yes', 'autoremove']
273 if purge:434 if purge:
274 cmd.append('--purge')435 cmd.append('--purge')
@@ -304,7 +465,7 @@
304 A Radix64 format keyid is also supported for backwards465 A Radix64 format keyid is also supported for backwards
305 compatibility. In this case Ubuntu keyserver will be466 compatibility. In this case Ubuntu keyserver will be
306 queried for a key via HTTPS by its keyid. This method467 queried for a key via HTTPS by its keyid. This method
307 is less preferrable because https proxy servers may468 is less preferable because https proxy servers may
308 require traffic decryption which is equivalent to a469 require traffic decryption which is equivalent to a
309 man-in-the-middle attack (a proxy server impersonates470 man-in-the-middle attack (a proxy server impersonates
310 keyserver TLS certificates and has to be explicitly471 keyserver TLS certificates and has to be explicitly
@@ -481,6 +642,10 @@
481 with be used. If staging is NOT used then the cloud archive [3] will be642 with be used. If staging is NOT used then the cloud archive [3] will be
482 added, and the 'ubuntu-cloud-keyring' package will be added for the643 added, and the 'ubuntu-cloud-keyring' package will be added for the
483 current distro.644 current distro.
645 '<openstack-version>': translate to cloud:<release> based on the current
646 distro version (i.e. for 'ussuri' this will either be 'bionic-ussuri' or
647 'distro'.
648 '<openstack-version>/proposed': as above, but for proposed.
484649
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.
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.
@@ -499,7 +664,7 @@
499 id may also be used, but be aware that only insecure protocols are664 id may also be used, but be aware that only insecure protocols are
500 available to retrieve the actual public key from a public keyserver665 available to retrieve the actual public key from a public keyserver
501 placing your Juju environment at risk. ppa and cloud archive keys666 placing your Juju environment at risk. ppa and cloud archive keys
502 are securely added automtically, so sould not be provided.667 are securely added automatically, so should not be provided.
503668
504 @param fail_invalid: (boolean) if True, then the function raises a669 @param fail_invalid: (boolean) if True, then the function raises a
505 SourceConfigError is there is no matching installation source.670 SourceConfigError is there is no matching installation source.
@@ -507,6 +672,12 @@
507 @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a672 @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
508 valid pocket in CLOUD_ARCHIVE_POCKETS673 valid pocket in CLOUD_ARCHIVE_POCKETS
509 """674 """
675 # extract the OpenStack versions from the CLOUD_ARCHIVE_POCKETS; can't use
676 # the list in contrib.openstack.utils as it might not be included in
677 # classic charms and would break everything. Having OpenStack specific
678 # code in this file is a bit of an antipattern, anyway.
679 os_versions_regex = "({})".format("|".join(OPENSTACK_RELEASES))
680
510 _mapping = OrderedDict([681 _mapping = OrderedDict([
511 (r"^distro$", lambda: None), # This is a NOP682 (r"^distro$", lambda: None), # This is a NOP
512 (r"^(?:proposed|distro-proposed)$", _add_proposed),683 (r"^(?:proposed|distro-proposed)$", _add_proposed),
@@ -516,6 +687,9 @@
516 (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),687 (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
517 (r"^cloud:(.*)$", _add_cloud_pocket),688 (r"^cloud:(.*)$", _add_cloud_pocket),
518 (r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),689 (r"^snap:.*-(.*)-(.*)$", _add_cloud_distro_check),
690 (r"^{}\/proposed$".format(os_versions_regex),
691 _add_bare_openstack_proposed),
692 (r"^{}$".format(os_versions_regex), _add_bare_openstack),
519 ])693 ])
520 if source is None:694 if source is None:
521 source = ''695 source = ''
@@ -547,7 +721,7 @@
547 Uses get_distrib_codename to determine the correct stanza for721 Uses get_distrib_codename to determine the correct stanza for
548 the deb line.722 the deb line.
549723
550 For intel architecutres PROPOSED_POCKET is used for the release, but for724 For Intel architectures PROPOSED_POCKET is used for the release, but for
551 other architectures PROPOSED_PORTS_POCKET is used for the release.725 other architectures PROPOSED_PORTS_POCKET is used for the release.
552 """726 """
553 release = get_distrib_codename()727 release = get_distrib_codename()
@@ -568,11 +742,9 @@
568 if '{series}' in spec:742 if '{series}' in spec:
569 series = get_distrib_codename()743 series = get_distrib_codename()
570 spec = spec.replace('{series}', series)744 spec = spec.replace('{series}', series)
571 # software-properties package for bionic properly reacts to proxy settings
572 # passed as environment variables (See lp:1433761). This is not the case
573 # LTS and non-LTS releases below bionic.
574 _run_with_retries(['add-apt-repository', '--yes', spec],745 _run_with_retries(['add-apt-repository', '--yes', spec],
575 cmd_env=env_proxy_settings(['https']))746 cmd_env=env_proxy_settings(['https', 'http', 'no_proxy'])
747 )
576748
577749
578def _add_cloud_pocket(pocket):750def _add_cloud_pocket(pocket):
@@ -648,25 +820,102 @@
648 'version ({})'.format(release, os_release, ubuntu_rel))820 'version ({})'.format(release, os_release, ubuntu_rel))
649821
650822
823def _add_bare_openstack(openstack_release):
824 """Add cloud or distro based on the release given.
825
826 The spec given is, say, 'ussuri', but this could apply cloud:bionic-ussuri
827 or 'distro' depending on whether the ubuntu release is bionic or focal.
828
829 :param openstack_release: the OpenStack codename to determine the release
830 for.
831 :type openstack_release: str
832 :raises: SourceConfigError
833 """
834 # TODO(ajkavanagh) - surely this means we should be removing cloud archives
835 # if they exist?
836 __add_bare_helper(openstack_release, "{}-{}", lambda: None)
837
838
839def _add_bare_openstack_proposed(openstack_release):
840 """Add cloud of distro but with proposed.
841
842 The spec given is, say, 'ussuri' but this could apply
843 cloud:bionic-ussuri/proposed or 'distro/proposed' depending on whether the
844 ubuntu release is bionic or focal.
845
846 :param openstack_release: the OpenStack codename to determine the release
847 for.
848 :type openstack_release: str
849 :raises: SourceConfigError
850 """
851 __add_bare_helper(openstack_release, "{}-{}/proposed", _add_proposed)
852
853
854def __add_bare_helper(openstack_release, pocket_format, final_function):
855 """Helper for _add_bare_openstack[_proposed]
856
857 The bulk of the work between the two functions is exactly the same except
858 for the pocket format and the function that is run if it's the distro
859 version.
860
861 :param openstack_release: the OpenStack codename. e.g. ussuri
862 :type openstack_release: str
863 :param pocket_format: the pocket formatter string to construct a pocket str
864 from the openstack_release and the current ubuntu version.
865 :type pocket_format: str
866 :param final_function: the function to call if it is the distro version.
867 :type final_function: Callable
868 :raises SourceConfigError on error
869 """
870 ubuntu_version = get_distrib_codename()
871 possible_pocket = pocket_format.format(ubuntu_version, openstack_release)
872 if possible_pocket in CLOUD_ARCHIVE_POCKETS:
873 _add_cloud_pocket(possible_pocket)
874 return
875 # Otherwise it's almost certainly the distro version; verify that it
876 # exists.
877 try:
878 assert UBUNTU_OPENSTACK_RELEASE[ubuntu_version] == openstack_release
879 except KeyError:
880 raise SourceConfigError(
881 "Invalid ubuntu version {} isn't known to this library"
882 .format(ubuntu_version))
883 except AssertionError:
884 raise SourceConfigError(
885 'Invalid OpenStack release specified: {} for Ubuntu version {}'
886 .format(openstack_release, ubuntu_version))
887 final_function()
888
889
651def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),890def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
652 retry_message="", cmd_env=None):891 retry_message="", cmd_env=None, quiet=False):
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.
654893
655 :param: cmd: str: The apt command to run.894 :param cmd: The apt command to run.
656 :param: max_retries: int: The number of retries to attempt on a fatal895 :type cmd: str
657 command. Defaults to CMD_RETRY_COUNT.896 :param max_retries: The number of retries to attempt on a fatal
658 :param: retry_exitcodes: tuple: Optional additional exit codes to retry.897 command. Defaults to CMD_RETRY_COUNT.
659 Defaults to retry on exit code 1.898 :type max_retries: int
660 :param: retry_message: str: Optional log prefix emitted during retries.899 :param retry_exitcodes: Optional additional exit codes to retry.
661 :param: cmd_env: dict: Environment variables to add to the command run.900 Defaults to retry on exit code 1.
901 :type retry_exitcodes: tuple
902 :param retry_message: Optional log prefix emitted during retries.
903 :type retry_message: str
904 :param: cmd_env: Environment variables to add to the command run.
905 :type cmd_env: Option[None, Dict[str, str]]
906 :param quiet: if True, silence the output of the command from stdout and
907 stderr
908 :type quiet: bool
662 """909 """
910 env = get_apt_dpkg_env()
911 if cmd_env:
912 env.update(cmd_env)
663913
664 env = None
665 kwargs = {}914 kwargs = {}
666 if cmd_env:915 if quiet:
667 env = os.environ.copy()916 devnull = os.devnull if six.PY2 else subprocess.DEVNULL
668 env.update(cmd_env)917 kwargs['stdout'] = devnull
669 kwargs['env'] = env918 kwargs['stderr'] = devnull
670919
671 if not retry_message:920 if not retry_message:
672 retry_message = "Failed executing '{}'".format(" ".join(cmd))921 retry_message = "Failed executing '{}'".format(" ".join(cmd))
@@ -678,8 +927,7 @@
678 retry_results = (None,) + retry_exitcodes927 retry_results = (None,) + retry_exitcodes
679 while result in retry_results:928 while result in retry_results:
680 try:929 try:
681 # result = subprocess.check_call(cmd, env=env)930 result = subprocess.check_call(cmd, env=env, **kwargs)
682 result = subprocess.check_call(cmd, **kwargs)
683 except subprocess.CalledProcessError as e:931 except subprocess.CalledProcessError as e:
684 retry_count = retry_count + 1932 retry_count = retry_count + 1
685 if retry_count > max_retries:933 if retry_count > max_retries:
@@ -689,25 +937,30 @@
689 time.sleep(CMD_RETRY_DELAY)937 time.sleep(CMD_RETRY_DELAY)
690938
691939
692def _run_apt_command(cmd, fatal=False):940def _run_apt_command(cmd, fatal=False, quiet=False):
693 """Run an apt command with optional retries.941 """Run an apt command with optional retries.
694942
695 :param: cmd: str: The apt command to run.943 :param cmd: The apt command to run.
696 :param: fatal: bool: Whether the command's output should be checked and944 :type cmd: str
697 retried.945 :param fatal: Whether the command's output should be checked and
946 retried.
947 :type fatal: bool
948 :param quiet: if True, silence the output of the command from stdout and
949 stderr
950 :type quiet: bool
698 """951 """
699 # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment.
700 cmd_env = {
701 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')}
702
703 if fatal:952 if fatal:
704 _run_with_retries(953 _run_with_retries(
705 cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,),954 cmd, retry_exitcodes=(1, APT_NO_LOCK,),
706 retry_message="Couldn't acquire DPKG lock")955 retry_message="Couldn't acquire DPKG lock",
956 quiet=quiet)
707 else:957 else:
708 env = os.environ.copy()958 kwargs = {}
709 env.update(cmd_env)959 if quiet:
710 subprocess.call(cmd, env=env)960 devnull = os.devnull if six.PY2 else subprocess.DEVNULL
961 kwargs['stdout'] = devnull
962 kwargs['stderr'] = devnull
963 subprocess.call(cmd, env=get_apt_dpkg_env(), **kwargs)
711964
712965
713def get_upstream_version(package):966def get_upstream_version(package):
@@ -715,7 +968,6 @@
715968
716 @returns None (if not installed) or the upstream version969 @returns None (if not installed) or the upstream version
717 """970 """
718 import apt_pkg
719 cache = apt_cache()971 cache = apt_cache()
720 try:972 try:
721 pkg = cache[package]973 pkg = cache[package]
@@ -727,4 +979,34 @@
727 # package is known, but no version is currently installed.979 # package is known, but no version is currently installed.
728 return None980 return None
729981
730 return apt_pkg.upstream_version(pkg.current_ver.ver_str)982 return ubuntu_apt_pkg.upstream_version(pkg.current_ver.ver_str)
983
984
985def get_installed_version(package):
986 """Determine installed version of a package
987
988 @returns None (if not installed) or the installed version as
989 Version object
990 """
991 cache = apt_cache()
992 dpkg_result = cache._dpkg_list([package]).get(package, {})
993 current_ver = None
994 installed_version = dpkg_result.get('version')
995
996 if installed_version:
997 current_ver = ubuntu_apt_pkg.Version({'ver_str': installed_version})
998 return current_ver
999
1000
1001def get_apt_dpkg_env():
1002 """Get environment suitable for execution of APT and DPKG tools.
1003
1004 We keep this in a helper function instead of in a global constant to
1005 avoid execution on import of the library.
1006 :returns: Environment suitable for execution of APT and DPKG tools.
1007 :rtype: Dict[str, str]
1008 """
1009 # The fallback is used in the event of ``/etc/environment`` not containing
1010 # avalid PATH variable.
1011 return {'DEBIAN_FRONTEND': 'noninteractive',
1012 'PATH': get_system_env('PATH', '/usr/sbin:/usr/bin:/sbin:/bin')}
7311013
=== added file 'charmhelpers/fetch/ubuntu_apt_pkg.py'
--- charmhelpers/fetch/ubuntu_apt_pkg.py 1970-01-01 00:00:00 +0000
+++ charmhelpers/fetch/ubuntu_apt_pkg.py 2021-11-10 05:36:20 +0000
@@ -0,0 +1,312 @@
1# Copyright 2019-2021 Canonical Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Provide a subset of the ``python-apt`` module API.
16
17Data collection is done through subprocess calls to ``apt-cache`` and
18``dpkg-query`` commands.
19
20The main purpose for this module is to avoid dependency on the
21``python-apt`` python module.
22
23The indicated python module is a wrapper around the ``apt`` C++ library
24which is tightly connected to the version of the distribution it was
25shipped on. It is not developed in a backward/forward compatible manner.
26
27This in turn makes it incredibly hard to distribute as a wheel for a piece
28of python software that supports a span of distro releases [0][1].
29
30Upstream feedback like [2] does not give confidence in this ever changing,
31so with this we get rid of the dependency.
32
330: https://github.com/juju-solutions/layer-basic/pull/135
341: https://bugs.launchpad.net/charm-octavia/+bug/1824112
352: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=845330#10
36"""
37
38import locale
39import os
40import subprocess
41import sys
42
43
44class _container(dict):
45 """Simple container for attributes."""
46 __getattr__ = dict.__getitem__
47 __setattr__ = dict.__setitem__
48
49
50class Package(_container):
51 """Simple container for package attributes."""
52
53
54class Version(_container):
55 """Simple container for version attributes."""
56
57
58class Cache(object):
59 """Simulation of ``apt_pkg`` Cache object."""
60 def __init__(self, progress=None):
61 pass
62
63 def __contains__(self, package):
64 try:
65 pkg = self.__getitem__(package)
66 return pkg is not None
67 except KeyError:
68 return False
69
70 def __getitem__(self, package):
71 """Get information about a package from apt and dpkg databases.
72
73 :param package: Name of package
74 :type package: str
75 :returns: Package object
76 :rtype: object
77 :raises: KeyError, subprocess.CalledProcessError
78 """
79 apt_result = self._apt_cache_show([package])[package]
80 apt_result['name'] = apt_result.pop('package')
81 pkg = Package(apt_result)
82 dpkg_result = self._dpkg_list([package]).get(package, {})
83 current_ver = None
84 installed_version = dpkg_result.get('version')
85 if installed_version:
86 current_ver = Version({'ver_str': installed_version})
87 pkg.current_ver = current_ver
88 pkg.architecture = dpkg_result.get('architecture')
89 return pkg
90
91 def _dpkg_list(self, packages):
92 """Get data from system dpkg database for package.
93
94 :param packages: Packages to get data from
95 :type packages: List[str]
96 :returns: Structured data about installed packages, keys like
97 ``dpkg-query --list``
98 :rtype: dict
99 :raises: subprocess.CalledProcessError
100 """
101 pkgs = {}
102 cmd = ['dpkg-query', '--list']
103 cmd.extend(packages)
104 if locale.getlocale() == (None, None):
105 # subprocess calls out to locale.getpreferredencoding(False) to
106 # determine encoding. Workaround for Trusty where the
107 # environment appears to not be set up correctly.
108 locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
109 try:
110 output = subprocess.check_output(cmd,
111 stderr=subprocess.STDOUT,
112 universal_newlines=True)
113 except subprocess.CalledProcessError as cp:
114 # ``dpkg-query`` may return error and at the same time have
115 # produced useful output, for example when asked for multiple
116 # packages where some are not installed
117 if cp.returncode != 1:
118 raise
119 output = cp.output
120 headings = []
121 for line in output.splitlines():
122 if line.startswith('||/'):
123 headings = line.split()
124 headings.pop(0)
125 continue
126 elif (line.startswith('|') or line.startswith('+') or
127 line.startswith('dpkg-query:')):
128 continue
129 else:
130 data = line.split(None, 4)
131 status = data.pop(0)
132 if status not in ('ii', 'hi'):
133 continue
134 pkg = {}
135 pkg.update({k.lower(): v for k, v in zip(headings, data)})
136 if 'name' in pkg:
137 pkgs.update({pkg['name']: pkg})
138 return pkgs
139
140 def _apt_cache_show(self, packages):
141 """Get data from system apt cache for package.
142
143 :param packages: Packages to get data from
144 :type packages: List[str]
145 :returns: Structured data about package, keys like
146 ``apt-cache show``
147 :rtype: dict
148 :raises: subprocess.CalledProcessError
149 """
150 pkgs = {}
151 cmd = ['apt-cache', 'show', '--no-all-versions']
152 cmd.extend(packages)
153 if locale.getlocale() == (None, None):
154 # subprocess calls out to locale.getpreferredencoding(False) to
155 # determine encoding. Workaround for Trusty where the
156 # environment appears to not be set up correctly.
157 locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
158 try:
159 output = subprocess.check_output(cmd,
160 stderr=subprocess.STDOUT,
161 universal_newlines=True)
162 previous = None
163 pkg = {}
164 for line in output.splitlines():
165 if not line:
166 if 'package' in pkg:
167 pkgs.update({pkg['package']: pkg})
168 pkg = {}
169 continue
170 if line.startswith(' '):
171 if previous and previous in pkg:
172 pkg[previous] += os.linesep + line.lstrip()
173 continue
174 if ':' in line:
175 kv = line.split(':', 1)
176 key = kv[0].lower()
177 if key == 'n':
178 continue
179 previous = key
180 pkg.update({key: kv[1].lstrip()})
181 except subprocess.CalledProcessError as cp:
182 # ``apt-cache`` returns 100 if none of the packages asked for
183 # exist in the apt cache.
184 if cp.returncode != 100:
185 raise
186 return pkgs
187
188
189class Config(_container):
190 def __init__(self):
191 super(Config, self).__init__(self._populate())
192
193 def _populate(self):
194 cfgs = {}
195 cmd = ['apt-config', 'dump']
196 output = subprocess.check_output(cmd,
197 stderr=subprocess.STDOUT,
198 universal_newlines=True)
199 for line in output.splitlines():
200 if not line.startswith("CommandLine"):
201 k, v = line.split(" ", 1)
202 cfgs[k] = v.strip(";").strip("\"")
203
204 return cfgs
205
206
207# Backwards compatibility with old apt_pkg module
208sys.modules[__name__].config = Config()
209
210
211def init():
212 """Compatibility shim that does nothing."""
213 pass
214
215
216def upstream_version(version):
217 """Extracts upstream version from a version string.
218
219 Upstream reference: https://salsa.debian.org/apt-team/apt/blob/master/
220 apt-pkg/deb/debversion.cc#L259
221
222 :param version: Version string
223 :type version: str
224 :returns: Upstream version
225 :rtype: str
226 """
227 if version:
228 version = version.split(':')[-1]
229 version = version.split('-')[0]
230 return version
231
232
233def version_compare(a, b):
234 """Compare the given versions.
235
236 Call out to ``dpkg`` to make sure the code doing the comparison is
237 compatible with what the ``apt`` library would do. Mimic the return
238 values.
239
240 Upstream reference:
241 https://apt-team.pages.debian.net/python-apt/library/apt_pkg.html
242 ?highlight=version_compare#apt_pkg.version_compare
243
244 :param a: version string
245 :type a: str
246 :param b: version string
247 :type b: str
248 :returns: >0 if ``a`` is greater than ``b``, 0 if a equals b,
249 <0 if ``a`` is smaller than ``b``
250 :rtype: int
251 :raises: subprocess.CalledProcessError, RuntimeError
252 """
253 for op in ('gt', 1), ('eq', 0), ('lt', -1):
254 try:
255 subprocess.check_call(['dpkg', '--compare-versions',
256 a, op[0], b],
257 stderr=subprocess.STDOUT,
258 universal_newlines=True)
259 return op[1]
260 except subprocess.CalledProcessError as cp:
261 if cp.returncode == 1:
262 continue
263 raise
264 else:
265 raise RuntimeError('Unable to compare "{}" and "{}", according to '
266 'our logic they are neither greater, equal nor '
267 'less than each other.'.format(a, b))
268
269
270class PkgVersion():
271 """Allow package versions to be compared.
272
273 For example::
274
275 >>> import charmhelpers.fetch as fetch
276 >>> (fetch.apt_pkg.PkgVersion('2:20.4.0') <
277 ... fetch.apt_pkg.PkgVersion('2:20.5.0'))
278 True
279 >>> pkgs = [fetch.apt_pkg.PkgVersion('2:20.4.0'),
280 ... fetch.apt_pkg.PkgVersion('2:21.4.0'),
281 ... fetch.apt_pkg.PkgVersion('2:17.4.0')]
282 >>> pkgs.sort()
283 >>> pkgs
284 [2:17.4.0, 2:20.4.0, 2:21.4.0]
285 """
286
287 def __init__(self, version):
288 self.version = version
289
290 def __lt__(self, other):
291 return version_compare(self.version, other.version) == -1
292
293 def __le__(self, other):
294 return self.__lt__(other) or self.__eq__(other)
295
296 def __gt__(self, other):
297 return version_compare(self.version, other.version) == 1
298
299 def __ge__(self, other):
300 return self.__gt__(other) or self.__eq__(other)
301
302 def __eq__(self, other):
303 return version_compare(self.version, other.version) == 0
304
305 def __ne__(self, other):
306 return not self.__eq__(other)
307
308 def __repr__(self):
309 return self.version
310
311 def __hash__(self):
312 return hash(repr(self))
0313
=== modified file 'charmhelpers/osplatform.py'
--- charmhelpers/osplatform.py 2017-03-04 02:42:23 +0000
+++ charmhelpers/osplatform.py 2021-11-10 05:36:20 +0000
@@ -1,4 +1,5 @@
1import platform1import platform
2import os
23
34
4def get_platform():5def get_platform():
@@ -9,9 +10,13 @@
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.
10 """11 """
11 # linux_distribution is deprecated and will be removed in Python 3.712 # linux_distribution is deprecated and will be removed in Python 3.7
12 # Warings *not* disabled, as we certainly need to fix this.13 # Warnings *not* disabled, as we certainly need to fix this.
13 tuple_platform = platform.linux_distribution()14 if hasattr(platform, 'linux_distribution'):
14 current_platform = tuple_platform[0]15 tuple_platform = platform.linux_distribution()
16 current_platform = tuple_platform[0]
17 else:
18 current_platform = _get_platform_from_fs()
19
15 if "Ubuntu" in current_platform:20 if "Ubuntu" in current_platform:
16 return "ubuntu"21 return "ubuntu"
17 elif "CentOS" in current_platform:22 elif "CentOS" in current_platform:
@@ -20,6 +25,25 @@
20 # Stock Python does not detect Ubuntu and instead returns debian.25 # Stock Python does not detect Ubuntu and instead returns debian.
21 # Or at least it does in some build environments like Travis CI26 # Or at least it does in some build environments like Travis CI
22 return "ubuntu"27 return "ubuntu"
28 elif "elementary" in current_platform:
29 # ElementaryOS fails to run tests locally without this.
30 return "ubuntu"
31 elif "Pop!_OS" in current_platform:
32 # Pop!_OS also fails to run tests locally without this.
33 return "ubuntu"
23 else:34 else:
24 raise RuntimeError("This module is not supported on {}."35 raise RuntimeError("This module is not supported on {}."
25 .format(current_platform))36 .format(current_platform))
37
38
39def _get_platform_from_fs():
40 """Get Platform from /etc/os-release."""
41 with open(os.path.join(os.sep, 'etc', 'os-release')) as fin:
42 content = dict(
43 line.split('=', 1)
44 for line in fin.read().splitlines()
45 if '=' in line
46 )
47 for k, v in content.items():
48 content[k] = v.strip('"')
49 return content["NAME"]
2650
=== modified file 'config.yaml'
--- config.yaml 2021-10-07 00:38:56 +0000
+++ config.yaml 2021-11-10 05:36:20 +0000
@@ -121,3 +121,19 @@
121 type: string121 type: string
122 default: ''122 default: ''
123 description: An unique site name for Landscape deployment123 description: An unique site name for Landscape deployment
124 nagios_context:
125 default: "juju"
126 type: string
127 description: |
128 Used by the nrpe subordinate charms.
129 A string that will be prepended to instance name to set the host name
130 in nagios. So for instance the hostname would be something like:
131 juju-myservice-0
132 If you're running multiple environments with the same services in them
133 this allows you to differentiate between them.
134 nagios_servicegroups:
135 default: ""
136 type: string
137 description: |
138 A comma-separated list of nagios servicegroups.
139 If left empty, the nagios_context will be used as the servicegroup
124140
=== added file 'hooks/nrpe-external-master-relation-changed'
--- hooks/nrpe-external-master-relation-changed 1970-01-01 00:00:00 +0000
+++ hooks/nrpe-external-master-relation-changed 2021-11-10 05:36:20 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2import sys
3
4from lib.services import ServicesHook
5
6
7if __name__ == "__main__":
8 hook = ServicesHook()
9 sys.exit(hook())
010
=== added file 'hooks/nrpe-external-master-relation-joined'
--- hooks/nrpe-external-master-relation-joined 1970-01-01 00:00:00 +0000
+++ hooks/nrpe-external-master-relation-joined 2021-11-10 05:36:20 +0000
@@ -0,0 +1,9 @@
1#!/usr/bin/python
2import sys
3
4from lib.services import ServicesHook
5
6
7if __name__ == "__main__":
8 hook = ServicesHook()
9 sys.exit(hook())
010
=== added file 'lib/callbacks/nrpe.py'
--- lib/callbacks/nrpe.py 1970-01-01 00:00:00 +0000
+++ lib/callbacks/nrpe.py 2021-11-10 05:36:20 +0000
@@ -0,0 +1,51 @@
1from charmhelpers.core import hookenv
2from charmhelpers.core.services.base import ManagerCallback
3from charmhelpers.contrib.charmsupport import nrpe
4
5
6# services running on all nodes
7DEFAULT_SERVICES = ['landscape-api', 'landscape-appserver',
8 'landscape-async-frontend', 'landscape-job-handler',
9 'landscape-msgserver', 'landscape-pingserver']
10
11# services running only on the leader
12LEADER_SERVICES = ['landscape-package-search', 'landscape-package-upload']
13
14
15class ConfigureNRPE(ManagerCallback):
16 """Configure service checks if nrpe-external-master relation exists"""
17
18 def __init__(self, hookenv=hookenv, nrpe_config=None):
19 self._hookenv = hookenv
20 self._unit = self._hookenv.local_unit()
21 if nrpe_config:
22 self._nrpe_config = nrpe_config
23 else:
24 self._nrpe_config = nrpe.NRPE()
25
26 def __call__(self, manager, service_name, event_name):
27 self._hookenv.log('Configuring NRPE checks')
28 if self._hookenv.relations_of_type('nrpe-external-master'):
29 if self._hookenv.is_leader():
30 self._add_checks(DEFAULT_SERVICES + LEADER_SERVICES)
31 else:
32 self._add_checks(DEFAULT_SERVICES)
33 self._remove_checks(LEADER_SERVICES)
34 else:
35 self._remove_checks(DEFAULT_SERVICES + LEADER_SERVICES)
36 self._nrpe_config.write()
37
38 def _add_checks(self, services):
39 """ Add a service check """
40 for service in services:
41 hookenv.log('Adding nrpe check: %s' % service, hookenv.DEBUG)
42 self._nrpe_config.add_check(
43 shortname='%s' % service,
44 description='process check {%s}' % self._unit,
45 check_cmd='check_systemd.py %s' % service)
46
47 def _remove_checks(self, services):
48 """ Remove a service check """
49 for service in services:
50 hookenv.log('Removing nrpe check: %s' % service, hookenv.DEBUG)
51 self._nrpe_config.remove_check(shortname=service)
052
=== added file 'lib/callbacks/tests/test_nrpe.py'
--- lib/callbacks/tests/test_nrpe.py 1970-01-01 00:00:00 +0000
+++ lib/callbacks/tests/test_nrpe.py 2021-11-10 05:36:20 +0000
@@ -0,0 +1,36 @@
1from charmhelpers.core.services.base import ServiceManager
2
3from lib.tests.helpers import HookenvTest
4from lib.tests.stubs import NrpeConfigStub
5from lib.callbacks.nrpe import (
6 ConfigureNRPE,
7 DEFAULT_SERVICES,
8 LEADER_SERVICES)
9
10
11class ConfigureNRPETest(HookenvTest):
12 def setUp(self):
13 super(ConfigureNRPETest, self).setUp()
14 self.manager = ServiceManager([])
15 self.fake_nrpe = NrpeConfigStub()
16 self.callback = ConfigureNRPE(hookenv=self.hookenv,
17 nrpe_config=self.fake_nrpe)
18
19 def test_add_nrpe_check(self):
20 """Test adding NRPE checks."""
21 config = self.hookenv.config()
22 config["nagios_context"] = "juju"
23 self.hookenv.relations['nrpe-external-master'] = {"id": "1"}
24 self.callback(self.manager, None, None)
25 nrpe_checks = self.fake_nrpe.get_nrpe_checks()
26 for svc in DEFAULT_SERVICES:
27 self.assertIn(svc, nrpe_checks)
28 for svc in LEADER_SERVICES:
29 self.assertIn(svc, nrpe_checks)
30
31 def test_remove_nrpe_check(self):
32 config = self.hookenv.config()
33 config["nagios_context"] = "juju"
34 self.callback(self.manager, None, None)
35 nrpe_checks = self.fake_nrpe.get_nrpe_checks()
36 self.assertTrue(len(nrpe_checks) == 0)
037
=== modified file 'lib/services.py'
--- lib/services.py 2021-10-29 00:55:43 +0000
+++ lib/services.py 2021-11-10 05:36:20 +0000
@@ -21,6 +21,7 @@
21from lib.callbacks.filesystem import (21from lib.callbacks.filesystem import (
22 EnsureConfigDir, WriteCustomSSLCertificate, WriteLicenseFile)22 EnsureConfigDir, WriteCustomSSLCertificate, WriteLicenseFile)
23from lib.callbacks.apt import SetAPTSources23from lib.callbacks.apt import SetAPTSources
24from lib.callbacks.nrpe import ConfigureNRPE
2425
2526
26class ServicesHook(Hook):27class ServicesHook(Hook):
@@ -32,7 +33,7 @@
32 """33 """
33 def __init__(self, hookenv=hookenv, host=host,34 def __init__(self, hookenv=hookenv, host=host,
34 subprocess=subprocess, paths=default_paths, fetch=fetch,35 subprocess=subprocess, paths=default_paths, fetch=fetch,
35 psutil=psutil):36 psutil=psutil, nrpe_config=None):
36 super(ServicesHook, self).__init__(hookenv=hookenv)37 super(ServicesHook, self).__init__(hookenv=hookenv)
37 self._hookenv = hookenv38 self._hookenv = hookenv
38 self._host = host39 self._host = host
@@ -40,6 +41,7 @@
40 self._psutil = psutil41 self._psutil = psutil
41 self._subprocess = subprocess42 self._subprocess = subprocess
42 self._fetch = fetch43 self._fetch = fetch
44 self._nrpe_config = nrpe_config
4345
44 def _run(self):46 def _run(self):
4547
@@ -88,6 +90,8 @@
88 WriteLicenseFile(host=self._host, paths=self._paths),90 WriteLicenseFile(host=self._host, paths=self._paths),
89 ConfigureSMTP(91 ConfigureSMTP(
90 hookenv=self._hookenv, subprocess=self._subprocess),92 hookenv=self._hookenv, subprocess=self._subprocess),
93 ConfigureNRPE(hookenv=self._hookenv,
94 nrpe_config=self._nrpe_config),
91 ],95 ],
92 "start": LSCtl(subprocess=self._subprocess, hookenv=self._hookenv),96 "start": LSCtl(subprocess=self._subprocess, hookenv=self._hookenv),
93 }])97 }])
9498
=== modified file 'lib/tests/stubs.py'
--- lib/tests/stubs.py 2019-07-15 20:01:06 +0000
+++ lib/tests/stubs.py 2021-11-10 05:36:20 +0000
@@ -33,6 +33,9 @@
33 def config(self):33 def config(self):
34 return self._config34 return self._config
3535
36 def relations_of_type(self, reltype):
37 return self.relations.get(reltype, None)
38
36 def log(self, message, level=None):39 def log(self, message, level=None):
37 self.messages.append((message, level))40 self.messages.append((message, level))
3841
@@ -264,3 +267,24 @@
264267
265 def virtual_memory(self):268 def virtual_memory(self):
266 return PsutilUsageStub(self._physical_memory)269 return PsutilUsageStub(self._physical_memory)
270
271
272class NrpeConfigStub(object):
273 def __init__(self):
274 self._nrpe_checks = {}
275
276 def write(self):
277 pass
278
279 def add_check(self, shortname, description, check_cmd):
280 self._nrpe_checks[shortname] = {
281 "description": description,
282 "command": check_cmd,
283 }
284
285 def remove_check(self, shortname):
286 if self._nrpe_checks.get(shortname):
287 del self._nrpe_checks[shortname]
288
289 def get_nrpe_checks(self):
290 return self._nrpe_checks
267291
=== modified file 'lib/tests/test_services.py'
--- lib/tests/test_services.py 2021-04-13 19:03:22 +0000
+++ lib/tests/test_services.py 2021-11-10 05:36:20 +0000
@@ -5,7 +5,12 @@
5from charmhelpers.core import templating5from charmhelpers.core import templating
66
7from lib.tests.helpers import HookenvTest7from lib.tests.helpers import HookenvTest
8from lib.tests.stubs import HostStub, PsutilStub, SubprocessStub, FetchStub8from lib.tests.stubs import (
9 HostStub,
10 PsutilStub,
11 SubprocessStub,
12 FetchStub,
13 NrpeConfigStub)
9from lib.tests.sample import (14from lib.tests.sample import (
10 SAMPLE_DB_UNIT_DATA, SAMPLE_LEADER_DATA, SAMPLE_AMQP_UNIT_DATA,15 SAMPLE_DB_UNIT_DATA, SAMPLE_LEADER_DATA, SAMPLE_AMQP_UNIT_DATA,
11 SAMPLE_CONFIG_LICENSE_DATA, SAMPLE_CONFIG_OPENID_DATA,16 SAMPLE_CONFIG_LICENSE_DATA, SAMPLE_CONFIG_OPENID_DATA,
@@ -34,9 +39,11 @@
34 self.root_dir = self.useFixture(RootDir())39 self.root_dir = self.useFixture(RootDir())
35 self.fetch = FetchStub(self.hookenv.config)40 self.fetch = FetchStub(self.hookenv.config)
36 self.psutil = PsutilStub(num_cpus=2, physical_memory=1*1024**3)41 self.psutil = PsutilStub(num_cpus=2, physical_memory=1*1024**3)
42 self.nrpe_config = NrpeConfigStub()
37 self.hook = ServicesHook(43 self.hook = ServicesHook(
38 hookenv=self.hookenv, host=self.host, subprocess=self.subprocess,44 hookenv=self.hookenv, host=self.host, subprocess=self.subprocess,
39 paths=self.paths, fetch=self.fetch, psutil=self.psutil)45 paths=self.paths, fetch=self.fetch, psutil=self.psutil,
46 nrpe_config=self.nrpe_config)
4047
41 # XXX Monkey patch the templating API, charmhelpers doesn't sport48 # XXX Monkey patch the templating API, charmhelpers doesn't sport
42 # any dependency injection here as well.49 # any dependency injection here as well.
4350
=== modified file 'metadata.yaml'
--- metadata.yaml 2021-10-05 08:24:58 +0000
+++ metadata.yaml 2021-11-10 05:36:20 +0000
@@ -21,6 +21,9 @@
21 hosted:21 hosted:
22 interface: landscape-hosted22 interface: landscape-hosted
23 scope: container23 scope: container
24 nrpe-external-master:
25 interface: nrpe-external-master
26 scope: container
24series:27series:
25 - bionic28 - bionic
26 - xenial29 - xenial

Subscribers

People subscribed via source and target branches