~johnsca/charms/trusty/cloudfoundry/reconciler-ui

« back to all changes in this revision

Viewing changes to reconciler/strategy.py

  • Committer: Whit Morriss
  • Date: 2014-10-13 06:50:17 UTC
  • Revision ID: whit.morriss@canonical.com-20141013065017-0feo2ku3yllymkol
reorg reconciler

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import logging
 
2
import threading
 
3
 
 
4
import tornado.ioloop
 
5
from tornado import gen
 
6
 
 
7
from cloudfoundry import tactics
 
8
from cloudfoundry.config import (PENDING, COMPLETE, FAILED, RUNNING)
 
9
from cloudfoundry import utils
 
10
from cloudfoundry import tsort
 
11
 
 
12
from jujuclient import Environment
 
13
from deployer.service import Service
 
14
 
 
15
 
 
16
class StateEngine(object):
 
17
    def __init__(self, config):
 
18
        self.config = config
 
19
        self._env = None
 
20
        # expected is the state we are
 
21
        # transitioning to
 
22
        self.expected = {}
 
23
        # previous is the last (optional)
 
24
        # expected state
 
25
        self.previous = {}
 
26
        self.strategy = Strategy(self.env)
 
27
        self.history = []
 
28
        self.exec_lock = threading.Lock()
 
29
 
 
30
    def reset(self):
 
31
        self.expected = {}
 
32
        self._reset_strategy()
 
33
 
 
34
    def _reset_strategy(self):
 
35
        if self.strategy:
 
36
            self.history.append(self.strategy)
 
37
        self.strategy = Strategy(self.env)
 
38
 
 
39
    @property
 
40
    def env(self):
 
41
        if self._env:
 
42
            return self._env
 
43
        c = self.config
 
44
        api_address = c.get('juju.api_address')
 
45
        if api_address:
 
46
            api_address = api_address.encode('utf8')
 
47
        self._env = self.get_env(
 
48
            env_name=c.get('juju.environment'),
 
49
            api_address=api_address,
 
50
            user=c['credentials.user'],
 
51
            password=c['credentials.password'])
 
52
        return self._env
 
53
 
 
54
    @property
 
55
    def real(self):
 
56
        return self.env.status()
 
57
 
 
58
    def sort_by_placement(self, service_names):
 
59
        dependencies = {}
 
60
        for name in self.expected['services'].keys():
 
61
            dependencies[name] = []
 
62
            service = Service(name, self.expected['services'][name])
 
63
            for placement in service.unit_placement:
 
64
                target = placement.split(':')[-1].split('=')[0]
 
65
                if not target.isdigit():
 
66
                    # only placement onto another named service matter
 
67
                    dependencies[name].append(target)
 
68
        ordering = tsort.sort(dependencies)
 
69
        return sorted(service_names, key=ordering.index)
 
70
 
 
71
    def build_strategy(self, reality=None):
 
72
        if reality is None:
 
73
            reality = self.real
 
74
        if reality is None:
 
75
            return []
 
76
 
 
77
        # Service Deltas
 
78
        self.strategy.extend(self.build_services())
 
79
        self.strategy.extend(self.build_relations())
 
80
 
 
81
    def build_services(self):
 
82
        # This should do a 3-way merge from the previous expected state
 
83
        # to current with a delta for reality
 
84
        current = self.expected
 
85
        result = []
 
86
        #if current:
 
87
        #    return result
 
88
 
 
89
        real = self.real
 
90
        prev = self.previous
 
91
        # Note: This deals with bundle format and the juju-core status output
 
92
        # formats, so the key names are a little different
 
93
        # XXX This is a very shallow diff in the sense that it only does
 
94
        # service name merges and not charm revision
 
95
        adds = set(current.get('services', {}).keys()) - set(real.get('Services', {}).keys())
 
96
        deletes = set()
 
97
        if prev:
 
98
            deletes = set(prev.get('services', {}).keys()) - set(
 
99
                current.get('services', {}).keys()) & set(
 
100
                real.get('Services', {}).keys())
 
101
 
 
102
        # XXX detect when we really want to do this,
 
103
        # ie, hash has changed or something
 
104
        result.append(tactics.GenerateTactic(
 
105
            repo=self.config['server.repository']))
 
