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
diff --git a/cloudinit/config/cc_chef.py b/cloudinit/config/cc_chef.py
index c192dd3..46abedd 100644
--- a/cloudinit/config/cc_chef.py
+++ b/cloudinit/config/cc_chef.py
@@ -58,6 +58,9 @@ file).
58 log_level:58 log_level:
59 log_location:59 log_location:
60 node_name:60 node_name:
61 omnibus_url:
62 omnibus_url_retries:
63 omnibus_version:
61 pid_file:64 pid_file:
62 server_url:65 server_url:
63 show_time:66 show_time:
@@ -71,7 +74,6 @@ import itertools
71import json74import json
72import os75import os
7376
74from cloudinit import temp_utils
75from cloudinit import templater77from cloudinit import templater
76from cloudinit import url_helper78from cloudinit import url_helper
77from cloudinit import util79from cloudinit import util
@@ -280,6 +282,31 @@ def run_chef(chef_cfg, log):
280 util.subp(cmd, capture=False)282 util.subp(cmd, capture=False)
281283
282284
285def install_chef_from_omnibus(url=None, retries=None, omnibus_version=None):
286 """Install an omnibus unified package from url.
287
288 @param url: URL where blob of chef content may be downloaded. Defaults to
289 OMNIBUS_URL.
290 @param retries: Number of retries to perform when attempting to read url.
291 Defaults to OMNIBUS_URL_RETRIES
292 @param omnibus_version: Optional version string to require for omnibus
293 install.
294 """
295 if url is None:
296 url = OMNIBUS_URL
297 if retries is None:
298 retries = OMNIBUS_URL_RETRIES
299
300 if omnibus_version is None:
301 args = []
302 else:
303 args = ['-v', omnibus_version]
304 content = url_helper.readurl(url=url, retries=retries).contents
305 return util.subp_blob_in_tempfile(
306 blob=content, args=args,
307 basename='chef-omnibus-install', capture=False)
308
309
283def install_chef(cloud, chef_cfg, log):310def install_chef(cloud, chef_cfg, log):
284 # If chef is not installed, we install chef based on 'install_type'311 # If chef is not installed, we install chef based on 'install_type'
285 install_type = util.get_cfg_option_str(chef_cfg, 'install_type',312 install_type = util.get_cfg_option_str(chef_cfg, 'install_type',
@@ -298,17 +325,11 @@ def install_chef(cloud, chef_cfg, log):
298 # This will install and run the chef-client from packages325 # This will install and run the chef-client from packages
299 cloud.distro.install_packages(('chef',))326 cloud.distro.install_packages(('chef',))
300 elif install_type == 'omnibus':327 elif install_type == 'omnibus':
301 # This will install as a omnibus unified package328 omnibus_version = util.get_cfg_option_str(chef_cfg, "omnibus_version")
302 url = util.get_cfg_option_str(chef_cfg, "omnibus_url", OMNIBUS_URL)329 install_chef_from_omnibus(
303 retries = max(0, util.get_cfg_option_int(chef_cfg,330 url=util.get_cfg_option_str(chef_cfg, "omnibus_url"),
304 "omnibus_url_retries",331 retries=util.get_cfg_option_int(chef_cfg, "omnibus_url_retries"),
305 default=OMNIBUS_URL_RETRIES))332 omnibus_version=omnibus_version)
306 content = url_helper.readurl(url=url, retries=retries).contents
307 with temp_utils.tempdir() as tmpd:
308 # Use tmpdir over tmpfile to avoid 'text file busy' on execute
309 tmpf = "%s/chef-omnibus-install" % tmpd
310 util.write_file(tmpf, content, mode=0o700)
311 util.subp([tmpf], capture=False)
312 else:333 else:
313 log.warn("Unknown chef install type '%s'", install_type)334 log.warn("Unknown chef install type '%s'", install_type)
314 run = False335 run = False
diff --git a/cloudinit/util.py b/cloudinit/util.py
index ae5cda8..7e9d94f 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1742,6 +1742,31 @@ def delete_dir_contents(dirname):
1742 del_file(node_fullpath)1742 del_file(node_fullpath)
17431743
17441744
1745def subp_blob_in_tempfile(blob, *args, **kwargs):
1746 """Write blob to a tempfile, and call subp with args, kwargs. Then cleanup.
1747
1748 'basename' as a kwarg allows providing the basename for the file.
1749 The 'args' argument to subp will be updated with the full path to the
1750 filename as the first argument.
1751 """
1752 basename = kwargs.pop('basename', "subp_blob")
1753
1754 if len(args) == 0 and 'args' not in kwargs:
1755 args = [tuple()]
1756
1757 # Use tmpdir over tmpfile to avoid 'text file busy' on execute
1758 with temp_utils.tempdir() as tmpd:
1759 tmpf = os.path.join(tmpd, basename)
1760 if 'args' in kwargs:
1761 kwargs['args'] = [tmpf] + list(kwargs['args'])
1762 else:
1763 args = list(args)
1764 args[0] = [tmpf] + args[0]
1765
1766 write_file(tmpf, blob, mode=0o700)
1767 return subp(*args, **kwargs)
1768
1769
1745def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,1770def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1746 logstring=False, decode="replace", target=None, update_env=None):1771 logstring=False, decode="replace", target=None, update_env=None):
17471772
diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt
index 9d23581..58d5fdc 100644
--- a/doc/examples/cloud-config-chef.txt
+++ b/doc/examples/cloud-config-chef.txt
@@ -94,6 +94,10 @@ chef:
94 # if install_type is 'omnibus', change the url to download94 # if install_type is 'omnibus', change the url to download
95 omnibus_url: "https://www.chef.io/chef/install.sh"95 omnibus_url: "https://www.chef.io/chef/install.sh"
9696
97 # if install_type is 'omnibus', pass pinned version string
98 # to the install script
99 omnibus_version: "12.3.0"
100
97101
98# Capture all subprocess output into a logfile102# Capture all subprocess output into a logfile
99# Useful for troubleshooting cloud-init issues103# Useful for troubleshooting cloud-init issues
diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/test_handler/test_handler_chef.py
index e5785cf..0136a93 100644
--- a/tests/unittests/test_handler/test_handler_chef.py
+++ b/tests/unittests/test_handler/test_handler_chef.py
@@ -1,11 +1,10 @@
1# This file is part of cloud-init. See LICENSE file for license information.1# This file is part of cloud-init. See LICENSE file for license information.
22
3import httpretty
3import json4import json
4import logging5import logging
5import os6import os
6import shutil
7import six7import six
8import tempfile
98
10from cloudinit import cloud9from cloudinit import cloud
11from cloudinit.config import cc_chef10from cloudinit.config import cc_chef
@@ -14,18 +13,83 @@ from cloudinit import helpers
14from cloudinit.sources import DataSourceNone13from cloudinit.sources import DataSourceNone
15from cloudinit import util14from cloudinit import util
1615
17from cloudinit.tests import helpers as t_help16from cloudinit.tests.helpers import (
17 CiTestCase, FilesystemMockingTestCase, mock, skipIf)
1818
19LOG = logging.getLogger(__name__)19LOG = logging.getLogger(__name__)
2020
21CLIENT_TEMPL = os.path.sep.join(["templates", "chef_client.rb.tmpl"])21CLIENT_TEMPL = os.path.sep.join(["templates", "chef_client.rb.tmpl"])
2222
2323
24class TestChef(t_help.FilesystemMockingTestCase):24class TestInstallChefOmnibus(CiTestCase):
25
26 def setUp(self):
27 self.new_root = self.tmp_dir()
28
29 @httpretty.activate
30 def test_install_chef_from_omnibus_runs_chef_url_content(self):
31 """install_chef_from_omnibus runs downloaded OMNIBUS_URL as script."""
32 chef_outfile = self.tmp_path('chef.out', self.new_root)
33 response = '#!/bin/bash\necho "Hi Mom" > {0}'.format(chef_outfile)
34 httpretty.register_uri(
35 httpretty.GET, cc_chef.OMNIBUS_URL, body=response, status=200)
36 cc_chef.install_chef_from_omnibus()
37 self.assertEqual('Hi Mom\n', util.load_file(chef_outfile))
38
39 @mock.patch('cloudinit.config.cc_chef.url_helper.readurl')
40 @mock.patch('cloudinit.config.cc_chef.util.subp_blob_in_tempfile')
41 def test_install_chef_from_omnibus_retries_url(self, m_subp_blob, m_rdurl):
42 """install_chef_from_omnibus retries OMNIBUS_URL upon failure."""
43
44 class FakeURLResponse(object):
45 contents = '#!/bin/bash\necho "Hi Mom" > {0}/chef.out'.format(
46 self.new_root)
47
48 m_rdurl.return_value = FakeURLResponse()
49
50 cc_chef.install_chef_from_omnibus()
51 expected_kwargs = {'retries': cc_chef.OMNIBUS_URL_RETRIES,
52 'url': cc_chef.OMNIBUS_URL}
53 self.assertItemsEqual(expected_kwargs, m_rdurl.call_args_list[0][1])
54 cc_chef.install_chef_from_omnibus(retries=10)
55 expected_kwargs = {'retries': 10,
56 'url': cc_chef.OMNIBUS_URL}
57 self.assertItemsEqual(expected_kwargs, m_rdurl.call_args_list[1][1])
58 expected_subp_kwargs = {
59 'args': ['-v', '2.0'],
60 'basename': 'chef-omnibus-install',
61 'blob': m_rdurl.return_value.contents,
62 'capture': False
63 }
64 self.assertItemsEqual(
65 expected_subp_kwargs,
66 m_subp_blob.call_args_list[0][1])
67
68 @httpretty.activate
69 @mock.patch('cloudinit.config.cc_chef.util.subp_blob_in_tempfile')
70 def test_install_chef_from_omnibus_has_omnibus_version(self, m_subp_blob):
71 """install_chef_from_omnibus provides version arg to OMNIBUS_URL."""
72 chef_outfile = self.tmp_path('chef.out', self.new_root)
73 response = '#!/bin/bash\necho "Hi Mom" > {0}'.format(chef_outfile)
74 httpretty.register_uri(
75 httpretty.GET, cc_chef.OMNIBUS_URL, body=response)
76 cc_chef.install_chef_from_omnibus(omnibus_version='2.0')
77
78 called_kwargs = m_subp_blob.call_args_list[0][1]
79 expected_kwargs = {
80 'args': ['-v', '2.0'],
81 'basename': 'chef-omnibus-install',
82 'blob': response,
83 'capture': False
84 }
85 self.assertItemsEqual(expected_kwargs, called_kwargs)
86
87
88class TestChef(FilesystemMockingTestCase):
89
25 def setUp(self):90 def setUp(self):
26 super(TestChef, self).setUp()91 super(TestChef, self).setUp()
27 self.tmp = tempfile.mkdtemp()92 self.tmp = self.tmp_dir()
28 self.addCleanup(shutil.rmtree, self.tmp)
2993
30 def fetch_cloud(self, distro_kind):94 def fetch_cloud(self, distro_kind):
31 cls = distros.fetch(distro_kind)95 cls = distros.fetch(distro_kind)
@@ -43,8 +107,8 @@ class TestChef(t_help.FilesystemMockingTestCase):
43 for d in cc_chef.CHEF_DIRS:107 for d in cc_chef.CHEF_DIRS:
44 self.assertFalse(os.path.isdir(d))108 self.assertFalse(os.path.isdir(d))
45109
46 @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL),110 @skipIf(not os.path.isfile(CLIENT_TEMPL),
47 CLIENT_TEMPL + " is not available")111 CLIENT_TEMPL + " is not available")
48 def test_basic_config(self):112 def test_basic_config(self):
49 """113 """
50 test basic config looks sane114 test basic config looks sane
@@ -122,8 +186,8 @@ class TestChef(t_help.FilesystemMockingTestCase):
122 'c': 'd',186 'c': 'd',
123 }, json.loads(c))187 }, json.loads(c))
124188
125 @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL),189 @skipIf(not os.path.isfile(CLIENT_TEMPL),
126 CLIENT_TEMPL + " is not available")190 CLIENT_TEMPL + " is not available")
127 def test_template_deletes(self):191 def test_template_deletes(self):
128 tpl_file = util.load_file('templates/chef_client.rb.tmpl')192 tpl_file = util.load_file('templates/chef_client.rb.tmpl')
129 self.patchUtils(self.tmp)193 self.patchUtils(self.tmp)
@@ -143,8 +207,8 @@ class TestChef(t_help.FilesystemMockingTestCase):
143 self.assertNotIn('json_attribs', c)207 self.assertNotIn('json_attribs', c)
144 self.assertNotIn('Formatter.show_time', c)208 self.assertNotIn('Formatter.show_time', c)
145209
146 @t_help.skipIf(not os.path.isfile(CLIENT_TEMPL),210 @skipIf(not os.path.isfile(CLIENT_TEMPL),
147 CLIENT_TEMPL + " is not available")211 CLIENT_TEMPL + " is not available")
148 def test_validation_cert_and_validation_key(self):212 def test_validation_cert_and_validation_key(self):
149 # test validation_cert content is written to validation_key path213 # test validation_cert content is written to validation_key path
150 tpl_file = util.load_file('templates/chef_client.rb.tmpl')214 tpl_file = util.load_file('templates/chef_client.rb.tmpl')

Subscribers

People subscribed via source and target branches