~newell-jensen/maas/update-fix-1508741-1.9

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
# Copyright 2014-2015 Canonical Ltd.  This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).

"""Service to periodically refresh the boot images."""

from __future__ import (
    absolute_import,
    print_function,
    unicode_literals,
    )

str = None

__metaclass__ = type
__all__ = [
    "ImageDownloadService",
    ]


from datetime import timedelta

from provisioningserver.boot import tftppath
from provisioningserver.logger import get_maas_logger
from provisioningserver.rpc.boot_images import import_boot_images
from provisioningserver.rpc.exceptions import NoConnectionsAvailable
from provisioningserver.rpc.region import (
    GetBootSources,
    GetBootSourcesV2,
    GetProxies,
)
from provisioningserver.utils.twisted import (
    pause,
    retries,
)
from twisted.application.internet import TimerService
from twisted.internet.defer import (
    inlineCallbacks,
    returnValue,
)
from twisted.python import log
from twisted.spread.pb import NoSuchMethod


maaslog = get_maas_logger("boot_image_download_service")


class ImageDownloadService(TimerService, object):
    """Twisted service to periodically refresh ephemeral images.

    :param client_service: A `ClusterClientService` instance for talking
        to the region controller.
    :param reactor: An `IReactor` instance.
    """

    check_interval = timedelta(minutes=5).total_seconds()

    def __init__(self, client_service, reactor, cluster_uuid):
        # Call self.check() every self.check_interval.
        super(ImageDownloadService, self).__init__(
            self.check_interval, self.try_download)
        self.clock = reactor
        self.client_service = client_service
        self.uuid = cluster_uuid

    def try_download(self):
        """Wrap download attempts in something that catches Failures.

        Log the full error to the Twisted log, and a concise error to
        the maas log.
        """
        def download_failure(failure):
            log.err(failure, "Downloading images failed.")
            maaslog.error(
                "Failed to download images: %s", failure.getErrorMessage())

        return self.maybe_start_download().addErrback(download_failure)

    @inlineCallbacks
    def _get_boot_sources(self, client):
        """Gets the boot sources from the region."""
        try:
            sources = yield client(GetBootSourcesV2, uuid=self.uuid)
        except NoSuchMethod:
            # Region has not been upgraded to support the new call, use the
            # old call. The old call did not provide the new os selection
            # parameter. Region does not support boot source selection by os,
            # so its set too allow all operating systems.
            sources = yield client(GetBootSources, uuid=self.uuid)
            for source in sources['sources']:
                for selection in source['selections']:
                    selection['os'] = '*'
        returnValue(sources)

    @inlineCallbacks
    def _start_download(self):
        client = None
        # Retry a few times, since this service usually comes up before
        # the RPC service.
        for elapsed, remaining, wait in retries(15, 5, self.clock):
            try:
                client = self.client_service.getClient()
                break
            except NoConnectionsAvailable:
                yield pause(wait, self.clock)
        else:
            maaslog.error(
                "Can't initiate image download, no RPC connection to region.")
            return

        # Get sources from region
        sources = yield self._get_boot_sources(client)
        # Get http proxy from region
        proxies = yield client(GetProxies)

        def get_proxy_url(scheme):
            url = proxies.get(scheme)  # url is a ParsedResult.
            return None if url is None else url.geturl()

        yield import_boot_images(
            sources.get("sources"), get_proxy_url("http"),
            get_proxy_url("https"))

    @inlineCallbacks
    def maybe_start_download(self):
        """Check the time the last image refresh happened and initiate a new
        one if older than 15 minutes.
        """
        last_modified = tftppath.maas_meta_last_modified()
        if last_modified is None:
            yield self._start_download()
            return

        age_in_seconds = self.clock.seconds() - last_modified
        if age_in_seconds >= timedelta(minutes=15).total_seconds():
            yield self._start_download()