Merge ~smoser/cloud-init:feature/pregen-locale into cloud-init:master

Proposed by Scott Moser on 2017-07-17
Status: Merged
Approved by: Scott Moser on 2017-07-25
Approved revision: 9dd9bb47b48c80ea0b1f4a1bf78e44ee81c14c97
Merged at revision: 0ef61b289472665f4e3059a24a8b9b91246f06ee
Proposed branch: ~smoser/cloud-init:feature/pregen-locale
Merge into: cloud-init:master
Diff against target: 184 lines (+131/-11)
3 files modified
cloudinit/distros/debian.py (+37/-11)
tests/unittests/helpers.py (+12/-0)
tests/unittests/test_distros/test_debian.py (+82/-0)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve on 2017-07-25
Chad Smith 2017-07-17 Approve on 2017-07-25
Review via email: mp+327532@code.launchpad.net

Commit Message

locale: Do not re-run locale-gen if provided locale is system default.

If the system configure default in /etc/default/locale is set to the same
value that is provided for cloud-init's "locale" setting, then do not
re-run locale-gen. This allows images built with a locale already
generated to not re-run locale-gen (which can be very heavy).

Also here is a fix to invoke update-locale correctly and remove the
internal writing of /etc/default/locale. We were calling
  update-locale <locale>
This ends up having no affect. The more correct invocation is:
  update-locale LANG=<locale>

Also added some support here should we ever want to change setting
LANG to setting LC_ALL (or any other key).

Lastly, a test change to allow us to use assert_not_called from mock.
Versions of mock in CentOS 6 do not have assert_not_called.

To post a comment you must log in.
Scott Moser (smoser) wrote :

This is a update to
 https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/325406
with the suggestions I had to that MP fixed
(added unit tests)

FAILED: Continuous integration, rev:9a607ecd6efecd189a5dd3dd1b50aece02b6d281
https://jenkins.ubuntu.com/server/job/cloud-init-ci/42/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/42/rebuild

review: Needs Fixing (continuous-integration)

PASSED: Continuous integration, rev:2a44d6f5d36db7c322891f590752e38069b75b08
https://jenkins.ubuntu.com/server/job/cloud-init-ci/47/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/47/rebuild

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:724ef696e776e6937b8e75a41da35607bede7a6a
https://jenkins.ubuntu.com/server/job/cloud-init-ci/50/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/50/rebuild

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:8b220d36d04538e37de3e7a188dffce3ac67c55d
https://jenkins.ubuntu.com/server/job/cloud-init-ci/56/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/56/rebuild

review: Approve (continuous-integration)
Chad Smith (chad.smith) :
Chad Smith (chad.smith) :
Scott Moser (smoser) wrote :

chad thanks for review. i'll get these fixes.

Ryan Harper (raharper) :
Scott Moser (smoser) :

FAILED: Continuous integration, rev:8faec3079c58a4db6cbac5cf3ecd8e319fe5509b
https://jenkins.ubuntu.com/server/job/cloud-init-ci/73/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/73/rebuild

review: Needs Fixing (continuous-integration)

FAILED: Continuous integration, rev:2c7f87273d3f0ada0a22984067dae9e3ea6958e7
https://jenkins.ubuntu.com/server/job/cloud-init-ci/74/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/74/rebuild

review: Needs Fixing (continuous-integration)

PASSED: Continuous integration, rev:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/75/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/75/rebuild

review: Approve (continuous-integration)

PASSED: Continuous integration, rev:9dd9bb47b48c80ea0b1f4a1bf78e44ee81c14c97
https://jenkins.ubuntu.com/server/job/cloud-init-ci/77/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/77/rebuild

review: Approve (continuous-integration)
Scott Moser (smoser) wrote :

please re-review. i think i've addressed all comments.

Chad Smith (chad.smith) wrote :

just tested on a live system and the work looks good. Thanks +1

review: Approve

