~allenap/maas/repackage

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
#!/usr/bin/env python
# Copyright 2013 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Distutils installer for MAAS."""

# unicode_literals is *not* included here to *better* support setup
# scripts that work on both Python 2 and 3. Both expect their default
# string type, and are fussy about it.
from __future__ import (
    absolute_import,
    print_function,
    )

__version__ = "1.4"
__metaclass__ = type


from codecs import open
from contextlib import contextmanager
from distutils import (
    errors,
    log,
    )
from inspect import getsourcefile
from operator import methodcaller
from os import environ

import setuptools


@contextmanager
def envvar(name, value):
    """Context manager for temporarily setting an environment variable."""
    original = environ.get(name)
    environ[name] = value
    try:
        yield
    finally:
        if original is None:
            environ.pop(name, None)
        else:
            environ[name] = original


class install_deps(setuptools.Command):
    """Distutils/setuptools command to install package dependencies."""

    user_options = [
        ('list-from=', None,
         "file from which to read packages to install"),
    ]

    def initialize_options(self):
        self.list_from = "packages.txt"

    def gen_package_ops(self, package_lines):
        install = methodcaller("mark_install")
        delete = methodcaller("mark_delete", purge=True)
        for line in package_lines:
            if len(line) == 0:
                pass  # Ignore blank lines.
            elif line.startswith("#"):
                pass  # Ignore whole-line comments.
            elif line.endswith("-"):
                package_name = line.rstrip("-")
                yield package_name, delete
            else:
                package_name = line.rstrip("+")
                yield package_name, install

    def finalize_options(self):
        self.ensure_filename("list_from")
        self.packages = set()
        with open(self.list_from, "r", encoding="utf-8") as fd:
            lines = (line.strip() for line in fd)
            self.package_ops = dict(self.gen_package_ops(lines))
            self.packages = set(self.package_ops)

    def run(self):
        self.calculate_changes()
        if len(self.changes) > 0:
            self.apply_changes()

    def calculate_changes(self):
        from apt import Cache
        cache = Cache()
        for package_name, op in self.package_ops.iteritems():
            op(cache[package_name])
        self.changes = cache.get_changes()

    def get_packages_to_install(self):
        return self.packages.intersection(
            package.name for package in self.changes
            if package.marked_install or package.marked_upgrade)

    def get_packages_to_delete(self):
        return self.packages.intersection(
            package.name for package in self.changes
            if package.marked_delete)

    def apply_changes(self):
        """Use ``apt`` to check for package installs.

        Spawns ``apt-get`` if necessary.
        """
        to_install = {
            "%s+" % package
            for package in self.get_packages_to_install()
        }
        if len(to_install) == 0:
            log.info("system dependencies are all in place")

        to_delete = {
            "%s-" % package
            for package in self.get_packages_to_delete()
        }
        if len(to_delete) == 0:
            log.info("no forbidden system dependencies are installed")

        to_do = sorted(to_install | to_delete)
        if len(to_do) > 0:
            install = [
                "sudo", "apt-get", "--assume-yes",
                "--no-install-recommends", "--purge",
                "install",
            ]
            with envvar("DEBIAN_FRONTEND", "noninteractive"):
                self.spawn(install + to_do)


def check_settings(settings):
    """Check setup settings for obvious issues.

    - ``data_files`` should not be specified; setuptools ignores it.

    - ``include_package_data`` should not be specified; it has weird
      behaviour in setuptools.
    """
    if "data_files" in settings:
        raise errors.DistutilsOptionError(
            "data_files is ignored by setuptools")
    if "include_package_data" in settings:
        raise errors.DistutilsOptionError(
            "include_package_data does not work as you "
            "might expect with setuptools")


def setup(**kwargs):
    """MAAS-specific setup function.

    Determines sensible defaults for many parameters.
    """
    command_classes = {}

    try:
        import apt
    except ImportError:
        log.warn("apt not available (install 'python-apt'?)")
    else:
        command_classes["install_deps"] = install_deps
        apt  # Silence lint.

    settings = {
        "cmdclass": command_classes,
        "packages": setuptools.find_packages(".", exclude={"common"}),
        "version": __version__,
        "maintainer": "Canonical",
        "maintainer_email": "maas-devel@lists.launchpad.net",
        "url": "https://launchpad.net/maas",
        "license": "AGPLv3",
        "test_loader": "maastesting.loader:MAASTestLoader",
        "test_suite": ".",
        "tests_require": {"maas-testing >= %s" % __version__},
    }
    settings.update(kwargs)
    check_settings(settings)

    return setuptools.setup(**settings)


def configure(**settings):
    """Create a new ``setup.py`` file in the currnet directory.

    Uses the file in which this function is originally defined as a
    template, adding on a call to ``setup`` with the given ``settings``.
    """
    check_settings(settings)
    thisfile = getsourcefile(configure)
    with open(thisfile, "r", encoding="utf-8") as fin:
        template = fin.read()
    with open("setup.py", "w", encoding="utf-8") as fout:
        fout.write(template)
        fout.write(b"\n\nsetup(**%r)\n" % settings)


setup(**{'install_requires': ['celery >= 2.5.3', 'crochet >= 1.0.0', 'formencode >= 1.2.4', 'lxml >= 3.2.0', 'maas-apiclient >= 1.4', 'netaddr >= 0.7.7', 'netifaces >= 0.8', 'oops >= 0.0.10', 'oops-datedir-repo >= 0.0.15', 'oops-twisted >= 0.0.6', 'paramiko >= 1.10.1', 'pexpect >= 3.1', 'python-seamicroclient >= 0.1', 'PyYAML >= 3.1.0', 'simplejson >= 3.3.0', 'tempita >= 0.5.1', 'twisted >= 13.0.0', 'txamqp >= 0.6.1', 'zope.interface >= 4.0.5'], 'entry_points': {'console_scripts': set(['maas-cluster-worker = maascluster.entrypoints:celeryd', 'maas-cluster-twistd = maascluster.entrypoints:twistd', 'maas-probe-dhcp = maascluster.entrypoints:probe_dhcp', 'maas-provision = maascluster.entrypoints:provision'])}, 'name': 'maas-cluster', 'tests_require': ['fixtures >= 0.3.8', 'maas-testing >= 1.4', 'mock >= 1.0b1', 'rabbitfixture >= 0.3.5', 'testtools >= 0.9.32'], 'description': 'MAAS Provisioning Server (part of a Cluster Controller).'})