Merge ~barryprice/charm-nrpe/+git/nrpe-charm:master into ~nrpe-charmers/charm-nrpe:master

Proposed by Barry Price
Status: Superseded
Proposed branch: ~barryprice/charm-nrpe/+git/nrpe-charm:master
Merge into: ~nrpe-charmers/charm-nrpe:master
Diff against target: 544 lines (+227/-58)
11 files modified
bin/charm_helpers_sync.py (+27/-22)
hooks/charmhelpers/core/hookenv.py (+69/-2)
hooks/charmhelpers/core/host.py (+74/-1)
hooks/charmhelpers/core/host_factory/ubuntu.py (+1/-0)
hooks/charmhelpers/core/services/base.py (+6/-15)
hooks/charmhelpers/core/strutils.py (+11/-5)
hooks/charmhelpers/core/templating.py (+18/-9)
hooks/charmhelpers/core/unitdata.py (+3/-1)
hooks/charmhelpers/fetch/snap.py (+16/-0)
hooks/charmhelpers/fetch/ubuntu.py (+1/-1)
metadata.yaml (+1/-2)
Reviewer Review Type Date Requested Status
Junien F Approve
Review via email: mp+337038@code.launchpad.net

Commit message

Freshen charm_helpers_sync.py, make sync, remove yakkety and zesty support (EOL), add artful & bionic support

To post a comment you must log in.
Revision history for this message
Junien F (axino) wrote :

+1

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

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

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

