5
from tornado import gen
7
from cloudfoundry import tactics
8
from cloudfoundry.config import (PENDING, COMPLETE, FAILED, RUNNING)
9
from cloudfoundry import utils
10
from cloudfoundry import tsort
12
from jujuclient import Environment
13
from deployer.service import Service
16
class StateEngine(object):
17
def __init__(self, config):
20
# expected is the state we are
23
# previous is the last (optional)
26
self.strategy = Strategy(self.env)
28
self.exec_lock = threading.Lock()
32
self._reset_strategy()
34
def _reset_strategy(self):
36
self.history.append(self.strategy)
37
self.strategy = Strategy(self.env)
44
api_address = c.get('juju.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'])
56
return self.env.status()
58
def sort_by_placement(self, service_names):
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)
71
def build_strategy(self, reality=None):
78
self.strategy.extend(self.build_services())
79
self.strategy.extend(self.build_relations())
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
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())
98
deletes = set(prev.get('services', {}).keys()) - set(
99
current.get('services', {}).keys()) & set(
100
real.get('Services', {}).keys())
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(
113
repo=self.config['server.repository']))
114
result.append(tactics.DeployTactic(service=service,
115
repo=self.config['server.repository']))
117
for service_name in deletes:
118
result.append(tactics.RemoveServiceTactic(service_name=service_name))
120
logging.debug("Build New %s", result)
123
def build_relations(self):
124
current = self.expected
130
# prev = self.previous
132
crels = utils.flatten_relations(current.get('relations', []))
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]))
138
# XXX skip deletes for now
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:
153
if not self.exec_lock.acquire(False):
156
logging.debug("Exec Strat %s", 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)
164
self._reset_strategy()
166
self.exec_lock.release()
169
def get_env(cls, env_name=None, api_address=None, user=None, password=None):
171
# use the explicit API address option
172
env = Environment(api_address)
173
env.login(user=user, password=password)
175
# use the local option which parses local jenv info
176
env = Environment.connect(env_name)
178
raise ValueError('Either name or api_address must be given')
182
class Strategy(list):
183
def __init__(self, env):
187
def find_next_tactic(self):
191
if tactic.state == FAILED:
193
if tactic.state == PENDING:
199
return bool(self.find_next_tactic())
202
def __call__(self, env=None):
206
current = self.find_next_tactic()
208
self.state = COMPLETE
212
if current.state == FAILED:
214
next = self.find_next_tactic()
215
if not next and current.state == COMPLETE:
216
self.state = COMPLETE
219
return "Strategy %s" % [str(t) for t in self]
221
def serialized(self):