From bccc7d5215d7ad734023853d6de8ee1e94c84a63 Mon Sep 17 00:00:00 2001 From: Claudiu Popa Date: Tue, 30 Jun 2015 21:30:43 +0300 Subject: [PATCH] Check the latest version of cloudbaseinit We're hitting an API with the current version and we expect to receive a response if the current version is not the latest. This helps the user to see that a new version of cloudbaseinit is available, which is a big improvement than going to the github page to see the latest version. Change-Id: Ibfb721973c84474c8fef8e1989dfb7566938134f --- cloudbaseinit/init.py | 14 +++++ cloudbaseinit/tests/test_init.py | 24 ++++++++- cloudbaseinit/tests/test_version.py | 84 ++++++++++++++++++++++++++++- cloudbaseinit/version.py | 47 ++++++++++++++++ requirements.txt | 1 + 5 files changed, 167 insertions(+), 3 deletions(-) diff --git a/cloudbaseinit/init.py b/cloudbaseinit/init.py index e2ea788a..8b3c55c3 100644 --- a/cloudbaseinit/init.py +++ b/cloudbaseinit/init.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import sys from oslo.config import cfg @@ -29,6 +30,10 @@ opts = [ cfg.BoolOpt('stop_service_on_exit', default=True, help='In case of ' 'execution as a service, specifies if the service ' 'must be gracefully stopped before exiting'), + cfg.BoolOpt('check_latest_version', default=True, help='Check if ' + 'there is a newer version of cloudbase-init available. ' + 'If this option is activated, a log message will be ' + 'emitted if there is a newer version available.') ] CONF = cfg.CONF @@ -94,6 +99,13 @@ class InitManager(object): 'supported' % plugin_name) return supported + @staticmethod + def _check_latest_version(): + if CONF.check_latest_version: + log_version = functools.partial( + LOG.info, 'Found new version of cloudbase-init %s') + version.check_latest_version(log_version) + def configure_host(self): LOG.info('Cloudbase-Init version: %s', version.get_version()) @@ -104,6 +116,8 @@ class InitManager(object): LOG.info('Metadata service loaded: \'%s\'' % service.get_name()) + self._check_latest_version() + instance_id = service.get_instance_id() LOG.debug('Instance id: %s', instance_id) diff --git a/cloudbaseinit/tests/test_init.py b/cloudbaseinit/tests/test_init.py index dbfbed2c..2777fe11 100644 --- a/cloudbaseinit/tests/test_init.py +++ b/cloudbaseinit/tests/test_init.py @@ -136,6 +136,7 @@ class InitManagerTest(unittest.TestCase): def test_check_plugin_os_requirements_other_requirenments(self): self._test_check_plugin_os_requirements(('linux', (5, 2))) + @mock.patch('cloudbaseinit.init.InitManager._check_latest_version') @mock.patch('cloudbaseinit.version.get_version') @mock.patch('cloudbaseinit.init.InitManager' '._check_plugin_os_requirements') @@ -147,7 +148,8 @@ class InitManagerTest(unittest.TestCase): mock_get_os_utils, mock_load_plugins, mock_exec_plugin, mock_check_os_requirements, - mock_get_version, expected_logging, + mock_get_version, mock_check_latest_version, + expected_logging, version, name, instance_id, reboot=True): mock_get_version.return_value = version @@ -175,6 +177,7 @@ class InitManagerTest(unittest.TestCase): self.osutils.reboot.assert_called_once_with() else: self.assertFalse(self.osutils.reboot.called) + mock_check_latest_version.assert_called_once_with() def _test_configure_host_with_logging(self, extra_logging, reboot=True): instance_id = 'fake id' @@ -209,3 +212,22 @@ class InitManagerTest(unittest.TestCase): def test_configure_host_reboot(self): self._test_configure_host_with_logging( extra_logging=['Rebooting']) + + @testutils.ConfPatcher('check_latest_version', False) + @mock.patch('cloudbaseinit.version.check_latest_version') + def test_configure_host(self, mock_check_last_version): + self._init._check_latest_version() + + self.assertFalse(mock_check_last_version.called) + + @testutils.ConfPatcher('check_latest_version', True) + @mock.patch('functools.partial') + @mock.patch('cloudbaseinit.version.check_latest_version') + def test_configure_host_with_version_check(self, mock_check_last_version, + mock_partial): + self._init._check_latest_version() + + mock_check_last_version.assert_called_once_with( + mock_partial.return_value) + mock_partial.assert_called_once_with( + init.LOG.info, 'Found new version of cloudbase-init %s') diff --git a/cloudbaseinit/tests/test_version.py b/cloudbaseinit/tests/test_version.py index 8a84ce67..24fac7ea 100644 --- a/cloudbaseinit/tests/test_version.py +++ b/cloudbaseinit/tests/test_version.py @@ -12,19 +12,99 @@ # License for the specific language governing permissions and limitations # under the License. +import importlib import unittest import mock +import six -from cloudbaseinit import version +from cloudbaseinit.tests import testutils class TestVersion(unittest.TestCase): + def setUp(self): + self.version = importlib.import_module('cloudbaseinit.version') + @mock.patch('pbr.version.VersionInfo') def test_get_version(self, mock_version_info): - package_version = version.get_version() + package_version = self.version.get_version() mock_version_info.assert_called_once_with('cloudbase-init') release_string = mock_version_info.return_value.release_string self.assertEqual(release_string.return_value, package_version) + + @mock.patch('requests.get') + @mock.patch('json.loads') + def test__read_url(self, mock_loads, mock_get): + mock_url = mock.Mock() + + result = self.version._read_url(mock_url) + + headers = {'User-Agent': self.version._PRODUCT_NAME} + mock_get.assert_called_once_with(mock_url, verify=six.PY3, + headers=headers) + request = mock_get.return_value + request.raise_for_status.assert_called_once_with() + mock_loads.assert_called_once_with(request.text) + self.assertEqual(mock_loads.return_value, result) + + @mock.patch('requests.get') + def test__read_url_empty_text(self, mock_get): + mock_get.return_value.text = None + + result = self.version._read_url(mock.Mock()) + + self.assertIsNone(result) + + @mock.patch('threading.Thread') + def test_check_latest_version(self, mock_thread): + mock_callback = mock.Mock() + + self.version.check_latest_version(mock_callback) + + mock_thread.assert_called_once_with( + target=self.version._check_latest_version, + args=(mock_callback, )) + thread = mock_thread.return_value + thread.start.assert_called_once_with() + + @mock.patch('cloudbaseinit.version._read_url') + def test__check_latest_version(self, mock_read_url): + mock_read_url.return_value = {'new_version': 42} + mock_callback = mock.Mock() + + self.version._check_latest_version(mock_callback) + + mock_callback.assert_called_once_with(42) + + @mock.patch('cloudbaseinit.version._read_url') + def test__check_latest_version_fails(self, mock_read_url): + mock_read_url.side_effect = Exception('no worky') + mock_callback = mock.Mock() + + with testutils.LogSnatcher('cloudbaseinit.version') as snatcher: + self.version._check_latest_version(mock_callback) + + expected_logging = ['Failed checking for new versions: no worky'] + self.assertEqual(expected_logging, snatcher.output) + self.assertFalse(mock_callback.called) + + @mock.patch('cloudbaseinit.version._read_url') + def test__check_latest_version_no_content(self, mock_read_url): + mock_read_url.return_value = None + mock_callback = mock.Mock() + + self.version._check_latest_version(mock_callback) + + self.assertFalse(mock_callback.called) + + @mock.patch('cloudbaseinit.version._read_url') + def test__check_latest_version_no_new_version(self, mock_read_url): + mock_read_url.return_value = {'new_versio': 42} + mock_callback = mock.Mock() + + result = self.version._check_latest_version(mock_callback) + + self.assertFalse(mock_callback.called) + self.assertIsNone(result) diff --git a/cloudbaseinit/version.py b/cloudbaseinit/version.py index 8d50be45..d53ecec0 100644 --- a/cloudbaseinit/version.py +++ b/cloudbaseinit/version.py @@ -12,7 +12,54 @@ # License for the specific language governing permissions and limitations # under the License. +import json +import threading + import pbr.version +import requests +import six + +from cloudbaseinit.openstack.common import log as logging + + +_UPDATE_CHECK_URL = 'https://www.cloudbase.it/checkupdates.php?p={0}&v={1}' +_PRODUCT_NAME = 'Cloudbase-Init' +LOG = logging.getLogger(__name__) + + +def _read_url(url): + # Disable certificate verification on Python 2 as + # requests's CA list is incomplete. Works fine on Python3. + req = requests.get(url, verify=six.PY3, + headers={'User-Agent': _PRODUCT_NAME}) + req.raise_for_status() + if req.text: + return json.loads(req.text) + + +def _check_latest_version(callback): + product_version = get_version() + url = _UPDATE_CHECK_URL.format(_PRODUCT_NAME, product_version) + try: + content = _read_url(url) + if not content: + return + + version = content.get('new_version') + if version: + callback(version) + + except Exception as exc: + LOG.debug('Failed checking for new versions: %s', exc) + return + + +def check_latest_version(done_callback): + """Try to obtain the latest version of the product.""" + thread = threading.Thread(target=_check_latest_version, + args=(done_callback, )) + thread.daemon = True + thread.start() def get_version(): diff --git a/requirements.txt b/requirements.txt index d1c06275..e649de1c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ oauthlib netifaces PyYAML tzlocal +requests \ No newline at end of file