File: apt.py

package info (click to toggle)
pyinfra 0.2.2%2Bgit20161227.ec708ef-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 11,804 kB
  • ctags: 677
  • sloc: python: 5,944; sh: 71; makefile: 11
file content (275 lines) | stat: -rw-r--r-- 8,153 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# pyinfra
# File: pyinfra/modules/apt.py
# Desc: manage apt packages & repositories

'''
Manage apt packages and repositories.
'''

from __future__ import unicode_literals

from datetime import timedelta

from six.moves.urllib.parse import urlparse

from pyinfra.api import operation
from pyinfra.facts.apt import parse_apt_repo

from . import files
from .util.packaging import ensure_packages

APT_UPDATE_FILENAME = '/var/lib/apt/periodic/update-success-stamp'


def noninteractive_apt(command, force=False):
    args = ['DEBIAN_FRONTEND=noninteractive apt-get -y']

    if force:
        args.append('--force-yes')

    args.extend((
        '-o Dpkg::Options::="--force-confdef"',
        '-o Dpkg::Options::="--force-confold"',
        command,
    ))

    return ' '.join(args)


@operation
def key(state, host, key=None, keyserver=None, keyid=None):
    '''
    Add apt gpg keys with ``apt-key``.

    + key: filename or URL
    + keyserver: URL of keyserver to fetch key from
    + keyid: key identifier when using keyserver

    Note:
        Always returns an add command, not state checking.

    keyserver/id:
        These must be provided together.
    '''

    if key:
        # If URL, wget the key to stdout and pipe into apt-key, because the "adv"
        # apt-key passes to gpg which doesn't always support https!
        if urlparse(key).scheme:
            yield 'wget -O- {0} | apt-key add -'.format(key)
        else:
            yield 'apt-key add {0}'.format(key)

    if keyserver and keyid:
        yield 'apt-key adv --keyserver {0} --recv-keys {1}'.format(keyserver, keyid)


@operation
def repo(state, host, name, present=True, filename=None):
    '''
    Manage apt repositories.

    + name: apt source string eg ``deb http://X hardy main``
    + present: whether the repo should exist on the system
    + filename: optional filename to use ``/etc/apt/sources.list.d/<filename>.list``. By
      default uses ``/etc/apt/sources.list``.
    '''

    # Get the target .list file to manage
    if filename:
        filename = '/etc/apt/sources.list.d/{0}.list'.format(filename)
    else:
        filename = '/etc/apt/sources.list'

    # Work out if the repo exists already
    apt_sources = host.fact.apt_sources

    is_present = False
    repo = parse_apt_repo(name)
    if repo and repo in apt_sources:
        is_present = True

    # Doesn't exist and we want it
    if not is_present and present:
        yield files.line(
            state, host, filename, name
        )

    # Exists and we don't want it
    if is_present and not present:
        yield files.line(
            state, host, filename, name,
            present=False,
        )


@operation
def ppa(state, host, name, present=True):
    '''
    Manage Ubuntu ppa repositories.

    + name: the PPA name (full ppa:user/repo format)
    + present: whether it should exist

    Note:
        requires ``apt-add-repository`` on the remote host
    '''

    if present:
        yield 'apt-add-repository -y "{0}"'.format(name)

    if not present:
        yield 'apt-add-repository -y --remove "{0}"'.format(name)


