Merge ~chad.smith/cloud-init:chef_omnibus_version into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Chad Smith
Approved revision: accd83c97fa3b3b928af2a7955febad410471fc2
Merged at revision: cf10a2ff2e2f666d9370f38297a5a105e809ea3c
Proposed branch: ~chad.smith/cloud-init:chef_omnibus_version
Merge into: cloud-init:master
Diff against target: 266 lines (+138/-24)
4 files modified
cloudinit/config/cc_chef.py (+33/-12)
cloudinit/util.py (+25/-0)
doc/examples/cloud-config-chef.txt (+4/-0)
tests/unittests/test_handler/test_handler_chef.py (+76/-12)
Reviewer Review Type Date Requested Status
Chad Smith Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+330712@code.launchpad.net

Description of the change

Add option to pin chef omnibus install version

Most users of chef will want to pin the version that is installed.
Typically new versions of chef have to be evaluated for breakage etc.

This change proposes a new optional `omnibus_version` field to the
chef configuration. This changes also adds an reference to the new
field to the chef example.

LP: #1462693

To post a comment you must log in.
Revision history for this message
Chad Smith (chad.smith) wrote :

Pulled the original content from https://code.launchpad.net/~papodaca/cloud-init/+git/cloud-init/+merge/328943 and addresses review comments as well as added unit tests.

6cfd206... by Chad Smith

tempdir moved to temp_utils in master. fix per rebase

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
f74d44e... by Chad Smith

rework unit tests to handle py2 py3 string/bytes differences and mock readurl to avoid test_install_chef_from_omnibus_retries_url to avoid executing time-consuming retries inside readurl

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

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

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

review: Needs Fixing (continuous-integration)
accd83c... by Chad Smith