Change successfully merged at revision f2747e0ca46b6acd170929458192bf8f6538a19b

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/bin/charm_helpers_sync.py b/bin/charm_helpers_sync.py
2index f67fdb9..e3f0e74 100644
3--- a/bin/charm_helpers_sync.py
4+++ b/bin/charm_helpers_sync.py
5@@ -2,19 +2,17 @@
6
7 # Copyright 2014-2015 Canonical Limited.
8 #
9-# This file is part of charm-helpers.
10+# Licensed under the Apache License, Version 2.0 (the "License");
11+# you may not use this file except in compliance with the License.
12+# You may obtain a copy of the License at
13 #
14-# charm-helpers is free software: you can redistribute it and/or modify
15-# it under the terms of the GNU Lesser General Public License version 3 as
16-# published by the Free Software Foundation.
17+# http://www.apache.org/licenses/LICENSE-2.0
18 #
19-# charm-helpers is distributed in the hope that it will be useful,
20-# but WITHOUT ANY WARRANTY; without even the implied warranty of
21-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22-# GNU Lesser General Public License for more details.
23-#
24-# You should have received a copy of the GNU Lesser General Public License
25-# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
26+# Unless required by applicable law or agreed to in writing, software
27+# distributed under the License is distributed on an "AS IS" BASIS,
28+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29+# See the License for the specific language governing permissions and
30+# limitations under the License.
31
32 # Authors:
33 # Adam Gandelman <adamg@ubuntu.com>
34@@ -31,7 +29,7 @@ from fnmatch import fnmatch
35
36 import six
37
38-CHARM_HELPERS_BRANCH = 'lp:charm-helpers'
39+CHARM_HELPERS_REPO = 'https://github.com/juju/charm-helpers'
40
41
42 def parse_config(conf_file):
43@@ -41,10 +39,16 @@ def parse_config(conf_file):
44 return yaml.load(open(conf_file).read())
45
46
47-def clone_helpers(work_dir, branch):
48+def clone_helpers(work_dir, repo):
49 dest = os.path.join(work_dir, 'charm-helpers')
50- logging.info('Checking out %s to %s.' % (branch, dest))
51- cmd = ['bzr', 'checkout', '--lightweight', branch, dest]
52+ logging.info('Cloning out %s to %s.' % (repo, dest))
53+ branch = None
54+ if '@' in repo:
55+ repo, branch = repo.split('@', 1)
56+ cmd = ['git', 'clone', '--depth=1']
57+ if branch is not None:
58+ cmd += ['--branch', branch]
59+ cmd += [repo, dest]
60 subprocess.check_call(cmd)
61 return dest
62
63@@ -193,14 +197,15 @@ def sync_helpers(include, src, dest, options=None):
64 inc, opts = extract_options(m, global_options)
65 sync(src, dest, '%s.%s' % (k, inc), opts)
66
67+
68 if __name__ == '__main__':
69 parser = optparse.OptionParser()
70 parser.add_option('-c', '--config', action='store', dest='config',
71 default=None, help='helper config file')
72 parser.add_option('-D', '--debug', action='store_true', dest='debug',
73 default=False, help='debug')
74- parser.add_option('-b', '--branch', action='store', dest='branch',
75- help='charm-helpers bzr branch (overrides config)')
76+ parser.add_option('-r', '--repository', action='store', dest='repo',
77+ help='charm-helpers git repository (overrides config)')
78 parser.add_option('-d', '--destination', action='store', dest='dest_dir',
79 help='sync destination dir (overrides config)')
80 (opts, args) = parser.parse_args()
81@@ -219,10 +224,10 @@ if __name__ == '__main__':
82 else:
83 config = {}
84
85- if 'branch' not in config:
86- config['branch'] = CHARM_HELPERS_BRANCH
87- if opts.branch:
88- config['branch'] = opts.branch
89+ if 'repo' not in config:
90+ config['repo'] = CHARM_HELPERS_REPO
91+ if opts.repo:
92+ config['repo'] = opts.repo
93 if opts.dest_dir:
94 config['destination'] = opts.dest_dir
95
96@@ -242,7 +247,7 @@ if __name__ == '__main__':
97 sync_options = config['options']
98 tmpd = tempfile.mkdtemp()
99 try:
100- checkout = clone_helpers(tmpd, config['branch'])
101+ checkout = clone_helpers(tmpd, config['repo'])
102 sync_helpers(config['include'], checkout, config['destination'],
103 options=sync_options)
104 except Exception as e:
105diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
106index c7feeaf..7ed1cc4 100644
107--- a/hooks/charmhelpers/core/hookenv.py
108+++ b/hooks/charmhelpers/core/hookenv.py
109@@ -22,6 +22,7 @@ from __future__ import print_function
110 import copy
111 from distutils.version import LooseVersion
112 from functools import wraps
113+from collections import namedtuple
114 import glob
115 import os
116 import json
117@@ -38,6 +39,7 @@ if not six.PY3:
118 else:
119 from collections import UserDict
120
121+
122 CRITICAL = "CRITICAL"
123 ERROR = "ERROR"
124 WARNING = "WARNING"
125@@ -343,6 +345,7 @@ class Config(dict):
126
127 """
128 with open(self.path, 'w') as f:
129+ os.fchmod(f.fileno(), 0o600)
130 json.dump(self, f)
131
132 def _implicit_save(self):
133@@ -654,7 +657,7 @@ def _port_op(op_name, port, protocol="TCP"):
134 _args.append('{}/{}'.format(port, protocol))
135 try:
136 subprocess.check_call(_args)
137- except:
138+ except subprocess.CalledProcessError:
139 # Older Juju pre 2.3 doesn't support ICMP
140 # so treat it as a no-op if it fails.
141 if not icmp:
142@@ -685,6 +688,17 @@ def close_ports(start, end, protocol="TCP"):
143 subprocess.check_call(_args)
144
145
146+def opened_ports():
147+ """Get the opened ports
148+
149+ *Note that this will only show ports opened in a previous hook*
150+
151+ :returns: Opened ports as a list of strings: ``['8080/tcp', '8081-8083/tcp']``
152+ """
153+ _args = ['opened-ports', '--format=json']
154+ return json.loads(subprocess.check_output(_args).decode('UTF-8'))
155+
156+
157 @cached
158 def unit_get(attribute):
159 """Get the unit ID for the remote unit"""
160@@ -806,6 +820,10 @@ class Hooks(object):
161 return wrapper
162
163
164+class NoNetworkBinding(Exception):
165+ pass
166+
167+
168 def charm_dir():
169 """Return the root directory of the current charm"""
170 d = os.environ.get('JUJU_CHARM_DIR')
171@@ -1092,7 +1110,17 @@ def network_get_primary_address(binding):
172 :raise: NotImplementedError if run on Juju < 2.0
173 '''
174 cmd = ['network-get', '--primary-address', binding]
175- return subprocess.check_output(cmd).decode('UTF-8').strip()
176+ try:
177+ response = subprocess.check_output(
178+ cmd,
179+ stderr=subprocess.STDOUT).decode('UTF-8').strip()
180+ except CalledProcessError as e:
181+ if 'no network config found for binding' in e.output.decode('UTF-8'):
182+ raise NoNetworkBinding("No network binding for {}"
183+ .format(binding))
184+ else:
185+ raise
186+ return response
187
188
189 @translate_exc(from_exc=OSError, to_exc=NotImplementedError)
190@@ -1153,3 +1181,42 @@ def meter_info():
191 """Get the meter status information, if running in the meter-status-changed
192 hook."""
193 return os.environ.get('JUJU_METER_INFO')
194+
195+
196+def iter_units_for_relation_name(relation_name):
197+ """Iterate through all units in a relation
198+
199+ Generator that iterates through all the units in a relation and yields
200+ a named tuple with rid and unit field names.
201+
202+ Usage:
203+ data = [(u.rid, u.unit)
204+ for u in iter_units_for_relation_name(relation_name)]
205+
206+ :param relation_name: string relation name
207+ :yield: Named Tuple with rid and unit field names
208+ """
209+ RelatedUnit = namedtuple('RelatedUnit', 'rid, unit')
210+ for rid in relation_ids(relation_name):
211+ for unit in related_units(rid):
212+ yield RelatedUnit(rid, unit)
213+
214+
215+def ingress_address(rid=None, unit=None):
216+ """
217+ Retrieve the ingress-address from a relation when available. Otherwise,
218+ return the private-address. This function is to be used on the consuming
219+ side of the relation.
220+
221+ Usage:
222+ addresses = [ingress_address(rid=u.rid, unit=u.unit)
223+ for u in iter_units_for_relation_name(relation_name)]
224+
225+ :param rid: string relation id
226+ :param unit: string unit name
227+ :side effect: calls relation_get
228+ :return: string IP address
229+ """
230+ settings = relation_get(rid=rid, unit=unit)
231+ return (settings.get('ingress-address') or
232+ settings.get('private-address'))
233diff --git a/hooks/charmhelpers/core/host.py b/hooks/charmhelpers/core/host.py
234index 5656e2f..fd14d60 100644
235--- a/hooks/charmhelpers/core/host.py
236+++ b/hooks/charmhelpers/core/host.py
237@@ -34,7 +34,7 @@ import six
238
239 from contextlib import contextmanager
240 from collections import OrderedDict
241-from .hookenv import log, DEBUG
242+from .hookenv import log, DEBUG, local_unit
243 from .fstab import Fstab
244 from charmhelpers.osplatform import get_platform
245
246@@ -441,6 +441,49 @@ def add_user_to_group(username, group):
247 subprocess.check_call(cmd)
248
249
250+def chage(username, lastday=None, expiredate=None, inactive=None,
251+ mindays=None, maxdays=None, root=None, warndays=None):
252+ """Change user password expiry information
253+
254+ :param str username: User to update
255+ :param str lastday: Set when password was changed in YYYY-MM-DD format
256+ :param str expiredate: Set when user's account will no longer be
257+ accessible in YYYY-MM-DD format.
258+ -1 will remove an account expiration date.
259+ :param str inactive: Set the number of days of inactivity after a password
260+ has expired before the account is locked.
261+ -1 will remove an account's inactivity.
262+ :param str mindays: Set the minimum number of days between password
263+ changes to MIN_DAYS.
264+ 0 indicates the password can be changed anytime.
265+ :param str maxdays: Set the maximum number of days during which a
266+ password is valid.
267+ -1 as MAX_DAYS will remove checking maxdays
268+ :param str root: Apply changes in the CHROOT_DIR directory
269+ :param str warndays: Set the number of days of warning before a password
270+ change is required
271+ :raises subprocess.CalledProcessError: if call to chage fails
272+ """
273+ cmd = ['chage']
274+ if root:
275+ cmd.extend(['--root', root])
276+ if lastday:
277+ cmd.extend(['--lastday', lastday])
278+ if expiredate:
279+ cmd.extend(['--expiredate', expiredate])
280+ if inactive:
281+ cmd.extend(['--inactive', inactive])
282+ if mindays:
283+ cmd.extend(['--mindays', mindays])
284+ if maxdays:
285+ cmd.extend(['--maxdays', maxdays])
286+ if warndays:
287+ cmd.extend(['--warndays', warndays])
288+ cmd.append(username)
289+ subprocess.check_call(cmd)
290+
291+remove_password_expiry = functools.partial(chage, expiredate='-1', inactive='-1', mindays='0', maxdays='-1')
292+
293 def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
294 """Replicate the contents of a path"""
295 options = options or ['--delete', '--executability']
296@@ -506,6 +549,8 @@ def write_file(path, content, owner='root', group='root', perms=0o444):
297 with open(path, 'wb') as target:
298 os.fchown(target.fileno(), uid, gid)
299 os.fchmod(target.fileno(), perms)
300+ if six.PY3 and isinstance(content, six.string_types):
301+ content = content.encode('UTF-8')
302 target.write(content)
303 return
304 # the contents were the same, but we might still need to change the
305@@ -946,3 +991,31 @@ def updatedb(updatedb_text, new_path):
306 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
307 output = "\n".join(lines)
308 return output
309+
310+
311+def modulo_distribution(modulo=3, wait=30):
312+ """ Modulo distribution
313+
314+ This helper uses the unit number, a modulo value and a constant wait time
315+ to produce a calculated wait time distribution. This is useful in large
316+ scale deployments to distribute load during an expensive operation such as
317+ service restarts.
318+
319+ If you have 1000 nodes that need to restart 100 at a time 1 minute at a
320+ time:
321+
322+ time.wait(modulo_distribution(modulo=100, wait=60))
323+ restart()
324+
325+ If you need restarts to happen serially set modulo to the exact number of
326+ nodes and set a high constant wait time:
327+
328+ time.wait(modulo_distribution(modulo=10, wait=120))
329+ restart()
330+
331+ @param modulo: int The modulo number creates the group distribution
332+ @param wait: int The constant time wait value
333+ @return: int Calculated time to wait for unit operation
334+ """
335+ unit_number = int(local_unit().split('/')[1])
336+ return (unit_number % modulo) * wait
337diff --git a/hooks/charmhelpers/core/host_factory/ubuntu.py b/hooks/charmhelpers/core/host_factory/ubuntu.py
338index d8dc378..99451b5 100644
339--- a/hooks/charmhelpers/core/host_factory/ubuntu.py
340+++ b/hooks/charmhelpers/core/host_factory/ubuntu.py
341@@ -20,6 +20,7 @@ UBUNTU_RELEASES = (
342 'yakkety',
343 'zesty',
344 'artful',
345+ 'bionic',
346 )
347
348
349diff --git a/hooks/charmhelpers/core/services/base.py b/hooks/charmhelpers/core/services/base.py
350index 345b60d..ca9dc99 100644
351--- a/hooks/charmhelpers/core/services/base.py
352+++ b/hooks/charmhelpers/core/services/base.py
353@@ -313,26 +313,17 @@ class PortManagerCallback(ManagerCallback):
354 with open(port_file) as fp:
355 old_ports = fp.read().split(',')
356 for old_port in old_ports:
357- if bool(old_port) and not self.ports_contains(old_port, new_ports):
358- hookenv.close_port(old_port)
359+ if bool(old_port):
360+ old_port = int(old_port)
361+ if old_port not in new_ports:
362+ hookenv.close_port(old_port)
363 with open(port_file, 'w') as fp:
364 fp.write(','.join(str(port) for port in new_ports))
365 for port in new_ports:
366- # A port is either a number or 'ICMP'
367- protocol = 'TCP'
368- if str(port).upper() == 'ICMP':
369- protocol = 'ICMP'
370 if event_name == 'start':
371- hookenv.open_port(port, protocol)
372+ hookenv.open_port(port)
373 elif event_name == 'stop':
374- hookenv.close_port(port, protocol)
375-
376- def ports_contains(self, port, ports):
377- if not bool(port):
378- return False
379- if str(port).upper() != 'ICMP':
380- port = int(port)
381- return port in ports
382+ hookenv.close_port(port)
383
384
385 def service_stop(service_name):
386diff --git a/hooks/charmhelpers/core/strutils.py b/hooks/charmhelpers/core/strutils.py
387index 685dabd..e8df045 100644
388--- a/hooks/charmhelpers/core/strutils.py
389+++ b/hooks/charmhelpers/core/strutils.py
390@@ -61,13 +61,19 @@ def bytes_from_string(value):
391 if isinstance(value, six.string_types):
392 value = six.text_type(value)
393 else:
394- msg = "Unable to interpret non-string value '%s' as boolean" % (value)
395+ msg = "Unable to interpret non-string value '%s' as bytes" % (value)
396 raise ValueError(msg)
397 matches = re.match("([0-9]+)([a-zA-Z]+)", value)
398- if not matches:
399- msg = "Unable to interpret string value '%s' as bytes" % (value)
400- raise ValueError(msg)
401- return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
402+ if matches:
403+ size = int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
404+ else:
405+ # Assume that value passed in is bytes
406+ try:
407+ size = int(value)
408+ except ValueError:
409+ msg = "Unable to interpret string value '%s' as bytes" % (value)
410+ raise ValueError(msg)
411+ return size
412
413
414 class BasicStringComparator(object):
415diff --git a/hooks/charmhelpers/core/templating.py b/hooks/charmhelpers/core/templating.py
416index 7b801a3..9014015 100644
417--- a/hooks/charmhelpers/core/templating.py
418+++ b/hooks/charmhelpers/core/templating.py
419@@ -20,7 +20,8 @@ from charmhelpers.core import hookenv
420
421
422 def render(source, target, context, owner='root', group='root',
423- perms=0o444, templates_dir=None, encoding='UTF-8', template_loader=None):
424+ perms=0o444, templates_dir=None, encoding='UTF-8',
425+ template_loader=None, config_template=None):
426 """
427 Render a template.
428
429@@ -32,6 +33,9 @@ def render(source, target, context, owner='root', group='root',
430 The context should be a dict containing the values to be replaced in the
431 template.
432
433+ config_template may be provided to render from a provided template instead
434+ of loading from a file.
435+
436 The `owner`, `group`, and `perms` options will be passed to `write_file`.
437
438 If omitted, `templates_dir` defaults to the `templates` folder in the charm.
439@@ -65,14 +69,19 @@ def render(source, target, context, owner='root', group='root',
440 if templates_dir is None:
441 templates_dir = os.path.join(hookenv.charm_dir(), 'templates')
442 template_env = Environment(loader=FileSystemLoader(templates_dir))
443- try:
444- source = source
445- template = template_env.get_template(source)
446- except exceptions.TemplateNotFound as e:
447- hookenv.log('Could not load template %s from %s.' %
448- (source, templates_dir),
449- level=hookenv.ERROR)
450- raise e
451+
452+ # load from a string if provided explicitly
453+ if config_template is not None:
454+ template = template_env.from_string(config_template)
455+ else:
456+ try:
457+ source = source
458+ template = template_env.get_template(source)
459+ except exceptions.TemplateNotFound as e:
460+ hookenv.log('Could not load template %s from %s.' %
461+ (source, templates_dir),
462+ level=hookenv.ERROR)
463+ raise e
464 content = template.render(context)
465 if target is not None:
466 target_dir = os.path.dirname(target)
467diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
468index 54ec969..6d7b494 100644
469--- a/hooks/charmhelpers/core/unitdata.py
470+++ b/hooks/charmhelpers/core/unitdata.py
471@@ -175,6 +175,8 @@ class Storage(object):
472 else:
473 self.db_path = os.path.join(
474 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
475+ with open(self.db_path, 'a') as f:
476+ os.fchmod(f.fileno(), 0o600)
477 self.conn = sqlite3.connect('%s' % self.db_path)
478 self.cursor = self.conn.cursor()
479 self.revision = None
480@@ -358,7 +360,7 @@ class Storage(object):
481 try:
482 yield self.revision
483 self.revision = None
484- except:
485+ except Exception:
486 self.flush(False)
487 self.revision = None
488 raise
489diff --git a/hooks/charmhelpers/fetch/snap.py b/hooks/charmhelpers/fetch/snap.py
490index 112a54c..395836c 100644
491--- a/hooks/charmhelpers/fetch/snap.py
492+++ b/hooks/charmhelpers/fetch/snap.py
493@@ -41,6 +41,10 @@ class CouldNotAcquireLockException(Exception):
494 pass
495
496
497+class InvalidSnapChannel(Exception):
498+ pass
499+
500+
501 def _snap_exec(commands):
502 """
503 Execute snap commands.
504@@ -132,3 +136,15 @@ def snap_refresh(packages, *flags):
505
506 log(message, level='INFO')
507 return _snap_exec(['refresh'] + flags + packages)
508+
509+
510+def valid_snap_channel(channel):
511+ """ Validate snap channel exists
512+
513+ :raises InvalidSnapChannel: When channel does not exist
514+ :return: Boolean
515+ """
516+ if channel.lower() in SNAP_CHANNELS:
517+ return True
518+ else:
519+ raise InvalidSnapChannel("Invalid Snap Channel: {}".format(channel))
520diff --git a/hooks/charmhelpers/fetch/ubuntu.py b/hooks/charmhelpers/fetch/ubuntu.py
521index 40e1cb5..910e96a 100644
522--- a/hooks/charmhelpers/fetch/ubuntu.py
523+++ b/hooks/charmhelpers/fetch/ubuntu.py
524@@ -572,7 +572,7 @@ def get_upstream_version(package):
525 cache = apt_cache()
526 try:
527 pkg = cache[package]
528- except:
529+ except Exception:
530 # the package is unknown to the current apt cache.
531 return None
532
533diff --git a/metadata.yaml b/metadata.yaml
534index d0f196f..ace705f 100644
535--- a/metadata.yaml
536+++ b/metadata.yaml
537@@ -29,6 +29,5 @@ requires:
538 series:
539 - xenial
540 - trusty
541- - zesty
542- - yakkety
543 - artful
544+ - bionic

Subscribers

People subscribed via source and target branches