@operation
def deb(state, host, source, present=True, force=False):
    '''
    Install/manage ``.deb`` file packages.

    + source: filename or URL of the ``.deb`` file
    + present: whether or not the package should exist on the system
    + force: whether to force the package install by passing `--force-yes` to apt

    Note:
        when installing, ``apt-get install -f`` will be run to install any unmet
        dependencies

    URL sources with ``present=False``:
        if the ``.deb`` file isn't downloaded, pyinfra can't remove any existing package
        as the file won't exist until mid-deploy
    '''

    # If source is a url
    if urlparse(source).scheme:
        # Generate a temp filename
        temp_filename = state.get_temp_filename(source)

        # Ensure it's downloaded
        yield files.download(state, host, source, temp_filename)

        # Override the source with the downloaded file
        source = temp_filename

    # Check for file .deb information (if file is present)
    info = host.fact.deb_package(source)

    exists = False

    # We have deb info! Check against installed packages
    if info:
        current_packages = host.fact.deb_packages

        if (
            info['name'] in current_packages
            and info['version'] in current_packages[info['name']]
        ):
            exists = True

    # Package does not exist and we want?
    if present and not exists:
        # Install .deb file - ignoring failure (on unmet dependencies)
        yield 'dpkg --force-confdef --force-confold -i {0} || true'.format(source)
        # Attempt to install any missing dependencies
        yield '{0} -f'.format(noninteractive_apt('install', force=force))
        # Now reinstall, and critically configure, the package - if there are still
        # missing deps, now we error
        yield 'dpkg --force-confdef --force-confold -i {0}'.format(source)

    # Package exists but we don't want?
    if exists and not present:
        yield '{0} {1}'.format(
            noninteractive_apt('remove', force=force),
            info['name'],
        )


@operation
def update(state, host, touch_periodic=False):
    '''
    Updates apt repos.

    + touch_periodic: touch ``/var/lib/apt/periodic/update-success-stamp`` after update
    '''

    yield 'apt-get update'

    # Some apt systems (Debian) have the /var/lib/apt/periodic directory, but don't
    # bother touching anything in there - so pyinfra does it, enabling cache_time to work.
    if touch_periodic:
        yield 'touch {0}'.format(APT_UPDATE_FILENAME)

_update = update  # noqa


@operation
def upgrade(state, host):
    '''
    Upgrades all apt packages.
    '''

    yield noninteractive_apt('upgrade')

_upgrade = upgrade  # noqa


@operation
def packages(
    state, host,
    packages=None, present=True, latest=False,
    update=False, cache_time=None, upgrade=False,
    force=False, no_recommends=False,
):
    '''
    Install/remove/upgrade packages & update apt.

    + packages: list of packages to ensure
    + present: whether the packages should be installed
    + latest: whether to upgrade packages without a specified version
    + update: run apt update
    + cache_time: when used with update, cache for this many seconds
    + upgrade: run apt upgrade
    + force: whether to force package installs by passing `--force-yes` to apt
    + no_recommends: don't install recommended packages

    Versions:
        Package versions can be pinned like apt: ``<pkg>=<version>``

    Cache time:
        When ``cache_time`` is set the ``/var/lib/apt/periodic/update-success-stamp`` file
        is touched upon successful update. Some distros already do this (Ubuntu), but others
        simply leave the periodic directory empty (Debian).
    '''

    # If cache_time check when apt was last updated, prevent updates if within time
    if update and cache_time:
        # Ubuntu provides this handy file
        cache_info = host.fact.file(APT_UPDATE_FILENAME)

        # Time on files is not tz-aware, and will be the same tz as the server's time,
        # so we can safely remove the tzinfo from host.fact.date before comparison.
        host_cache_time = host.fact.date.replace(tzinfo=None) - timedelta(seconds=cache_time)
        if cache_info and cache_info['mtime'] and cache_info['mtime'] > host_cache_time:
            update = False

    if update:
        yield _update(state, host, touch_periodic=cache_time)

    if upgrade:
        yield _upgrade(state, host)

    install_command = (
        'install --no-install-recommends'
        if no_recommends is True
        else 'install'
    )

    # Compare/ensure packages are present/not
    yield ensure_packages(
        packages, host.fact.deb_packages, present,
        install_command=noninteractive_apt(install_command, force=force),
        uninstall_command=noninteractive_apt('remove', force=force),
        upgrade_command=noninteractive_apt(install_command, force=force),
        version_join='=',
        latest=latest,
    )