Merge lp:~freyes/charms/xenial/rsyslog/lp1694270 into lp:~bigdata-dev/charms/xenial/rsyslog/trunk

Proposed by Felipe Reyes
Status: Needs review
Proposed branch: lp:~freyes/charms/xenial/rsyslog/lp1694270
Merge into: lp:~bigdata-dev/charms/xenial/rsyslog/trunk
Diff against target: 1728 lines (+1013/-147)
17 files modified
hooks/charmhelpers/__init__.py (+61/-0)
hooks/charmhelpers/contrib/charmsupport/nrpe.py (+29/-10)
hooks/charmhelpers/core/hookenv.py (+47/-0)
hooks/charmhelpers/core/host.py (+225/-35)
hooks/charmhelpers/core/host_factory/centos.py (+16/-0)
hooks/charmhelpers/core/host_factory/ubuntu.py (+33/-0)
hooks/charmhelpers/core/kernel_factory/ubuntu.py (+1/-1)
hooks/charmhelpers/core/strutils.py (+53/-0)
hooks/charmhelpers/fetch/__init__.py (+17/-9)
hooks/charmhelpers/fetch/centos.py (+1/-1)
hooks/charmhelpers/fetch/snap.py (+122/-0)
hooks/charmhelpers/fetch/ubuntu.py (+314/-82)
hooks/charmhelpers/osplatform.py (+6/-0)
hooks/hooks.py (+8/-0)
templates/rsyslog.conf (+13/-0)
tox.ini (+1/-1)
unit_tests/test_hooks.py (+66/-8)
To merge this branch: bzr merge lp:~freyes/charms/xenial/rsyslog/lp1694270
Reviewer Review Type Date Requested Status
Jorge Niedbalski Pending
Juju Big Data Development Pending
Review via email: mp+325877@code.launchpad.net

This proposal supersedes a proposal from 2017-05-29.

Description of the change

Use 'rotate' action from /etc/init.d/rsyslog

'reload' command does not exist in Xenial, the rsyslog package uses the
'rotate' functionality implemented in the sysvinit script to close all
open file descriptors.

To post a comment you must log in.
Revision history for this message
Jorge Niedbalski (niedbalski) wrote : Posted in a previous version of this proposal

Felipe,

I'd like to backport this change into trusty series, please make sure
to make this change back compatible from xenial.

<freyes> # invoke-rc.d rsyslog rotate
<freyes> initctl: invalid command: rotate

I will set it to needs fixing for now.

Thanks.

review: Needs Fixing
Revision history for this message
Felipe Reyes (freyes) wrote : Posted in a previous version of this proposal

Jorge,

Pushed some changes to address trusty. http://pastebin.ubuntu.com/24707222/

Now this patch uses CompareHostsReleases() to decide which command to use after log rotation.

Thanks

Unmerged revisions

40. By Felipe Reyes

Remove print() statement left from testing

39. By Felipe Reyes

Use invoke-rc.d only for >=Xenial and add unit tests

38. By Felipe Reyes

Add py27 to tox

This will prevent regressions with python2.7 needed to run in trusty

37. By Felipe Reyes

Sync up charm-helpers

36. By Felipe Reyes

Replace space indentation with tab

35. By Felipe Reyes

Use 'rotate' action from /etc/init.d/rsyslog

'reload' command does not exist in Xenial, the rsyslog package uses the
'rotate' functionality implemented in the sysvinit script to close all
open file descriptors.

