File: ppa.py

package info (click to toggle)
software-properties 0.111-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 6,944 kB
  • sloc: python: 8,238; makefile: 19; sh: 18; xml: 10
file content (262 lines) | stat: -rw-r--r-- 9,636 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
#  software-properties PPA support, using launchpadlib
#
#  Copyright (c) 2019 Canonical Ltd.
#
#  Original Author: Michael Vogt <mvo@debian.org>
#  Rewrite: Dan Streetman <ddstreet@canonical.com>
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License as
#  published by the Free Software Foundation; either version 2 of the
#  License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
#  USA

from gettext import gettext as _

from lazr.restfulclient.errors import (NotFound, BadRequest, Unauthorized)

from softwareproperties.shortcuthandler import (ShortcutHandler, ShortcutException,
                                                InvalidShortcutException)
from softwareproperties.sourceslist import SourcesListShortcutHandler
from softwareproperties.uri import URIShortcutHandler

from aptsources.sourceslist import Deb822SourceEntry

from urllib.parse import urlparse

try:
    from launchpadlib.launchpad import Launchpad
except ImportError as e:
    Launchpad = None


PPA_URI_FORMAT = 'https://ppa.launchpadcontent.net/{team}/{ppa}/ubuntu/'
PRIVATE_PPA_URI_FORMAT = 'https://private-ppa.launchpadcontent.net/{team}/{ppa}/ubuntu/'
PPA_VALID_HOSTNAMES = [
    urlparse(PPA_URI_FORMAT).hostname,
    urlparse(PRIVATE_PPA_URI_FORMAT).hostname,
    # Old hostnames.
    'ppa.launchpad.net',
    'private-ppa.launchpad.net',
]

PPA_VALID_COMPS = ['main', 'main/debug']