PASSED: Continuous integration, rev:0d69e5a67889edae66be84ce66a173bbdcdd01c2
https://jenkins.ubuntu.com/server/job/cloud-init-ci/93/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: CentOS 6 & 7: Build & Test
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/93/rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py
2index d06d46a..abfb81f 100644
3--- a/cloudinit/distros/debian.py
4+++ b/cloudinit/distros/debian.py
5@@ -37,11 +37,11 @@ ENI_HEADER = """# This file is generated from information provided by
6 """
7
8 NETWORK_CONF_FN = "/etc/network/interfaces.d/50-cloud-init.cfg"
9+LOCALE_CONF_FN = "/etc/default/locale"
10
11
12 class Distro(distros.Distro):
13 hostname_conf_fn = "/etc/hostname"
14- locale_conf_fn = "/etc/default/locale"
15 network_conf_fn = {
16 "eni": "/etc/network/interfaces.d/50-cloud-init.cfg",
17 "netplan": "/etc/netplan/50-cloud-init.yaml"
18@@ -64,16 +64,8 @@ class Distro(distros.Distro):
19
20 def apply_locale(self, locale, out_fn=None):
21 if not out_fn:
22- out_fn = self.locale_conf_fn
23- util.subp(['locale-gen', locale], capture=False)
24- util.subp(['update-locale', locale], capture=False)
25- # "" provides trailing newline during join
26- lines = [
27- util.make_header(),
28- 'LANG="%s"' % (locale),
29- "",
30- ]
31- util.write_file(out_fn, "\n".join(lines))
32+ out_fn = LOCALE_CONF_FN
33+ apply_locale(locale, out_fn)
34
35 def install_packages(self, pkglist):
36 self.update_package_sources()
37@@ -225,4 +217,38 @@ def _maybe_remove_legacy_eth0(path="/etc/network/interfaces.d/eth0.cfg"):
38
39 LOG.warning(msg)
40
41+
42+def apply_locale(locale, sys_path=LOCALE_CONF_FN, keyname='LANG'):
43+ """Apply the locale.
44+
45+ Run locale-gen for the provided locale and set the default
46+ system variable `keyname` appropriately in the provided `sys_path`.
47+
48+ If sys_path indicates that `keyname` is already set to `locale`
49+ then no changes will be made and locale-gen not called.
50+ This allows images built with a locale already generated to not re-run
51+ locale-gen which can be very heavy.
52+ """
53+ if not locale:
54+ raise ValueError('Failed to provide locale value.')
55+
56+ if not sys_path:
57+ raise ValueError('Invalid path: %s' % sys_path)
58+
59+ if os.path.exists(sys_path):
60+ locale_content = util.load_file(sys_path)
61+ # if LANG isn't present, regen
62+ sys_defaults = util.load_shell_content(locale_content)
63+ sys_val = sys_defaults.get(keyname, "")
64+ if sys_val.lower() == locale.lower():
65+ LOG.debug(
66+ "System has '%s=%s' requested '%s', skipping regeneration.",
67+ keyname, sys_val, locale)
68+ return
69+
70+ util.subp(['locale-gen', locale], capture=False)
71+ util.subp(
72+ ['update-locale', '--locale-file=' + sys_path,
73+ '%s=%s' % (keyname, locale)], capture=False)
74+
75 # vi: ts=4 expandtab
76diff --git a/tests/unittests/helpers.py b/tests/unittests/helpers.py
77index 6691cf8..08c5c46 100644
78--- a/tests/unittests/helpers.py
79+++ b/tests/unittests/helpers.py
80@@ -376,4 +376,16 @@ except AttributeError:
81 return wrapper
82 return decorator
83
84+
85+# older versions of mock do not have the useful 'assert_not_called'
86+if not hasattr(mock.Mock, 'assert_not_called'):
87+ def __mock_assert_not_called(mmock):
88+ if mmock.call_count != 0:
89+ msg = ("[citest] Expected '%s' to not have been called. "
90+ "Called %s times." %
91+ (mmock._mock_name or 'mock', mmock.call_count))
92+ raise AssertionError(msg)
93+ mock.Mock.assert_not_called = __mock_assert_not_called
94+
95+
96 # vi: ts=4 expandtab
97diff --git a/tests/unittests/test_distros/test_debian.py b/tests/unittests/test_distros/test_debian.py
98new file mode 100644
99index 0000000..2330ad5
100--- /dev/null
101+++ b/tests/unittests/test_distros/test_debian.py
102@@ -0,0 +1,82 @@
103+# This file is part of cloud-init. See LICENSE file for license information.
104+
105+from ..helpers import (CiTestCase, mock)
106+
107+from cloudinit.distros.debian import apply_locale
108+from cloudinit import util
109+
110+
111+@mock.patch("cloudinit.distros.debian.util.subp")
112+class TestDebianApplyLocale(CiTestCase):
113+ def test_no_rerun(self, m_subp):
114+ """If system has defined locale, no re-run is expected."""
115+ spath = self.tmp_path("default-locale")
116+ m_subp.return_value = (None, None)
117+ locale = 'en_US.UTF-8'
118+ util.write_file(spath, 'LANG=%s\n' % locale, omode="w")
119+ apply_locale(locale, sys_path=spath)
120+ m_subp.assert_not_called()
121+
122+ def test_rerun_if_different(self, m_subp):
123+ """If system has different locale, locale-gen should be called."""
124+ spath = self.tmp_path("default-locale")
125+ m_subp.return_value = (None, None)
126+ locale = 'en_US.UTF-8'
127+ util.write_file(spath, 'LANG=fr_FR.UTF-8', omode="w")
128+ apply_locale(locale, sys_path=spath)
129+ self.assertEqual(
130+ [['locale-gen', locale],
131+ ['update-locale', '--locale-file=' + spath, 'LANG=%s' % locale]],
132+ [p[0][0] for p in m_subp.call_args_list])
133+
134+ def test_rerun_if_no_file(self, m_subp):
135+ """If system has no locale file, locale-gen should be called."""
136+ spath = self.tmp_path("default-locale")
137+ m_subp.return_value = (None, None)
138+ locale = 'en_US.UTF-8'
139+ apply_locale(locale, sys_path=spath)
140+ self.assertEqual(
141+ [['locale-gen', locale],
142+ ['update-locale', '--locale-file=' + spath, 'LANG=%s' % locale]],
143+ [p[0][0] for p in m_subp.call_args_list])
144+
145+ def test_rerun_on_unset_system_locale(self, m_subp):
146+ """If system has unset locale, locale-gen should be called."""
147+ m_subp.return_value = (None, None)
148+ spath = self.tmp_path("default-locale")
149+ locale = 'en_US.UTF-8'
150+ util.write_file(spath, 'LANG=', omode="w")
151+ apply_locale(locale, sys_path=spath)
152+ self.assertEqual(
153+ [['locale-gen', locale],
154+ ['update-locale', '--locale-file=' + spath, 'LANG=%s' % locale]],
155+ [p[0][0] for p in m_subp.call_args_list])
156+
157+ def test_rerun_on_mismatched_keys(self, m_subp):
158+ """If key is LC_ALL and system has only LANG, rerun is expected."""
159+ m_subp.return_value = (None, None)
160+ spath = self.tmp_path("default-locale")
161+ locale = 'en_US.UTF-8'
162+ util.write_file(spath, 'LANG=', omode="w")
163+ apply_locale(locale, sys_path=spath, keyname='LC_ALL')
164+ self.assertEqual(
165+ [['locale-gen', locale],
166+ ['update-locale', '--locale-file=' + spath,
167+ 'LC_ALL=%s' % locale]],
168+ [p[0][0] for p in m_subp.call_args_list])
169+
170+ def test_falseish_locale_raises_valueerror(self, m_subp):
171+ """locale as None or "" is invalid and should raise ValueError."""
172+
173+ with self.assertRaises(ValueError) as ctext_m:
174+ apply_locale(None)
175+ m_subp.assert_not_called()
176+
177+ self.assertEqual(
178+ 'Failed to provide locale value.', str(ctext_m.exception))
179+
180+ with self.assertRaises(ValueError) as ctext_m:
181+ apply_locale("")
182+ m_subp.assert_not_called()
183+ self.assertEqual(
184+ 'Failed to provide locale value.', str(ctext_m.exception))

Subscribers

People subscribed via source and target branches

to all changes: