~barryprice/juju-deployer/LP1892423

« back to all changes in this revision

Viewing changes to deployer/deployment.py

  • Committer: Adam Gandelman
  • Date: 2013-09-03 20:44:14 UTC
  • mfrom: (69.3.45 darwin)
  • Revision ID: adamg@canonical.com-20130903204414-xsqqz2gp83dp5d2o
MergeĀ lp:juju-deployer/darwin.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
from base64 import b64encode
 
2
 
 
3
import logging
 
4
import pprint
 
5
import os
 
6
import yaml
 
7
 
 
8
from .charm import Charm
 
9
from .service import Service
 
10
from .relation import Endpoint
 
11
from .utils import path_join, yaml_dump, ErrorExit, resolve_include
 
12
 
 
13
 
 
14
class Deployment(object):
 
15
 
 
16
    log = logging.getLogger("deployer.deploy")
 
17
 
 
18
    def __init__(self, name, data, include_dirs, repo_path=""):
 
19
        self.name = name
 
20
        self.data = data
 
21
        self.include_dirs = include_dirs
 
22
        self.repo_path = repo_path
 
23
 
 
24
    @property
 
25
    def series(self):
 
26
        # Series could use a little help, charm series should be inferred
 
27
        # directly from a store url
 
28
        return self.data.get('series', 'precise')
 
29
 
 
30
    @property
 
31
    def series_path(self):
 
32
        return path_join(self.repo_path, self.series)
 
33
 
 
34
    def pretty_print(self):
 
35
        pprint.pprint(self.data)
 
36
 
 
37
    def get_service(self, name):
 
38
        if not name in self.data['services']:
 
39
            return
 
40
        return Service(name, self.data['services'][name])
 
41
 
 
42
    def get_services(self):
 
43
        for name, svc_data in self.data.get('services', {}).items():
 
44
            yield Service(name, svc_data)
 
45
 
 
46
    def get_relations(self):
 
47
        if 'relations' not in self.data:
 
48
            return
 
49
 
 
50
        # Strip duplicate rels
 
51
        seen = set()
 
52
 
 
53
        def check(a, b):
 
54
            k = tuple(sorted([a, b]))
 
55
            if k in seen:
 
56
                #self.log.warning(" Skipping duplicate relation %r" % (k,))
 
57
                return
 
58
            seen.add(k)
 
59
            return True
 
60
 
 
61
        # Support an ordered list of [endpoints]
 
62
        if isinstance(self.data['relations'], list):
 
63
            for end_a, end_b in self.data['relations']:
 
64
                # Allow shorthand of [end_a, [end_b, end_c]]
 
65
                if isinstance(end_b, list):
 
66
                    for eb in end_b:
 
67
                        if check(end_a, eb):
 
68
                            yield (end_a, eb)
 
69
                else:
 
70
                    if check(end_a, end_b):
 
71
                        yield (end_a, end_b)
 
72
            return
 
73
 
 
74
        # Legacy format (dictionary of dictionaries with weights)
 
75
        rels = {}
 
76
        for k, v in self.data['relations'].items():
 
77
            expanded = []
 
78
            for c in v['consumes']:
 
79
                expanded.append((k, c))
 
80
            by_weight = rels.setdefault(v.get('weight', 0), [])
 
81
            by_weight.extend(expanded)
 
82
        for k in sorted(rels, reverse=True):
 
83
            for r in rels[k]:
 
84
                if check(*r):
 
85
                    yield r
 
86
        #self.log.debug(
 
87
        #    "Found relations %s\n  %s" % (" ".join(map(str, seen))))
 
88
 
 
89
    def get_charms(self):
 
90
        for k, v in self.data.get('services', {}).items():
 
91
            yield Charm.from_service(k, self.series_path, v)
 
92
 
 
93
    def get_charm_for(self, svc_name):
 
94
        svc_data = self.data['services'][svc_name]
 
95
        return Charm.from_service(svc_name, self.series_path, svc_data)
 
96
 
 
97
    def fetch_charms(self, update=False, no_local_mods=False):
 
98
        if not os.path.exists(self.series_path):
 
99
            os.mkdir(self.series_path)
 
100
        for charm in self.get_charms():
 
101
            if charm.is_local():
 
