From 12611efbc8581ecbbda060debf1a082c62e28446 Mon Sep 17 00:00:00 2001 From: "Alexis Rivera De La Torre (gardlt)" Date: Wed, 17 Aug 2016 11:37:29 -0500 Subject: [PATCH] adding-apt-update-functionality * adding apt tool to update package Closes-bug: #1614600 Co-Authored-By: Anastasiya Tolochkova Co-Authored-By: Ilya Kharin Change-Id: I6651055e798c01e22b9ba2b6fe573bb2abf7231c --- octane/tests/test_apt.py | 136 +++++++++++++++++++++++++++++++++++++++ octane/util/apt.py | 92 ++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 octane/tests/test_apt.py create mode 100644 octane/util/apt.py diff --git a/octane/tests/test_apt.py b/octane/tests/test_apt.py new file mode 100644 index 00000000..3ea6741d --- /dev/null +++ b/octane/tests/test_apt.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import pytest + +from octane.util import apt + +RELEASES = [ + """\ +Origin: Mirantis +Label: mos9.0 +Suite: mos9.0-updates +Codename: mos9.0-updates +MD5Sum: """, + """\ +Origin: Ubuntu +Label:Ubuntu + Suite: vivid +Version: 15.04 + +Codename: vivid + +SHA1:""", + """\ +SHA256:""", +] +PARAMS = [ + {'origin': 'Mirantis', + 'label': 'mos9.0', + 'codename': 'mos9.0-updates', + 'suite': 'mos9.0-updates'}, + {'origin': 'Ubuntu', + 'label': 'Ubuntu', + 'suite': 'vivid', + 'codename': 'vivid', + 'version': '15.04'}, +] +REPOS = [ + {'uri': 'http://mirror.fuel-infra.org/mos-repos/ubuntu/9.0/', + 'suite': 'mos9.0-updates', + 'type': 'deb', + 'section': 'main', + 'name': 'mos-updates', + 'priority': 1050}, + {'uri': 'http://us.archive.ubuntu.com/ubuntu/', + 'suite': 'vivid', + 'type': 'deb-src', + 'section': 'main universe', + 'name': 'ubuntu', + 'priority': 601}, +] + + +@pytest.mark.parametrize('repo,release,expected_params,url,code,is_error', [ + (REPOS[0], RELEASES[0], PARAMS[0], + 'http://mirror.fuel-infra.org/mos-repos/ubuntu/9.0/dists/' + 'mos9.0-updates/Release', + 200, False), + (REPOS[1], RELEASES[1], PARAMS[1], + 'http://us.archive.ubuntu.com/ubuntu/dists/vivid/Release', 200, False), + (REPOS[1], RELEASES[2], {}, + 'http://us.archive.ubuntu.com/ubuntu/dists/vivid/Release', 200, False), + ({'uri': 'https://example.com', 'suite': 'none'}, '', None, + 'https://example.com/dists/none/Release', 300, True), + ({'uri': 'http://example.com/ubuntu', 'suite': 'trusty'}, '', None, + 'https://example.com/ubuntu/dists/none/Release', 300, True), +]) +def test_fetch_release_parameters(mocker, repo, release, expected_params, url, + code, is_error): + mock_urlopen = mocker.patch('six.moves.urllib.request.urlopen') + resp = mocker.MagicMock(code=code) + resp.__iter__.return_value = iter(release.splitlines(True)) + mock_urlopen.return_value = resp + + if not is_error: + params = apt.fetch_release_parameters(repo) + mock_urlopen.assert_called_once_with(url) + assert params == expected_params + else: + with pytest.raises(apt.UnavailableRelease): + apt.fetch_release_parameters(repo) + + +@pytest.mark.parametrize('repo,filename,content', [ + (REPOS[0], '/etc/apt/sources.list.d/mos-updates.list', + 'deb http://mirror.fuel-infra.org/mos-repos/ubuntu/9.0/ mos9.0-updates ' + 'main'), + (REPOS[1], '/etc/apt/sources.list.d/ubuntu.list', + 'deb-src http://us.archive.ubuntu.com/ubuntu/ vivid main universe'), +]) +def test_create_repo_source(repo, filename, content): + result = apt.create_repo_source(repo) + assert result[0] == filename + assert result[1] == content + + +PREFERENCES = [ + """\ +Package: * +Pin: release a=mos9.0-updates,o=Mirantis,n=mos9.0-updates,l=mos9.0,c=main +Pin-Priority: 1050""", + """\ +Package: * +Pin: release a=vivid,o=Ubuntu,n=vivid,l=Ubuntu,c=main,v=15.04 +Pin-Priority: 601 + +Package: * +Pin: release a=vivid,o=Ubuntu,n=vivid,l=Ubuntu,c=universe,v=15.04 +Pin-Priority: 601""", +] + + +@pytest.mark.parametrize('params,repo,filename,content', [ + (PARAMS[0], REPOS[0], '/etc/apt/preferences.d/mos-updates.pref', + PREFERENCES[0]), + (PARAMS[1], REPOS[1], '/etc/apt/preferences.d/ubuntu.pref', + PREFERENCES[1]), +]) +def test_create_repo_preferences(mocker, params, repo, filename, content): + mock_fetch_release = mocker.patch( + 'octane.util.apt.fetch_release_parameters') + mock_fetch_release.return_value = params + result = apt.create_repo_preferences(repo) + assert result[0] == filename + assert result[1] == content diff --git a/octane/util/apt.py b/octane/util/apt.py new file mode 100644 index 00000000..a73a624b --- /dev/null +++ b/octane/util/apt.py @@ -0,0 +1,92 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from six.moves import urllib + +from octane.util import ssh as ssh_util + +PREFERENCES = [ + ('suite', 'a'), + ('origin', 'o'), + ('codename', 'n'), + ('label', 'l'), + ('component', 'c'), + ('version', 'v'), +] + + +class UnavailableRelease(Exception): + message = "Unexpected response status {0} for URL {1}." + + def __init__(self, status, url): + super(UnavailableRelease, self).__init__(self.message.format( + status, url)) + + +def upgrade_packages(node, packages, fix_broken=False): + pkgs = ' '.join(packages) + update_cmd = ['sh', '-c', + 'DEBIAN_FRONTEND=noninteractive apt-get ' + 'install --only-upgrade --yes --force-yes ' + '-o Dpkg::Options::="--force-confdef" ' + '-o Dpkg::Options::="--force-confold" ' + '{0}'.format(pkgs)] + return ssh_util.call(update_cmd, node=node) + + +def fetch_release_parameters(repo): + url_release_part = "./{0}".format('/'.join(('dists', repo['suite'], + 'Release'))) + base_url = repo['uri'] + if not base_url.endswith('/'): + base_url = "{0}/".format(repo['uri']) + release_url = urllib.parse.urljoin(base_url, url_release_part) + resp = urllib.request.urlopen(release_url) + if resp.code != 200: + raise UnavailableRelease(resp.code, release_url) + params = {} + for line in resp: + key, _, value = line.partition(':') + key = key.strip().lower() + value = value.strip() + if not key or not value: + continue + # NOTE(akscram): Normal Release files contain meaningful fields + # at the beginning. + if key in ('md5sum', 'sha1', 'sha256'): + break + params[key] = value + return params + + +def create_repo_source(repo): + filename = "/etc/apt/sources.list.d/{0}.list".format(repo['name']) + content = "{type} {uri} {suite} {section}".format(**repo) + return filename, content + + +def create_repo_preferences(repo): + filename = "/etc/apt/preferences.d/{0}.pref".format(repo['name']) + release_params = fetch_release_parameters(repo) + content = [] + components = repo['section'].split() + for component in components: + params = dict(release_params, component=component) + release = ','.join("{0}={1}".format(key, params[name]) + for name, key in PREFERENCES + if name in params) + content.append( + "Package: *\n" + "Pin: release {release}\n" + "Pin-Priority: {priority}" + .format(release=release, priority=repo['priority'])) + return filename, '\n\n'.join(content)