lint param name is now omnibus_version

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:accd83c97fa3b3b928af2a7955febad410471fc2
https://jenkins.ubuntu.com/server/job/cloud-init-ci/295/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

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

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
2index c192dd3..46abedd 100644
3--- a/cloudinit/config/cc_chef.py
4+++ b/cloudinit/config/cc_chef.py
5@@ -58,6 +58,9 @@ file).
6 log_level:
7 log_location:
8 node_name:
9+ omnibus_url:
10+ omnibus_url_retries:
11+ omnibus_version:
12 pid_file:
13 server_url:
14 show_time:
15@@ -71,7 +74,6 @@ import itertools
16 import json
17 import os
18
19-from cloudinit import temp_utils
20 from cloudinit import templater
21 from cloudinit import url_helper
22 from cloudinit import util
23@@ -280,6 +282,31 @@ def run_chef(chef_cfg, log):
24 util.subp(cmd, capture=False)
25
26
27+def install_chef_from_omnibus(url=None, retries=None, omnibus_version=None):
28+ """Install an omnibus unified package from url.
29+
30+ @param url: URL where blob of chef content may be downloaded. Defaults to
31+ OMNIBUS_URL.
32+ @param retries: Number of retries to perform when attempting to read url.
33+ Defaults to OMNIBUS_URL_RETRIES
34+ @param omnibus_version: Optional version string to require for omnibus
35+ install.
36+ """
37+ if url is None:
38+ url = OMNIBUS_URL
39+ if retries is None:
40+ retries = OMNIBUS_URL_RETRIES
41+
42+ if omnibus_version is None:
43+ args = []
44+ else:
45+ args = ['-v', omnibus_version]
46+ content = url_helper.readurl(url=url, retries=retries).contents
47+ return util.subp_blob_in_tempfile(
48+ blob=content, args=args,
49+ basename='chef-omnibus-install', capture=False)
50+
51+
52 def install_chef(cloud, chef_cfg, log):
53 # If chef is not installed, we install chef based on 'install_type'
54 install_type = util.get_cfg_option_str(chef_cfg, 'install_type',
55@@ -298,17 +325,11 @@ def install_chef(cloud, chef_cfg, log):
56 # This will install and run the chef-client from packages
57 cloud.distro.install_packages(('chef',))
58 elif install_type == 'omnibus':
59- # This will install as a omnibus unified package
60- url = util.get_cfg_option_str(chef_cfg, "omnibus_url", OMNIBUS_URL)
61- retries = max(0, util.get_cfg_option_int(chef_cfg,
62- "omnibus_url_retries",
63- default=OMNIBUS_URL_RETRIES))
64- content = url_helper.readurl(url=url, retries=retries).contents
65- with temp_utils.tempdir() as tmpd:
66- # Use tmpdir over tmpfile to avoid 'text file busy' on execute
67- tmpf = "%s/chef-omnibus-install" % tmpd
68- util.write_file(tmpf, content, mode=0o700)
69- util.subp([tmpf], capture=False)
70+ omnibus_version = util.get_cfg_option_str(chef_cfg, "omnibus_version")
71+ install_chef_from_omnibus(
72+ url=util.get_cfg_option_str(chef_cfg, "omnibus_url"),
73+ retries=util.get_cfg_option_int(chef_cfg, "omnibus_url_retries"),
74+ omnibus_version=omnibus_version)
75 else:
76 log.warn("Unknown chef install type '%s'", install_type)
77 run = False
78diff --git a/cloudinit/util.py b/cloudinit/util.py
79index ae5cda8..7e9d94f 100644
80--- a/cloudinit/util.py
81+++ b/cloudinit/util.py
82@@ -1742,6 +1742,31 @@ def delete_dir_contents(dirname):
83 del_file(node_fullpath)
84
85
86+def subp_blob_in_tempfile(blob, *args, **kwargs):
87+ """Write blob to a tempfile, and call subp with args, kwargs. Then cleanup.
88+
89+ 'basename' as a kwarg allows providing the basename for the file.
90+ The 'args' argument to subp will be updated with the full path to the
91+ filename as the first argument.
92+ """
93+ basename = kwargs.pop('basename', "subp_blob")
94+
95+ if len(args) == 0 and 'args' not in kwargs:
96+ args = [tuple()]
97+
98+ # Use tmpdir over tmpfile to avoid 'text file busy' on execute
99+ with temp_utils.tempdir() as tmpd:
100+ tmpf = os.path.join(tmpd, basename)
101+ if 'args' in kwargs:
102+ kwargs['args'] = [tmpf] + list(kwargs['args'])
103+ else:
104+ args = list(args)
105+ args[0] = [tmpf] + args[0]
106+
107+ write_file(tmpf, blob, mode=0o700)
108+ return subp(*args, **kwargs)
109+
110+
111 def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
112 logstring=False, decode="replace", target=None, update_env=None):
113
114diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt
115index 9d23581..58d5fdc 100644
116--- a/doc/examples/cloud-config-chef.txt
117+++ b/doc/examples/cloud-config-chef.txt
118@@ -94,6 +94,10 @@ chef:
119 # if install_type is 'omnibus', change the url to download
120 omnibus_url: "https://www.chef.io/chef/install.sh"
121
122+ # if install_type is 'omnibus', pass pinned version string
123+ # to the install script
124+ omnibus_version: "12.3.0"
125+
126
127 # Capture all subprocess output into a logfile
128 # Useful for troubleshooting cloud-init issues
129diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/test_handler/test_handler_chef.py
130index e5785cf..0136a93 100644
131--- a/tests/unittests/test_handler/test_handler_chef.py
132+++ b/tests/unittests/test_handler/test_handler_chef.py
133@@ -1,11 +1,10 @@
134 # This file is part of cloud-init. See LICENSE file for license information.
135
136+import httpretty
137 import json
138 import logging
139 import os
140-import shutil
141 import six
142-import tempfile
143
144 from cloudinit import cloud
145 from cloudinit.config import cc_chef
146@@ -14,18 +13,83 @@ from cloudinit import helpers
147 from cloudinit.sources import DataSourceNone
148 from cloudinit import util
149
150-from cloudinit.tests import helpers as t_help
151+from cloudinit.tests.helpers import (
152+ CiTestCase, FilesystemMockingTestCase, mock, skipIf)
153
154 LOG = logging.getLogger(__name__)
155
156 CLIENT_TEMPL = os.path.sep.join(["templates", "chef_client.rb.tmpl"])
157
158
159-class TestChef(t_help.FilesystemMockingTestCase):
160+class TestInstallChefOmnibus(CiTestCase):
161+
162+ def setUp(self):
163+ self.new_root = self.tmp_dir()
164+
165+ @httpretty.activate
166+ def test_install_chef_from_omnibus_runs_chef_url_content(self):
167+ """install_chef_from_omnibus runs downloaded OMNIBUS_URL as script."""
168+ chef_outfile = self.tmp_path('chef.out', self.new_root)
169+ response = '#!/bin/bash\necho "Hi Mom" > {0}'.format(chef_outfile)
170+ httpretty.register_uri(
171+ httpretty.GET, cc_chef.OMNIBUS_URL, body=response, status=200)
172+ cc_chef.install_chef_from_omnibus()
173+ self.assertEqual('Hi Mom\n', util.load_file(chef_outfile))
174+
175+ @mock.patch('cloudinit.config.cc_chef.url_helper.readurl')
176+ @mock.patch('cloudinit.config.cc_chef.util.subp_blob_in_tempfile')
177+ def test_install_chef_from_omnibus_retries_url(self, m_subp_blob, m_rdurl):
178+ """install_chef_from_omnibus retries OMNIBUS_URL upon failure."""
179+
180+ class FakeURLResponse(object):
181+ contents = '#!/bin/bash\necho "Hi Mom" > {0}/chef.out'.format(
182+ self.new_root)
183+
184+ m_rdurl.return_value = FakeURLResponse()
185+
186+ cc_chef.install_chef_from_omnibus()
187+ expected_kwargs = {'retries': cc_chef.OMNIBUS_URL_RETRIES,
188+ 'url': cc_chef.OMNIBUS_URL}
189+ self.assertItemsEqual(expected_kwargs, m_rdurl.call_args_list[0][1])
190+ cc_chef.install_chef_from_omnibus(retries=10)
191+ expected_kwargs = {'retries': 10,
192+ 'url': cc_chef.OMNIBUS_URL}
193+ self.assertItemsEqual(expected_kwargs, m_rdurl.call_args_list[1][1])
194+ expected_subp_kwargs = {
195+ 'args': ['-v', '2.0'],
196+ 'basename': 'chef-omnibus-install',
197+ 'blob': m_rdurl.return_value.contents,
198+ 'capture': False
199+ }
200+ self.assertItemsEqual(
201+ expected_subp_kwargs,
202+ m_subp_blob.call_args_list[0][1])
203+
204+ @httpretty.activate
205+ @mock.patch('cloudinit.config.cc_chef.util.subp_blob_in_tempfile')
206+ def test_install_chef_from_omnibus_has_omnibus_version(self, m_subp_blob):
207+ """install_chef_from_omnibus provides version arg to OMNIBUS_URL."""
208+ chef_outfile = self.tmp_path('chef.out', self.new_root)
209+ response = '#!/bin/bash\necho "Hi Mom" > {0}'.format(chef_outfile)
210+ httpretty.register_uri(
211+ httpretty.GET, cc_chef.OMNIBUS_URL, body=response)
212+ cc_chef.install_chef_from_omnibus(omnibus_version='2.0')
213+
214+ called_kwargs = m_subp_blob.call_args_list[0][1]
215+ expected_kwargs = {
216+ 'args': ['-v', '2.0'],
217+ 'basename': 'chef-omnibus-install',
218+ 'blob': response,
219+ 'capture': False
220+ }
221+ self.assertItemsEqual(expected_kwargs, called_kwargs)
222+
223+
224+class TestChef(FilesystemMockingTestCase):
225+
226 def setUp(self):
227 super(TestChef, self).setUp()
228- self.tmp = tempfile.mkdtemp()
229- self.addCleanup(shutil.rmtree, self.tmp)
230+ self.tmp = self.tmp_dir()
231
232 def fetch_cloud(self, distro_kind):
233 cls = distros.fetch(distro_kind)
234@@ -43,8 +107,8 @@ class TestChef(t_help.FilesystemMockingTestCase):
235 for d in cc_chef.CHEF_DIRS:
236 self.assertFalse(os.path.isdir(d))
237
238- @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL),
239- CLIENT_TEMPL + " is not available")
240+ @skipIf(not os.path.isfile(CLIENT_TEMPL),
241+ CLIENT_TEMPL + " is not available")
242 def test_basic_config(self):
243 """
244 test basic config looks sane
245@@ -122,8 +186,8 @@ class TestChef(t_help.FilesystemMockingTestCase):
246 'c': 'd',
247 }, json.loads(c))
248
249- @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL),
250- CLIENT_TEMPL + " is not available")
251+ @skipIf(not os.path.isfile(CLIENT_TEMPL),
252+ CLIENT_TEMPL + " is not available")
253 def test_template_deletes(self):
254 tpl_file = util.load_file('templates/chef_client.rb.tmpl')
255 self.patchUtils(self.tmp)
256@@ -143,8 +207,8 @@ class TestChef(t_help.FilesystemMockingTestCase):
257 self.assertNotIn('json_attribs', c)
258 self.assertNotIn('Formatter.show_time', c)
259
260- @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL),
261- CLIENT_TEMPL + " is not available")
262+ @skipIf(not os.path.isfile(CLIENT_TEMPL),
263+ CLIENT_TEMPL + " is not available")
264 def test_validation_cert_and_validation_key(self):
265 # test validation_cert content is written to validation_key path
266 tpl_file = util.load_file('templates/chef_client.rb.tmpl')

Subscribers

People subscribed via source and target branches