102
                if charm.exists():
 
103
                    if no_local_mods:
 
104
                        if charm.is_modified():
 
105
                            self.log.warning(
 
106
                                "Charm %r has local modifications",
 
107
                                charm.path)
 
108
                            raise ErrorExit()
 
109
                    if update:
 
110
                        charm.update(build=True)
 
111
                    continue
 
112
            charm.fetch()
 
113
 
 
114
    def resolve(self, cli_overides=()):
 
115
        # Once we have charms definitions available, we can do verification
 
116
        # of config options.
 
117
        self.load_overrides(cli_overides)
 
118
        self.resolve_config()
 
119
        self.validate_relations()
 
120
 
 
121
    def load_overrides(self, cli_overrides=()):
 
122
        """Load overrides."""
 
123
        overrides = {}
 
124
        overrides.update(self.data.get('overrides', {}))
 
125
 
 
126
        for o in cli_overrides:
 
127
            key, value = o.split('=', 1)
 
128
            overrides[key] = value
 
129
 
 
130
        for k, v in overrides.iteritems():
 
131
            found = False
 
132
            for svc_name, svc_data in self.data['services'].items():
 
133
                charm = self.get_charm_for(svc_name)
 
134
                if k in charm.config:
 
135
                    if 'options' not in svc_data:
 
136
                        svc_data['options'] = {}
 
137
                    svc_data['options'][k] = v
 
138
                    found = True
 
139
            if not found:
 
140
                self.log.warning(
 
141
                    "Override %s does not match any charms", k)
 
142
 
 
143
    def resolve_config(self):
 
144
        """Load any lazy config values (includes), and verify config options.
 
145
        """
 
146
        self.log.debug("Resolving configuration")
 
147
        # XXX TODO, rename resolve, validate relations
 
148
        # against defined services
 
149
        for svc_name, svc_data in self.data.get('services', {}).items():
 
150
            if not 'options' in svc_data:
 
151
                continue
 
152
            charm = self.get_charm_for(svc_name)
 
153
            config = charm.config
 
154
            options = {}
 
155
 
 
156
            for k, v in svc_data['options'].items():
 
157
                if not k in config:
 
158
                    self.log.error(
 
159
                        "Invalid config charm %s %s=%s", charm.name, k, v)
 
160
                    raise ErrorExit()
 
161
                iv = self._resolve_include(svc_name, k, v)
 
162
                if iv is not None:
 
163
                    v = iv
 
164
                options[k] = v
 
165
            svc_data['options'] = options
 
166
 
 
167
    def _resolve_include(self, svc_name, k, v):
 
168
        for include_type in ["file", "base64"]:
 
169
            if (not isinstance(v, basestring)
 
170
                or not v.startswith(
 
171
                    "include-%s://" % include_type)):
 
172
                continue
 
173
            include, fname = v.split("://", 1)
 
174
            ip = resolve_include(fname, self.include_dirs)
 
175
            if ip is None:
 
176
                self.log.warning(
 
177
                    "Invalid config %s.%s include not found %s",
 
178
                    svc_name, k, v)
 
179
                continue
 
180
            with open(ip) as fh:
 
181
                v = fh.read()
 
182
                if include_type == "base64":
 
183
                    v = b64encode(v)
 
184
                return v
 
185
 
 
186
    def validate_relations(self):
 
187
        # Could extend to do interface matching against charms.
 
188
        services = dict([(s.name, s) for s in self.get_services()])
 
189
        for e_a, e_b in self.get_relations():
 
190
            for ep in [Endpoint(e_a), Endpoint(e_b)]:
 
191
                if not ep.service in services:
 
192
                    self.log.error(
 
193
                        ("Invalid relation in config,"
 
194
                         " service %s not found, rel %s"),
 
195
                        ep.service, "%s <-> %s" % (e_a, e_b))
 
196
                    raise ErrorExit()
 
197
 
 
198
    def save(self, path):
 
199
        with open(path, "w") as fh:
 
200
            fh.write(yaml_dump(self.data))
 
201
 
 
202
    @staticmethod
 
203
    def to_yaml(dumper, deployment):
 
204
        return dumper.represent_dict(deployment.data)
 
205
 
 
206
yaml.add_representer(Deployment, Deployment.to_yaml)