~rcj/ubuntu/trusty/cloud-init/joyent-lxbrand

« back to all changes in this revision

Viewing changes to .pc/lp-1470890-include-regions-in-dynamic-mirror-discovery.patch/cloudinit/distros/__init__.py

  • Committer: Scott Moser
  • Author(s): Daniel Watkins
  • Date: 2015-08-14 12:54:02 UTC
  • Revision ID: smoser@ubuntu.com-20150814125402-514m056vv2ziyivh
Tags: 0.7.5-0ubuntu1.8
* debian/patches/lp-1411582-azure-udev-ephemeral-disks.patch:
    - Use udev rules to discover ephemeral disk locations rather than
      hard-coded device names (LP: #1411582).
* debian/patches/lp-1470880-fix-gce-az-determination.patch:
    - Correctly parse GCE's availability zones (LP: #1470880).
* d/patches/lp-1470890-include-regions-in-dynamic-mirror-discovery.patch:
    - Make %(region)s a valid substitution in mirror discovery
      (LP: #1470890).
* Remove python-serial from Build-Depends; it was mistakenly added last
  upload.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vi: ts=4 expandtab
 
2
#
 
3
#    Copyright (C) 2012 Canonical Ltd.
 
4
#    Copyright (C) 2012, 2013 Hewlett-Packard Development Company, L.P.
 
5
#    Copyright (C) 2012 Yahoo! Inc.
 
6
#
 
7
#    Author: Scott Moser <scott.moser@canonical.com>
 
8
#    Author: Juerg Haefliger <juerg.haefliger@hp.com>
 
9
#    Author: Joshua Harlow <harlowja@yahoo-inc.com>
 
10
#    Author: Ben Howard <ben.howard@canonical.com>
 
11
#
 
12
#    This program is free software: you can redistribute it and/or modify
 
13
#    it under the terms of the GNU General Public License version 3, as
 
14
#    published by the Free Software Foundation.
 
15
#
 
16
#    This program is distributed in the hope that it will be useful,
 
17
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
18
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
19
#    GNU General Public License for more details.
 
20
#
 
21
#    You should have received a copy of the GNU General Public License
 
22
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
23
 
 
24
from StringIO import StringIO
 
25
 
 
26
import abc
 
27
import itertools
 
28
import os
 
29
import re
 
30
 
 
31
from cloudinit import importer
 
32
from cloudinit import log as logging
 
33
from cloudinit import ssh_util
 
34
from cloudinit import type_utils
 
35
from cloudinit import util
 
36
 
 
37
from cloudinit.distros.parsers import hosts
 
38
 
 
39
OSFAMILIES = {
 
40
    'debian': ['debian', 'ubuntu'],
 
41
    'redhat': ['fedora', 'rhel'],
 
42
    'gentoo': ['gentoo'],
 
43
    'freebsd': ['freebsd'],
 
44
    'suse': ['sles'],
 
45
    'arch': ['arch'],
 
46
}
 
47
 
 
48
LOG = logging.getLogger(__name__)
 
49
 
 
50
 
 
51
class Distro(object):
 
52
    __metaclass__ = abc.ABCMeta
 
53
 
 
54
    hosts_fn = "/etc/hosts"
 
55
    ci_sudoers_fn = "/etc/sudoers.d/90-cloud-init-users"
 
56
    hostname_conf_fn = "/etc/hostname"
 
57
    tz_zone_dir = "/usr/share/zoneinfo"
 
58
    init_cmd = ['service']  # systemctl, service etc
 
59
 
 
60
    def __init__(self, name, cfg, paths):
 
61
        self._paths = paths
 
62
        self._cfg = cfg
 
63
        self.name = name
 
64
 
 
65
    @abc.abstractmethod
 
66
    def install_packages(self, pkglist):
 
67
        raise NotImplementedError()
 
68
 
 
69
    @abc.abstractmethod
 
70
    def _write_network(self, settings):
 
71
        # In the future use the http://fedorahosted.org/netcf/
 
72
        # to write this blob out in a distro format
 
73
        raise NotImplementedError()
 
74
 
 
75
    def _find_tz_file(self, tz):
 
76
        tz_file = os.path.join(self.tz_zone_dir, str(tz))
 
77
        if not os.path.isfile(tz_file):
 
78
            raise IOError(("Invalid timezone %s,"
 
79
                           " no file found at %s") % (tz, tz_file))
 
80
        return tz_file
 
81
 
 
82
    def get_option(self, opt_name, default=None):
 
83
        return self._cfg.get(opt_name, default)
 
84
 
 
85
    def set_hostname(self, hostname, fqdn=None):
 
86
        writeable_hostname = self._select_hostname(hostname, fqdn)
 
87
        self._write_hostname(writeable_hostname, self.hostname_conf_fn)
 
88
        self._apply_hostname(hostname)
 
89
 
 
90
    @abc.abstractmethod
 
91
    def package_command(self, cmd, args=None, pkgs=None):
 
92
        raise NotImplementedError()
 
93
 
 
94
    @abc.abstractmethod
 
95
    def update_package_sources(self):
 
96
        raise NotImplementedError()
 
97
 
 
98
    def get_primary_arch(self):
 
99
        arch = os.uname[4]
 
100
        if arch in ("i386", "i486", "i586", "i686"):
 
101
            return "i386"
 
102
        return arch
 
103
 
 
104
    def _get_arch_package_mirror_info(self, arch=None):
 
105
        mirror_info = self.get_option("package_mirrors", [])
 
106
        if not arch:
 
107
            arch = self.get_primary_arch()
 
108
        return _get_arch_package_mirror_info(mirror_info, arch)
 
109
 
 
110
    def get_package_mirror_info(self, arch=None,
 
111
                                availability_zone=None):
 
112
        # This resolves the package_mirrors config option
 
113
        # down to a single dict of {mirror_name: mirror_url}
 
114
        arch_info = self._get_arch_package_mirror_info(arch)
 
115
        return _get_package_mirror_info(availability_zone=availability_zone,
 
116
                                        mirror_info=arch_info)
 
117
 
 
118
    def apply_network(self, settings, bring_up=True):
 
119
        # Write it out
 
120
        dev_names = self._write_network(settings)
 
121
        # Now try to bring them up
 
122
        if bring_up:
 
123
            return self._bring_up_interfaces(dev_names)
 
124
        return False
 
125
 
 
126
    @abc.abstractmethod
 
127
    def apply_locale(self, locale, out_fn=None):
 
128
        raise NotImplementedError()
 
129
 
 
130
    @abc.abstractmethod
 
131
    def set_timezone(self, tz):
 
132
        raise NotImplementedError()
 
133
 
 
134
    def _get_localhost_ip(self):
 
135
        return "127.0.0.1"
 
136
 
 
137
    @abc.abstractmethod
 
138
    def _read_hostname(self, filename, default=None):
 
139
        raise NotImplementedError()
 
140
 
 
141
    @abc.abstractmethod
 
142
    def _write_hostname(self, hostname, filename):
 
143
        raise NotImplementedError()
 
144
 
 
145
    @abc.abstractmethod
 
146
    def _read_system_hostname(self):
 
147
        raise NotImplementedError()
 
148
 
 
149
    def _apply_hostname(self, hostname):
 
150
        # This really only sets the hostname
 
151
        # temporarily (until reboot so it should
 
152
        # not be depended on). Use the write
 
153
        # hostname functions for 'permanent' adjustments.
 
154
        LOG.debug("Non-persistently setting the system hostname to %s",
 
155
                  hostname)
 
156
        try:
 
157
            util.subp(['hostname', hostname])
 
158
        except util.ProcessExecutionError:
 
159
            util.logexc(LOG, "Failed to non-persistently adjust the system "
 
160
                        "hostname to %s", hostname)
 
161
 
 
162
    @abc.abstractmethod
 
163
    def _select_hostname(self, hostname, fqdn):
 
164
        raise NotImplementedError()
 
165
 
 
166
    @staticmethod
 
167
    def expand_osfamily(family_list):
 
168
        distros = []
 
169
        for family in family_list:
 
170
            if not family in OSFAMILIES:
 
171
                raise ValueError("No distibutions found for osfamily %s"
 
172
                                 % (family))
 
173
            distros.extend(OSFAMILIES[family])
 
174
        return distros
 
175
 
 
176
    def update_hostname(self, hostname, fqdn, prev_hostname_fn):
 
177
        applying_hostname = hostname
 
178
 
 
179
        # Determine what the actual written hostname should be
 
180
        hostname = self._select_hostname(hostname, fqdn)
 
181
 
 
182
        # If the previous hostname file exists lets see if we
 
183
        # can get a hostname from it
 
184
        if prev_hostname_fn and os.path.exists(prev_hostname_fn):
 
185
            prev_hostname = self._read_hostname(prev_hostname_fn)
 
186
        else:
 
187
            prev_hostname = None
 
188
 
 
189
        # Lets get where we should write the system hostname
 
190
        # and what the system hostname is
 
191
        (sys_fn, sys_hostname) = self._read_system_hostname()
 
192
        update_files = []
 
193
 
 
194
        # If there is no previous hostname or it differs
 
195
        # from what we want, lets update it or create the
 
196
        # file in the first place
 
197
        if not prev_hostname or prev_hostname != hostname:
 
198
            update_files.append(prev_hostname_fn)
 
199
 
 
200
        # If the system hostname is different than the previous
 
201
        # one or the desired one lets update it as well
 
202
        if (not sys_hostname) or (sys_hostname == prev_hostname
 
203
                                  and sys_hostname != hostname):
 
204
            update_files.append(sys_fn)
 
205
 
 
206
        # Remove duplicates (incase the previous config filename)
 
207
        # is the same as the system config filename, don't bother
 
208
        # doing it twice
 
209
        update_files = set([f for f in update_files if f])
 
210
        LOG.debug("Attempting to update hostname to %s in %s files",
 
211
                  hostname, len(update_files))
 
212
 
 
213
        for fn in update_files:
 
214
            try:
 
215
                self._write_hostname(hostname, fn)
 
216
            except IOError:
 
217
                util.logexc(LOG, "Failed to write hostname %s to %s", hostname,
 
218
                            fn)
 
219
 
 
220
        if (sys_hostname and prev_hostname and
 
221
            sys_hostname != prev_hostname):
 
222
            LOG.debug("%s differs from %s, assuming user maintained hostname.",
 
223
                       prev_hostname_fn, sys_fn)
 
224
 
 
225
        # If the system hostname file name was provided set the
 
226
        # non-fqdn as the transient hostname.
 
227
        if sys_fn in update_files:
 
228
            self._apply_hostname(applying_hostname)
 
229
 
 
230
    def update_etc_hosts(self, hostname, fqdn):
 
231
        header = ''
 
232
        if os.path.exists(self.hosts_fn):
 
233
            eh = hosts.HostsConf(util.load_file(self.hosts_fn))
 
234
        else:
 
235
            eh = hosts.HostsConf('')
 
236
            header = util.make_header(base="added")
 
237
        local_ip = self._get_localhost_ip()
 
238
        prev_info = eh.get_entry(local_ip)
 
239
        need_change = False
 
240
        if not prev_info:
 
241
            eh.add_entry(local_ip, fqdn, hostname)
 
242
            need_change = True
 
243
        else:
 
244
            need_change = True
 
245
            for entry in prev_info:
 
246
                entry_fqdn = None
 
247
                entry_aliases = []
 
248
                if len(entry) >= 1:
 
249
                    entry_fqdn = entry[0]
 
250
                if len(entry) >= 2:
 
251
                    entry_aliases = entry[1:]
 
252
                if entry_fqdn is not None and entry_fqdn == fqdn:
 
253
                    if hostname in entry_aliases:
 
254
                        # Exists already, leave it be
 
255
                        need_change = False
 
256
            if need_change:
 
257
                # Doesn't exist, add that entry in...
 
258
                new_entries = list(prev_info)
 
259
                new_entries.append([fqdn, hostname])
 
260
                eh.del_entries(local_ip)
 
261
                for entry in new_entries:
 
262
                    if len(entry) == 1:
 
263
                        eh.add_entry(local_ip, entry[0])
 
264
                    elif len(entry) >= 2:
 
265
                        eh.add_entry(local_ip, *entry)
 
266
        if need_change:
 
267
            contents = StringIO()
 
268
            if header:
 
269
                contents.write("%s\n" % (header))
 
270
            contents.write("%s\n" % (eh))
 
271
            util.write_file(self.hosts_fn, contents.getvalue(), mode=0644)
 
272
 
 
273
    def _bring_up_interface(self, device_name):
 
274
        cmd = ['ifup', device_name]
 
275
        LOG.debug("Attempting to run bring up interface %s using command %s",
 
276
                   device_name, cmd)
 
277
        try:
 
278
            (_out, err) = util.subp(cmd)
 
279
            if len(err):
 
280
                LOG.warn("Running %s resulted in stderr output: %s", cmd, err)
 
281
            return True
 
282
        except util.ProcessExecutionError:
 
283
            util.logexc(LOG, "Running interface command %s failed", cmd)
 
284
            return False
 
285
 
 
286
    def _bring_up_interfaces(self, device_names):
 
287
        am_failed = 0
 
288
        for d in device_names:
 
289
            if not self._bring_up_interface(d):
 
290
                am_failed += 1
 
291
        if am_failed == 0:
 
292
            return True
 
293
        return False
 
294
 
 
295
    def get_default_user(self):
 
296
        return self.get_option('default_user')
 
297
 
 
298
    def add_user(self, name, **kwargs):
 
299
        """
 
300
        Add a user to the system using standard GNU tools
 
301
        """
 
302
        if util.is_user(name):
 
303
            LOG.info("User %s already exists, skipping." % name)
 
304
            return
 
305
 
 
306
        adduser_cmd = ['useradd', name]
 
307
        log_adduser_cmd = ['useradd', name]
 
308
 
 
309
        # Since we are creating users, we want to carefully validate the
 
310
        # inputs. If something goes wrong, we can end up with a system
 
311
        # that nobody can login to.
 
312
        adduser_opts = {
 
313
            "gecos": '--comment',
 
314
            "homedir": '--home',
 
315
            "primary_group": '--gid',
 
316
            "groups": '--groups',
 
317
            "passwd": '--password',
 
318
            "shell": '--shell',
 
319
            "expiredate": '--expiredate',
 
320
            "inactive": '--inactive',
 
321
            "selinux_user": '--selinux-user',
 
322
        }
 
323
 
 
324
        adduser_flags = {
 
325
            "no_user_group": '--no-user-group',
 
326
            "system": '--system',
 
327
            "no_log_init": '--no-log-init',
 
328
        }
 
329
 
 
330
        redact_opts = ['passwd']
 
331
 
 
332
        # Check the values and create the command
 
333
        for key, val in kwargs.iteritems():
 
334
 
 
335
            if key in adduser_opts and val and isinstance(val, str):
 
336
                adduser_cmd.extend([adduser_opts[key], val])
 
337
 
 
338
                # Redact certain fields from the logs
 
339
                if key in redact_opts:
 
340
                    log_adduser_cmd.extend([adduser_opts[key], 'REDACTED'])
 
341
                else:
 
342
                    log_adduser_cmd.extend([adduser_opts[key], val])
 
343
 
 
344
            elif key in adduser_flags and val:
 
345
                adduser_cmd.append(adduser_flags[key])
 
346
                log_adduser_cmd.append(adduser_flags[key])
 
347
 
 
348
        # Don't create the home directory if directed so or if the user is a
 
349
        # system user
 
350
        if 'no_create_home' in kwargs or 'system' in kwargs:
 
351
            adduser_cmd.append('-M')
 
352
            log_adduser_cmd.append('-M')
 
353
        else:
 
354
            adduser_cmd.append('-m')
 
355
            log_adduser_cmd.append('-m')
 
356
 
 
357
        # Run the command
 
358
        LOG.debug("Adding user %s", name)
 
359
        try:
 
360
            util.subp(adduser_cmd, logstring=log_adduser_cmd)
 
361
        except Exception as e:
 
362
            util.logexc(LOG, "Failed to create user %s", name)
 
363
            raise e
 
364
 
 
365
    def create_user(self, name, **kwargs):
 
366
        """
 
367
        Creates users for the system using the GNU passwd tools. This
 
368
        will work on an GNU system. This should be overriden on
 
369
        distros where useradd is not desirable or not available.
 
370
        """
 
371
 
 
372
        # Add the user
 
373
        self.add_user(name, **kwargs)
 
374
 
 
375
        # Set password if plain-text password provided and non-empty
 
376
        if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']:
 
377
            self.set_passwd(name, kwargs['plain_text_passwd'])
 
378
 
 
379
        # Default locking down the account.  'lock_passwd' defaults to True.
 
380
        # lock account unless lock_password is False.
 
381
        if kwargs.get('lock_passwd', True):
 
382
            self.lock_passwd(name)
 
383
 
 
384
        # Configure sudo access
 
385
        if 'sudo' in kwargs:
 
386
            self.write_sudo_rules(name, kwargs['sudo'])
 
387
 
 
388
        # Import SSH keys
 
389
        if 'ssh_authorized_keys' in kwargs:
 
390
            keys = set(kwargs['ssh_authorized_keys']) or []
 
391
            ssh_util.setup_user_keys(keys, name, options=None)
 
392
 
 
393
        return True
 
394
 
 
395
    def lock_passwd(self, name):
 
396
        """
 
397
        Lock the password of a user, i.e., disable password logins
 
398
        """
 
399
        try:
 
400
            # Need to use the short option name '-l' instead of '--lock'
 
401
            # (which would be more descriptive) since SLES 11 doesn't know
 
402
            # about long names.
 
403
            util.subp(['passwd', '-l', name])
 
404
        except Exception as e:
 
405
            util.logexc(LOG, 'Failed to disable password for user %s', name)
 
406
            raise e
 
407
 
 
408
    def set_passwd(self, user, passwd, hashed=False):
 
409
        pass_string = '%s:%s' % (user, passwd)
 
410
        cmd = ['chpasswd']
 
411
 
 
412
        if hashed:
 
413
            # Need to use the short option name '-e' instead of '--encrypted'
 
414
            # (which would be more descriptive) since SLES 11 doesn't know
 
415
            # about long names.
 
416
            cmd.append('-e')
 
417
 
 
418
        try:
 
419
            util.subp(cmd, pass_string, logstring="chpasswd for %s" % user)
 
420
        except Exception as e:
 
421
            util.logexc(LOG, "Failed to set password for %s", user)
 
422
            raise e
 
423
 
 
424
        return True
 
425
 
 
426
    def ensure_sudo_dir(self, path, sudo_base='/etc/sudoers'):
 
427
        # Ensure the dir is included and that
 
428
        # it actually exists as a directory
 
429
        sudoers_contents = ''
 
430
        base_exists = False
 
431
        if os.path.exists(sudo_base):
 
432
            sudoers_contents = util.load_file(sudo_base)
 
433
            base_exists = True
 
434
        found_include = False
 
435
        for line in sudoers_contents.splitlines():
 
436
            line = line.strip()
 
437
            include_match = re.search(r"^#includedir\s+(.*)$", line)
 
438
            if not include_match:
 
439
                continue
 
440
            included_dir = include_match.group(1).strip()
 
441
            if not included_dir:
 
442
                continue
 
443
            included_dir = os.path.abspath(included_dir)
 
444
            if included_dir == path:
 
445
                found_include = True
 
446
                break
 
447
        if not found_include:
 
448
            try:
 
449
                if not base_exists:
 
450
                    lines = [('# See sudoers(5) for more information'
 
451
                              ' on "#include" directives:'), '',
 
452
                             util.make_header(base="added"),
 
453
                             "#includedir %s" % (path), '']
 
454
                    sudoers_contents = "\n".join(lines)
 
455
                    util.write_file(sudo_base, sudoers_contents, 0440)
 
456
                else:
 
457
                    lines = ['', util.make_header(base="added"),
 
458
                             "#includedir %s" % (path), '']
 
459
                    sudoers_contents = "\n".join(lines)
 
460
                    util.append_file(sudo_base, sudoers_contents)
 
461
                LOG.debug("Added '#includedir %s' to %s" % (path, sudo_base))
 
462
            except IOError as e:
 
463
                util.logexc(LOG, "Failed to write %s", sudo_base)
 
464
                raise e
 
465
        util.ensure_dir(path, 0750)
 
466
 
 
467
    def write_sudo_rules(self, user, rules, sudo_file=None):
 
468
        if not sudo_file:
 
469
            sudo_file = self.ci_sudoers_fn
 
470
 
 
471
        lines = [
 
472
            '',
 
473
            "# User rules for %s" % user,
 
474
        ]
 
475
        if isinstance(rules, (list, tuple)):
 
476
            for rule in rules:
 
477
                lines.append("%s %s" % (user, rule))
 
478
        elif isinstance(rules, (basestring, str)):
 
479
            lines.append("%s %s" % (user, rules))
 
480
        else:
 
481
            msg = "Can not create sudoers rule addition with type %r"
 
482
            raise TypeError(msg % (type_utils.obj_name(rules)))
 
483
        content = "\n".join(lines)
 
484
        content += "\n"  # trailing newline
 
485
 
 
486
        self.ensure_sudo_dir(os.path.dirname(sudo_file))
 
487
        if not os.path.exists(sudo_file):
 
488
            contents = [
 
489
                util.make_header(),
 
490
                content,
 
491
            ]
 
492
            try:
 
493
                util.write_file(sudo_file, "\n".join(contents), 0440)
 
494
            except IOError as e:
 
495
                util.logexc(LOG, "Failed to write sudoers file %s", sudo_file)
 
496
                raise e
 
497
        else:
 
498
            try:
 
499
                util.append_file(sudo_file, content)
 
500
            except IOError as e:
 
501
                util.logexc(LOG, "Failed to append sudoers file %s", sudo_file)
 
502
                raise e
 
503
 
 
504
    def create_group(self, name, members):
 
505
        group_add_cmd = ['groupadd', name]
 
506
 
 
507
        # Check if group exists, and then add it doesn't
 
508
        if util.is_group(name):
 
509
            LOG.warn("Skipping creation of existing group '%s'" % name)
 
510
        else:
 
511
            try:
 
512
                util.subp(group_add_cmd)
 
513
                LOG.info("Created new group %s" % name)
 
514
            except Exception:
 
515
                util.logexc("Failed to create group %s", name)
 
516
 
 
517
        # Add members to the group, if so defined
 
518
        if len(members) > 0:
 
519
            for member in members:
 
520
                if not util.is_user(member):
 
521
                    LOG.warn("Unable to add group member '%s' to group '%s'"
 
522
                            "; user does not exist.", member, name)
 
523
                    continue
 
524
 
 
525
                util.subp(['usermod', '-a', '-G', name, member])
 
526
                LOG.info("Added user '%s' to group '%s'" % (member, name))
 
527
 
 
528
 
 
529
def _get_package_mirror_info(mirror_info, availability_zone=None,
 
530
                             mirror_filter=util.search_for_mirror):
 
531
    # given a arch specific 'mirror_info' entry (from package_mirrors)
 
532
    # search through the 'search' entries, and fallback appropriately
 
533
    # return a dict with only {name: mirror} entries.
 
534
    if not mirror_info:
 
535
        mirror_info = {}
 
536
 
 
537
    # ec2 availability zones are named cc-direction-[0-9][a-d] (us-east-1b)
 
538
    # the region is us-east-1. so region = az[0:-1]
 
539
    directions_re = '|'.join([
 
540
        'central', 'east', 'north', 'northeast', 'northwest',
 
541
        'south', 'southeast', 'southwest', 'west'])
 
542
    ec2_az_re = ("^[a-z][a-z]-(%s)-[1-9][0-9]*[a-z]$" % directions_re)
 
543
 
 
544
    subst = {}
 
545
    if availability_zone:
 
546
        subst['availability_zone'] = availability_zone
 
547
 
 
548
    if availability_zone and re.match(ec2_az_re, availability_zone):
 
549
        subst['ec2_region'] = "%s" % availability_zone[0:-1]
 
550
 
 
551
    results = {}
 
552
    for (name, mirror) in mirror_info.get('failsafe', {}).iteritems():
 
553
        results[name] = mirror
 
554
 
 
555
    for (name, searchlist) in mirror_info.get('search', {}).iteritems():
 
556
        mirrors = []
 
557
        for tmpl in searchlist:
 
558
            try:
 
559
                mirrors.append(tmpl % subst)
 
560
            except KeyError:
 
561
                pass
 
562
 
 
563
        found = mirror_filter(mirrors)
 
564
        if found:
 
565
            results[name] = found
 
566
 
 
567
    LOG.debug("filtered distro mirror info: %s" % results)
 
568
 
 
569
    return results
 
570
 
 
571
 
 
572
def _get_arch_package_mirror_info(package_mirrors, arch):
 
573
    # pull out the specific arch from a 'package_mirrors' config option
 
574
    default = None
 
575
    for item in package_mirrors:
 
576
        arches = item.get("arches")
 
577
        if arch in arches:
 
578
            return item
 
579
        if "default" in arches:
 
580
            default = item
 
581
    return default
 
582
 
 
583
 
 
584
# Normalizes a input group configuration
 
585
# which can be a comma seperated list of
 
586
# group names, or a list of group names
 
587
# or a python dictionary of group names
 
588
# to a list of members of that group.
 
589
#
 
590
# The output is a dictionary of group
 
591
# names => members of that group which
 
592
# is the standard form used in the rest
 
593
# of cloud-init
 
594
def _normalize_groups(grp_cfg):
 
595
    if isinstance(grp_cfg, (str, basestring)):
 
596
        grp_cfg = grp_cfg.strip().split(",")
 
597
    if isinstance(grp_cfg, (list)):
 
598
        c_grp_cfg = {}
 
599
        for i in grp_cfg:
 
600
            if isinstance(i, (dict)):
 
601
                for k, v in i.items():
 
602
                    if k not in c_grp_cfg:
 
603
                        if isinstance(v, (list)):
 
604
                            c_grp_cfg[k] = list(v)
 
605
                        elif isinstance(v, (basestring, str)):
 
606
                            c_grp_cfg[k] = [v]
 
607
                        else:
 
608
                            raise TypeError("Bad group member type %s" %
 
609
                                            type_utils.obj_name(v))
 
610
                    else:
 
611
                        if isinstance(v, (list)):
 
612
                            c_grp_cfg[k].extend(v)
 
613
                        elif isinstance(v, (basestring, str)):
 
614
                            c_grp_cfg[k].append(v)
 
615
                        else:
 
616
                            raise TypeError("Bad group member type %s" %
 
617
                                            type_utils.obj_name(v))
 
618
            elif isinstance(i, (str, basestring)):
 
619
                if i not in c_grp_cfg:
 
620
                    c_grp_cfg[i] = []
 
621
            else:
 
622
                raise TypeError("Unknown group name type %s" %
 
623
                                type_utils.obj_name(i))
 
624
        grp_cfg = c_grp_cfg
 
625
    groups = {}
 
626
    if isinstance(grp_cfg, (dict)):
 
627
        for (grp_name, grp_members) in grp_cfg.items():
 
628
            groups[grp_name] = util.uniq_merge_sorted(grp_members)
 
629
    else:
 
630
        raise TypeError(("Group config must be list, dict "
 
631
                         " or string types only and not %s") %
 
632
                        type_utils.obj_name(grp_cfg))
 
633
    return groups
 
634
 
 
635
 
 
636
# Normalizes a input group configuration
 
637
# which can be a comma seperated list of
 
638
# user names, or a list of string user names
 
639
# or a list of dictionaries with components
 
640
# that define the user config + 'name' (if
 
641
# a 'name' field does not exist then the
 
642
# default user is assumed to 'own' that
 
643
# configuration.
 
644
#
 
645
# The output is a dictionary of user
 
646
# names => user config which is the standard
 
647
# form used in the rest of cloud-init. Note
 
648
# the default user will have a special config
 
649
# entry 'default' which will be marked as true
 
650
# all other users will be marked as false.
 
651
def _normalize_users(u_cfg, def_user_cfg=None):
 
652
    if isinstance(u_cfg, (dict)):
 
653
        ad_ucfg = []
 
654
        for (k, v) in u_cfg.items():
 
655
            if isinstance(v, (bool, int, basestring, str, float)):
 
656
                if util.is_true(v):
 
657
                    ad_ucfg.append(str(k))
 
658
            elif isinstance(v, (dict)):
 
659
                v['name'] = k
 
660
                ad_ucfg.append(v)
 
661
            else:
 
662
                raise TypeError(("Unmappable user value type %s"
 
663
                                 " for key %s") % (type_utils.obj_name(v), k))
 
664
        u_cfg = ad_ucfg
 
665
    elif isinstance(u_cfg, (str, basestring)):
 
666
        u_cfg = util.uniq_merge_sorted(u_cfg)
 
667
 
 
668
    users = {}
 
669
    for user_config in u_cfg:
 
670
        if isinstance(user_config, (str, basestring, list)):
 
671
            for u in util.uniq_merge(user_config):
 
672
                if u and u not in users:
 
673
                    users[u] = {}
 
674
        elif isinstance(user_config, (dict)):
 
675
            if 'name' in user_config:
 
676
                n = user_config.pop('name')
 
677
                prev_config = users.get(n) or {}
 
678
                users[n] = util.mergemanydict([prev_config,
 
679
                                               user_config])
 
680
            else:
 
681
                # Assume the default user then
 
682
                prev_config = users.get('default') or {}
 
683
                users['default'] = util.mergemanydict([prev_config,
 
684
                                                       user_config])
 
685
        else:
 
686
            raise TypeError(("User config must be dictionary/list "
 
687
                             " or string types only and not %s") %
 
688
                            type_utils.obj_name(user_config))
 
689
 
 
690
    # Ensure user options are in the right python friendly format
 
691
    if users:
 
692
        c_users = {}
 
693
        for (uname, uconfig) in users.items():
 
694
            c_uconfig = {}
 
695
            for (k, v) in uconfig.items():
 
696
                k = k.replace('-', '_').strip()
 
697
                if k:
 
698
                    c_uconfig[k] = v
 
699
            c_users[uname] = c_uconfig
 
700
        users = c_users
 
701
 
 
702
    # Fixup the default user into the real
 
703
    # default user name and replace it...
 
704
    def_user = None
 
705
    if users and 'default' in users:
 
706
        def_config = users.pop('default')
 
707
        if def_user_cfg:
 
708
            # Pickup what the default 'real name' is
 
709
            # and any groups that are provided by the
 
710
            # default config
 
711
            def_user_cfg = def_user_cfg.copy()
 
712
            def_user = def_user_cfg.pop('name')
 
713
            def_groups = def_user_cfg.pop('groups', [])
 
714
            # Pickup any config + groups for that user name
 
715
            # that we may have previously extracted
 
716
            parsed_config = users.pop(def_user, {})
 
717
            parsed_groups = parsed_config.get('groups', [])
 
718
            # Now merge our extracted groups with
 
719
            # anything the default config provided
 
720
            users_groups = util.uniq_merge_sorted(parsed_groups, def_groups)
 
721
            parsed_config['groups'] = ",".join(users_groups)
 
722
            # The real config for the default user is the
 
723
            # combination of the default user config provided
 
724
            # by the distro, the default user config provided
 
725
            # by the above merging for the user 'default' and
 
726
            # then the parsed config from the user's 'real name'
 
727
            # which does not have to be 'default' (but could be)
 
728
            users[def_user] = util.mergemanydict([def_user_cfg,
 
729
                                                  def_config,
 
730
                                                  parsed_config])
 
731
 
 
732
    # Ensure that only the default user that we
 
733
    # found (if any) is actually marked as being
 
734
    # the default user
 
735
    if users:
 
736
        for (uname, uconfig) in users.items():
 
737
            if def_user and uname == def_user:
 
738
                uconfig['default'] = True
 
739
            else:
 
740
                uconfig['default'] = False
 
741
 
 
742
    return users
 
743
 
 
744
 
 
745
# Normalizes a set of user/users and group
 
746
# dictionary configuration into a useable
 
747
# format that the rest of cloud-init can
 
748
# understand using the default user
 
749
# provided by the input distrobution (if any)
 
750
# to allow for mapping of the 'default' user.
 
751
#
 
752
# Output is a dictionary of group names -> [member] (list)
 
753
# and a dictionary of user names -> user configuration (dict)
 
754
#
 
755
# If 'user' exists it will override
 
756
# the 'users'[0] entry (if a list) otherwise it will
 
757
# just become an entry in the returned dictionary (no override)
 
758
def normalize_users_groups(cfg, distro):
 
759
    if not cfg:
 
760
        cfg = {}
 
761
 
 
762
    users = {}
 
763
    groups = {}
 
764
    if 'groups' in cfg:
 
765
        groups = _normalize_groups(cfg['groups'])
 
766
 
 
767
    # Handle the previous style of doing this where the first user
 
768
    # overrides the concept of the default user if provided in the user: XYZ
 
769
    # format.
 
770
    old_user = {}
 
771
    if 'user' in cfg and cfg['user']:
 
772
        old_user = cfg['user']
 
773
        # Translate it into the format that is more useful
 
774
        # going forward
 
775
        if isinstance(old_user, (basestring, str)):
 
776
            old_user = {
 
777
                'name': old_user,
 
778
            }
 
779
        if not isinstance(old_user, (dict)):
 
780
            LOG.warn(("Format for 'user' key must be a string or "
 
781
                      "dictionary and not %s"), type_utils.obj_name(old_user))
 
782
            old_user = {}
 
783
 
 
784
    # If no old user format, then assume the distro
 
785
    # provides what the 'default' user maps to, but notice
 
786
    # that if this is provided, we won't automatically inject
 
787
    # a 'default' user into the users list, while if a old user
 
788
    # format is provided we will.
 
789
    distro_user_config = {}
 
790
    try:
 
791
        distro_user_config = distro.get_default_user()
 
792
    except NotImplementedError:
 
793
        LOG.warn(("Distro has not implemented default user "
 
794
                  "access. No distribution provided default user"
 
795
                  " will be normalized."))
 
796
 
 
797
    # Merge the old user (which may just be an empty dict when not
 
798
    # present with the distro provided default user configuration so
 
799
    # that the old user style picks up all the distribution specific
 
800
    # attributes (if any)
 
801
    default_user_config = util.mergemanydict([old_user, distro_user_config])
 
802
 
 
803
    base_users = cfg.get('users', [])
 
804
    if not isinstance(base_users, (list, dict, str, basestring)):
 
805
        LOG.warn(("Format for 'users' key must be a comma separated string"
 
806
                  " or a dictionary or a list and not %s"),
 
807
                 type_utils.obj_name(base_users))
 
808
        base_users = []
 
809
 
 
810
    if old_user:
 
811
        # Ensure that when user: is provided that this user
 
812
        # always gets added (as the default user)
 
813
        if isinstance(base_users, (list)):
 
814
            # Just add it on at the end...
 
815
            base_users.append({'name': 'default'})
 
816
        elif isinstance(base_users, (dict)):
 
817
            base_users['default'] = dict(base_users).get('default', True)
 
818
        elif isinstance(base_users, (str, basestring)):
 
819
            # Just append it on to be re-parsed later
 
820
            base_users += ",default"
 
821
 
 
822
    users = _normalize_users(base_users, default_user_config)
 
823
    return (users, groups)
 
824
 
 
825
 
 
826
# Given a user dictionary config it will
 
827
# extract the default user name and user config
 
828
# from that list and return that tuple or
 
829
# return (None, None) if no default user is
 
830
# found in the given input
 
831
def extract_default(users, default_name=None, default_config=None):
 
832
    if not users:
 
833
        users = {}
 
834
 
 
835
    def safe_find(entry):
 
836
        config = entry[1]
 
837
        if not config or 'default' not in config:
 
838
            return False
 
839
        else:
 
840
            return config['default']
 
841
 
 
842
    tmp_users = users.items()
 
843
    tmp_users = dict(itertools.ifilter(safe_find, tmp_users))
 
844
    if not tmp_users:
 
845
        return (default_name, default_config)
 
846
    else:
 
847
        name = tmp_users.keys()[0]
 
848
        config = tmp_users[name]
 
849
        config.pop('default', None)
 
850
        return (name, config)
 
851
 
 
852
 
 
853
def fetch(name):
 
854
    locs = importer.find_module(name,
 
855
                                ['', __name__],
 
856
                                ['Distro'])
 
857
    if not locs:
 
858
        raise ImportError("No distribution found for distro %s"
 
859
                           % (name))
 
860
    mod = importer.import_module(locs[0])
 
861
    cls = getattr(mod, 'Distro')
 
862
    return cls