1
# Copyright 2014-2015 Canonical Limited.
3
# Licensed under the Apache License, Version 2.0 (the "License");
4
# you may not use this file except in compliance with the License.
5
# You may obtain a copy of the License at
7
# http://www.apache.org/licenses/LICENSE-2.0
9
# Unless required by applicable law or agreed to in writing, software
10
# distributed under the License is distributed on an "AS IS" BASIS,
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
# See the License for the specific language governing permissions and
13
# limitations under the License.
20
from tempfile import NamedTemporaryFile
21
from charmhelpers.core.host import (
24
from charmhelpers.core.hookenv import log
25
from charmhelpers.fetch import SourceConfigError
27
CLOUD_ARCHIVE = ('# Ubuntu Cloud Archive deb'
28
' http://ubuntu-cloud.archive.canonical.com/ubuntu'
30
PROPOSED_POCKET = ('# Proposed deb http://archive.ubuntu.com/ubuntu'
31
' {}-proposed main universe multiverse restricted')
32
CLOUD_ARCHIVE_POCKETS = {
34
'folsom': 'precise-updates/folsom',
35
'precise-folsom': 'precise-updates/folsom',
36
'precise-folsom/updates': 'precise-updates/folsom',
37
'precise-updates/folsom': 'precise-updates/folsom',
38
'folsom/proposed': 'precise-proposed/folsom',
39
'precise-folsom/proposed': 'precise-proposed/folsom',
40
'precise-proposed/folsom': 'precise-proposed/folsom',
42
'grizzly': 'precise-updates/grizzly',
43
'precise-grizzly': 'precise-updates/grizzly',
44
'precise-grizzly/updates': 'precise-updates/grizzly',
45
'precise-updates/grizzly': 'precise-updates/grizzly',
46
'grizzly/proposed': 'precise-proposed/grizzly',
47
'precise-grizzly/proposed': 'precise-proposed/grizzly',
48
'precise-proposed/grizzly': 'precise-proposed/grizzly',
50
'havana': 'precise-updates/havana',
51
'precise-havana': 'precise-updates/havana',
52
'precise-havana/updates': 'precise-updates/havana',
53
'precise-updates/havana': 'precise-updates/havana',
54
'havana/proposed': 'precise-proposed/havana',
55
'precise-havana/proposed': 'precise-proposed/havana',
56
'precise-proposed/havana': 'precise-proposed/havana',
58
'icehouse': 'precise-updates/icehouse',
59
'precise-icehouse': 'precise-updates/icehouse',
60
'precise-icehouse/updates': 'precise-updates/icehouse',
61
'precise-updates/icehouse': 'precise-updates/icehouse',
62
'icehouse/proposed': 'precise-proposed/icehouse',
63
'precise-icehouse/proposed': 'precise-proposed/icehouse',
64
'precise-proposed/icehouse': 'precise-proposed/icehouse',
66
'juno': 'trusty-updates/juno',
67
'trusty-juno': 'trusty-updates/juno',
68
'trusty-juno/updates': 'trusty-updates/juno',
69
'trusty-updates/juno': 'trusty-updates/juno',
70
'juno/proposed': 'trusty-proposed/juno',
71
'trusty-juno/proposed': 'trusty-proposed/juno',
72
'trusty-proposed/juno': 'trusty-proposed/juno',
74
'kilo': 'trusty-updates/kilo',
75
'trusty-kilo': 'trusty-updates/kilo',
76
'trusty-kilo/updates': 'trusty-updates/kilo',
77
'trusty-updates/kilo': 'trusty-updates/kilo',
78
'kilo/proposed': 'trusty-proposed/kilo',
79
'trusty-kilo/proposed': 'trusty-proposed/kilo',
80
'trusty-proposed/kilo': 'trusty-proposed/kilo',
82
'liberty': 'trusty-updates/liberty',
83
'trusty-liberty': 'trusty-updates/liberty',
84
'trusty-liberty/updates': 'trusty-updates/liberty',
85
'trusty-updates/liberty': 'trusty-updates/liberty',
86
'liberty/proposed': 'trusty-proposed/liberty',
87
'trusty-liberty/proposed': 'trusty-proposed/liberty',
88
'trusty-proposed/liberty': 'trusty-proposed/liberty',
90
'mitaka': 'trusty-updates/mitaka',
91
'trusty-mitaka': 'trusty-updates/mitaka',
92
'trusty-mitaka/updates': 'trusty-updates/mitaka',
93
'trusty-updates/mitaka': 'trusty-updates/mitaka',
94
'mitaka/proposed': 'trusty-proposed/mitaka',
95
'trusty-mitaka/proposed': 'trusty-proposed/mitaka',
96
'trusty-proposed/mitaka': 'trusty-proposed/mitaka',
98
'newton': 'xenial-updates/newton',
99
'xenial-newton': 'xenial-updates/newton',
100
'xenial-newton/updates': 'xenial-updates/newton',
101
'xenial-updates/newton': 'xenial-updates/newton',
102
'newton/proposed': 'xenial-proposed/newton',
103
'xenial-newton/proposed': 'xenial-proposed/newton',
104
'xenial-proposed/newton': 'xenial-proposed/newton',
107
APT_NO_LOCK = 100 # The return code for "couldn't acquire lock" in APT.
108
APT_NO_LOCK_RETRY_DELAY = 10 # Wait 10 seconds between apt lock checks.
109
APT_NO_LOCK_RETRY_COUNT = 30 # Retry to acquire the lock X times.
112
def filter_installed_packages(packages):
113
"""Return a list of packages that require installation."""
116
for package in packages:
119
p.current_ver or _pkgs.append(package)
121
log('Package {} has no installation candidate.'.format(package),
123
_pkgs.append(package)
127
def apt_cache(in_memory=True, progress=None):
128
"""Build and return an apt cache."""
129
from apt import apt_pkg
132
apt_pkg.config.set("Dir::Cache::pkgcache", "")
133
apt_pkg.config.set("Dir::Cache::srcpkgcache", "")
134
return apt_pkg.Cache(progress)
137
def install(packages, options=None, fatal=False):
138
"""Install one or more packages."""
140
options = ['--option=Dpkg::Options::=--force-confold']
142
cmd = ['apt-get', '--assume-yes']
144
cmd.append('install')
145
if isinstance(packages, six.string_types):
149
log("Installing {} with options: {}".format(packages,
151
_run_apt_command(cmd, fatal)
154
def upgrade(options=None, fatal=False, dist=False):
155
"""Upgrade all packages."""
157
options = ['--option=Dpkg::Options::=--force-confold']
159
cmd = ['apt-get', '--assume-yes']
162
cmd.append('dist-upgrade')
164
cmd.append('upgrade')
165
log("Upgrading with options: {}".format(options))
166
_run_apt_command(cmd, fatal)
169
def update(fatal=False):
170
"""Update local apt cache."""
171
cmd = ['apt-get', 'update']
172
_run_apt_command(cmd, fatal)
175
def purge(packages, fatal=False):
176
"""Purge one or more packages."""
177
cmd = ['apt-get', '--assume-yes', 'purge']
178
if isinstance(packages, six.string_types):
182
log("Purging {}".format(packages))
183
_run_apt_command(cmd, fatal)
186
def apt_mark(packages, mark, fatal=False):
187
"""Flag one or more packages using apt-mark."""
188
log("Marking {} as {}".format(packages, mark))
189
cmd = ['apt-mark', mark]
190
if isinstance(packages, six.string_types):
196
subprocess.check_call(cmd, universal_newlines=True)
198
subprocess.call(cmd, universal_newlines=True)
201
def apt_hold(packages, fatal=False):
202
return apt_mark(packages, 'hold', fatal=fatal)
205
def apt_unhold(packages, fatal=False):
206
return apt_mark(packages, 'unhold', fatal=fatal)
209
def add_source(source, key=None):
210
"""Add a package source to this system.
212
@param source: a URL or sources.list entry, as supported by
213
add-apt-repository(1). Examples::
216
deb https://stub:key@private.example.com/ubuntu trusty main
219
'proposed:' may be used to enable the standard 'proposed'
220
pocket for the release.
221
'cloud:' may be used to activate official cloud archive pockets,
222
such as 'cloud:icehouse'
223
'distro' may be used as a noop
225
@param key: A key to be added to the system's APT keyring and used
226
to verify the signatures on packages. Ideally, this should be an
227
ASCII format GPG public key including the block headers. A GPG key
228
id may also be used, but be aware that only insecure protocols are
229
available to retrieve the actual public key from a public keyserver
230
placing your Juju environment at risk. ppa and cloud archive keys
231
are securely added automtically, so sould not be provided.
234
log('Source is not present. Skipping')
237
if (source.startswith('ppa:') or
238
source.startswith('http') or
239
source.startswith('deb ') or
240
source.startswith('cloud-archive:')):
241
subprocess.check_call(['add-apt-repository', '--yes', source])
242
elif source.startswith('cloud:'):
243
install(filter_installed_packages(['ubuntu-cloud-keyring']),
245
pocket = source.split(':')[-1]
246
if pocket not in CLOUD_ARCHIVE_POCKETS:
247
raise SourceConfigError(
248
'Unsupported cloud: source option %s' %
250
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
251
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
252
apt.write(CLOUD_ARCHIVE.format(actual_pocket))
253
elif source == 'proposed':
254
release = lsb_release()['DISTRIB_CODENAME']
255
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
256
apt.write(PROPOSED_POCKET.format(release))
257
elif source == 'distro':
260
log("Unknown source: {!r}".format(source))
263
if '-----BEGIN PGP PUBLIC KEY BLOCK-----' in key:
264
with NamedTemporaryFile('w+') as key_file:
268
subprocess.check_call(['apt-key', 'add', '-'], stdin=key_file)
270
# Note that hkp: is in no way a secure protocol. Using a
271
# GPG key id is pointless from a security POV unless you
272
# absolutely trust your network and DNS.
273
subprocess.check_call(['apt-key', 'adv', '--keyserver',
274
'hkp://keyserver.ubuntu.com:80', '--recv',
278
def _run_apt_command(cmd, fatal=False):
279
"""Run an APT command.
281
Checks the output and retries if the fatal flag is set
284
:param: cmd: str: The apt command to run.
285
:param: fatal: bool: Whether the command's output should be checked and
288
env = os.environ.copy()
290
if 'DEBIAN_FRONTEND' not in env:
291
env['DEBIAN_FRONTEND'] = 'noninteractive'
297
# If the command is considered "fatal", we need to retry if the apt
298
# lock was not acquired.
300
while result is None or result == APT_NO_LOCK:
302
result = subprocess.check_call(cmd, env=env)
303
except subprocess.CalledProcessError as e:
304
retry_count = retry_count + 1
305
if retry_count > APT_NO_LOCK_RETRY_COUNT:
307
result = e.returncode
308
log("Couldn't acquire DPKG lock. Will retry in {} seconds."
309
"".format(APT_NO_LOCK_RETRY_DELAY))
310
time.sleep(APT_NO_LOCK_RETRY_DELAY)
313
subprocess.call(cmd, env=env)