class PPAShortcutHandler(ShortcutHandler):
    def __init__(self, shortcut, login=False, lp=None, **kwargs):
        super(PPAShortcutHandler, self).__init__(shortcut, deb822=True, **kwargs)
        self._lp_anon = not login
        self._signing_key_data = None

        self._lp = lp                       # LP object
        self._lpteam = None                 # Person/Team LP object
        self._lpppa = None                  # PPA Archive LP object

        self._is_sourceslist = False

        # one of these will set teamname and ppaname, and maybe _source_entry
        if not self._match_ppa(shortcut):
            # The input is either a sources.list line or a URI. Both cases lead
            # to the SourcesListShortcutHandler being used, so unset
            # self.deb822 (LP: #2037210).
            self.deb822 = False

            if not any((self._match_uri(shortcut),
                        self._match_sourceslist(shortcut))):
                msg = (_("ERROR: '%s' is not a valid ppa format") % shortcut)
                raise InvalidShortcutException(msg)

        self._filebase = "%s-ubuntu-%s" % (self.teamname, self.ppaname)
        self._set_auth()

        # Make sure we can find/access the PPA, lp:#1965180
        if self._is_sourceslist:
            try:
                self.lpppa
            except ShortcutException:
                raise InvalidShortcutException(_("ERROR: Can't find ppa"))

        if not self._source_entry:
            comps = self.components
            if not comps:
                comps = ['main']
                if self.lpppa.publish_debug_symbols:
                    print("PPA publishes dbgsym, you may need to include 'main/debug' component")
                    # comps += ['main/debug']

            uri_format = PRIVATE_PPA_URI_FORMAT if self.lpppa.private else PPA_URI_FORMAT
            uri = uri_format.format(team=self.teamname, ppa=self.ppaname)

            entry = Deb822SourceEntry(None, '')
            entry.types = [self.binary_type]
            entry.uris = [uri]
            entry.suites = [self.dist]
            entry.comps = comps

            self._set_source_entry(str(entry))

    @property
    def lp(self):
        if not Launchpad:
            return None
        if not self._lp:
            if self._lp_anon:
                login_func = Launchpad.login_anonymously
            else:
                login_func = Launchpad.login_with
            self._lp = login_func("%s.%s" % (self.__module__, self.__class__.__name__),
                                  service_root='production',
                                  version='devel')
        return self._lp

    @property
    def lpteam(self):
        if not self._lpteam:
            try:
                self._lpteam = self.lp.people(self.teamname)
            except NotFound:
                msg = (_("ERROR: user/team '%s' not found (use --login if private)") % self.teamname)
                raise ShortcutException(msg)
            except Unauthorized:
                msg = (_("ERROR: invalid user/team name '%s'") % self.teamname)
                raise ShortcutException(msg)
        return self._lpteam

    @property
    def lpppa(self):
        if not self._lpppa:
            try:
                self._lpppa = self.lpteam.getPPAByName(name=self.ppaname)
            except NotFound:
                msg = (_("ERROR: ppa '%s/%s' not found (use --login if private)") %
                       (self.teamname, self.ppaname))
                raise ShortcutException(msg)
            except BadRequest:
                msg = (_("ERROR: invalid ppa name '%s'") % self.ppaname)
                raise ShortcutException(msg)
        return self._lpppa

    @property
    def description(self):
        return self.lpppa.description

    @property
    def web_link(self):
        return self.lpppa.web_link

    @property
    def trustedparts_content(self):
        if not self._signing_key_data:
            key = self.lpppa.getSigningKeyData()
            fingerprint = self.lpppa.signing_key_fingerprint

            if not fingerprint:
                print(_("Warning: could not get PPA signing_key_fingerprint from LP, using anyway"))
            elif 'redacted' in fingerprint:
                print(_("Private PPA fingerprint redacted, using key anyway (LP: #1879781)"))
            elif not fingerprint in self.fingerprints(key):
                msg = (_("Fingerprints do not match, not importing: '%s' != '%s'") %
                       (fingerprint, ','.join(self.fingerprints(key))))
                raise ShortcutException(msg)

            self._signing_key_data = key
        return self._signing_key_data

    def SourceEntry(self, pkgtype=None):
        entry = super(PPAShortcutHandler, self).SourceEntry(pkgtype=pkgtype)
        if pkgtype != self.source_type or self.components:
            return entry

        # 'main/debug' is needed to get dbgsyms from ppas,
        # but it's not a valid component for ppa deb-src lines.
        # Sigh.
        entry.comps = list(set(entry.comps) - set(['main/debug']))
        return entry

    def _set_source_entry(self, line):
        super(PPAShortcutHandler, self)._set_source_entry(line)

        invalid_comps = set(self.SourceEntry().comps) - set(PPA_VALID_COMPS)
        if invalid_comps:
            print(_("Warning: components '%s' not valid for PPA") % ' '.join(invalid_comps))

    def _match_ppa(self, shortcut):
        (prefix, _, ppa) = shortcut.rpartition(':')
        if not prefix.lower() == 'ppa':
            return False

        (teamname, _, ppaname) = ppa.partition('/')
        teamname = teamname.lstrip('~')
        if '/' in ppaname:
            (ubuntu, _, ppaname) = ppaname.partition('/')
            if ubuntu.lower() != 'ubuntu':
                # PPAs only support ubuntu
                return False
            if '/' in ppaname:
                # Path is too long for valid ppa
                return False

        self.teamname = teamname
        self.ppaname = ppaname or 'ppa'
        return True

    def _match_uri(self, shortcut):
        try:
            return self._match_handler(URIShortcutHandler(shortcut))
        except InvalidShortcutException:
            return False

    def _match_sourceslist(self, shortcut):
        try:
            handler = self._match_handler(SourcesListShortcutHandler(shortcut))
        except InvalidShortcutException:
            return False
        self._is_sourceslist = True
        return handler

    def _match_handler(self, handler):
        parsed = urlparse(handler.SourceEntry().uri)
        if not parsed.hostname in PPA_VALID_HOSTNAMES:
            return False

        path = parsed.path.strip().strip('/').split('/')
        if len(path) < 2:
            return False
        self.teamname = path[0]
        self.ppaname = path[1]

        self._username = handler.username
        self._password = handler.password

        self._set_source_entry(handler.SourceEntry().line)
        return True

    def _set_auth(self):
        if self._lp_anon or not self.lpppa.private:
            return

        if self._username and self._password:
            return

        try:
            # Named ops work against actual person, not the me alias object
            me = self.lp.people(self.lp.me.name)
            url = me.getArchiveSubscriptionURL(archive=self.lpppa)
        except Unauthorized:
            msg = (_("Could not find PPA subscription for ppa:%s/%s, you may need to request access") %
                   (self.teamname, self.ppaname))
            raise ShortcutException(msg)
        else:
            parsed = urlparse(url)
            self._username = parsed.username
            self._password = parsed.password