Closes-Bug: 1694270

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'hooks/charmhelpers/__init__.py'
--- hooks/charmhelpers/__init__.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/__init__.py 2017-06-16 22:43:26 +0000
@@ -14,6 +14,11 @@
1414
15# Bootstrap charm-helpers, installing its dependencies if necessary using15# Bootstrap charm-helpers, installing its dependencies if necessary using
16# only standard libraries.16# only standard libraries.
17from __future__ import print_function
18from __future__ import absolute_import
19
20import functools
21import inspect
17import subprocess22import subprocess
18import sys23import sys
1924
@@ -34,3 +39,59 @@
34 else:39 else:
35 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])40 subprocess.check_call(['apt-get', 'install', '-y', 'python3-yaml'])
36 import yaml # flake8: noqa41 import yaml # flake8: noqa
42
43
44# Holds a list of mapping of mangled function names that have been deprecated
45# using the @deprecate decorator below. This is so that the warning is only
46# printed once for each usage of the function.
47__deprecated_functions = {}
48
49
50def deprecate(warning, date=None, log=None):
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-month
53 that the function is officially going to be removed.
54
55 usage:
56
57 @deprecate('use core/fetch/add_source() instead', '2017-04')
58 def contributed_add_source_thing(...):
59 ...
60
61 And it then prints to the log ONCE that the function is deprecated.
62 The reason for passing the logging function (log) is so that hookenv.log
63 can be used for a charm if needed.
64
65 :param warning: String to indicat where it has moved ot.
66 :param date: optional sting, in YYYY-MM format to indicate when the
67 function will definitely (probably) be removed.
68 :param log: The log function to call to log. If not, logs to stdout
69 """
70 def wrap(f):
71
72 @functools.wraps(f)
73 def wrapped_f(*args, **kwargs):
74 try:
75 module = inspect.getmodule(f)
76 file = inspect.getsourcefile(f)
77 lines = inspect.getsourcelines(f)
78 f_name = "{}-{}-{}..{}-{}".format(
79 module.__name__, file, lines[0], lines[-1], f.__name__)
80 except (IOError, TypeError):
81 # assume it was local, so just use the name of the function
82 f_name = f.__name__
83 if f_name not in __deprecated_functions:
84 __deprecated_functions[f_name] = True
85 s = "DEPRECATION WARNING: Function {} is being removed".format(
86 f.__name__)
87 if date:
88 s = "{} on/around {}".format(s, date)
89 if warning:
90 s = "{} : {}".format(s, warning)
91 if log:
92 log(s)
93 else:
94 print(s)
95 return f(*args, **kwargs)
96 return wrapped_f
97 return wrap
3798
=== modified file 'hooks/charmhelpers/contrib/charmsupport/nrpe.py'
--- hooks/charmhelpers/contrib/charmsupport/nrpe.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/contrib/charmsupport/nrpe.py 2017-06-16 22:43:26 +0000
@@ -193,6 +193,13 @@
193 nrpe_check_file = self._get_check_filename()193 nrpe_check_file = self._get_check_filename()
194 with open(nrpe_check_file, 'w') as nrpe_check_config:194 with open(nrpe_check_file, 'w') as nrpe_check_config:
195 nrpe_check_config.write("# check {}\n".format(self.shortname))195 nrpe_check_config.write("# check {}\n".format(self.shortname))
196 if nagios_servicegroups:
197 nrpe_check_config.write(
198 "# The following header was added automatically by juju\n")
199 nrpe_check_config.write(
200 "# Modifying it will affect nagios monitoring and alerting\n")
201 nrpe_check_config.write(
202 "# servicegroups: {}\n".format(nagios_servicegroups))
196 nrpe_check_config.write("command[{}]={}\n".format(203 nrpe_check_config.write("command[{}]={}\n".format(
197 self.command, self.check_cmd))204 self.command, self.check_cmd))
198205
@@ -227,6 +234,7 @@
227 nagios_logdir = '/var/log/nagios'234 nagios_logdir = '/var/log/nagios'
228 nagios_exportdir = '/var/lib/nagios/export'235 nagios_exportdir = '/var/lib/nagios/export'
229 nrpe_confdir = '/etc/nagios/nrpe.d'236 nrpe_confdir = '/etc/nagios/nrpe.d'
237 homedir = '/var/lib/nagios' # home dir provided by nagios-nrpe-server
230238
231 def __init__(self, hostname=None, primary=True):239 def __init__(self, hostname=None, primary=True):
232 super(NRPE, self).__init__()240 super(NRPE, self).__init__()
@@ -338,13 +346,14 @@
338 return unit346 return unit
339347
340348
341def add_init_service_checks(nrpe, services, unit_name):349def add_init_service_checks(nrpe, services, unit_name, immediate_check=True):
342 """350 """
343 Add checks for each service in list351 Add checks for each service in list
344352
345 :param NRPE nrpe: NRPE object to add check to353 :param NRPE nrpe: NRPE object to add check to
346 :param list services: List of services to check354 :param list services: List of services to check
347 :param str unit_name: Unit name to use in check description355 :param str unit_name: Unit name to use in check description
356 :param bool immediate_check: For sysv init, run the service check immediately
348 """357 """
349 for svc in services:358 for svc in services:
350 # Don't add a check for these services from neutron-gateway359 # Don't add a check for these services from neutron-gateway
@@ -368,21 +377,31 @@
368 )377 )
369 elif os.path.exists(sysv_init):378 elif os.path.exists(sysv_init):
370 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc379 cronpath = '/etc/cron.d/nagios-service-check-%s' % svc
371 cron_file = ('*/5 * * * * root '380 checkpath = '%s/service-check-%s.txt' % (nrpe.homedir, svc)
372 '/usr/local/lib/nagios/plugins/check_exit_status.pl '381 croncmd = (
373 '-s /etc/init.d/%s status > '382 '/usr/local/lib/nagios/plugins/check_exit_status.pl '
374 '/var/lib/nagios/service-check-%s.txt\n' % (svc,383 '-e -s /etc/init.d/%s status' % svc
375 svc)384 )
376 )385 cron_file = '*/5 * * * * root %s > %s\n' % (croncmd, checkpath)
377 f = open(cronpath, 'w')386 f = open(cronpath, 'w')
378 f.write(cron_file)387 f.write(cron_file)
379 f.close()388 f.close()
380 nrpe.add_check(389 nrpe.add_check(
381 shortname=svc,390 shortname=svc,
382 description='process check {%s}' % unit_name,391 description='service check {%s}' % unit_name,
383 check_cmd='check_status_file.py -f '392 check_cmd='check_status_file.py -f %s' % checkpath,
384 '/var/lib/nagios/service-check-%s.txt' % svc,
385 )393 )
394 # if /var/lib/nagios doesn't exist open(checkpath, 'w') will fail
395 # (LP: #1670223).
396 if immediate_check and os.path.isdir(nrpe.homedir):
397 f = open(checkpath, 'w')
398 subprocess.call(
399 croncmd.split(),
400 stdout=f,
401 stderr=subprocess.STDOUT
402 )
403 f.close()
404 os.chmod(checkpath, 0o644)
386405
387406
388def copy_nrpe_checks():407def copy_nrpe_checks():
389408
=== modified file 'hooks/charmhelpers/core/hookenv.py'
--- hooks/charmhelpers/core/hookenv.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/core/hookenv.py 2017-06-16 22:43:26 +0000
@@ -332,6 +332,8 @@
332 config_cmd_line = ['config-get']332 config_cmd_line = ['config-get']
333 if scope is not None:333 if scope is not None:
334 config_cmd_line.append(scope)334 config_cmd_line.append(scope)
335 else:
336 config_cmd_line.append('--all')
335 config_cmd_line.append('--format=json')337 config_cmd_line.append('--format=json')
336 try:338 try:
337 config_data = json.loads(339 config_data = json.loads(
@@ -614,6 +616,20 @@
614 subprocess.check_call(_args)616 subprocess.check_call(_args)
615617
616618
619def open_ports(start, end, protocol="TCP"):
620 """Opens a range of service network ports"""
621 _args = ['open-port']
622 _args.append('{}-{}/{}'.format(start, end, protocol))
623 subprocess.check_call(_args)
624
625
626def close_ports(start, end, protocol="TCP"):
627 """Close a range of service network ports"""
628 _args = ['close-port']
629 _args.append('{}-{}/{}'.format(start, end, protocol))
630 subprocess.check_call(_args)
631
632
617@cached633@cached
618def unit_get(attribute):634def unit_get(attribute):
619 """Get the unit ID for the remote unit"""635 """Get the unit ID for the remote unit"""
@@ -1019,3 +1035,34 @@
1019 '''1035 '''
1020 cmd = ['network-get', '--primary-address', binding]1036 cmd = ['network-get', '--primary-address', binding]
1021 return subprocess.check_output(cmd).decode('UTF-8').strip()1037 return subprocess.check_output(cmd).decode('UTF-8').strip()
1038
1039
1040def add_metric(*args, **kwargs):
1041 """Add metric values. Values may be expressed with keyword arguments. For
1042 metric names containing dashes, these may be expressed as one or more
1043 'key=value' positional arguments. May only be called from the collect-metrics
1044 hook."""
1045 _args = ['add-metric']
1046 _kvpairs = []
1047 _kvpairs.extend(args)
1048 _kvpairs.extend(['{}={}'.format(k, v) for k, v in kwargs.items()])
1049 _args.extend(sorted(_kvpairs))
1050 try:
1051 subprocess.check_call(_args)
1052 return
1053 except EnvironmentError as e:
1054 if e.errno != errno.ENOENT:
1055 raise
1056 log_message = 'add-metric failed: {}'.format(' '.join(_kvpairs))
1057 log(log_message, level='INFO')
1058
1059
1060def meter_status():
1061 """Get the meter status, if running in the meter-status-changed hook."""
1062 return os.environ.get('JUJU_METER_STATUS')
1063
1064
1065def meter_info():
1066 """Get the meter status information, if running in the meter-status-changed
1067 hook."""
1068 return os.environ.get('JUJU_METER_INFO')
10221069
=== modified file 'hooks/charmhelpers/core/host.py'
--- hooks/charmhelpers/core/host.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/core/host.py 2017-06-16 22:43:26 +0000
@@ -45,6 +45,7 @@
45 add_new_group,45 add_new_group,
46 lsb_release,46 lsb_release,
47 cmp_pkgrevno,47 cmp_pkgrevno,
48 CompareHostReleases,
48 ) # flake8: noqa -- ignore F401 for this import49 ) # flake8: noqa -- ignore F401 for this import
49elif __platform__ == "centos":50elif __platform__ == "centos":
50 from charmhelpers.core.host_factory.centos import (51 from charmhelpers.core.host_factory.centos import (
@@ -52,44 +53,146 @@
52 add_new_group,53 add_new_group,
53 lsb_release,54 lsb_release,
54 cmp_pkgrevno,55 cmp_pkgrevno,
56 CompareHostReleases,
55 ) # flake8: noqa -- ignore F401 for this import57 ) # flake8: noqa -- ignore F401 for this import
5658
5759UPDATEDB_PATH = '/etc/updatedb.conf'
58def service_start(service_name):60
59 """Start a system service"""61def service_start(service_name, **kwargs):
60 return service('start', service_name)62 """Start a system service.
6163
6264 The specified service name is managed via the system level init system.
63def service_stop(service_name):65 Some init systems (e.g. upstart) require that additional arguments be
64 """Stop a system service"""66 provided in order to directly control service instances whereas other init
65 return service('stop', service_name)67 systems allow for addressing instances of a service directly by name (e.g.
6668 systemd).
6769
68def service_restart(service_name):70 The kwargs allow for the additional parameters to be passed to underlying
69 """Restart a system service"""71 init systems for those systems which require/allow for them. For example,
72 the ceph-osd upstart script requires the id parameter to be passed along
73 in order to identify which running daemon should be reloaded. The follow-
74 ing example stops the ceph-osd service for instance id=4:
75
76 service_stop('ceph-osd', id=4)
77
78 :param service_name: the name of the service to stop
79 :param **kwargs: additional parameters to pass to the init system when
80 managing services. These will be passed as key=value
81 parameters to the init system's commandline. kwargs
82 are ignored for systemd enabled systems.
83 """
84 return service('start', service_name, **kwargs)
85
86
87def service_stop(service_name, **kwargs):
88 """Stop a system service.
89
90 The specified service name is managed via the system level init system.
91 Some init systems (e.g. upstart) require that additional arguments be
92 provided in order to directly control service instances whereas other init
93 systems allow for addressing instances of a service directly by name (e.g.
94 systemd).
95
96 The kwargs allow for the additional parameters to be passed to underlying
97 init systems for those systems which require/allow for them. For example,
98 the ceph-osd upstart script requires the id parameter to be passed along
99 in order to identify which running daemon should be reloaded. The follow-
100 ing example stops the ceph-osd service for instance id=4:
101
102 service_stop('ceph-osd', id=4)
103
104 :param service_name: the name of the service to stop
105 :param **kwargs: additional parameters to pass to the init system when
106 managing services. These will be passed as key=value
107 parameters to the init system's commandline. kwargs
108 are ignored for systemd enabled systems.
109 """
110 return service('stop', service_name, **kwargs)
111
112
113def service_restart(service_name, **kwargs):
114 """Restart a system service.
115
116 The specified service name is managed via the system level init system.
117 Some init systems (e.g. upstart) require that additional arguments be
118 provided in order to directly control service instances whereas other init
119 systems allow for addressing instances of a service directly by name (e.g.
120 systemd).
121
122 The kwargs allow for the additional parameters to be passed to underlying
123 init systems for those systems which require/allow for them. For example,
124 the ceph-osd upstart script requires the id parameter to be passed along
125 in order to identify which running daemon should be restarted. The follow-
126 ing example restarts the ceph-osd service for instance id=4:
127
128 service_restart('ceph-osd', id=4)
129
130 :param service_name: the name of the service to restart
131 :param **kwargs: additional parameters to pass to the init system when
132 managing services. These will be passed as key=value
133 parameters to the init system's commandline. kwargs
134 are ignored for init systems not allowing additional
135 parameters via the commandline (systemd).
136 """
70 return service('restart', service_name)137 return service('restart', service_name)
71138
72139
73def service_reload(service_name, restart_on_failure=False):140def service_reload(service_name, restart_on_failure=False, **kwargs):
74 """Reload a system service, optionally falling back to restart if141 """Reload a system service, optionally falling back to restart if
75 reload fails"""142 reload fails.
76 service_result = service('reload', service_name)143
144 The specified service name is managed via the system level init system.
145 Some init systems (e.g. upstart) require that additional arguments be
146 provided in order to directly control service instances whereas other init
147 systems allow for addressing instances of a service directly by name (e.g.
148 systemd).
149
150 The kwargs allow for the additional parameters to be passed to underlying
151 init systems for those systems which require/allow for them. For example,
152 the ceph-osd upstart script requires the id parameter to be passed along
153 in order to identify which running daemon should be reloaded. The follow-
154 ing example restarts the ceph-osd service for instance id=4:
155
156 service_reload('ceph-osd', id=4)
157
158 :param service_name: the name of the service to reload
159 :param restart_on_failure: boolean indicating whether to fallback to a
160 restart if the reload fails.
161 :param **kwargs: additional parameters to pass to the init system when
162 managing services. These will be passed as key=value
163 parameters to the init system's commandline. kwargs
164 are ignored for init systems not allowing additional
165 parameters via the commandline (systemd).
166 """
167 service_result = service('reload', service_name, **kwargs)
77 if not service_result and restart_on_failure:168 if not service_result and restart_on_failure:
78 service_result = service('restart', service_name)169 service_result = service('restart', service_name, **kwargs)
79 return service_result170 return service_result
80171
81172
82def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d"):173def service_pause(service_name, init_dir="/etc/init", initd_dir="/etc/init.d",
174 **kwargs):
83 """Pause a system service.175 """Pause a system service.
84176
85 Stop it, and prevent it from starting again at boot."""177 Stop it, and prevent it from starting again at boot.
178
179 :param service_name: the name of the service to pause
180 :param init_dir: path to the upstart init directory
181 :param initd_dir: path to the sysv init directory
182 :param **kwargs: additional parameters to pass to the init system when
183 managing services. These will be passed as key=value
184 parameters to the init system's commandline. kwargs
185 are ignored for init systems which do not support
186 key=value arguments via the commandline.
187 """
86 stopped = True188 stopped = True
87 if service_running(service_name):189 if service_running(service_name, **kwargs):
88 stopped = service_stop(service_name)190 stopped = service_stop(service_name, **kwargs)
89 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))191 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
90 sysv_file = os.path.join(initd_dir, service_name)192 sysv_file = os.path.join(initd_dir, service_name)
91 if init_is_systemd():193 if init_is_systemd():
92 service('disable', service_name)194 service('disable', service_name)
195 service('mask', service_name)
93 elif os.path.exists(upstart_file):196 elif os.path.exists(upstart_file):
94 override_path = os.path.join(197 override_path = os.path.join(
95 init_dir, '{}.override'.format(service_name))198 init_dir, '{}.override'.format(service_name))
@@ -106,13 +209,23 @@
106209
107210
108def service_resume(service_name, init_dir="/etc/init",211def service_resume(service_name, init_dir="/etc/init",
109 initd_dir="/etc/init.d"):212 initd_dir="/etc/init.d", **kwargs):
110 """Resume a system service.213 """Resume a system service.
111214
112 Reenable starting again at boot. Start the service"""215 Reenable starting again at boot. Start the service.
216
217 :param service_name: the name of the service to resume
218 :param init_dir: the path to the init dir
219 :param initd dir: the path to the initd dir
220 :param **kwargs: additional parameters to pass to the init system when
221 managing services. These will be passed as key=value
222 parameters to the init system's commandline. kwargs
223 are ignored for systemd enabled systems.
224 """
113 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))225 upstart_file = os.path.join(init_dir, "{}.conf".format(service_name))
114 sysv_file = os.path.join(initd_dir, service_name)226 sysv_file = os.path.join(initd_dir, service_name)
115 if init_is_systemd():227 if init_is_systemd():
228 service('unmask', service_name)
116 service('enable', service_name)229 service('enable', service_name)
117 elif os.path.exists(upstart_file):230 elif os.path.exists(upstart_file):
118 override_path = os.path.join(231 override_path = os.path.join(
@@ -126,19 +239,28 @@
126 "Unable to detect {0} as SystemD, Upstart {1} or"239 "Unable to detect {0} as SystemD, Upstart {1} or"
127 " SysV {2}".format(240 " SysV {2}".format(
128 service_name, upstart_file, sysv_file))241 service_name, upstart_file, sysv_file))
242 started = service_running(service_name, **kwargs)
129243
130 started = service_running(service_name)
131 if not started:244 if not started:
132 started = service_start(service_name)245 started = service_start(service_name, **kwargs)
133 return started246 return started
134247
135248
136def service(action, service_name):249def service(action, service_name, **kwargs):
137 """Control a system service"""250 """Control a system service.
251
252 :param action: the action to take on the service
253 :param service_name: the name of the service to perform th action on
254 :param **kwargs: additional params to be passed to the service command in
255 the form of key=value.
256 """
138 if init_is_systemd():257 if init_is_systemd():
139 cmd = ['systemctl', action, service_name]258 cmd = ['systemctl', action, service_name]
140 else:259 else:
141 cmd = ['service', service_name, action]260 cmd = ['service', service_name, action]
261 for key, value in six.iteritems(kwargs):
262 parameter = '%s=%s' % (key, value)
263 cmd.append(parameter)
142 return subprocess.call(cmd) == 0264 return subprocess.call(cmd) == 0
143265
144266
@@ -146,15 +268,26 @@
146_INIT_D_CONF = "/etc/init.d/{}"268_INIT_D_CONF = "/etc/init.d/{}"
147269
148270
149def service_running(service_name):271def service_running(service_name, **kwargs):
150 """Determine whether a system service is running"""272 """Determine whether a system service is running.
273
274 :param service_name: the name of the service
275 :param **kwargs: additional args to pass to the service command. This is
276 used to pass additional key=value arguments to the
277 service command line for managing specific instance
278 units (e.g. service ceph-osd status id=2). The kwargs
279 are ignored in systemd services.
280 """
151 if init_is_systemd():281 if init_is_systemd():
152 return service('is-active', service_name)282 return service('is-active', service_name)
153 else:283 else:
154 if os.path.exists(_UPSTART_CONF.format(service_name)):284 if os.path.exists(_UPSTART_CONF.format(service_name)):
155 try:285 try:
156 output = subprocess.check_output(286 cmd = ['status', service_name]
157 ['status', service_name],287 for key, value in six.iteritems(kwargs):
288 parameter = '%s=%s' % (key, value)
289 cmd.append(parameter)
290 output = subprocess.check_output(cmd,
158 stderr=subprocess.STDOUT).decode('UTF-8')291 stderr=subprocess.STDOUT).decode('UTF-8')
159 except subprocess.CalledProcessError:292 except subprocess.CalledProcessError:
160 return False293 return False
@@ -177,6 +310,8 @@
177310
178def init_is_systemd():311def init_is_systemd():
179 """Return True if the host system uses systemd, False otherwise."""312 """Return True if the host system uses systemd, False otherwise."""
313 if lsb_release()['DISTRIB_CODENAME'] == 'trusty':
314 return False
180 return os.path.isdir(SYSTEMD_SYSTEM)315 return os.path.isdir(SYSTEMD_SYSTEM)
181316
182317
@@ -306,15 +441,17 @@
306 subprocess.check_call(cmd)441 subprocess.check_call(cmd)
307442
308443
309def rsync(from_path, to_path, flags='-r', options=None):444def rsync(from_path, to_path, flags='-r', options=None, timeout=None):
310 """Replicate the contents of a path"""445 """Replicate the contents of a path"""
311 options = options or ['--delete', '--executability']446 options = options or ['--delete', '--executability']
312 cmd = ['/usr/bin/rsync', flags]447 cmd = ['/usr/bin/rsync', flags]
448 if timeout:
449 cmd = ['timeout', str(timeout)] + cmd
313 cmd.extend(options)450 cmd.extend(options)
314 cmd.append(from_path)451 cmd.append(from_path)
315 cmd.append(to_path)452 cmd.append(to_path)
316 log(" ".join(cmd))453 log(" ".join(cmd))
317 return subprocess.check_output(cmd).decode('UTF-8').strip()454 return subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode('UTF-8').strip()
318455
319456
320def symlink(source, destination):457def symlink(source, destination):
@@ -684,7 +821,7 @@
684 :param str path: The string path to start changing ownership.821 :param str path: The string path to start changing ownership.
685 :param str owner: The owner string to use when looking up the uid.822 :param str owner: The owner string to use when looking up the uid.
686 :param str group: The group string to use when looking up the gid.823 :param str group: The group string to use when looking up the gid.
687 :param bool follow_links: Also Chown links if True824 :param bool follow_links: Also follow and chown links if True
688 :param bool chowntopdir: Also chown path itself if True825 :param bool chowntopdir: Also chown path itself if True
689 """826 """
690 uid = pwd.getpwnam(owner).pw_uid827 uid = pwd.getpwnam(owner).pw_uid
@@ -698,7 +835,7 @@
698 broken_symlink = os.path.lexists(path) and not os.path.exists(path)835 broken_symlink = os.path.lexists(path) and not os.path.exists(path)
699 if not broken_symlink:836 if not broken_symlink:
700 chown(path, uid, gid)837 chown(path, uid, gid)
701 for root, dirs, files in os.walk(path):838 for root, dirs, files in os.walk(path, followlinks=follow_links):
702 for name in dirs + files:839 for name in dirs + files:
703 full = os.path.join(root, name)840 full = os.path.join(root, name)
704 broken_symlink = os.path.lexists(full) and not os.path.exists(full)841 broken_symlink = os.path.lexists(full) and not os.path.exists(full)
@@ -718,6 +855,20 @@
718 chownr(path, owner, group, follow_links=False)855 chownr(path, owner, group, follow_links=False)
719856
720857
858def owner(path):
859 """Returns a tuple containing the username & groupname owning the path.
860
861 :param str path: the string path to retrieve the ownership
862 :return tuple(str, str): A (username, groupname) tuple containing the
863 name of the user and group owning the path.
864 :raises OSError: if the specified path does not exist
865 """
866 stat = os.stat(path)
867 username = pwd.getpwuid(stat.st_uid)[0]
868 groupname = grp.getgrgid(stat.st_gid)[0]
869 return username, groupname
870
871
721def get_total_ram():872def get_total_ram():
722 """The total amount of system RAM in bytes.873 """The total amount of system RAM in bytes.
723874
@@ -732,3 +883,42 @@
732 assert unit == 'kB', 'Unknown unit'883 assert unit == 'kB', 'Unknown unit'
733 return int(value) * 1024 # Classic, not KiB.884 return int(value) * 1024 # Classic, not KiB.
734 raise NotImplementedError()885 raise NotImplementedError()
886
887
888UPSTART_CONTAINER_TYPE = '/run/container_type'
889
890
891def is_container():
892 """Determine whether unit is running in a container
893
894 @return: boolean indicating if unit is in a container
895 """
896 if init_is_systemd():
897 # Detect using systemd-detect-virt
898 return subprocess.call(['systemd-detect-virt',
899 '--container']) == 0
900 else:
901 # Detect using upstart container file marker
902 return os.path.exists(UPSTART_CONTAINER_TYPE)
903
904
905def add_to_updatedb_prunepath(path, updatedb_path=UPDATEDB_PATH):
906 with open(updatedb_path, 'r+') as f_id:
907 updatedb_text = f_id.read()
908 output = updatedb(updatedb_text, path)
909 f_id.seek(0)
910 f_id.write(output)
911 f_id.truncate()
912
913
914def updatedb(updatedb_text, new_path):
915 lines = [line for line in updatedb_text.split("\n")]
916 for i, line in enumerate(lines):
917 if line.startswith("PRUNEPATHS="):
918 paths_line = line.split("=")[1].replace('"', '')
919 paths = paths_line.split(" ")
920 if new_path not in paths:
921 paths.append(new_path)
922 lines[i] = 'PRUNEPATHS="{}"'.format(' '.join(paths))
923 output = "\n".join(lines)
924 return output
735925
=== modified file 'hooks/charmhelpers/core/host_factory/centos.py'
--- hooks/charmhelpers/core/host_factory/centos.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/core/host_factory/centos.py 2017-06-16 22:43:26 +0000
@@ -2,6 +2,22 @@
2import yum2import yum
3import os3import os
44
5from charmhelpers.core.strutils import BasicStringComparator
6
7
8class CompareHostReleases(BasicStringComparator):
9 """Provide comparisons of Host releases.
10
11 Use in the form of
12
13 if CompareHostReleases(release) > 'trusty':
14 # do something with mitaka
15 """
16
17 def __init__(self, item):
18 raise NotImplementedError(
19 "CompareHostReleases() is not implemented for CentOS")
20
521
6def service_available(service_name):22def service_available(service_name):
7 # """Determine whether a system service is available."""23 # """Determine whether a system service is available."""
824
=== modified file 'hooks/charmhelpers/core/host_factory/ubuntu.py'
--- hooks/charmhelpers/core/host_factory/ubuntu.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/core/host_factory/ubuntu.py 2017-06-16 22:43:26 +0000
@@ -1,5 +1,38 @@
1import subprocess1import subprocess
22
3from charmhelpers.core.strutils import BasicStringComparator
4
5
6UBUNTU_RELEASES = (
7 'lucid',
8 'maverick',
9 'natty',
10 'oneiric',
11 'precise',
12 'quantal',
13 'raring',
14 'saucy',
15 'trusty',
16 'utopic',
17 'vivid',
18 'wily',
19 'xenial',
20 'yakkety',
21 'zesty',
22 'artful',
23)
24
25
26class CompareHostReleases(BasicStringComparator):
27 """Provide comparisons of Ubuntu releases.
28
29 Use in the form of
30
31 if CompareHostReleases(release) > 'trusty':
32 # do something with mitaka
33 """
34 _list = UBUNTU_RELEASES
35
336
4def service_available(service_name):37def service_available(service_name):
5 """Determine whether a system service is available"""38 """Determine whether a system service is available"""
639
=== modified file 'hooks/charmhelpers/core/kernel_factory/ubuntu.py'
--- hooks/charmhelpers/core/kernel_factory/ubuntu.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/core/kernel_factory/ubuntu.py 2017-06-16 22:43:26 +0000
@@ -5,7 +5,7 @@
5 """Load a kernel module and configure for auto-load on reboot."""5 """Load a kernel module and configure for auto-load on reboot."""
6 with open('/etc/modules', 'r+') as modules:6 with open('/etc/modules', 'r+') as modules:
7 if module not in modules.read():7 if module not in modules.read():
8 modules.write(module)8 modules.write(module + "\n")
99
1010
11def update_initramfs(version='all'):11def update_initramfs(version='all'):
1212
=== modified file 'hooks/charmhelpers/core/strutils.py'
--- hooks/charmhelpers/core/strutils.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/core/strutils.py 2017-06-16 22:43:26 +0000
@@ -68,3 +68,56 @@
68 msg = "Unable to interpret string value '%s' as bytes" % (value)68 msg = "Unable to interpret string value '%s' as bytes" % (value)
69 raise ValueError(msg)69 raise ValueError(msg)
70 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])70 return int(matches.group(1)) * (1024 ** BYTE_POWER[matches.group(2)])
71
72
73class BasicStringComparator(object):
74 """Provides a class that will compare strings from an iterator type object.
75 Used to provide > and < comparisons on strings that may not necessarily be
76 alphanumerically ordered. e.g. OpenStack or Ubuntu releases AFTER the
77 z-wrap.
78 """
79
80 _list = None
81
82 def __init__(self, item):
83 if self._list is None:
84 raise Exception("Must define the _list in the class definition!")
85 try:
86 self.index = self._list.index(item)
87 except Exception:
88 raise KeyError("Item '{}' is not in list '{}'"
89 .format(item, self._list))
90
91 def __eq__(self, other):
92 assert isinstance(other, str) or isinstance(other, self.__class__)
93 return self.index == self._list.index(other)
94
95 def __ne__(self, other):
96 return not self.__eq__(other)
97
98 def __lt__(self, other):
99 assert isinstance(other, str) or isinstance(other, self.__class__)
100 return self.index < self._list.index(other)
101
102 def __ge__(self, other):
103 return not self.__lt__(other)
104
105 def __gt__(self, other):
106 assert isinstance(other, str) or isinstance(other, self.__class__)
107 return self.index > self._list.index(other)
108
109 def __le__(self, other):
110 return not self.__gt__(other)
111
112 def __str__(self):
113 """Always give back the item at the index so it can be used in
114 comparisons like:
115
116 s_mitaka = CompareOpenStack('mitaka')
117 s_newton = CompareOpenstack('newton')
118
119 assert s_newton > s_mitaka
120
121 @returns: <string>
122 """
123 return self._list[self.index]
71124
=== modified file 'hooks/charmhelpers/fetch/__init__.py'
--- hooks/charmhelpers/fetch/__init__.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/fetch/__init__.py 2017-06-16 22:43:26 +0000
@@ -48,6 +48,13 @@
48 pass48 pass
4949
5050
51class GPGKeyError(Exception):
52 """Exception occurs when a GPG key cannot be fetched or used. The message
53 indicates what the problem is.
54 """
55 pass
56
57
51class BaseFetchHandler(object):58class BaseFetchHandler(object):
5259
53 """Base class for FetchHandler implementations in fetch plugins"""60 """Base class for FetchHandler implementations in fetch plugins"""
@@ -77,21 +84,22 @@
77fetch = importlib.import_module(module)84fetch = importlib.import_module(module)
7885
79filter_installed_packages = fetch.filter_installed_packages86filter_installed_packages = fetch.filter_installed_packages
80install = fetch.install87install = fetch.apt_install
81upgrade = fetch.upgrade88upgrade = fetch.apt_upgrade
82update = fetch.update89update = _fetch_update = fetch.apt_update
83purge = fetch.purge90purge = fetch.apt_purge
84add_source = fetch.add_source91add_source = fetch.add_source
8592
86if __platform__ == "ubuntu":93if __platform__ == "ubuntu":
87 apt_cache = fetch.apt_cache94 apt_cache = fetch.apt_cache
88 apt_install = fetch.install95 apt_install = fetch.apt_install
89 apt_update = fetch.update96 apt_update = fetch.apt_update
90 apt_upgrade = fetch.upgrade97 apt_upgrade = fetch.apt_upgrade
91 apt_purge = fetch.purge98 apt_purge = fetch.apt_purge
92 apt_mark = fetch.apt_mark99 apt_mark = fetch.apt_mark
93 apt_hold = fetch.apt_hold100 apt_hold = fetch.apt_hold
94 apt_unhold = fetch.apt_unhold101 apt_unhold = fetch.apt_unhold
102 import_key = fetch.import_key
95 get_upstream_version = fetch.get_upstream_version103 get_upstream_version = fetch.get_upstream_version
96elif __platform__ == "centos":104elif __platform__ == "centos":
97 yum_search = fetch.yum_search105 yum_search = fetch.yum_search
@@ -135,7 +143,7 @@
135 for source, key in zip(sources, keys):143 for source, key in zip(sources, keys):
136 add_source(source, key)144 add_source(source, key)
137 if update:145 if update:
138 fetch.update(fatal=True)146 _fetch_update(fatal=True)
139147
140148
141def install_remote(source, *args, **kwargs):149def install_remote(source, *args, **kwargs):
142150
=== modified file 'hooks/charmhelpers/fetch/centos.py'
--- hooks/charmhelpers/fetch/centos.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/fetch/centos.py 2017-06-16 22:43:26 +0000
@@ -132,7 +132,7 @@
132 key_file.write(key)132 key_file.write(key)
133 key_file.flush()133 key_file.flush()
134 key_file.seek(0)134 key_file.seek(0)
135 subprocess.check_call(['rpm', '--import', key_file])135 subprocess.check_call(['rpm', '--import', key_file.name])
136 else:136 else:
137 subprocess.check_call(['rpm', '--import', key])137 subprocess.check_call(['rpm', '--import', key])
138138
139139
=== added file 'hooks/charmhelpers/fetch/snap.py'
--- hooks/charmhelpers/fetch/snap.py 1970-01-01 00:00:00 +0000
+++ hooks/charmhelpers/fetch/snap.py 2017-06-16 22:43:26 +0000
@@ -0,0 +1,122 @@
1# Copyright 2014-2017 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"""
15Charm helpers snap for classic charms.
16
17If writing reactive charms, use the snap layer:
18https://lists.ubuntu.com/archives/snapcraft/2016-September/001114.html
19"""
20import subprocess
21from os import environ
22from time import sleep
23from charmhelpers.core.hookenv import log
24
25__author__ = 'Joseph Borg <joseph.borg@canonical.com>'
26
27SNAP_NO_LOCK = 1 # The return code for "couldn't acquire lock" in Snap (hopefully this will be improved).
28SNAP_NO_LOCK_RETRY_DELAY = 10 # Wait X seconds between Snap lock checks.
29SNAP_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
30
31
32class CouldNotAcquireLockException(Exception):
33 pass
34
35
36def _snap_exec(commands):
37 """
38 Execute snap commands.
39
40 :param commands: List commands
41 :return: Integer exit code
42 """
43 assert type(commands) == list
44
45 retry_count = 0
46 return_code = None
47
48 while return_code is None or return_code == SNAP_NO_LOCK:
49 try:
50 return_code = subprocess.check_call(['snap'] + commands, env=environ)
51 except subprocess.CalledProcessError as e:
52 retry_count += + 1
53 if retry_count > SNAP_NO_LOCK_RETRY_COUNT:
54 raise CouldNotAcquireLockException('Could not aquire lock after %s attempts' % SNAP_NO_LOCK_RETRY_COUNT)
55 return_code = e.returncode
56 log('Snap failed to acquire lock, trying again in %s seconds.' % SNAP_NO_LOCK_RETRY_DELAY, level='WARN')
57 sleep(SNAP_NO_LOCK_RETRY_DELAY)
58
59 return return_code
60
61
62def snap_install(packages, *flags):
63 """
64 Install a snap package.
65
66 :param packages: String or List String package name
67 :param flags: List String flags to pass to install command
68 :return: Integer return code from snap
69 """
70 if type(packages) is not list:
71 packages = [packages]
72
73 flags = list(flags)
74
75 message = 'Installing snap(s) "%s"' % ', '.join(packages)
76 if flags:
77 message += ' with option(s) "%s"' % ', '.join(flags)
78
79 log(message, level='INFO')
80 return _snap_exec(['install'] + flags + packages)
81
82
83def snap_remove(packages, *flags):
84 """
85 Remove a snap package.
86
87 :param packages: String or List String package name
88 :param flags: List String flags to pass to remove command
89 :return: Integer return code from snap
90 """
91 if type(packages) is not list:
92 packages = [packages]
93
94 flags = list(flags)
95
96 message = 'Removing snap(s) "%s"' % ', '.join(packages)
97 if flags:
98 message += ' with options "%s"' % ', '.join(flags)
99
100 log(message, level='INFO')
101 return _snap_exec(['remove'] + flags + packages)
102
103
104def snap_refresh(packages, *flags):
105 """
106 Refresh / Update snap package.
107
108 :param packages: String or List String package name
109 :param flags: List String flags to pass to refresh command
110 :return: Integer return code from snap
111 """
112 if type(packages) is not list:
113 packages = [packages]
114
115 flags = list(flags)
116
117 message = 'Refreshing snap(s) "%s"' % ', '.join(packages)
118 if flags:
119 message += ' with options "%s"' % ', '.join(flags)
120
121 log(message, level='INFO')
122 return _snap_exec(['refresh'] + flags + packages)
0123
=== modified file 'hooks/charmhelpers/fetch/ubuntu.py'
--- hooks/charmhelpers/fetch/ubuntu.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/fetch/ubuntu.py 2017-06-16 22:43:26 +0000
@@ -12,29 +12,47 @@
12# See the License for the specific language governing permissions and12# See the License for the specific language governing permissions and
13# limitations under the License.13# limitations under the License.
1414
15from collections import OrderedDict
15import os16import os
17import platform
18import re
16import six19import six
17import time20import time
18import subprocess21import subprocess
19
20from tempfile import NamedTemporaryFile22from tempfile import NamedTemporaryFile
23
21from charmhelpers.core.host import (24from charmhelpers.core.host import (
22 lsb_release25 lsb_release
23)26)
24from charmhelpers.core.hookenv import log27from charmhelpers.core.hookenv import (
25from charmhelpers.fetch import SourceConfigError28 log,
29 DEBUG,
30)
31from charmhelpers.fetch import SourceConfigError, GPGKeyError
2632
33PROPOSED_POCKET = (
34 "# Proposed\n"
35 "deb http://archive.ubuntu.com/ubuntu {}-proposed main universe "
36 "multiverse restricted\n")
37PROPOSED_PORTS_POCKET = (
38 "# Proposed\n"
39 "deb http://ports.ubuntu.com/ubuntu-ports {}-proposed main universe "
40 "multiverse restricted\n")
41# Only supports 64bit and ppc64 at the moment.
42ARCH_TO_PROPOSED_POCKET = {
43 'x86_64': PROPOSED_POCKET,
44 'ppc64le': PROPOSED_PORTS_POCKET,
45 'aarch64': PROPOSED_PORTS_POCKET,
46}
47CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
48CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
27CLOUD_ARCHIVE = """# Ubuntu Cloud Archive49CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
28deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main50deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
29"""51"""
30
31PROPOSED_POCKET = """# Proposed
32deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
33"""
34
35CLOUD_ARCHIVE_POCKETS = {52CLOUD_ARCHIVE_POCKETS = {
36 # Folsom53 # Folsom
37 'folsom': 'precise-updates/folsom',54 'folsom': 'precise-updates/folsom',
55 'folsom/updates': 'precise-updates/folsom',
38 'precise-folsom': 'precise-updates/folsom',56 'precise-folsom': 'precise-updates/folsom',
39 'precise-folsom/updates': 'precise-updates/folsom',57 'precise-folsom/updates': 'precise-updates/folsom',
40 'precise-updates/folsom': 'precise-updates/folsom',58 'precise-updates/folsom': 'precise-updates/folsom',
@@ -43,6 +61,7 @@
43 'precise-proposed/folsom': 'precise-proposed/folsom',61 'precise-proposed/folsom': 'precise-proposed/folsom',
44 # Grizzly62 # Grizzly
45 'grizzly': 'precise-updates/grizzly',63 'grizzly': 'precise-updates/grizzly',
64 'grizzly/updates': 'precise-updates/grizzly',
46 'precise-grizzly': 'precise-updates/grizzly',65 'precise-grizzly': 'precise-updates/grizzly',
47 'precise-grizzly/updates': 'precise-updates/grizzly',66 'precise-grizzly/updates': 'precise-updates/grizzly',
48 'precise-updates/grizzly': 'precise-updates/grizzly',67 'precise-updates/grizzly': 'precise-updates/grizzly',
@@ -51,6 +70,7 @@
51 'precise-proposed/grizzly': 'precise-proposed/grizzly',70 'precise-proposed/grizzly': 'precise-proposed/grizzly',
52 # Havana71 # Havana
53 'havana': 'precise-updates/havana',72 'havana': 'precise-updates/havana',
73 'havana/updates': 'precise-updates/havana',
54 'precise-havana': 'precise-updates/havana',74 'precise-havana': 'precise-updates/havana',
55 'precise-havana/updates': 'precise-updates/havana',75 'precise-havana/updates': 'precise-updates/havana',
56 'precise-updates/havana': 'precise-updates/havana',76 'precise-updates/havana': 'precise-updates/havana',
@@ -59,6 +79,7 @@
59 'precise-proposed/havana': 'precise-proposed/havana',79 'precise-proposed/havana': 'precise-proposed/havana',
60 # Icehouse80 # Icehouse
61 'icehouse': 'precise-updates/icehouse',81 'icehouse': 'precise-updates/icehouse',
82 'icehouse/updates': 'precise-updates/icehouse',
62 'precise-icehouse': 'precise-updates/icehouse',83 'precise-icehouse': 'precise-updates/icehouse',
63 'precise-icehouse/updates': 'precise-updates/icehouse',84 'precise-icehouse/updates': 'precise-updates/icehouse',
64 'precise-updates/icehouse': 'precise-updates/icehouse',85 'precise-updates/icehouse': 'precise-updates/icehouse',
@@ -67,6 +88,7 @@
67 'precise-proposed/icehouse': 'precise-proposed/icehouse',88 'precise-proposed/icehouse': 'precise-proposed/icehouse',
68 # Juno89 # Juno
69 'juno': 'trusty-updates/juno',90 'juno': 'trusty-updates/juno',
91 'juno/updates': 'trusty-updates/juno',
70 'trusty-juno': 'trusty-updates/juno',92 'trusty-juno': 'trusty-updates/juno',
71 'trusty-juno/updates': 'trusty-updates/juno',93 'trusty-juno/updates': 'trusty-updates/juno',
72 'trusty-updates/juno': 'trusty-updates/juno',94 'trusty-updates/juno': 'trusty-updates/juno',
@@ -75,6 +97,7 @@
75 'trusty-proposed/juno': 'trusty-proposed/juno',97 'trusty-proposed/juno': 'trusty-proposed/juno',
76 # Kilo98 # Kilo
77 'kilo': 'trusty-updates/kilo',99 'kilo': 'trusty-updates/kilo',
100 'kilo/updates': 'trusty-updates/kilo',
78 'trusty-kilo': 'trusty-updates/kilo',101 'trusty-kilo': 'trusty-updates/kilo',
79 'trusty-kilo/updates': 'trusty-updates/kilo',102 'trusty-kilo/updates': 'trusty-updates/kilo',
80 'trusty-updates/kilo': 'trusty-updates/kilo',103 'trusty-updates/kilo': 'trusty-updates/kilo',
@@ -83,6 +106,7 @@
83 'trusty-proposed/kilo': 'trusty-proposed/kilo',106 'trusty-proposed/kilo': 'trusty-proposed/kilo',
84 # Liberty107 # Liberty
85 'liberty': 'trusty-updates/liberty',108 'liberty': 'trusty-updates/liberty',
109 'liberty/updates': 'trusty-updates/liberty',
86 'trusty-liberty': 'trusty-updates/liberty',110 'trusty-liberty': 'trusty-updates/liberty',
87 'trusty-liberty/updates': 'trusty-updates/liberty',111 'trusty-liberty/updates': 'trusty-updates/liberty',
88 'trusty-updates/liberty': 'trusty-updates/liberty',112 'trusty-updates/liberty': 'trusty-updates/liberty',
@@ -91,6 +115,7 @@
91 'trusty-proposed/liberty': 'trusty-proposed/liberty',115 'trusty-proposed/liberty': 'trusty-proposed/liberty',
92 # Mitaka116 # Mitaka
93 'mitaka': 'trusty-updates/mitaka',117 'mitaka': 'trusty-updates/mitaka',
118 'mitaka/updates': 'trusty-updates/mitaka',
94 'trusty-mitaka': 'trusty-updates/mitaka',119 'trusty-mitaka': 'trusty-updates/mitaka',
95 'trusty-mitaka/updates': 'trusty-updates/mitaka',120 'trusty-mitaka/updates': 'trusty-updates/mitaka',
96 'trusty-updates/mitaka': 'trusty-updates/mitaka',121 'trusty-updates/mitaka': 'trusty-updates/mitaka',
@@ -99,17 +124,44 @@
99 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',124 'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
100 # Newton125 # Newton
101 'newton': 'xenial-updates/newton',126 'newton': 'xenial-updates/newton',
127 'newton/updates': 'xenial-updates/newton',
102 'xenial-newton': 'xenial-updates/newton',128 'xenial-newton': 'xenial-updates/newton',
103 'xenial-newton/updates': 'xenial-updates/newton',129 'xenial-newton/updates': 'xenial-updates/newton',
104 'xenial-updates/newton': 'xenial-updates/newton',130 'xenial-updates/newton': 'xenial-updates/newton',
105 'newton/proposed': 'xenial-proposed/newton',131 'newton/proposed': 'xenial-proposed/newton',
106 'xenial-newton/proposed': 'xenial-proposed/newton',132 'xenial-newton/proposed': 'xenial-proposed/newton',
107 'xenial-proposed/newton': 'xenial-proposed/newton',133 'xenial-proposed/newton': 'xenial-proposed/newton',
134 # Ocata
135 'ocata': 'xenial-updates/ocata',
136 'ocata/updates': 'xenial-updates/ocata',
137 'xenial-ocata': 'xenial-updates/ocata',
138 'xenial-ocata/updates': 'xenial-updates/ocata',
139 'xenial-updates/ocata': 'xenial-updates/ocata',
140 'ocata/proposed': 'xenial-proposed/ocata',
141 'xenial-ocata/proposed': 'xenial-proposed/ocata',
142 'xenial-ocata/newton': 'xenial-proposed/ocata',
143 # Pike
144 'pike': 'xenial-updates/pike',
145 'xenial-pike': 'xenial-updates/pike',
146 'xenial-pike/updates': 'xenial-updates/pike',
147 'xenial-updates/pike': 'xenial-updates/pike',
148 'pike/proposed': 'xenial-proposed/pike',
149 'xenial-pike/proposed': 'xenial-proposed/pike',
150 'xenial-pike/newton': 'xenial-proposed/pike',
151 # Queens
152 'queens': 'xenial-updates/queens',
153 'xenial-queens': 'xenial-updates/queens',
154 'xenial-queens/updates': 'xenial-updates/queens',
155 'xenial-updates/queens': 'xenial-updates/queens',
156 'queens/proposed': 'xenial-proposed/queens',
157 'xenial-queens/proposed': 'xenial-proposed/queens',
158 'xenial-queens/newton': 'xenial-proposed/queens',
108}159}
109160
161
110APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.162APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
111APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.163CMD_RETRY_DELAY = 10 # Wait 10 seconds between command retries.
112APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.164CMD_RETRY_COUNT = 30 # Retry a failing fatal command X times.
113165
114166
115def filter_installed_packages(packages):167def filter_installed_packages(packages):
@@ -137,7 +189,7 @@
137 return apt_pkg.Cache(progress)189 return apt_pkg.Cache(progress)
138190
139191
140def install(packages, options=None, fatal=False):192def apt_install(packages, options=None, fatal=False):
141 """Install one or more packages."""193 """Install one or more packages."""
142 if options is None:194 if options is None:
143 options = ['--option=Dpkg::Options::=--force-confold']195 options = ['--option=Dpkg::Options::=--force-confold']
@@ -154,7 +206,7 @@
154 _run_apt_command(cmd, fatal)206 _run_apt_command(cmd, fatal)
155207
156208
157def upgrade(options=None, fatal=False, dist=False):209def apt_upgrade(options=None, fatal=False, dist=False):
158 """Upgrade all packages."""210 """Upgrade all packages."""
159 if options is None:211 if options is None:
160 options = ['--option=Dpkg::Options::=--force-confold']212 options = ['--option=Dpkg::Options::=--force-confold']
@@ -169,13 +221,13 @@
169 _run_apt_command(cmd, fatal)221 _run_apt_command(cmd, fatal)
170222
171223
172def update(fatal=False):224def apt_update(fatal=False):
173 """Update local apt cache."""225 """Update local apt cache."""
174 cmd = ['apt-get', 'update']226 cmd = ['apt-get', 'update']
175 _run_apt_command(cmd, fatal)227 _run_apt_command(cmd, fatal)
176228
177229
178def purge(packages, fatal=False):230def apt_purge(packages, fatal=False):
179 """Purge one or more packages."""231 """Purge one or more packages."""
180 cmd = ['apt-get', '--assume-yes', 'purge']232 cmd = ['apt-get', '--assume-yes', 'purge']
181 if isinstance(packages, six.string_types):233 if isinstance(packages, six.string_types):
@@ -209,7 +261,45 @@
209 return apt_mark(packages, 'unhold', fatal=fatal)261 return apt_mark(packages, 'unhold', fatal=fatal)
210262
211263
212def add_source(source, key=None):264def import_key(keyid):
265 """Import a key in either ASCII Armor or Radix64 format.
266
267 `keyid` is either the keyid to fetch from a PGP server, or
268 the key in ASCII armor foramt.
269
270 :param keyid: String of key (or key id).
271 :raises: GPGKeyError if the key could not be imported
272 """
273 key = keyid.strip()
274 if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
275 key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
276 log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
277 log("Importing ASCII Armor PGP key", level=DEBUG)
278 with NamedTemporaryFile() as keyfile:
279 with open(keyfile.name, 'w') as fd:
280 fd.write(key)
281 fd.write("\n")
282 cmd = ['apt-key', 'add', keyfile.name]
283 try:
284 subprocess.check_call(cmd)
285 except subprocess.CalledProcessError:
286 error = "Error importing PGP key '{}'".format(key)
287 log(error)
288 raise GPGKeyError(error)
289 else:
290 log("PGP key found (looks like Radix64 format)", level=DEBUG)
291 log("Importing PGP key from keyserver", level=DEBUG)
292 cmd = ['apt-key', 'adv', '--keyserver',
293 'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
294 try:
295 subprocess.check_call(cmd)
296 except subprocess.CalledProcessError:
297 error = "Error importing PGP key '{}'".format(key)
298 log(error)
299 raise GPGKeyError(error)
300
301
302def add_source(source, key=None, fail_invalid=False):
213 """Add a package source to this system.303 """Add a package source to this system.
214304
215 @param source: a URL or sources.list entry, as supported by305 @param source: a URL or sources.list entry, as supported by
@@ -225,6 +315,33 @@
225 such as 'cloud:icehouse'315 such as 'cloud:icehouse'
226 'distro' may be used as a noop316 'distro' may be used as a noop
227317
318 Full list of source specifications supported by the function are:
319
320 'distro': A NOP; i.e. it has no effect.
321 'proposed': the proposed deb spec [2] is wrtten to
322 /etc/apt/sources.list/proposed
323 'distro-proposed': adds <version>-proposed to the debs [2]
324 'ppa:<ppa-name>': add-apt-repository --yes <ppa_name>
325 'deb <deb-spec>': add-apt-repository --yes deb <deb-spec>
326 'http://....': add-apt-repository --yes http://...
327 'cloud-archive:<spec>': add-apt-repository -yes cloud-archive:<spec>
328 'cloud:<release>[-staging]': specify a Cloud Archive pocket <release> with
329 optional staging version. If staging is used then the staging PPA [2]
330 with be used. If staging is NOT used then the cloud archive [3] will be
331 added, and the 'ubuntu-cloud-keyring' package will be added for the
332 current distro.
333
334 Otherwise the source is not recognised and this is logged to the juju log.
335 However, no error is raised, unless sys_error_on_exit is True.
336
337 [1] deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
338 where {} is replaced with the derived pocket name.
339 [2] deb http://archive.ubuntu.com/ubuntu {}-proposed \
340 main universe multiverse restricted
341 where {} is replaced with the lsb_release codename (e.g. xenial)
342 [3] deb http://ubuntu-cloud.archive.canonical.com/ubuntu <pocket>
343 to /etc/apt/sources.list.d/cloud-archive-list
344
228 @param key: A key to be added to the system's APT keyring and used345 @param key: A key to be added to the system's APT keyring and used
229 to verify the signatures on packages. Ideally, this should be an346 to verify the signatures on packages. Ideally, this should be an
230 ASCII format GPG public key including the block headers. A GPG key347 ASCII format GPG public key including the block headers. A GPG key
@@ -232,87 +349,202 @@
232 available to retrieve the actual public key from a public keyserver349 available to retrieve the actual public key from a public keyserver
233 placing your Juju environment at risk. ppa and cloud archive keys350 placing your Juju environment at risk. ppa and cloud archive keys
234 are securely added automtically, so sould not be provided.351 are securely added automtically, so sould not be provided.
352
353 @param fail_invalid: (boolean) if True, then the function raises a
354 SourceConfigError is there is no matching installation source.
355
356 @raises SourceConfigError() if for cloud:<pocket>, the <pocket> is not a
357 valid pocket in CLOUD_ARCHIVE_POCKETS
235 """358 """
359 _mapping = OrderedDict([
360 (r"^distro$", lambda: None), # This is a NOP
361 (r"^(?:proposed|distro-proposed)$", _add_proposed),
362 (r"^cloud-archive:(.*)$", _add_apt_repository),
363 (r"^((?:deb |http:|https:|ppa:).*)$", _add_apt_repository),
364 (r"^cloud:(.*)-(.*)\/staging$", _add_cloud_staging),
365 (r"^cloud:(.*)-(.*)$", _add_cloud_distro_check),
366 (r"^cloud:(.*)$", _add_cloud_pocket),
367 ])
236 if source is None:368 if source is None:
237 log('Source is not present. Skipping')369 source = ''
238 return370 for r, fn in six.iteritems(_mapping):
239371 m = re.match(r, source)
240 if (source.startswith('ppa:') or372 if m:
241 source.startswith('http') or373 # call the assoicated function with the captured groups
242 source.startswith('deb ') or374 # raises SourceConfigError on error.
243 source.startswith('cloud-archive:')):375 fn(*m.groups())
244 subprocess.check_call(['add-apt-repository', '--yes', source])376 if key:
245 elif source.startswith('cloud:'):377 try:
246 install(filter_installed_packages(['ubuntu-cloud-keyring']),378 import_key(key)
379 except GPGKeyError as e:
380 raise SourceConfigError(str(e))
381 break
382 else:
383 # nothing matched. log an error and maybe sys.exit
384 err = "Unknown source: {!r}".format(source)
385 log(err)
386 if fail_invalid:
387 raise SourceConfigError(err)
388
389
390def _add_proposed():
391 """Add the PROPOSED_POCKET as /etc/apt/source.list.d/proposed.list
392
393 Uses lsb_release()['DISTRIB_CODENAME'] to determine the correct staza for
394 the deb line.
395
396 For intel architecutres PROPOSED_POCKET is used for the release, but for
397 other architectures PROPOSED_PORTS_POCKET is used for the release.
398 """
399 release = lsb_release()['DISTRIB_CODENAME']
400 arch = platform.machine()
401 if arch not in six.iterkeys(ARCH_TO_PROPOSED_POCKET):
402 raise SourceConfigError("Arch {} not supported for (distro-)proposed"
403 .format(arch))
404 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
405 apt.write(ARCH_TO_PROPOSED_POCKET[arch].format(release))
406
407
408def _add_apt_repository(spec):
409 """Add the spec using add_apt_repository
410
411 :param spec: the parameter to pass to add_apt_repository
412 """
413 _run_with_retries(['add-apt-repository', '--yes', spec])
414
415
416def _add_cloud_pocket(pocket):
417 """Add a cloud pocket as /etc/apt/sources.d/cloud-archive.list
418
419 Note that this overwrites the existing file if there is one.
420
421 This function also converts the simple pocket in to the actual pocket using
422 the CLOUD_ARCHIVE_POCKETS mapping.
423
424 :param pocket: string representing the pocket to add a deb spec for.
425 :raises: SourceConfigError if the cloud pocket doesn't exist or the
426 requested release doesn't match the current distro version.
427 """
428 apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
247 fatal=True)429 fatal=True)
248 pocket = source.split(':')[-1]430 if pocket not in CLOUD_ARCHIVE_POCKETS:
249 if pocket not in CLOUD_ARCHIVE_POCKETS:431 raise SourceConfigError(
250 raise SourceConfigError(432 'Unsupported cloud: source option %s' %
251 'Unsupported cloud: source option %s' %433 pocket)
252 pocket)434 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
253 actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]435 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
254 with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:436 apt.write(CLOUD_ARCHIVE.format(actual_pocket))
255 apt.write(CLOUD_ARCHIVE.format(actual_pocket))437
256 elif source == 'proposed':438
257 release = lsb_release()['DISTRIB_CODENAME']439def _add_cloud_staging(cloud_archive_release, openstack_release):
258 with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:440 """Add the cloud staging repository which is in
259 apt.write(PROPOSED_POCKET.format(release))441 ppa:ubuntu-cloud-archive/<openstack_release>-staging
260 elif source == 'distro':442
261 pass443 This function checks that the cloud_archive_release matches the current
262 else:444 codename for the distro that charm is being installed on.
263 log("Unknown source: {!r}".format(source))445
264446 :param cloud_archive_release: string, codename for the release.
265 if key:447 :param openstack_release: String, codename for the openstack release.
266 if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:448 :raises: SourceConfigError if the cloud_archive_release doesn't match the
267 with NamedTemporaryFile('w+') as key_file:449 current version of the os.
268 key_file.write(key)450 """
269 key_file.flush()451 _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
270 key_file.seek(0)452 ppa = 'ppa:ubuntu-cloud-archive/{}-staging'.format(openstack_release)
271 subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)453 cmd = 'add-apt-repository -y {}'.format(ppa)
272 else:454 _run_with_retries(cmd.split(' '))
273 # Note that hkp: is in no way a secure protocol. Using a455
274 # GPG key id is pointless from a security POV unless you456
275 # absolutely trust your network and DNS.457def _add_cloud_distro_check(cloud_archive_release, openstack_release):
276 subprocess.check_call(['apt-key', 'adv', '--keyserver',458 """Add the cloud pocket, but also check the cloud_archive_release against
277 'hkp://keyserver.ubuntu.com:80', '--recv',459 the current distro, and use the openstack_release as the full lookup.
278 key])460
461 This just calls _add_cloud_pocket() with the openstack_release as pocket
462 to get the correct cloud-archive.list for dpkg to work with.
463
464 :param cloud_archive_release:String, codename for the distro release.
465 :param openstack_release: String, spec for the release to look up in the
466 CLOUD_ARCHIVE_POCKETS
467 :raises: SourceConfigError if this is the wrong distro, or the pocket spec
468 doesn't exist.
469 """
470 _verify_is_ubuntu_rel(cloud_archive_release, openstack_release)
471 _add_cloud_pocket("{}-{}".format(cloud_archive_release, openstack_release))
472
473
474def _verify_is_ubuntu_rel(release, os_release):
475 """Verify that the release is in the same as the current ubuntu release.
476
477 :param release: String, lowercase for the release.
478 :param os_release: String, the os_release being asked for
479 :raises: SourceConfigError if the release is not the same as the ubuntu
480 release.
481 """
482 ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
483 if release != ubuntu_rel:
484 raise SourceConfigError(
485 'Invalid Cloud Archive release specified: {}-{} on this Ubuntu'
486 'version ({})'.format(release, os_release, ubuntu_rel))
487
488
489def _run_with_retries(cmd, max_retries=CMD_RETRY_COUNT, retry_exitcodes=(1,),
490 retry_message="", cmd_env=None):
491 """Run a command and retry until success or max_retries is reached.
492
493 :param: cmd: str: The apt command to run.
494 :param: max_retries: int: The number of retries to attempt on a fatal
495 command. Defaults to CMD_RETRY_COUNT.
496 :param: retry_exitcodes: tuple: Optional additional exit codes to retry.
497 Defaults to retry on exit code 1.
498 :param: retry_message: str: Optional log prefix emitted during retries.
499 :param: cmd_env: dict: Environment variables to add to the command run.
500 """
501
502 env = None
503 kwargs = {}
504 if cmd_env:
505 env = os.environ.copy()
506 env.update(cmd_env)
507 kwargs['env'] = env
508
509 if not retry_message:
510 retry_message = "Failed executing '{}'".format(" ".join(cmd))
511 retry_message += ". Will retry in {} seconds".format(CMD_RETRY_DELAY)
512
513 retry_count = 0
514 result = None
515
516 retry_results = (None,) + retry_exitcodes
517 while result in retry_results:
518 try:
519 # result = subprocess.check_call(cmd, env=env)
520 result = subprocess.check_call(cmd, **kwargs)
521 except subprocess.CalledProcessError as e:
522 retry_count = retry_count + 1
523 if retry_count > max_retries:
524 raise
525 result = e.returncode
526 log(retry_message)
527 time.sleep(CMD_RETRY_DELAY)
279528
280529
281def _run_apt_command(cmd, fatal=False):530def _run_apt_command(cmd, fatal=False):
282 """Run an APT command.531 """Run an apt command with optional retries.
283
284 Checks the output and retries if the fatal flag is set
285 to True.
286532
287 :param: cmd: str: The apt command to run.533 :param: cmd: str: The apt command to run.
288 :param: fatal: bool: Whether the command's output should be checked and534 :param: fatal: bool: Whether the command's output should be checked and
289 retried.535 retried.
290 """536 """
291 env = os.environ.copy()537 # Provide DEBIAN_FRONTEND=noninteractive if not present in the environment.
292538 cmd_env = {
293 if 'DEBIAN_FRONTEND' not in env:539 'DEBIAN_FRONTEND': os.environ.get('DEBIAN_FRONTEND', 'noninteractive')}
294 env['DEBIAN_FRONTEND'] = 'noninteractive'
295540
296 if fatal:541 if fatal:
297 retry_count = 0542 _run_with_retries(
298 result = None543 cmd, cmd_env=cmd_env, retry_exitcodes=(1, APT_NO_LOCK,),
299544 retry_message="Couldn't acquire DPKG lock")
300 # If the command is considered "fatal", we need to retry if the apt
301 # lock was not acquired.
302
303 while result is None or result == APT_NO_LOCK:
304 try:
305 result = subprocess.check_call(cmd, env=env)
306 except subprocess.CalledProcessError as e:
307 retry_count = retry_count + 1
308 if retry_count > APT_NO_LOCK_RETRY_COUNT:
309 raise
310 result = e.returncode
311 log("Couldn't acquire DPKG lock. Will retry in {} seconds."
312 "".format(APT_NO_LOCK_RETRY_DELAY))
313 time.sleep(APT_NO_LOCK_RETRY_DELAY)
314
315 else:545 else:
546 env = os.environ.copy()
547 env.update(cmd_env)
316 subprocess.call(cmd, env=env)548 subprocess.call(cmd, env=env)
317549
318550
319551
=== modified file 'hooks/charmhelpers/osplatform.py'
--- hooks/charmhelpers/osplatform.py 2016-10-26 18:19:59 +0000
+++ hooks/charmhelpers/osplatform.py 2017-06-16 22:43:26 +0000
@@ -8,12 +8,18 @@
8 will be returned (which is the name of the module).8 will be returned (which is the name of the module).
9 This string is used to decide which platform module should be imported.9 This string is used to decide which platform module should be imported.
10 """10 """
11 # linux_distribution is deprecated and will be removed in Python 3.7
12 # Warings *not* disabled, as we certainly need to fix this.
11 tuple_platform = platform.linux_distribution()13 tuple_platform = platform.linux_distribution()
12 current_platform = tuple_platform[0]14 current_platform = tuple_platform[0]
13 if "Ubuntu" in current_platform:15 if "Ubuntu" in current_platform:
14 return "ubuntu"16 return "ubuntu"
15 elif "CentOS" in current_platform:17 elif "CentOS" in current_platform:
16 return "centos"18 return "centos"
19 elif "debian" in current_platform:
20 # Stock Python does not detect Ubuntu and instead returns debian.
21 # Or at least it does in some build environments like Travis CI
22 return "ubuntu"
17 else:23 else:
18 raise RuntimeError("This module is not supported on {}."24 raise RuntimeError("This module is not supported on {}."
19 .format(current_platform))25 .format(current_platform))
2026
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2016-11-15 18:15:20 +0000
+++ hooks/hooks.py 2017-06-16 22:43:26 +0000
@@ -4,6 +4,8 @@
4import sys4import sys
55
6from charmhelpers.core.host import (6from charmhelpers.core.host import (
7 CompareHostReleases,
8 lsb_release,
7 service_start,9 service_start,
8 service_stop,10 service_stop,
9 service_restart,11 service_restart,
@@ -69,6 +71,12 @@
69 changed[config_key] = config_get(config_key)71 changed[config_key] = config_get(config_key)
70 juju_log("Configuration key:%s set to value: %s" %72 juju_log("Configuration key:%s set to value: %s" %
71 (config_key, changed[config_key]))73 (config_key, changed[config_key]))
74
75
76 # allows templates to generate series-dependent configuration
77 lsb = lsb_release()
78 changed['series'] = CompareHostReleases(lsb['DISTRIB_CODENAME'])
79
72 return changed80 return changed
7381
7482
7583
=== modified file 'templates/rsyslog.conf'
--- templates/rsyslog.conf 2014-04-22 19:45:41 +0000
+++ templates/rsyslog.conf 2017-06-16 22:43:26 +0000
@@ -1,3 +1,8 @@
1###############################################################################
2# [ WARNING ]
3# Configuration file maintained by Juju. Local changes may be overwritten.
4###############################################################################
5
1/var/log/syslog6/var/log/syslog
2{7{
3 rotate {{syslog_rotate}}8 rotate {{syslog_rotate}}
@@ -7,7 +12,11 @@
7 delaycompress12 delaycompress
8 compress13 compress
9 postrotate14 postrotate
15 {%- if series <= 'trusty' %}
10 reload rsyslog >/dev/null 2>&1 || true16 reload rsyslog >/dev/null 2>&1 || true
17 {%- else %}
18 invoke-rc.d rsyslog rotate > /dev/null
19 {%- endif %}
11 endscript20 endscript
12}21}
1322
@@ -32,6 +41,10 @@
32 delaycompress41 delaycompress
33 sharedscripts42 sharedscripts
34 postrotate43 postrotate
44 {%- if series <= 'trusty' %}
35 reload rsyslog >/dev/null 2>&1 || true45 reload rsyslog >/dev/null 2>&1 || true
46 {%- else %}
47 invoke-rc.d rsyslog rotate > /dev/null
48 {%- endif %}
36 endscript49 endscript
37}50}
3851
=== modified file 'tox.ini'
--- tox.ini 2016-10-27 15:33:35 +0000
+++ tox.ini 2017-06-16 22:43:26 +0000
@@ -1,6 +1,6 @@
1[tox]1[tox]
2skipsdist=True2skipsdist=True
3envlist = py34, py353envlist = py27, py34, py35
4skip_missing_interpreters = True4skip_missing_interpreters = True
55
6[testenv]6[testenv]
77
=== modified file 'unit_tests/test_hooks.py'
--- unit_tests/test_hooks.py 2017-01-10 17:09:58 +0000
+++ unit_tests/test_hooks.py 2017-06-16 22:43:26 +0000
@@ -1,7 +1,8 @@
1#!/usr/bin/env python1#!/usr/bin/env python
2# -*- coding: utf-8 -*-2# -*- coding: utf-8 -*-
3import os3import os
4import hooks4import six
5import tempfile
56
6__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'7__author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
7_HERE = os.path.abspath(os.path.dirname(__file__))8_HERE = os.path.abspath(os.path.dirname(__file__))
@@ -12,6 +13,7 @@
12except ImportError as ex:13except ImportError as ex:
13 raise ImportError("Please install unittest and mock modules")14 raise ImportError("Please install unittest and mock modules")
1415
16import hooks
1517
16TO_PATCH = [18TO_PATCH = [
17 "apt_install",19 "apt_install",
@@ -28,7 +30,7 @@
28]30]
2931
3032
31class HooksTestCase(unittest.TestCase):33class BaseTestCase(unittest.TestCase):
3234
33 def setUp(self):35 def setUp(self):
34 unittest.TestCase.setUp(self)36 unittest.TestCase.setUp(self)
@@ -37,6 +39,14 @@
37 self.juju_log.return_value = True39 self.juju_log.return_value = True
38 self.apt_install.return_value = True40 self.apt_install.return_value = True
39 self.charm_dir.return_value = os.path.join(_HERE, '..')41 self.charm_dir.return_value = os.path.join(_HERE, '..')
42 self.tmpdir = tempfile.mkdtemp()
43 self.maxDiff = None
44
45 def tearDown(self):
46 try:
47 shutil.rmtree(self.tmpdir)
48 except:
49 pass
4050
41 def patch(self, method):51 def patch(self, method):
42 _m = mock.patch.object(hooks, method)52 _m = mock.patch.object(hooks, method)
@@ -48,6 +58,8 @@
48 for method in TO_PATCH:58 for method in TO_PATCH:
49 setattr(self, method, self.patch(method))59 setattr(self, method, self.patch(method))
5060
61class TestHooks(BaseTestCase):
62
51 def test_install_hook(self):63 def test_install_hook(self):
52 """Check if install hooks is correctly executed64 """Check if install hooks is correctly executed
53 """65 """
@@ -141,8 +153,16 @@
141 def test_config_changed(self):153 def test_config_changed(self):
142 """Check if config-changed is executed correctly"""154 """Check if config-changed is executed correctly"""
143 _open = mock.mock_open(read_data=b'foo')155 _open = mock.mock_open(read_data=b'foo')
144156 lsb = hooks.lsb_release()
145 with mock.patch('builtins.open', _open, create=True):157
158 if six .PY2:
159 open_function = '__builtin__.open'
160 else:
161 open_function = 'builtins.open'
162
163 with mock.patch(open_function, _open, create=True) as mock_open, \
164 mock.patch.object(hooks, 'lsb_release') as mock_lsb:
165 mock_lsb.return_value = lsb
146 hooks.config_changed()166 hooks.config_changed()
147167
148 # I'm not quite sure why config_changed appears to be called twice but168 # I'm not quite sure why config_changed appears to be called twice but
@@ -154,12 +174,15 @@
154 '60-aggregator.conf'), 'rb'),174 '60-aggregator.conf'), 'rb'),
155 mock.call(os.path.join(hooks.DEFAULT_RSYSLOG_PATH,175 mock.call(os.path.join(hooks.DEFAULT_RSYSLOG_PATH,
156 '60-aggregator.conf'), 'w'),176 '60-aggregator.conf'), 'w'),
157 mock.call(os.path.join(hooks.get_template_dir(),177
158 '70-forward.conf'), 'rb'),178 mock.call(os.path.join(hooks.get_template_dir(),
159 mock.call(os.path.join(hooks.get_template_dir(),179 '70-forward.conf'), 'rb'),
160 '70-forward.conf'), 'rb'),180 mock.call(os.path.join(hooks.get_template_dir(),
181 '70-forward.conf'), 'rb'),
182
161 mock.call(os.path.join(hooks.DEFAULT_RSYSLOG_PATH,183 mock.call(os.path.join(hooks.DEFAULT_RSYSLOG_PATH,
162 '70-forward.conf'), 'w'),184 '70-forward.conf'), 'w'),
185
163 mock.call(os.path.join(hooks.get_template_dir(),186 mock.call(os.path.join(hooks.get_template_dir(),
164 'rsyslog.conf'), 'rb'),187 'rsyslog.conf'), 'rb'),
165 mock.call(os.path.join(hooks.get_template_dir(),188 mock.call(os.path.join(hooks.get_template_dir(),
@@ -169,3 +192,38 @@
169192
170 self.assertEquals(sorted(_open.call_args_list), sorted(expected))193 self.assertEquals(sorted(_open.call_args_list), sorted(expected))
171 self.service_restart.assert_called_once_with("rsyslog")194 self.service_restart.assert_called_once_with("rsyslog")
195
196
197class TestSeries(BaseTestCase):
198
199 def setUp(self):
200 super(TestSeries, self).setUp()
201 self._logrotate_path = hooks.DEFAULT_LOGROTATE_PATH
202 self._rsyslog_path = hooks.DEFAULT_RSYSLOG_PATH
203
204 def tearDown(self):
205 hooks.DEFAULT_LOGROTATE_PATH = self._logrotate_path
206 hooks.DEFAULT_RSYSLOG_PATH = self._rsyslog_path
207 super(TestSeries, self).tearDown()
208
209 def test_series(self):
210 rsyslog_config = os.path.join(self.tmpdir, 'rsyslog.conf')
211 hooks.DEFAULT_LOGROTATE_PATH = rsyslog_config
212 hooks.DEFAULT_RSYSLOG_PATH = os.path.join(self.tmpdir)
213 lsb = hooks.lsb_release()
214 with mock.patch.object(hooks, 'lsb_release') as mock_lsb:
215 lsb['DISTRIB_CODENAME'] = 'artful'
216 mock_lsb.return_value = lsb
217 hooks.config_changed()
218
219 with open(rsyslog_config, 'r') as f:
220 content = f.read()
221 self.assertIn('invoke-rc.d rsyslog rotate > /dev/null',
222 content)
223
224 lsb['DISTRIB_CODENAME'] = 'trusty'
225 hooks.config_changed()
226 with open(rsyslog_config, 'r') as f:
227 content = f.read()
228 self.assertIn('reload rsyslog >/dev/null 2>&1 || true',
229 content)

Subscribers

People subscribed via source and target branches