106
        for service_name in self.sort_by_placement(adds):
 
107
            service = current['services'][service_name].copy()
 
108
            service['service_name'] = service_name
 
109
            branch = service.get('branch')
 
110
            if branch and branch.startswith('local:'):
 
111
                result.append(tactics.UpdateCharmTactic(
 
112
                    charm_url=branch,
 
113
                    repo=self.config['server.repository']))
 
114
            result.append(tactics.DeployTactic(service=service,
 
115
                                               repo=self.config['server.repository']))
 
116
 
 
117
        for service_name in deletes:
 
118
            result.append(tactics.RemoveServiceTactic(service_name=service_name))
 
119
 
 
120
        logging.debug("Build New %s", result)
 
121
        return result
 
122
 
 
123
    def build_relations(self):
 
124
        current = self.expected
 
125
        result = []
 
126
        if not current:
 
127
            return result
 
128
 
 
129
        real = self.real
 
130
        # prev = self.previous
 
131
 
 
132
        crels = utils.flatten_relations(current.get('relations', []))
 
133
        for rel in crels:
 
134
            if not utils.rel_exists(real, rel[0], rel[1]):
 
135
                result.append(tactics.AddRelationTactic(
 
136
                    endpoint_a=rel[0], endpoint_b=rel[1]))
 
137
 
 
138
        # XXX skip deletes for now
 
139
 
 
140
        return result
 
141
 
 
142
    def execute_strategy(self):
 
143
        # each strategy is a list of tactics,
 
144
        # we track the state of each of those
 
145
        # by mutating the tactic in the list
 
146
        # execute strategy shouldn't block
 
147
        # so if the lock is taken we continue
 
148
        # without modifying the strategy
 
149
        # and pick up in the next timeout cycle
 
150
        if not self.strategy.runnable:
 
151
            return
 
152
 
 
153
        if not self.exec_lock.acquire(False):
 
154
            return
 
155
        try:
 
156
            logging.debug("Exec Strat %s", self.strategy)
 
157
            self.strategy()
 
158
            if self.strategy.state == COMPLETE:
 
159
                self.previous = self.expected
 
160
            if self.strategy.runnable:
 
161
                tornado.ioloop.IOLoop.instance().add_callback(
 
162
                    self.execute_strategy)
 
163
            else:
 
164
                self._reset_strategy()
 
165
        finally:
 
166
            self.exec_lock.release()
 
167
 
 
168
    @classmethod
 
169
    def get_env(cls, env_name=None, api_address=None, user=None, password=None):
 
170
        if api_address:
 
171
            # use the explicit API address option
 
172
            env = Environment(api_address)
 
173
            env.login(user=user, password=password)
 
174
        elif env_name:
 
175
            # use the local option which parses local jenv info
 
176
            env = Environment.connect(env_name)
 
177
        else:
 
178
            raise ValueError('Either name or api_address must be given')
 
179
        return env
 
180
 
 
181
 
 
182
class Strategy(list):
 
183
    def __init__(self, env):
 
184
        self.state = PENDING
 
185
        self.env = env
 
186
 
 
187
    def find_next_tactic(self):
 
188
        if not self:
 
189
            return None
 
190
        for tactic in self:
 
191
            if tactic.state == FAILED:
 
192
                return None
 
193
            if tactic.state == PENDING:
 
194
                return tactic
 
195
        return None
 
196
 
 
197
    @property
 
198
    def runnable(self):
 
199
        return bool(self.find_next_tactic())
 
200
 
 
201
    @gen.coroutine
 
202
    def __call__(self, env=None):
 
203
        if env is None:
 
204
            env = self.env
 
205
 
 
206
        current = self.find_next_tactic()
 
207
        if not current:
 
208
            self.state = COMPLETE
 
209
        else:
 
210
            self.state = RUNNING
 
211
            current.run(env)
 
212
            if current.state == FAILED:
 
213
                self.state = FAILED
 
214
            next = self.find_next_tactic()
 
215
            if not next and current.state == COMPLETE:
 
216
                self.state = COMPLETE
 
217
 
 
218
    def __str__(self):
 
219
        return "Strategy %s" % [str(t) for t in self]
 
220
 
 
221
    def serialized(self):
 
222
        return self