Merge lp:~therve/pyjuju/rapi-uuid into lp:pyjuju
- rapi-uuid
- Merge into trunk
Proposed by
Thomas Herve
Status: | Superseded |
---|---|
Proposed branch: | lp:~therve/pyjuju/rapi-uuid |
Merge into: | lp:pyjuju |
Diff against target: |
7904 lines (+6849/-143) 67 files modified
improv.py (+463/-0) juju/agents/api.py (+159/-0) juju/agents/keys/juju.crt (+19/-0) juju/agents/keys/juju.key (+27/-0) juju/charm/repository.py (+6/-2) juju/charm/tests/test_metadata.py (+9/-4) juju/lib/constants.py (+359/-0) juju/lib/serializer.py (+3/-1) juju/lib/service.py (+39/-40) juju/lib/tests/test_service.py (+44/-21) juju/lib/tests/test_websockets.py (+227/-0) juju/lib/websockets.py (+521/-0) juju/providers/common/cloudinit.py (+12/-0) juju/providers/common/tests/data/cloud_init_bootstrap (+24/-1) juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers (+24/-1) juju/providers/common/tests/test_cloudinit.py (+2/-1) juju/providers/dummy.py (+2/-2) juju/providers/local/__init__.py (+25/-4) juju/providers/local/agent.py (+60/-3) juju/providers/local/files.py (+35/-16) juju/providers/local/tests/test_agent.py (+9/-10) juju/providers/local/tests/test_files.py (+32/-20) juju/providers/local/tests/test_provider.py (+9/-3) juju/providers/tests/test_dummy.py (+4/-1) juju/rapi/__init__.py (+1/-0) juju/rapi/cmd/__init__.py (+1/-0) juju/rapi/cmd/add_relation.py (+43/-0) juju/rapi/cmd/add_unit.py (+24/-0) juju/rapi/cmd/config_get.py (+37/-0) juju/rapi/cmd/config_set.py (+35/-0) juju/rapi/cmd/constraints_get.py (+33/-0) juju/rapi/cmd/constraints_set.py (+26/-0) juju/rapi/cmd/debug_hooks.py (+72/-0) juju/rapi/cmd/deploy.py (+102/-0) juju/rapi/cmd/destroy_service.py (+42/-0) juju/rapi/cmd/export.py (+91/-0) juju/rapi/cmd/expose.py (+16/-0) juju/rapi/cmd/import_env.py (+98/-0) juju/rapi/cmd/remove_relation.py (+58/-0) juju/rapi/cmd/remove_unit.py (+30/-0) juju/rapi/cmd/resolved.py (+70/-0) juju/rapi/cmd/status.py (+471/-0) juju/rapi/cmd/terminate_machine.py (+41/-0) juju/rapi/cmd/tests/__init__.py (+1/-0) juju/rapi/cmd/unexpose.py (+16/-0) juju/rapi/context.py (+332/-0) juju/rapi/delta.py (+275/-0) juju/rapi/rest.py (+121/-0) juju/rapi/tests/common.py (+98/-0) juju/rapi/tests/test_context.py (+229/-0) juju/rapi/tests/test_delta.py (+423/-0) juju/rapi/tests/test_rest.py (+103/-0) juju/rapi/transport/__init__.py (+1/-0) juju/rapi/transport/tests/__init__.py (+1/-0) juju/rapi/transport/tests/test_ws.py (+205/-0) juju/rapi/transport/ws.py (+201/-0) juju/state/annotation.py (+289/-0) juju/state/errors.py (+6/-0) juju/state/initialize.py (+4/-0) juju/state/relation.py (+26/-4) juju/state/tests/test_annotation.py (+119/-0) juju/state/tests/test_initialize.py (+7/-2) juju/state/tests/test_topology.py (+1/-1) juju/state/topology.py (+5/-6) large.json (+398/-0) openstack.json (+396/-0) sample.json (+187/-0) |
To merge this branch: | bzr merge lp:~therve/pyjuju/rapi-uuid |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+143662@code.launchpad.net |
This proposal has been superseded by a proposal from 2013-01-17.
Commit message
Description of the change
The branch merges the changes from trunk including the new environment UUID, and expose that UUID in the first API message.
To post a comment you must log in.
Unmerged revisions
- 624. By Thomas Herve
-
Add env uuid to the API
- 623. By Thomas Herve
-
Merge from trunk
- 622. By Kapil Thangavelu
-
Merged rapi-annotation into rapi-rollup.
- 621. By Kapil Thangavelu
-
Merged rapi-annotation into rapi-rollup.
- 620. By Kapil Thangavelu
-
merge tls for ws
- 619. By Kapil Thangavelu
-
merge rapi-login
- 618. By Kapil Thangavelu
-
Merged export-import into rapi-rollup.
- 617. By Kapil Thangavelu
-
add openstack export
- 616. By Kapil Thangavelu
-
Merged svc-change-units into export-import.
- 615. By Kapil Thangavelu
-
Merged svc-change-units into export-import.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === added file 'improv.py' |
2 | --- improv.py 1970-01-01 00:00:00 +0000 |
3 | +++ improv.py 2013-01-17 11:02:31 +0000 |
4 | @@ -0,0 +1,463 @@ |
5 | +#!/usr/bin/which python |
6 | +# |
7 | +# Launch a juju api server with some sample data. |
8 | +# |
9 | +# |
10 | +# author: kapilt |
11 | +""" |
12 | + |
13 | +TODO: |
14 | + |
15 | + Subordinate service unit handling. |
16 | + |
17 | + - data generator on new relation or new unit |
18 | + - check scope of relation if subordinate, add sub units |
19 | + - on new unit, check service relations for sub and add sub unit |
20 | + - on removed unit, check service relations for sub and remove sub unit. |
21 | +""" |
22 | + |
23 | +import argparse |
24 | +import json |
25 | +import logging |
26 | +import os |
27 | +import random |
28 | +import shutil |
29 | +import tempfile |
30 | +import yaml |
31 | +import zookeeper |
32 | + |
33 | +from twisted.internet.defer import Deferred, inlineCallbacks, succeed |
34 | +from twisted.internet import reactor |
35 | + |
36 | +from juju.agents.api import APIEndpointAgent, keys_directory |
37 | +from juju.charm.publisher import CharmPublisher |
38 | +from juju.charm.repository import ( |
39 | + CharmURL, RemoteCharmRepository, CS_STORE_URL) |
40 | + |
41 | +from juju.environment.config import EnvironmentsConfig |
42 | +from juju.errors import JujuError |
43 | +from juju.state.environment import EnvironmentStateManager |
44 | + |
45 | +from juju.machine.constraints import ConstraintSet |
46 | +from juju.providers.dummy import MachineProvider, DummyMachine |
47 | +from juju.providers.common.files import FileStorage |
48 | + |
49 | +from juju.state.base import StateBase |
50 | +from juju.state.endpoint import RelationEndpoint |
51 | +from juju.state.errors import StopWatcher |
52 | +from juju.state.initialize import StateHierarchy |
53 | +from juju.state.machine import MachineStateManager |
54 | +from juju.state.placement import _unassigned_placement |
55 | +from juju.state.relation import RelationStateManager |
56 | +from juju.state.security import Principal |
57 | +from juju.state.service import ServiceStateManager |
58 | +from juju.state.utils import get_open_port, YAMLState |
59 | +from juju.tests.common import zookeeper_test_context |
60 | + |
61 | + |
62 | + |
63 | +from txzookeeper.managed import ManagedClient |
64 | + |
65 | +log = logging.getLogger("jitsu.scale") |
66 | + |
67 | + |
68 | +JUJU_ENV_CONF = """ |
69 | +environments: |
70 | + dervish: |
71 | + type: dummy |
72 | + default-series: precise |
73 | +""" |
74 | + |
75 | + |
76 | +class TempDir(object): |
77 | + |
78 | + def __enter__(self): |
79 | + self.temp_dir = tempfile.mkdtemp() |
80 | + return self.temp_dir |
81 | + |
82 | + def __exit__(self, exc_type, exc_value, traceback): |
83 | + if os.path.exists(self.temp_dir): |
84 | + shutil.rmtree(self.temp_dir) |
85 | + |
86 | + |
87 | +class Machine(DummyMachine): |
88 | + |
89 | + def __init__(self, instance_id, dns_name, private_dns, state): |
90 | + self.instance_id = instance_id |
91 | + self.dns_name = dns_name |
92 | + self.private_dns_name = private_dns |
93 | + self.state = state |
94 | + |
95 | + |
96 | +class MachineProvider(MachineProvider): |
97 | + |
98 | + def start_machine(self, machine_data, master=False): |
99 | + """Start a machine in the provider.""" |
100 | + |
101 | + machine = Machine( |
102 | + machine_data['machine_id'], |
103 | + machine_data['dns_name'], |
104 | + machine_data['priv_dns_name'], |
105 | + machine_data['state'] |
106 | + ) |
107 | + self._machines.append(machine) |
108 | + return succeed([machine]) |
109 | + |
110 | + |
111 | +class DataGenerator(StateBase): |
112 | + |
113 | + # weighted |
114 | + unit_states = ( |
115 | + 'pending', 'installed', 'install_error', |
116 | + 'started', 'started', 'started', 'started', |
117 | + 'start_error', 'stopped', 'stop_error') |
118 | + |
119 | + # weighted / 80% success |
120 | + relation_states = ('up', 'up', 'up', 'down', 'down') |
121 | + |
122 | + # weighted / 90% running |
123 | + machine_agent_states = ( |
124 | + 'pending', 'running', 'running', 'running') |
125 | + |
126 | + machine_states = ( |
127 | + 'running', 'pending', 'terminated', 'stopped') |
128 | + |
129 | + def __init__(self, client, provider): |
130 | + super(DataGenerator, self).__init__(client) |
131 | + self.address_block = "192.168.%s.%s" |
132 | + self.provider = provider |
133 | + self.addresses = set() |
134 | + self.seen = set() |
135 | + self.running = False |
136 | + self.finished = Deferred() |
137 | + |
138 | + @inlineCallbacks |
139 | + def init_service_unit(self, topo, service_id, unit_id, unit_name): |
140 | + |
141 | + relations = topo.get_relations_for_service(service_id) |
142 | + |
143 | + # Topology doesn't store subordinate status, this is a best |
144 | + # approximation, technically a subordinate can be the server. |
145 | + is_sub = bool( |
146 | + [r for r in relations if r['scope'] == 'container' \ |
147 | + and r['service']['role'] == 'client']) |
148 | + |
149 | + # Find the machine for the unit. |
150 | + if is_sub: |
151 | + container_id = topo.get_service_unit_principal(unit_id) |
152 | + assert container_id, "no container for subordinate %s" % unit_name |
153 | + container_svc_id = topo.get_service_unit_service(container_id) |
154 | + machine_id = topo.get_service_unit_machine( |
155 | + container_svc_id, container_id) |
156 | + else: |
157 | + machine_id = topo.get_service_unit_machine(service_id, unit_id) |
158 | + |
159 | + log.debug("Processing new unit %s:%s (sub: %s) on machine %s" % ( |
160 | + unit_name, unit_id, is_sub, machine_id)) |
161 | + |
162 | + # If the unit's machine is pending, bail. |
163 | + if machine_id is None: |
164 | + return |
165 | + |
166 | + machine = yield self.provider.get_machine(machine_id) |
167 | + if machine.state == 'pending': |
168 | + return |
169 | + |
170 | + self.seen.add(unit_id) |
171 | + |
172 | + # TODO for subs check principal is running. |
173 | + state = random.choice(self.unit_states) |
174 | + if state == 'pending': |
175 | + return |
176 | + |
177 | + node = YAMLState(self._client, "/units/%s" % unit_id) |
178 | + yield node.read() |
179 | + |
180 | + unit_address = self._new_address() |
181 | + |
182 | + w = {unit_name: yaml.safe_dump( |
183 | + {'state': state, 'state_variables': {}})} |
184 | + |
185 | + for r in relations: |
186 | + # simulate the unit's relation state enough for status. |
187 | + # TODO: subordinate relation state |
188 | + if r['scope'] == 'container': |
189 | + continue |
190 | + alive_path = "/relations/%s/%s/%s" % ( |
191 | + r['relation_id'], r['service']['role'], unit_id) |
192 | + yield self._client.create(alive_path) |
193 | + |
194 | + w[r['relation_id']] = yaml.safe_dump({ |
195 | + 'state': random.choice(self.relation_states), |
196 | + 'state_variables': {}}) |
197 | + |
198 | + d = {'public-address': unit_address, |
199 | + 'private-address': unit_address, |
200 | + 'workflow_state': w} |
201 | + |
202 | + node.update(d) |
203 | + |
204 | + try: |
205 | + yield self._client.create("/units/%s/agent" % unit_id, "") |
206 | + except zookeeper.NodeExistsException: |
207 | + log.debug("Unit: %s already has agent.." % unit_id) |
208 | + pass |
209 | + |
210 | + yield node.write() |
211 | + n, s = yield self._client.get("/units/%s" % unit_id) |
212 | + |
213 | + @inlineCallbacks |
214 | + def init_machine(self, mid): |
215 | + maddr = self._new_address() |
216 | + md = {'machine_id': mid, 'priv_dns_name': maddr, |
217 | + 'state': 'running', 'dns_name': maddr} |
218 | + yield self.provider.start_machine(md) |
219 | + node = YAMLState(self._client, "/machines/%s" % mid) |
220 | + yield node.read() |
221 | + node['provider-machine-id'] = mid |
222 | + yield node.write() |
223 | + if md['state'] == 'running': |
224 | + yield self._client.create("/machines/%s/agent" % mid, "") |
225 | + |
226 | + def _new_address(self): |
227 | + while 1: |
228 | + addr = self.address_block % ( |
229 | + random.randint(1, 256), random.randint(1, 256)) |
230 | + if addr in self.addresses: |
231 | + continue |
232 | + self.addresses.add(addr) |
233 | + return addr |
234 | + |
235 | + @inlineCallbacks |
236 | + def process(self, old, new): |
237 | + """ |
238 | + """ |
239 | + log.info("DataGen processing topology delta") |
240 | + if not self.running: |
241 | + self.finished.callback(True) |
242 | + raise StopWatcher() |
243 | + |
244 | + # Process new and removed machines. |
245 | + cur_machines = set(new.get_machines()) |
246 | + if old is None: |
247 | + old_machines = set() |
248 | + else: |
249 | + old_machines = set(old.get_machines()) |
250 | + |
251 | + for m in (cur_machines - old_machines): |
252 | + #log.info("Adding machine %s" % m) |
253 | + yield self.init_machine(m) |
254 | + |
255 | + removed = list(old_machines - cur_machines) |
256 | + p_machines = yield self.provider.get_machines(removed) |
257 | + yield self.provider.shutdown_machines(p_machines) |
258 | + |
259 | + # Process new units. |
260 | + cur_services = set(new.get_services()) |
261 | + cur_units = set() |
262 | + for s in cur_services: |
263 | + get_unit_name = lambda uid: ( |
264 | + s, uid, new.get_service_unit_name(s, uid)) |
265 | + cur_units.update( |
266 | + map(get_unit_name, new.get_service_units(s))) |
267 | + |
268 | + for su in cur_units: |
269 | + if su[1] in self.seen: |
270 | + continue |
271 | + #log.info("Adding unit %s" % (su,)) |
272 | + yield self.init_service_unit(new, *su) |
273 | + |
274 | + self.count += 1 |
275 | + |
276 | + def start(self): |
277 | + assert not self.running |
278 | + self.running = True |
279 | + self.count = 0 |
280 | + self.finished = Deferred() |
281 | + return self._watch_topology(self.process) |
282 | + |
283 | + def stop(self): |
284 | + if not self.running: |
285 | + return |
286 | + self.running = False |
287 | + return self.finished |
288 | + |
289 | + |
290 | +@inlineCallbacks |
291 | +def load(client, data): |
292 | + """load up an environment export (based on jitsu import) |
293 | + """ |
294 | + # Get state managers |
295 | + services = ServiceStateManager(client) |
296 | + relations = RelationStateManager(client) |
297 | + machines = MachineStateManager(client) |
298 | + |
299 | + # Resolve charms from charm store |
300 | + store = RemoteCharmRepository(CS_STORE_URL) |
301 | + charms = [] |
302 | + for s in data['services']: |
303 | + curl = CharmURL.infer(s['charm'], "precise") |
304 | + if s['charm'].startswith('local:'): |
305 | + raise JujuError("Local charm not supported") |
306 | + else: |
307 | + assert s['charm'].startswith('cs:') |
308 | + charm = yield store.find(curl) |
309 | + charms.append((s['charm'], charm)) |
310 | + |
311 | + # Upload charms to the environment |
312 | + with TempDir() as temp_dir: |
313 | + storage = FileStorage(temp_dir) |
314 | + publisher = CharmPublisher(client, storage) |
315 | + seen = set() |
316 | + for cid, c in charms: |
317 | + if cid in seen: |
318 | + continue |
319 | + seen.add(cid) |
320 | + log.info("Publishing charm %s", cid) |
321 | + yield publisher.add_charm( |
322 | + str(CharmURL.infer(cid, "precise")), |
323 | + c) |
324 | + charm_states = yield publisher.publish() |
325 | + |
326 | + # Index by url |
327 | + charm_map = dict([(ci[0], cs) for ci, cs in zip(charms, charm_states)]) |
328 | + constraint_set = ConstraintSet("dummy") |
329 | + constraint_set.register_generics([]) |
330 | + |
331 | + # Create services |
332 | + for s in data['services']: |
333 | + log.info("Creating service %s %s", s['name'], charm_map[s['charm']].id) |
334 | + constraints = constraint_set.load(s['constraints']) |
335 | + svc = yield services.add_service_state( |
336 | + s['name'], |
337 | + charm_map[s['charm']], |
338 | + constraints) |
339 | + config = yield svc.get_config() |
340 | + config.update(s['config']) |
341 | + yield config.write() |
342 | + |
343 | + # Create units |
344 | + log.info("Creating units %d", s['unit_count']) |
345 | + for i in range(s['unit_count']): |
346 | + unit = yield svc.add_unit_state() |
347 | + yield _unassigned_placement(client, machines, unit) |
348 | + |
349 | + # Add relations |
350 | + for r in data['relations']: |
351 | + if len(r) == 1: |
352 | + eps = [RelationEndpoint(*r[0])] |
353 | + else: |
354 | + eps = [RelationEndpoint(*r[0]), RelationEndpoint(*r[1])] |
355 | + log.info("Adding relation %s" % ( |
356 | + " ".join(map( |
357 | + lambda r: "%s:%s %s" % ( |
358 | + r.service_name, r.relation_name, r.relation_role), |
359 | + eps)))) |
360 | + yield relations.add_relation_state(*eps) |
361 | + |
362 | + |
363 | +@inlineCallbacks |
364 | +def initialize( |
365 | + api_port, api_secure, api_keys, zk_port, export_data, agent_dir): |
366 | + |
367 | + # Setup the state hierarchy. |
368 | + client = yield ManagedClient("localhost:%s" % zk_port).connect() |
369 | + principal = Principal("admin", "admin") |
370 | + constraints = {'arch': 'amd64', 'cpu': 1, |
371 | + 'ubuntu-series': 'precise', 'provider-type': 'dummy'} |
372 | + log.info("Initializing state") |
373 | + state = StateHierarchy( |
374 | + client, principal.get_token(), "0", constraints, "dummy") |
375 | + yield state.initialize() |
376 | + |
377 | + # Initialize the environment config state. |
378 | + config = EnvironmentsConfig() |
379 | + config.parse(JUJU_ENV_CONF) |
380 | + env = EnvironmentStateManager(client) |
381 | + yield env.set_config_state(config, "dervish") |
382 | + |
383 | + provider = MachineProvider("dervish", config.get_default()) |
384 | + |
385 | + # Attach the client |
386 | + yield principal.attach(client) |
387 | + |
388 | + # Load the tree |
389 | + yield load(client, export_data) |
390 | + |
391 | + # Hook up the runtime data simulator |
392 | + # Generates appropriate state for units, and machines. |
393 | + generator = DataGenerator(client, provider) |
394 | + yield generator.start() |
395 | + |
396 | + # Start the api agent |
397 | + agent = APIEndpointAgent() |
398 | + agent.configure({ |
399 | + "zookeeper_servers": "localhost:%s" % zk_port, |
400 | + "logfile": "-", |
401 | + "juju_directory": agent_dir, |
402 | + "port": api_port, |
403 | + "secure": api_secure, |
404 | + "keys": api_keys, |
405 | + "loglevel": "DEBUG", |
406 | + "session_file": os.path.join(agent_dir, "session.yml") |
407 | + }) |
408 | + log.info("Starting api agent..") |
409 | + yield agent.startService() |
410 | + |
411 | + |
412 | +@inlineCallbacks |
413 | +def run_one(func, *args, **kw): |
414 | + try: |
415 | + yield func(*args, **kw) |
416 | + except: |
417 | + import traceback |
418 | + traceback.print_exc() |
419 | + reactor.stop() |
420 | + |
421 | + |
422 | +def get_parser(): |
423 | + parser = argparse.ArgumentParser( |
424 | + description="Start an api server with data") |
425 | + parser.add_argument("-p", "--port", default=8081, type=int, |
426 | + help="Port to use") |
427 | + parser.add_argument("-s", "--secure", action='store_true', |
428 | + help="Enable secure websockets") |
429 | + parser.add_argument("-k", "--keys", type=keys_directory, metavar='PATH', |
430 | + help="Path to juju.key and juju.crt, used by secure " |
431 | + "websockets in place of the default ones") |
432 | + parser.add_argument("-f", "--file", required=True, |
433 | + type=argparse.FileType('r'), |
434 | + help="Environment json export") |
435 | + |
436 | + return parser |
437 | + |
438 | + |
439 | +def main(): |
440 | + parser = get_parser() |
441 | + options = parser.parse_args() |
442 | + log_options = { |
443 | + "level": logging.DEBUG, |
444 | + "format": "%(asctime)s %(name)s:%(levelname)s %(message)s"} |
445 | + |
446 | + logging.basicConfig(**log_options) |
447 | + zookeeper.set_debug_level(0) |
448 | + export_data = json.loads(options.file.read()) |
449 | + zk_port = get_open_port() |
450 | + |
451 | + |
452 | + if not "ZOOKEEPER_PATH" in os.environ: |
453 | + if not os.path.exists('/etc/zookeeper/conf'): |
454 | + raise ValueError("ZK not found") |
455 | + os.environ['ZOOKEEPER_PATH'] = "system" |
456 | + |
457 | + with TempDir() as agent_dir: |
458 | + with zookeeper_test_context(os.environ['ZOOKEEPER_PATH'], zk_port): |
459 | + reactor.callWhenRunning( |
460 | + run_one, initialize, |
461 | + options.port, options.secure, options.keys, |
462 | + zk_port, export_data, agent_dir) |
463 | + reactor.run() |
464 | + |
465 | +if __name__ == '__main__': |
466 | + main() |
467 | + |
468 | |
469 | === added file 'juju/agents/api.py' |
470 | --- juju/agents/api.py 1970-01-01 00:00:00 +0000 |
471 | +++ juju/agents/api.py 2013-01-17 11:02:31 +0000 |
472 | @@ -0,0 +1,159 @@ |
473 | +""" |
474 | +API endpoint agent. |
475 | +""" |
476 | +import argparse |
477 | +import os |
478 | +import logging |
479 | + |
480 | +from twisted.internet.defer import inlineCallbacks, returnValue |
481 | + |
482 | +from juju.agents.base import BaseAgent |
483 | +from juju.environment.config import EnvironmentsConfig |
484 | +from juju.errors import JujuError |
485 | +from juju.lib.websockets import WebSocketsResource |
486 | +from juju.rapi.transport.ws import WebSocketAPIFactory |
487 | +from juju.state.auth import make_ace |
488 | + |
489 | +from twisted.internet import ssl |
490 | +from twisted.web.static import Data |
491 | +from twisted.web.server import Site |
492 | +from zookeeper import NoNodeException |
493 | + |
494 | +log = logging.getLogger("juju.agents.api") |
495 | +KEYS_DEFAULT_PATH = os.path.abspath( |
496 | + os.path.join(os.path.dirname(__file__), 'keys')) |
497 | + |
498 | + |
499 | +def keys_directory(argument): |
500 | + """Validate the path argument and return *(private key, certificate)*.""" |
501 | + path = os.path.abspath(os.path.expanduser(argument)) |
502 | + if os.path.isdir(path): |
503 | + key = os.path.join(path, 'juju.key') |
504 | + cert = os.path.join(path, 'juju.crt') |
505 | + if os.path.isfile(key) and os.path.isfile(cert): |
506 | + # The order is significant here. These paths will be passed to |
507 | + # ``twisted.internet.ssl.DefaultOpenSSLContextFactory``. |
508 | + return key, cert |
509 | + msg = 'cannot find the juju.key and juju.crt files in %r' % argument |
510 | + raise argparse.ArgumentTypeError(msg) |
511 | + msg = '%r: no such directory' % argument |
512 | + raise argparse.ArgumentTypeError(msg) |
513 | + |
514 | + |
515 | +class APIEndpointAgent(BaseAgent): |
516 | + |
517 | + name = "juju-api" |
518 | + |
519 | + @inlineCallbacks |
520 | + def start(self): |
521 | + log.info("Starting api server") |
522 | + |
523 | + self.environment = yield self.configure_environment() |
524 | + self.provider = yield self._get_provider() |
525 | + env_uuid = yield self.global_settings_state.get_environment_id() |
526 | + log.debug("Received environment data.") |
527 | + root = Data("Juju API Server\n", "text/plain") |
528 | + self.ws_factory = WebSocketAPIFactory( |
529 | + self.config['zookeeper_servers'], self.provider, env_uuid) |
530 | + yield self.ws_factory.startFactory() |
531 | + |
532 | + root.putChild('ws', WebSocketsResource(self.ws_factory)) |
533 | + |
534 | + from twisted.internet import reactor |
535 | + |
536 | + port = int(self.config.get('port', 80)) |
537 | + site = Site(root) |
538 | + if self.config.get('secure'): |
539 | + keys = self.config['keys'] or keys_directory(KEYS_DEFAULT_PATH) |
540 | + context_factory = ssl.DefaultOpenSSLContextFactory(*keys) |
541 | + self.api_socket = reactor.listenSSL(port, site, context_factory) |
542 | + log.info("Secure websocket active") |
543 | + else: |
544 | + self.api_socket = reactor.listenTCP(port, site) |
545 | + log.info("Websocket active") |
546 | + |
547 | + def stop(self): |
548 | + if self.api_socket: |
549 | + self.ws_factory.stopFactory() |
550 | + self.api_socket.stopListening() |
551 | + self.api_socket = None |
552 | + |
553 | + def _get_provider(self): |
554 | + return self.environment.get_machine_provider() |
555 | + |
556 | + # TODO Refactory into common for provisioning agent |
557 | + @inlineCallbacks |
558 | + def configure_environment(self): |
559 | + """The provisioning agent configure its environment on start or change. |
560 | + |
561 | + The environment contains the configuration th agent needs to interact |
562 | + with its machine provider, in order to do its work. This configuration |
563 | + data is deployed lazily over an encrypted connection upon first usage. |
564 | + |
565 | + The agent waits for this data to exist before completing its startup. |
566 | + """ |
567 | + try: |
568 | + get_d, watch_d = self.client.get_and_watch("/environment") |
569 | + environment_data, stat = yield get_d |
570 | + except NoNodeException: |
571 | + # Wait till the environment node appears. play twisted gymnastics |
572 | + exists_d, watch_d = self.client.exists_and_watch("/environment") |
573 | + stat = yield exists_d |
574 | + if stat: |
575 | + environment = yield self.configure_environment() |
576 | + else: |
577 | + watch_d.addCallback( |
578 | + lambda result: self.configure_environment()) |
579 | + if not stat: |
580 | + environment = yield watch_d |
581 | + returnValue(environment) |
582 | + |
583 | + # Lazy initialize login nodes if not present. |
584 | + yield self._initialize_login() |
585 | + |
586 | + config = EnvironmentsConfig() |
587 | + config.parse(environment_data) |
588 | + returnValue(config.get_default()) |
589 | + |
590 | + @inlineCallbacks |
591 | + def _initialize_login(self): |
592 | + children = yield self.client.get_children("/") |
593 | + if "login" in children: |
594 | + return |
595 | + |
596 | + acls, stat = yield self.client.get_acl("/initialized") |
597 | + admin_acl = None |
598 | + |
599 | + for a in acls: |
600 | + if a['id'].startswith('admin'): |
601 | + admin_acl = a |
602 | + break |
603 | + |
604 | + yield self.client.create( |
605 | + "/login", acls=[make_ace(admin_acl['id'], all=True)]) |
606 | + |
607 | + def configure(self, options): |
608 | + super(APIEndpointAgent, self).configure(options) |
609 | + if not options.get("port"): |
610 | + msg = ("--port must be provided in the command line") |
611 | + raise JujuError(msg) |
612 | + if options.get("keys") and not options.get("secure"): |
613 | + msg = ("in addition to providing keys, a secure connection must " |
614 | + "be explicitly enabled using --secure") |
615 | + raise JujuError(msg) |
616 | + self.api_socket = None |
617 | + |
618 | + @classmethod |
619 | + def setup_options(cls, parser): |
620 | + super(APIEndpointAgent, cls).setup_options(parser) |
621 | + port = os.environ.get("JUJU_API_PORT", "") |
622 | + default_port = port.isdigit() and int(port) or 80 |
623 | + parser.add_argument( |
624 | + "--port", default=default_port, type=int) |
625 | + parser.add_argument("--secure", action='store_true') |
626 | + parser.add_argument("--keys", default=os.getenv('JUJU_API_KEYS'), |
627 | + type=keys_directory, metavar='PATH') |
628 | + return parser |
629 | + |
630 | +if __name__ == '__main__': |
631 | + APIEndpointAgent.run() |
632 | |
633 | === added directory 'juju/agents/keys' |
634 | === added file 'juju/agents/keys/juju.crt' |
635 | --- juju/agents/keys/juju.crt 1970-01-01 00:00:00 +0000 |
636 | +++ juju/agents/keys/juju.crt 2013-01-17 11:02:31 +0000 |
637 | @@ -0,0 +1,19 @@ |
638 | +-----BEGIN CERTIFICATE----- |
639 | +MIIDFjCCAf4CCQDVditilj3gADANBgkqhkiG9w0BAQUFADBNMQswCQYDVQQGEwJV |
640 | +SzEWMBQGA1UECAwNR3JlYXQgQnJpdGFpbjEXMBUGA1UECgwOQ2Fub25pY2FsIEx0 |
641 | +ZC4xDTALBgNVBAMMBEp1anUwHhcNMTIxMDI0MTEwODExWhcNMjIxMDIyMTEwODEx |
642 | +WjBNMQswCQYDVQQGEwJVSzEWMBQGA1UECAwNR3JlYXQgQnJpdGFpbjEXMBUGA1UE |
643 | +CgwOQ2Fub25pY2FsIEx0ZC4xDTALBgNVBAMMBEp1anUwggEiMA0GCSqGSIb3DQEB |
644 | +AQUAA4IBDwAwggEKAoIBAQC06CPCvKhkehtxrJ8aVI+M2SoPT1XAJWxe6pOLRkAu |
645 | +laD6QlE7krsu3ERFnOQnMMwH38zFKEszBWuNSbff2DSPR3SXH+mFg8XNjzg4d04P |
646 | +hObvAUAa3M/h5kUnJpZ4TrU8eYWGE/sW5AGRmw6lIL6mBP5uYgF0/wJv3Wsa53NC |
647 | +khztRSvmiDFO52f2OVx5mUU8LkpuTrwc0Z4UL76lqRGFOUizDL/SCdbX273GH63J |
648 | +4Qt/ex3kYLyOVtbmVJRXLXoVDVFTU6VzhW4LaN1ql+OpvntY7NHCXu+2+v/VJKyY |
649 | +9vFHY1AFF6xhSAvOtGPwreZkCj1nDJIq/NroCs35EPwlAgMBAAEwDQYJKoZIhvcN |
650 | +AQEFBQADggEBAJ2a1ZeArxwI6qGp5NjpD3JBElG0Oe0xKqQNM27eWzsjq3vzWrhg |
651 | +wtqZZdmUWbnbj/cw5GUxZY6DB56D8HPgjFecyVoC1ffY5RU5QNZndw7C74C5Rty9 |
652 | +3Rc8m0IB3z6obEHk+0yW4w2kk7g/T49uHpumfc39zYSHMBsQISXdTvulx0ualbwq |
653 | +nmDIC96alu2mL7wyvSWzNbqOhIJR45uznqADeBEWSVVjYVG6DW41Ss/dr0XHRQKh |
654 | +5kBzaC8TlN9lauOVqJv+kr9FlrLus0vnouYbxFn1Cjf4PPGzqk7zQBDKOty0pBkc |
655 | +3NUUa63qxDE7DIw5Knk45cvsOba9nTaLnBU= |
656 | +-----END CERTIFICATE----- |
657 | |
658 | === added file 'juju/agents/keys/juju.key' |
659 | --- juju/agents/keys/juju.key 1970-01-01 00:00:00 +0000 |
660 | +++ juju/agents/keys/juju.key 2013-01-17 11:02:31 +0000 |
661 | @@ -0,0 +1,27 @@ |
662 | +-----BEGIN RSA PRIVATE KEY----- |
663 | +MIIEowIBAAKCAQEAtOgjwryoZHobcayfGlSPjNkqD09VwCVsXuqTi0ZALpWg+kJR |
664 | +O5K7LtxERZzkJzDMB9/MxShLMwVrjUm339g0j0d0lx/phYPFzY84OHdOD4Tm7wFA |
665 | +GtzP4eZFJyaWeE61PHmFhhP7FuQBkZsOpSC+pgT+bmIBdP8Cb91rGudzQpIc7UUr |
666 | +5ogxTudn9jlceZlFPC5Kbk68HNGeFC++pakRhTlIswy/0gnW19u9xh+tyeELf3sd |
667 | +5GC8jlbW5lSUVy16FQ1RU1Olc4VuC2jdapfjqb57WOzRwl7vtvr/1SSsmPbxR2NQ |
668 | +BResYUgLzrRj8K3mZAo9ZwySKvza6ArN+RD8JQIDAQABAoIBADKfXxpoNmrfbHyT |
669 | +xHXWwdC1GDruhV1eW4P3+k+X2e/vOPuuwRJ9jdmgE46zR3jcA8wUSTXGf7yIQL9p |
670 | +qIV/9708TpjXej49UIWkFSRf5j0bgI/S9sBNl/JrwGAjQSnrRHXmv4F0Z+fQ58t5 |
671 | +61Az6IWGkjgLPsMhdOoeuJDvwWYYCjTKAGxa9SHxHjqPg4uWk5Ndc+vvnoxhGgkm |
672 | +6b183JR6zc80r6F9xJyXOvP8RSHG/CC/O9I7St004EsQjXlFUUtZTQlgawnrZv20 |
673 | +0YPwTExHyB4aj9gXDVEykdoku2X+7nytCRKzxXTJshLO3uiLSOsrX43hWX0tCiK7 |
674 | ++foX8yECgYEA51WO3gifcmIrckcQbcJXzyL0zFrOs3VahXCh7zN9E21hgnGLaj0Y |
675 | +Q8bkSJGKzCWVHUoIkqaFgCuj6PGj6rpLYtkJ8HqTWxEwUrCa6chkv43oY3h1vRjP |
676 | +XqmPy+Arvz3ypPgWuu+E3fd+uDvJp5o9wp6F+PXxacV75IBDIQFtMx0CgYEAyDIg |
677 | +ktImbNv6wqOz8Bw/7GsmxN9XLpwNJzV4sP6/bhHuxn44qhM6YoZTKblt58TujtQX |
678 | +D3/snSiXBJIci/AR1nQw76exFUOhNt30efCKxjz5gKSj1cQ5fN0e/5JT4OhZrjZD |
679 | +RmY0Za6+TO+p1796r7/RM3a4FVWuM0j/hFhA1qkCgYEAnzoXtSIwRXXmu6jIWRP8 |
680 | +hplj78jNH162KMTm4RMKkzWleTiYIk7dBcG/XWe4Jl3z7g4IKpCtsRPYpTy1e5ow |
681 | +y1/iZICqLnN9VF00s5d4KRUPEHBYcgjCq0CAtm6dDewguIX+dzrntDRnJ34XheI3 |
682 | +gl7EjiESwp/ob9YM3onx1M0CgYBCYOyQAgIfoijZFLJ68ML5PuYR7QPZPoDV+VLV |
683 | +TQJoGqYTRpK/QbTgKi207fjVGmUHEqe6ye3Ihkbcix3QAH/JnakELZP3uv7fVGTV |
684 | +cb5x0JHh57UzEecF0cOPdhM9xYzGyNMxqn3BIVmT5Ptpv+GMGIvjBoAvAsPJ2XBv |
685 | +j1ugUQKBgFDooiQAvqjJFJbnsRuYUirf1J//PR76BAu5dt+0573VgRkJ+OULOe4b |
686 | +mJs9Hi5XHCgPP4lmltz9EFsgcD1cFp0KFuadltYkB3Hp4uJZtUDC/pWT5myn9gv0 |
687 | +aIOJKuRux7yhcavIazgLDIo/ko14Sp7pnpBFh70ruRNbTuSYBIS/ |
688 | +-----END RSA PRIVATE KEY----- |
689 | |
690 | === modified file 'juju/charm/repository.py' |
691 | --- juju/charm/repository.py 2012-08-23 23:57:09 +0000 |
692 | +++ juju/charm/repository.py 2013-01-17 11:02:31 +0000 |
693 | @@ -129,7 +129,10 @@ |
694 | self.url_base, urllib.quote(charm_id)) |
695 | try: |
696 | host = urlparse.urlparse(url).hostname |
697 | - all_info = json.loads((yield getPage(url, contextFactory=VerifyingContextFactory(host)))) |
698 | + all_info = json.loads( |
699 | + (yield getPage( |
700 | + url, |
701 | + contextFactory=VerifyingContextFactory(host)))) |
702 | charm_info = all_info[charm_id] |
703 | for warning in charm_info.get("warnings", []): |
704 | log.warning("%s: %s", charm_id, warning) |
705 | @@ -152,7 +155,8 @@ |
706 | downloading_path = f.name |
707 | host = urlparse.urlparse(url).hostname |
708 | try: |
709 | - yield downloadPage(url, downloading_path, contextFactory=VerifyingContextFactory(host)) |
710 | + yield downloadPage(url, downloading_path, |
711 | + contextFactory=VerifyingContextFactory(host)) |
712 | except Error: |
713 | raise CharmNotFound(self.url_base, charm_url) |
714 | os.rename(downloading_path, cache_path) |
715 | |
716 | === modified file 'juju/charm/tests/test_metadata.py' |
717 | --- juju/charm/tests/test_metadata.py 2012-09-14 15:33:33 +0000 |
718 | +++ juju/charm/tests/test_metadata.py 2013-01-17 11:02:31 +0000 |
719 | @@ -222,22 +222,27 @@ |
720 | def test_provide_implicit_relation(self): |
721 | """Verify providing a juju-* reserved relation errors""" |
722 | with self.change_sample() as data: |
723 | - data["provides"] = {"juju-foo": {"interface": "juju-magic", "scope": "container"}} |
724 | + data["provides"] = {"juju-foo": { |
725 | + "interface": "juju-magic", |
726 | + "scope": "container"}} |
727 | |
728 | # verify relation level error |
729 | error = self.assertRaises(MetaDataError, |
730 | self.metadata.parse, self.sample) |
731 | - self.assertIn("Charm dummy attempting to provide relation in implicit relation namespace: juju-foo", |
732 | + self.assertIn(("Charm dummy attempting to provide relation " |
733 | + "in implicit relation namespace: juju-foo"), |
734 | str(error)) |
735 | |
736 | # verify interface level error |
737 | with self.change_sample() as data: |
738 | - data["provides"] = {"foo-rel": {"interface": "juju-magic", "scope": "container"}} |
739 | + data["provides"] = {"foo-rel": { |
740 | + "interface": "juju-magic", "scope": "container"}} |
741 | |
742 | error = self.assertRaises(MetaDataError, |
743 | self.metadata.parse, self.sample) |
744 | self.assertIn( |
745 | - "Charm dummy attempting to provide interface in implicit namespace: juju-magic (relation: foo-rel)", |
746 | + ("Charm dummy attempting to provide interface " |
747 | + "in implicit namespace: juju-magic (relation: foo-rel)"), |
748 | str(error)) |
749 | |
750 | def test_format(self): |
751 | |
752 | === added file 'juju/lib/constants.py' |
753 | --- juju/lib/constants.py 1970-01-01 00:00:00 +0000 |
754 | +++ juju/lib/constants.py 2013-01-17 11:02:31 +0000 |
755 | @@ -0,0 +1,359 @@ |
756 | +# -*- test-case-name: twisted.python.test.test_constants -*- |
757 | +# Copyright (c) Twisted Matrix Laboratories. |
758 | +# See LICENSE for details. |
759 | +# License: MIT |
760 | +# See MIT http://opensource.org/licenses/mit-license.php for details. |
761 | +""" |
762 | +Symbolic constant support, including collections and constants with text, |
763 | +numeric, and bit flag values. |
764 | +""" |
765 | + |
766 | +__all__ = [ |
767 | + 'NamedConstant', 'ValueConstant', 'FlagConstant', |
768 | + 'Names', 'Values', 'Flags'] |
769 | + |
770 | +from itertools import count |
771 | +from operator import and_, or_, xor |
772 | + |
773 | +_unspecified = object() |
774 | +_constantOrder = count().next |
775 | + |
776 | + |
777 | +class _Constant(object): |
778 | + """ |
779 | + @ivar _index: A C{int} allocated from a shared counter in order to keep |
780 | + track of the order in which L{_Constant}s are instantiated. |
781 | + |
782 | + @ivar name: A C{str} giving the name of this constant; only set once the |
783 | + constant is initialized by L{_ConstantsContainer}. |
784 | + |
785 | + @ivar _container: The L{_ConstantsContainer} subclass this constant belongs |
786 | + to; only set once the constant is initialized by that subclass. |
787 | + """ |
788 | + def __init__(self): |
789 | + self._index = _constantOrder() |
790 | + |
791 | + def __get__(self, oself, cls): |
792 | + """ |
793 | + Ensure this constant has been initialized before returning it. |
794 | + """ |
795 | + cls._initializeEnumerants() |
796 | + return self |
797 | + |
798 | + def __repr__(self): |
799 | + """ |
800 | + Return text identifying both which constant this is and which |
801 | + collection it belongs to. |
802 | + """ |
803 | + return "<%s=%s>" % (self._container.__name__, self.name) |
804 | + |
805 | + def _realize(self, container, name, value): |
806 | + """ |
807 | + Complete the initialization of this L{_Constant}. |
808 | + |
809 | + @param container: The L{_ConstantsContainer} subclass this constant is |
810 | + part of. |
811 | + |
812 | + @param name: The name of this constant in its container. |
813 | + |
814 | + @param value: The value of this constant; not used, as named constants |
815 | + have no value apart from their identity. |
816 | + """ |
817 | + self._container = container |
818 | + self.name = name |
819 | + |
820 | + |
821 | +class _EnumerantsInitializer(object): |
822 | + """ |
823 | + L{_EnumerantsInitializer} is a descriptor used to initialize a |
824 | + cache of objects representing named constants for a particular |
825 | + L{_ConstantsContainer} subclass. |
826 | + """ |
827 | + def __get__(self, oself, cls): |
828 | + """ |
829 | + Trigger the initialization of the enumerants cache on C{cls} and then |
830 | + return it. |
831 | + """ |
832 | + cls._initializeEnumerants() |
833 | + return cls._enumerants |
834 | + |
835 | + |
836 | +class _ConstantsContainer(object): |
837 | + """ |
838 | + L{_ConstantsContainer} is a class with attributes used as symbolic |
839 | + constants. It is up to subclasses to specify what kind of constants are |
840 | + allowed. |
841 | + |
842 | + @cvar _constantType: Specified by a L{_ConstantsContainer} subclass to |
843 | + specify the type of constants allowed by that subclass. |
844 | + |
845 | + @cvar _enumerantsInitialized: A C{bool} tracking whether C{_enumerants} has |
846 | + been initialized yet or not. |
847 | + |
848 | + @cvar _enumerants: A C{dict} mapping the names of constants (eg |
849 | + L{NamedConstant} instances) found in the class definition to those |
850 | + instances. This is initialized via the L{_EnumerantsInitializer} |
851 | + descriptor the first time it is accessed. |
852 | + """ |
853 | + _constantType = None |
854 | + |
855 | + _enumerantsInitialized = False |
856 | + _enumerants = _EnumerantsInitializer() |
857 | + |
858 | + def __new__(cls): |
859 | + """ |
860 | + Classes representing constants containers are not intended to be |
861 | + instantiated. |
862 | + |
863 | + The class object itself is used directly. |
864 | + """ |
865 | + raise TypeError("%s may not be instantiated." % (cls.__name__,)) |
866 | + |
867 | + def _initializeEnumerants(cls): |
868 | + """ |
869 | + Find all of the L{NamedConstant} instances in the definition of C{cls}, |
870 | + initialize them with constant values, and build a mapping from their |
871 | + names to them to attach to C{cls}. |
872 | + """ |
873 | + if not cls._enumerantsInitialized: |
874 | + constants = [] |
875 | + for (name, descriptor) in cls.__dict__.iteritems(): |
876 | + if isinstance(descriptor, cls._constantType): |
877 | + constants.append((descriptor._index, name, descriptor)) |
878 | + enumerants = {} |
879 | + constants.sort() |
880 | + for (index, enumerant, descriptor) in constants: |
881 | + value = cls._constantFactory(enumerant, descriptor) |
882 | + descriptor._realize(cls, enumerant, value) |
883 | + enumerants[enumerant] = descriptor |
884 | + # Replace the _enumerants descriptor with the result so |
885 | + # future access will go directly to the values. The |
886 | + # _enumerantsInitialized flag is still necessary because |
887 | + # NamedConstant.__get__ may also call this method. |
888 | + cls._enumerants = enumerants |
889 | + cls._enumerantsInitialized = True |
890 | + _initializeEnumerants = classmethod(_initializeEnumerants) |
891 | + |
892 | + def _constantFactory(cls, name, descriptor): |
893 | + """ |
894 | + Construct the value for a new constant to add to this container. |
895 | + |
896 | + @param name: The name of the constant to create. |
897 | + |
898 | + @return: L{NamedConstant} instances have no value apart from identity, |
899 | + so return a meaningless dummy value. |
900 | + """ |
901 | + return _unspecified |
902 | + _constantFactory = classmethod(_constantFactory) |
903 | + |
904 | + def lookupByName(cls, name): |
905 | + """ |
906 | + Retrieve a constant by its name or raise a C{ValueError} if there is no |
907 | + constant associated with that name. |
908 | + |
909 | + @param name: A C{str} giving the name of one of the constants |
910 | + defined by C{cls}. |
911 | + |
912 | + @raise ValueError: If C{name} is not the name of one of the constants |
913 | + defined by C{cls}. |
914 | + |
915 | + @return: The L{NamedConstant} associated with C{name}. |
916 | + """ |
917 | + if name in cls._enumerants: |
918 | + return getattr(cls, name) |
919 | + raise ValueError(name) |
920 | + lookupByName = classmethod(lookupByName) |
921 | + |
922 | + def iterconstants(cls): |
923 | + """ |
924 | + Iteration over a L{Names} subclass results in all of the constants it |
925 | + contains. |
926 | + |
927 | + @return: an iterator the elements of which are the L{NamedConstant} |
928 | + instances defined in the body of this L{Names} subclass. |
929 | + """ |
930 | + constants = cls._enumerants.values() |
931 | + constants.sort(key=lambda descriptor: descriptor._index) |
932 | + return iter(constants) |
933 | + iterconstants = classmethod(iterconstants) |
934 | + |
935 | + |
936 | +class NamedConstant(_Constant): |
937 | + """ |
938 | + L{NamedConstant} defines an attribute to be a named constant within a |
939 | + collection defined by a L{Names} subclass. |
940 | + |
941 | + L{NamedConstant} is only for use in the definition of L{Names} |
942 | + subclasses. Do not instantiate L{NamedConstant} elsewhere and do not |
943 | + subclass it. |
944 | + """ |
945 | + |
946 | + |
947 | +class Names(_ConstantsContainer): |
948 | + """ |
949 | + A L{Names} subclass contains constants which differ only in their names and |
950 | + identities. |
951 | + """ |
952 | + _constantType = NamedConstant |
953 | + |
954 | + |
955 | +class ValueConstant(_Constant): |
956 | + """ |
957 | + L{ValueConstant} defines an attribute to be a named constant within a |
958 | + collection defined by a L{Values} subclass. |
959 | + |
960 | + L{ValueConstant} is only for use in the definition of L{Values} subclasses. |
961 | + Do not instantiate L{ValueConstant} elsewhere and do not subclass it. |
962 | + """ |
963 | + def __init__(self, value): |
964 | + _Constant.__init__(self) |
965 | + self.value = value |
966 | + |
967 | + |
968 | +class Values(_ConstantsContainer): |
969 | + """ |
970 | + A L{Values} subclass contains constants which are associated with arbitrary |
971 | + values. |
972 | + """ |
973 | + _constantType = ValueConstant |
974 | + |
975 | + def lookupByValue(cls, value): |
976 | + """ |
977 | + Retrieve a constant by its value or raise a C{ValueError} if |
978 | + there is no constant associated with that value. |
979 | + |
980 | + @param value: The value of one of the constants defined by C{cls}. |
981 | + |
982 | + @raise ValueError: If C{value} is not the value of one of the constants |
983 | + defined by C{cls}. |
984 | + |
985 | + @return: The L{ValueConstant} associated with C{value}. |
986 | + """ |
987 | + for constant in cls.iterconstants(): |
988 | + if constant.value == value: |
989 | + return constant |
990 | + raise ValueError(value) |
991 | + lookupByValue = classmethod(lookupByValue) |
992 | + |
993 | + |
994 | +def _flagOp(op, left, right): |
995 | + """ |
996 | + Implement a binary operator for a L{FlagConstant} instance. |
997 | + |
998 | + @param op: A two-argument callable implementing the binary operation. For |
999 | + example, C{operator.or_}. |
1000 | + |
1001 | + @param left: The left-hand L{FlagConstant} instance. |
1002 | + @param right: The right-hand L{FlagConstant} instance. |
1003 | + |
1004 | + @return: A new L{FlagConstant} instance representing the result of the |
1005 | + operation. |
1006 | + """ |
1007 | + value = op(left.value, right.value) |
1008 | + names = op(left.names, right.names) |
1009 | + result = FlagConstant() |
1010 | + result._realize(left._container, names, value) |
1011 | + return result |
1012 | + |
1013 | + |
1014 | +class FlagConstant(_Constant): |
1015 | + """ |
1016 | + L{FlagConstant} defines an attribute to be a flag constant within a |
1017 | + collection defined by a L{Flags} subclass. |
1018 | + |
1019 | + L{FlagConstant} is only for use in the definition of L{Flags} subclasses. |
1020 | + Do not instantiate L{FlagConstant} elsewhere and do not subclass it. |
1021 | + """ |
1022 | + def __init__(self, value=_unspecified): |
1023 | + _Constant.__init__(self) |
1024 | + self.value = value |
1025 | + |
1026 | + def _realize(self, container, names, value): |
1027 | + """ |
1028 | + Complete the initialization of this L{FlagConstant}. |
1029 | + |
1030 | + This implementation differs from other C{_realize} implementations in |
1031 | + that a L{FlagConstant} may have several names which apply to it, due to |
1032 | + flags being combined with various operators. |
1033 | + |
1034 | + @param container: The L{Flags} subclass this constant is part of. |
1035 | + |
1036 | + @param names: When a single-flag value is being initialized, a |
1037 | + C{str} giving the name of that flag. This is the case |
1038 | + which happens when a L{Flags} subclass is being |
1039 | + initialized and L{FlagConstant} instances from its body |
1040 | + are being realized. Otherwise, a C{set} of C{str} giving |
1041 | + names of all the flags set on this L{FlagConstant} |
1042 | + instance. This is the case when two flags are combined |
1043 | + using C{|}, for example. |
1044 | + """ |
1045 | + if isinstance(names, str): |
1046 | + name = names |
1047 | + names = set([names]) |
1048 | + elif len(names) == 1: |
1049 | + (name,) = names |
1050 | + else: |
1051 | + name = "{" + ",".join(sorted(names)) + "}" |
1052 | + _Constant._realize(self, container, name, value) |
1053 | + self.value = value |
1054 | + self.names = names |
1055 | + |
1056 | + def __or__(self, other): |
1057 | + """ |
1058 | + Define C{|} on two L{FlagConstant} instances to create a new |
1059 | + L{FlagConstant} instance with all flags set in either instance set. |
1060 | + """ |
1061 | + return _flagOp(or_, self, other) |
1062 | + |
1063 | + def __and__(self, other): |
1064 | + """ |
1065 | + Define C{&} on two L{FlagConstant} instances to create a new |
1066 | + L{FlagConstant} instance with only flags set in both instances set. |
1067 | + """ |
1068 | + return _flagOp(and_, self, other) |
1069 | + |
1070 | + def __xor__(self, other): |
1071 | + """ |
1072 | + Define C{^} on two L{FlagConstant} instances to create a new |
1073 | + L{FlagConstant} instance with only flags set on exactly one instance |
1074 | + set. |
1075 | + """ |
1076 | + return _flagOp(xor, self, other) |
1077 | + |
1078 | + |
1079 | + def __invert__(self): |
1080 | + """ |
1081 | + Define C{~} on a L{FlagConstant} instance to create a new |
1082 | + L{FlagConstant} instance with all flags not set on this instance set. |
1083 | + """ |
1084 | + result = FlagConstant() |
1085 | + result._realize(self._container, set(), 0) |
1086 | + for flag in self._container.iterconstants(): |
1087 | + if flag.value & self.value == 0: |
1088 | + result |= flag |
1089 | + return result |
1090 | + |
1091 | + |
1092 | +class Flags(Values): |
1093 | + """ |
1094 | + A L{Flags} subclass contains constants which can be combined using the |
1095 | + common bitwise operators (C{|}, C{&}, etc) similar to a I{bitvector} from a |
1096 | + language like C. |
1097 | + """ |
1098 | + _constantType = FlagConstant |
1099 | + |
1100 | + _value = 1 |
1101 | + |
1102 | + def _constantFactory(cls, name, descriptor): |
1103 | + """ |
1104 | + For L{FlagConstant} instances with no explicitly defined value, assign |
1105 | + the next power of two as its value. |
1106 | + """ |
1107 | + if descriptor.value is _unspecified: |
1108 | + value = cls._value |
1109 | + cls._value <<= 1 |
1110 | + else: |
1111 | + value = descriptor.value |
1112 | + cls._value = value << 1 |
1113 | + return value |
1114 | + _constantFactory = classmethod(_constantFactory) |
1115 | |
1116 | === modified file 'juju/lib/serializer.py' |
1117 | --- juju/lib/serializer.py 2012-09-14 15:33:33 +0000 |
1118 | +++ juju/lib/serializer.py 2013-01-17 11:02:31 +0000 |
1119 | @@ -2,20 +2,22 @@ |
1120 | from yaml import dump as _dump |
1121 | from yaml import load as _load |
1122 | |
1123 | + |
1124 | def dump(value): |
1125 | return _dump(value, Dumper=CSafeDumper) |
1126 | |
1127 | yaml_dump = dump |
1128 | |
1129 | + |
1130 | def load(value): |
1131 | return _load(value, Loader=CSafeLoader) |
1132 | |
1133 | yaml_load = load |
1134 | |
1135 | + |
1136 | def yaml_mark_with_path(path, mark): |
1137 | # yaml c ext, cant be modded, convert to capture path |
1138 | return Mark( |
1139 | path, mark.index, |
1140 | mark.line, mark.column, |
1141 | mark.buffer, mark.pointer) |
1142 | - |
1143 | |
1144 | === modified file 'juju/lib/service.py' |
1145 | --- juju/lib/service.py 2012-10-05 16:05:39 +0000 |
1146 | +++ juju/lib/service.py 2013-01-17 11:02:31 +0000 |
1147 | @@ -1,4 +1,4 @@ |
1148 | -from twisted.internet.defer import inlineCallbacks |
1149 | +from twisted.internet.defer import inlineCallbacks, Deferred |
1150 | from twisted.internet.threads import deferToThread |
1151 | |
1152 | from juju.errors import ServiceError |
1153 | @@ -9,20 +9,31 @@ |
1154 | |
1155 | def _check_call(args, env=None, output_path=None): |
1156 | if not output_path: |
1157 | - output_path = os.devnull |
1158 | - |
1159 | - with open(output_path, "a") as f: |
1160 | + f = open(os.devnull, "a") |
1161 | + else: |
1162 | + check_path = os.path.exists(output_path) and output_path \ |
1163 | + or os.path.dirname(output_path) |
1164 | + if os.access(check_path, os.W_OK): |
1165 | + f = open(output_path, "a") |
1166 | + else: |
1167 | + raise ValueError( |
1168 | + "Output path inaccessible %s" % output_path) |
1169 | + try: |
1170 | return subprocess.check_call( |
1171 | args, |
1172 | stdout=f.fileno(), stderr=f.fileno(), |
1173 | env=env) |
1174 | + finally: |
1175 | + f.close() |
1176 | |
1177 | |
1178 | def _cat(filename, use_sudo=False): |
1179 | args = ("cat", filename) |
1180 | if use_sudo and not os.access(filename, os.R_OK): |
1181 | args = ("sudo",) + args |
1182 | - |
1183 | + elif os.path.exists(filename): |
1184 | + with open(filename) as fh: |
1185 | + return (0, fh.read()) |
1186 | p = subprocess.Popen( |
1187 | args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
1188 | stdout_data, _ = p.communicate() |
1189 | @@ -37,28 +48,17 @@ |
1190 | specifies the location of the pid file that is used to track this service. |
1191 | |
1192 | """ |
1193 | - def __init__(self, name, pidfile, use_sudo=False): |
1194 | + def __init__(self, name, pidfile, output_path, use_sudo=False): |
1195 | self._name = name |
1196 | self._use_sudo = use_sudo |
1197 | self._description = None |
1198 | self._environ = None |
1199 | self._command = None |
1200 | self._daemon = True |
1201 | - self._output_path = None |
1202 | - |
1203 | + self.output_path = output_path |
1204 | self._pid_path = pidfile |
1205 | self._pid = None |
1206 | |
1207 | - @property |
1208 | - def output_path(self): |
1209 | - if self._output_path is not None: |
1210 | - return self._output_path |
1211 | - return "/tmp/%s.output" % self._name |
1212 | - |
1213 | - @output_path.setter |
1214 | - def output_path(self, path): |
1215 | - self._output_path = path |
1216 | - |
1217 | def set_description(self, description): |
1218 | self._description = description |
1219 | |
1220 | @@ -70,6 +70,9 @@ |
1221 | environ[k] = str(v) |
1222 | self._environ = environ |
1223 | |
1224 | + def set_output_path(self, output_path): |
1225 | + self._output_path = output_path |
1226 | + |
1227 | def set_command(self, command): |
1228 | if self._daemon: |
1229 | if "--pidfile" not in command: |
1230 | @@ -77,13 +80,12 @@ |
1231 | else: |
1232 | # pid file is in command (consume it for get_pid) |
1233 | idx = command.index("--pidfile") |
1234 | - self._pid_path = command[idx+1] |
1235 | - |
1236 | + self._pid_path = command[idx + 1] |
1237 | self._command = command |
1238 | |
1239 | @inlineCallbacks |
1240 | def _trash_output(self): |
1241 | - if os.path.exists(self.output_path): |
1242 | + if self.output_path and os.path.exists(self.output_path): |
1243 | # Just using os.unlink will fail when we're running TEST_SUDO |
1244 | # tests which hit this code path (because root will own |
1245 | # self.output_path) |
1246 | @@ -93,6 +95,7 @@ |
1247 | yield self._call("rm", "-f", self._pid_path) |
1248 | |
1249 | def _call(self, *args, **kwargs): |
1250 | + # sudo even with -E will strip pythonpath so pass it to the command. |
1251 | if self._use_sudo: |
1252 | if self._environ: |
1253 | _args = ["%s=%s" % (k, v) for k, v in self._environ.items()] |
1254 | @@ -105,28 +108,29 @@ |
1255 | return deferToThread(_check_call, args, env=self._environ, |
1256 | output_path=self.output_path) |
1257 | |
1258 | - def install(self): |
1259 | - if self._command is None: |
1260 | - raise ServiceError("Cannot manage agent: %s no command set" % ( |
1261 | - self._name)) |
1262 | - |
1263 | @inlineCallbacks |
1264 | def start(self): |
1265 | if (yield self.is_running()): |
1266 | + return |
1267 | raise ServiceError( |
1268 | "%s already running: pid (%s)" % ( |
1269 | self._name, self.get_pid())) |
1270 | - |
1271 | - if not self.is_installed(): |
1272 | - yield self.install() |
1273 | - |
1274 | yield self._trash_output() |
1275 | yield self._call(*self._command, output_path=self.output_path) |
1276 | + yield self._sleep(0.2) |
1277 | + |
1278 | + def _sleep(self, delay): |
1279 | + """Non-blocking sleep.""" |
1280 | + from twisted.internet import reactor |
1281 | + deferred = Deferred() |
1282 | + reactor.callLater(delay, deferred.callback, None) |
1283 | + return deferred |
1284 | |
1285 | @inlineCallbacks |
1286 | def destroy(self): |
1287 | - if (yield self.is_running()): |
1288 | - yield self._call("kill", self.get_pid()) |
1289 | + if not (yield self.is_running()): |
1290 | + return |
1291 | + yield self._call("kill", str(self.get_pid())) |
1292 | yield self._trash_output() |
1293 | |
1294 | def get_pid(self): |
1295 | @@ -139,12 +143,10 @@ |
1296 | if r != 0: |
1297 | return None |
1298 | |
1299 | - # verify that pid is a number but leave |
1300 | - # it as a string suitable for passing to kill |
1301 | - if not data.strip().isdigit(): |
1302 | + data = data.strip() |
1303 | + if not data.isdigit(): |
1304 | return None |
1305 | - pid = data.strip() |
1306 | - self._pid = pid |
1307 | + self._pid = int(data) |
1308 | return self._pid |
1309 | |
1310 | def is_running(self): |
1311 | @@ -155,6 +157,3 @@ |
1312 | if not os.path.exists(proc_file): |
1313 | return False |
1314 | return True |
1315 | - |
1316 | - def is_installed(self): |
1317 | - return False |
1318 | |
1319 | === modified file 'juju/lib/tests/test_service.py' |
1320 | --- juju/lib/tests/test_service.py 2012-10-05 16:05:39 +0000 |
1321 | +++ juju/lib/tests/test_service.py 2013-01-17 11:02:31 +0000 |
1322 | @@ -4,8 +4,10 @@ |
1323 | from juju.lib.mocker import MATCH, KWARGS |
1324 | from juju.lib.service import TwistedDaemonService |
1325 | from juju.lib.lxc.tests.test_lxc import uses_sudo |
1326 | +from juju.state.utils import get_open_port |
1327 | |
1328 | import os |
1329 | +import subprocess |
1330 | |
1331 | |
1332 | class TwistedDaemonServiceTest(TestCase): |
1333 | @@ -13,22 +15,16 @@ |
1334 | @inlineCallbacks |
1335 | def setUp(self): |
1336 | yield super(TwistedDaemonServiceTest, self).setUp() |
1337 | - self.setup_service() |
1338 | - |
1339 | - def setup_service(self): |
1340 | - service = TwistedDaemonService("juju-machine-agent", |
1341 | - "/tmp/.juju-test.pid", |
1342 | - use_sudo=False) |
1343 | + self.pid_path = self.makeFile() |
1344 | + self.output_path = self.makeFile() |
1345 | + service = TwistedDaemonService( |
1346 | + "juju-machine-agent", self.pid_path, |
1347 | + self.output_path, use_sudo=False) |
1348 | service.set_description("Juju machine agent") |
1349 | service.set_environ({"JUJU_MACHINE_ID": 0}) |
1350 | service.set_command(["/bin/true", ]) |
1351 | self.service = service |
1352 | |
1353 | - if os.path.exists("/tmp/.juju-test.pid"): |
1354 | - os.remove("/tmp/.juju-test.pid") |
1355 | - |
1356 | - return service |
1357 | - |
1358 | def setup_mock(self): |
1359 | self.check_call = self.mocker.replace("subprocess.check_call") |
1360 | |
1361 | @@ -45,13 +41,18 @@ |
1362 | self.setup_mock() |
1363 | self.mock_call("/bin/true") |
1364 | self.mocker.replay() |
1365 | - |
1366 | yield self.service.start() |
1367 | |
1368 | + def test_simple_service_failure(self): |
1369 | + self.service.set_command(["/bin/false"]) |
1370 | + return self.assertFailure( |
1371 | + self.service.start(), |
1372 | + subprocess.CalledProcessError) |
1373 | + |
1374 | def test_set_output_path(self): |
1375 | # defaults work |
1376 | - self.assertEqual(self.service.output_path, |
1377 | - "/tmp/juju-machine-agent.output") |
1378 | + self.assertEqual( |
1379 | + self.service.output_path, self.output_path) |
1380 | # override works |
1381 | self.service.output_path = "/tmp/valid.log" |
1382 | self.assertEqual(self.service.output_path, "/tmp/valid.log") |
1383 | @@ -82,23 +83,30 @@ |
1384 | def test_webservice_start(self): |
1385 | # test using a real twisted service (with --pidfile) |
1386 | # arg ordering matters here so we set pidfile manually |
1387 | + |
1388 | + pid_file = self.makeFile() |
1389 | + log_file = self.makeFile() |
1390 | + web_dir = self.makeDir() |
1391 | + |
1392 | self.service.set_command([ |
1393 | "env", "twistd", |
1394 | - "--pidfile", "/tmp/.juju-test.pid", |
1395 | - "--logfile", "/tmp/.juju-test.log", |
1396 | + "--pidfile", pid_file, |
1397 | + "--logfile", log_file, |
1398 | "web", |
1399 | - "--port", "9871", |
1400 | - "--path", "/lib", |
1401 | + "--port", str(get_open_port()), |
1402 | + "--path", web_dir, |
1403 | ]) |
1404 | |
1405 | yield self.service.start() |
1406 | - yield self.sleep(0.5) |
1407 | + yield self.sleep(0.2) |
1408 | + |
1409 | self.assertTrue(self.service.get_pid()) |
1410 | self.assertTrue(self.service.is_running()) |
1411 | - self.assertTrue(os.path.exists("/tmp/.juju-test.pid")) |
1412 | + self.assertTrue(os.path.exists(pid_file)) |
1413 | yield self.service.destroy() |
1414 | yield self.sleep(0.1) |
1415 | - self.assertFalse(os.path.exists("/tmp/.juju-test.pid")) |
1416 | + self.assertFalse(self.service.is_running()) |
1417 | + self.assertFalse(os.path.exists(pid_file)) |
1418 | |
1419 | @uses_sudo |
1420 | @inlineCallbacks |
1421 | @@ -114,3 +122,18 @@ |
1422 | contents = fh.read() |
1423 | self.assertIn("PYTHONPATH=foo2", contents) |
1424 | self.assertIn("JUJU_MACHINE_ID=0", contents) |
1425 | + |
1426 | + @uses_sudo |
1427 | + @inlineCallbacks |
1428 | + def test_command_tuple(self): |
1429 | + self.service.set_daemon(False) |
1430 | + self.service.output_path = self.makeFile() |
1431 | + self.service.set_environ( |
1432 | + {"JUJU_MACHINE_ID": 0, "PYTHONPATH": "foo2"}) |
1433 | + self.service.set_command(("/usr/bin/env",)) |
1434 | + yield self.service.start() |
1435 | + |
1436 | + with open(self.service.output_path) as fh: |
1437 | + contents = fh.read() |
1438 | + self.assertIn("PYTHONPATH=foo2", contents) |
1439 | + self.assertIn("JUJU_MACHINE_ID=0", contents) |
1440 | |
1441 | === added file 'juju/lib/tests/test_websockets.py' |
1442 | --- juju/lib/tests/test_websockets.py 1970-01-01 00:00:00 +0000 |
1443 | +++ juju/lib/tests/test_websockets.py 2013-01-17 11:02:31 +0000 |
1444 | @@ -0,0 +1,227 @@ |
1445 | +from twisted.trial import unittest |
1446 | + |
1447 | +from juju.lib.websockets import (_CONTROLS, _make_accept, _mask, |
1448 | + _make_hybi07_frame, _parse_hybi07_frames) |
1449 | + |
1450 | +""" |
1451 | +The WebSockets Protocol, according to RFC 6455 |
1452 | +(http://tools.ietf.org/html/rfc6455). When "RFC" is mentioned, it refers to |
1453 | +this RFC. Some tests reference HyBi-10 |
1454 | +(http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-10) or |
1455 | +HyBi-07 (http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07), |
1456 | +which are drafts of RFC 6455. |
1457 | +""" |
1458 | + |
1459 | + |
1460 | +class TestKeys(unittest.TestCase): |
1461 | + |
1462 | + def test_make_accept_rfc(self): |
1463 | + """ |
1464 | + L{_make_accept} makes responses according to the RFC. |
1465 | + """ |
1466 | + |
1467 | + key = "dGhlIHNhbXBsZSBub25jZQ==" |
1468 | + |
1469 | + self.assertEqual(_make_accept(key), "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") |
1470 | + |
1471 | + def test_make_accept_wikipedia(self): |
1472 | + """ |
1473 | + L{_make_accept} makes responses according to Wikipedia. |
1474 | + """ |
1475 | + |
1476 | + key = "x3JJHMbDL1EzLkh9GBhXDw==" |
1477 | + |
1478 | + self.assertEqual(_make_accept(key), "HSmrc0sMlYUkAGmm5OPpG2HaGWk=") |
1479 | + |
1480 | + |
1481 | +class TestHyBi07Helpers(unittest.TestCase): |
1482 | + """ |
1483 | + HyBi-07 is best understood as a large family of helper functions which |
1484 | + work together, somewhat dysfunctionally, to produce a mediocre |
1485 | + Thanksgiving every other year. |
1486 | + """ |
1487 | + |
1488 | + def test_mask_noop(self): |
1489 | + """ |
1490 | + Blank keys perform a no-op mask. |
1491 | + """ |
1492 | + |
1493 | + key = "\x00\x00\x00\x00" |
1494 | + self.assertEqual(_mask("Test", key), "Test") |
1495 | + |
1496 | + def test_mask_noop_long(self): |
1497 | + """ |
1498 | + Blank keys perform a no-op mask regardless of the length of the input. |
1499 | + """ |
1500 | + |
1501 | + key = "\x00\x00\x00\x00" |
1502 | + self.assertEqual(_mask("LongTest", key), "LongTest") |
1503 | + |
1504 | + def test_mask_noop_odd(self): |
1505 | + """ |
1506 | + Masking works even when the data to be masked isn't a multiple of four |
1507 | + in length. |
1508 | + """ |
1509 | + |
1510 | + key = "\x00\x00\x00\x00" |
1511 | + self.assertEqual(_mask("LongestTest", key), "LongestTest") |
1512 | + |
1513 | + def test_mask_hello(self): |
1514 | + """ |
1515 | + A sample mask for "Hello" according to RFC 6455, 5.7. |
1516 | + """ |
1517 | + |
1518 | + key = "\x37\xfa\x21\x3d" |
1519 | + self.assertEqual(_mask("Hello", key), "\x7f\x9f\x4d\x51\x58") |
1520 | + |
1521 | + def test_parse_hybi07_unmasked_text(self): |
1522 | + """ |
1523 | + A sample unmasked frame of "Hello" from HyBi-10, 4.7. |
1524 | + """ |
1525 | + |
1526 | + frame = "\x81\x05Hello" |
1527 | + frames, buf = _parse_hybi07_frames(frame) |
1528 | + self.assertEqual(len(frames), 1) |
1529 | + self.assertEqual(frames[0], (_CONTROLS.NORMAL, "Hello")) |
1530 | + self.assertEqual(buf, "") |
1531 | + |
1532 | + def test_parse_hybi07_masked_text(self): |
1533 | + """ |
1534 | + A sample masked frame of "Hello" from HyBi-10, 4.7. |
1535 | + """ |
1536 | + |
1537 | + frame = "\x81\x857\xfa!=\x7f\x9fMQX" |
1538 | + frames, buf = _parse_hybi07_frames(frame) |
1539 | + self.assertEqual(len(frames), 1) |
1540 | + self.assertEqual(frames[0], (_CONTROLS.NORMAL, "Hello")) |
1541 | + self.assertEqual(buf, "") |
1542 | + |
1543 | + def test_parse_hybi07_unmasked_text_fragments(self): |
1544 | + """ |
1545 | + Fragmented masked packets are handled. |
1546 | + |
1547 | + From HyBi-10, 4.7. |
1548 | + """ |
1549 | + |
1550 | + frame = "\x01\x03Hel\x80\x02lo" |
1551 | + frames, buf = _parse_hybi07_frames(frame) |
1552 | + self.assertEqual(len(frames), 2) |
1553 | + self.assertEqual(frames[0], (_CONTROLS.NORMAL, "Hel")) |
1554 | + self.assertEqual(frames[1], (_CONTROLS.NORMAL, "lo")) |
1555 | + self.assertEqual(buf, "") |
1556 | + |
1557 | + def test_parse_hybi07_ping(self): |
1558 | + """ |
1559 | + Ping packets are decoded. |
1560 | + |
1561 | + From HyBi-10, 4.7. |
1562 | + """ |
1563 | + |
1564 | + frame = "\x89\x05Hello" |
1565 | + frames, buf = _parse_hybi07_frames(frame) |
1566 | + self.assertEqual(len(frames), 1) |
1567 | + self.assertEqual(frames[0], (_CONTROLS.PING, "Hello")) |
1568 | + self.assertEqual(buf, "") |
1569 | + |
1570 | + def test_parse_hybi07_pong(self): |
1571 | + """ |
1572 | + Pong packets are decoded. |
1573 | + |
1574 | + From HyBi-10, 4.7. |
1575 | + """ |
1576 | + |
1577 | + frame = "\x8a\x05Hello" |
1578 | + frames, buf = _parse_hybi07_frames(frame) |
1579 | + self.assertEqual(len(frames), 1) |
1580 | + self.assertEqual(frames[0], (_CONTROLS.PONG, "Hello")) |
1581 | + self.assertEqual(buf, "") |
1582 | + |
1583 | + def test_parse_hybi07_close_empty(self): |
1584 | + """ |
1585 | + A HyBi-07 close packet may have no body. In that case, it decodes with |
1586 | + the generic error code 1000, and has no particular justification or |
1587 | + error message. |
1588 | + """ |
1589 | + |
1590 | + frame = "\x88\x00" |
1591 | + frames, buf = _parse_hybi07_frames(frame) |
1592 | + self.assertEqual(len(frames), 1) |
1593 | + self.assertEqual(frames[0], ( |
1594 | + _CONTROLS.CLOSE, (1000, "No reason given"))) |
1595 | + self.assertEqual(buf, "") |
1596 | + |
1597 | + def test_parse_hybi07_close_reason(self): |
1598 | + """ |
1599 | + A HyBi-07 close packet must have its first two bytes be a numeric |
1600 | + error code, and may optionally include trailing text explaining why |
1601 | + the connection was closed. |
1602 | + """ |
1603 | + |
1604 | + frame = "\x88\x0b\x03\xe8No reason" |
1605 | + frames, buf = _parse_hybi07_frames(frame) |
1606 | + self.assertEqual(len(frames), 1) |
1607 | + self.assertEqual(frames[0], (_CONTROLS.CLOSE, (1000, "No reason"))) |
1608 | + self.assertEqual(buf, "") |
1609 | + |
1610 | + def test_parse_hybi07_partial_no_length(self): |
1611 | + """ |
1612 | + Partial frames are stored for later decoding. |
1613 | + """ |
1614 | + |
1615 | + frame = "\x81" |
1616 | + frames, buf = _parse_hybi07_frames(frame) |
1617 | + self.assertEqual(len(frames), 0) |
1618 | + self.assertEqual(buf, "\x81") |
1619 | + |
1620 | + def test_parse_hybi07_partial_truncated_length_int(self): |
1621 | + """ |
1622 | + Partial frames are stored for later decoding, even if they are cut on |
1623 | + length boundaries. |
1624 | + """ |
1625 | + |
1626 | + frame = "\x81\xfe" |
1627 | + frames, buf = _parse_hybi07_frames(frame) |
1628 | + self.assertEqual(len(frames), 0) |
1629 | + self.assertEqual(buf, "\x81\xfe") |
1630 | + |
1631 | + def test_parse_hybi07_partial_truncated_length_double(self): |
1632 | + """ |
1633 | + Partial frames are stored for later decoding, even if they are marked |
1634 | + as being extra-long. |
1635 | + """ |
1636 | + |
1637 | + frame = "\x81\xff" |
1638 | + frames, buf = _parse_hybi07_frames(frame) |
1639 | + self.assertEqual(len(frames), 0) |
1640 | + self.assertEqual(buf, "\x81\xff") |
1641 | + |
1642 | + def test_parse_hybi07_partial_no_data(self): |
1643 | + """ |
1644 | + Partial frames with full headers but no data are stored for later |
1645 | + decoding. |
1646 | + """ |
1647 | + |
1648 | + frame = "\x81\x05" |
1649 | + frames, buf = _parse_hybi07_frames(frame) |
1650 | + self.assertEqual(len(frames), 0) |
1651 | + self.assertEqual(buf, "\x81\x05") |
1652 | + |
1653 | + def test_parse_hybi07_partial_truncated_data(self): |
1654 | + """ |
1655 | + Partial frames with full headers and partial data are stored for later |
1656 | + decoding. |
1657 | + """ |
1658 | + |
1659 | + frame = "\x81\x05Hel" |
1660 | + frames, buf = _parse_hybi07_frames(frame) |
1661 | + self.assertEqual(len(frames), 0) |
1662 | + self.assertEqual(buf, "\x81\x05Hel") |
1663 | + |
1664 | + def test_make_hybi07_hello(self): |
1665 | + """ |
1666 | + L{_make_hybi07_frame} makes valid HyBi-07 packets. |
1667 | + """ |
1668 | + |
1669 | + frame = "\x81\x05Hello" |
1670 | + buf = _make_hybi07_frame("Hello") |
1671 | + self.assertEqual(frame, buf) |
1672 | |
1673 | === added file 'juju/lib/websockets.py' |
1674 | --- juju/lib/websockets.py 1970-01-01 00:00:00 +0000 |
1675 | +++ juju/lib/websockets.py 2013-01-17 11:02:31 +0000 |
1676 | @@ -0,0 +1,521 @@ |
1677 | +# -*- test-case-name: twisted.web.test.test_websockets -*- |
1678 | +# Copyright (c) 2011-2012 Oregon State University Open Source Lab |
1679 | +# 2011-2012 Corbin Simpson |
1680 | +# 2012 Twisted Matrix Laboratories |
1681 | +# |
1682 | +# See MIT http://opensource.org/licenses/mit-license.php for details. |
1683 | + |
1684 | +""" |
1685 | +The WebSockets protocol (RFC 6455), provided as a resource which wraps a |
1686 | +factory. |
1687 | +""" |
1688 | + |
1689 | +__all__ = ("WebSocketsResource",) |
1690 | + |
1691 | +from base64 import b64encode, b64decode |
1692 | +from hashlib import sha1 |
1693 | +from struct import pack, unpack |
1694 | + |
1695 | +from twisted.protocols.policies import ProtocolWrapper, WrappingFactory |
1696 | +from twisted.python import log |
1697 | +from juju.lib.constants import NamedConstant, Names |
1698 | + |
1699 | +from twisted.web.resource import IResource, NoResource |
1700 | +from twisted.web.server import NOT_DONE_YET |
1701 | +from zope.interface import implements |
1702 | + |
1703 | +import logging |
1704 | + |
1705 | +ws_log = logging.getLogger('juju.ws') |
1706 | + |
1707 | + |
1708 | +class _WSException(Exception): |
1709 | + """ |
1710 | + Internal exception for control flow inside the WebSockets frame parser. |
1711 | + """ |
1712 | + |
1713 | +# Control frame specifiers. Some versions of WS have control signals sent |
1714 | +# in-band. Adorable, right? |
1715 | + |
1716 | + |
1717 | +class _CONTROLS(Names): |
1718 | + """ |
1719 | + Control frame specifiers. |
1720 | + """ |
1721 | + |
1722 | + NORMAL = NamedConstant() |
1723 | + CLOSE = NamedConstant() |
1724 | + PING = NamedConstant() |
1725 | + PONG = NamedConstant() |
1726 | + |
1727 | +_opcode_types = { |
1728 | + 0x0: _CONTROLS.NORMAL, |
1729 | + 0x1: _CONTROLS.NORMAL, |
1730 | + 0x2: _CONTROLS.NORMAL, |
1731 | + 0x8: _CONTROLS.CLOSE, |
1732 | + 0x9: _CONTROLS.PING, |
1733 | + 0xa: _CONTROLS.PONG, |
1734 | +} |
1735 | + |
1736 | +_opcode_for_type = { |
1737 | + _CONTROLS.NORMAL: 0x1, |
1738 | + _CONTROLS.CLOSE: 0x8, |
1739 | + _CONTROLS.PING: 0x9, |
1740 | + _CONTROLS.PONG: 0xa, |
1741 | +} |
1742 | + |
1743 | +_encoders = { |
1744 | + "base64": b64encode, |
1745 | +} |
1746 | + |
1747 | +_decoders = { |
1748 | + "base64": b64decode, |
1749 | +} |
1750 | + |
1751 | +# Authentication for WS. |
1752 | + |
1753 | +# The GUID for WebSockets, from RFC 6455. |
1754 | +_WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" |
1755 | + |
1756 | + |
1757 | +def _make_accept(key): |
1758 | + """ |
1759 | + Create an "accept" response for a given key. |
1760 | + |
1761 | + This dance is expected to somehow magically make WebSockets secure. |
1762 | + |
1763 | + @type key: C{str} |
1764 | + @param key: The key to respond to. |
1765 | + |
1766 | + @rtype: C{str} |
1767 | + @return: An encoded response. |
1768 | + """ |
1769 | + |
1770 | + return sha1("%s%s" % (key, _WS_GUID)).digest().encode("base64").strip() |
1771 | + |
1772 | +# Frame helpers. |
1773 | +# Separated out to make unit testing a lot easier. |
1774 | +# Frames are bonghits in newer WS versions, so helpers are appreciated. |
1775 | + |
1776 | + |
1777 | +def _mask(buf, key): |
1778 | + """ |
1779 | + Mask or unmask a buffer of bytes with a masking key. |
1780 | + |
1781 | + @type buf: C{str} |
1782 | + @param buf: A buffer of bytes. |
1783 | + |
1784 | + @type key: C{str} |
1785 | + @param key: The masking key. Must be exactly four bytes. |
1786 | + |
1787 | + @rtype: C{str} |
1788 | + @return: A masked buffer of bytes. |
1789 | + """ |
1790 | + |
1791 | + # This is super-secure, I promise~ |
1792 | + key = [ord(i) for i in key] |
1793 | + buf = list(buf) |
1794 | + for i, char in enumerate(buf): |
1795 | + buf[i] = chr(ord(char) ^ key[i % 4]) |
1796 | + return "".join(buf) |
1797 | + |
1798 | + |
1799 | +def _make_hybi07_frame(buf, _opcode=_CONTROLS.NORMAL): |
1800 | + """ |
1801 | + Make a HyBi-07 frame. |
1802 | + |
1803 | + This function always creates unmasked frames, and attempts to use the |
1804 | + smallest possible lengths. |
1805 | + |
1806 | + @type buf: C{str} |
1807 | + @param buf: A buffer of bytes. |
1808 | + |
1809 | + @type _opcode: C{_CONTROLS} |
1810 | + @param _opcode: Which type of frame to create. |
1811 | + |
1812 | + @rtype: C{str} |
1813 | + @return: A packed frame. |
1814 | + """ |
1815 | + |
1816 | + if len(buf) > 0xffff: |
1817 | + length = "\x7f%s" % pack(">Q", len(buf)) |
1818 | + elif len(buf) > 0x7d: |
1819 | + length = "\x7e%s" % pack(">H", len(buf)) |
1820 | + else: |
1821 | + length = chr(len(buf)) |
1822 | + |
1823 | + # Always make a normal packet. |
1824 | + header = chr(0x80 | _opcode_for_type[_opcode]) |
1825 | + frame = "%s%s%s" % (header, length, buf) |
1826 | + return frame |
1827 | + |
1828 | + |
1829 | +def _parse_hybi07_frames(buf): |
1830 | + """ |
1831 | + Parse HyBi-07 frames in a highly compliant manner. |
1832 | + |
1833 | + @type buf: C{str} |
1834 | + @param buf: A buffer of bytes. |
1835 | + |
1836 | + @rtype: C{list} |
1837 | + @return: A list of frames. |
1838 | + """ |
1839 | + |
1840 | + start = 0 |
1841 | + frames = [] |
1842 | + |
1843 | + while True: |
1844 | + # If there's not at least two bytes in the buffer, bail. |
1845 | + if len(buf) - start < 2: |
1846 | + break |
1847 | + |
1848 | + # Grab the header. This single byte holds some flags nobody cares |
1849 | + # about, and an opcode which nobody cares about. |
1850 | + header = ord(buf[start]) |
1851 | + if header & 0x70: |
1852 | + # At least one of the reserved flags is set. Pork chop sandwiches! |
1853 | + raise _WSException("Reserved flag in HyBi-07 frame (%d)" % header) |
1854 | + frames.append(("", _CONTROLS.CLOSE)) |
1855 | + return frames, buf |
1856 | + |
1857 | + # Get the opcode, and translate it to a local enum which we actually |
1858 | + # care about. |
1859 | + opcode = header & 0xf |
1860 | + try: |
1861 | + opcode = _opcode_types[opcode] |
1862 | + except KeyError: |
1863 | + raise _WSException("Unknown opcode %d in HyBi-07 frame" % opcode) |
1864 | + |
1865 | + # Get the payload length and determine whether we need to look for an |
1866 | + # extra length. |
1867 | + length = ord(buf[start + 1]) |
1868 | + masked = length & 0x80 |
1869 | + length &= 0x7f |
1870 | + |
1871 | + # The offset we're gonna be using to walk through the frame. We use |
1872 | + # this because the offset is variable depending on the length and |
1873 | + # mask. |
1874 | + offset = 2 |
1875 | + |
1876 | + # Extra length fields. |
1877 | + if length == 0x7e: |
1878 | + if len(buf) - start < 4: |
1879 | + break |
1880 | + |
1881 | + length = buf[start + 2:start + 4] |
1882 | + length = unpack(">H", length)[0] |
1883 | + offset += 2 |
1884 | + elif length == 0x7f: |
1885 | + if len(buf) - start < 10: |
1886 | + break |
1887 | + |
1888 | + # Protocol bug: The top bit of this long long *must* be cleared; |
1889 | + # that is, it is expected to be interpreted as signed. That's |
1890 | + # fucking stupid, if you don't mind me saying so, and so we're |
1891 | + # interpreting it as unsigned anyway. If you wanna send exabytes |
1892 | + # of data down the wire, then go ahead! |
1893 | + length = buf[start + 2:start + 10] |
1894 | + length = unpack(">Q", length)[0] |
1895 | + offset += 8 |
1896 | + |
1897 | + if masked: |
1898 | + if len(buf) - (start + offset) < 4: |
1899 | + break |
1900 | + |
1901 | + key = buf[start + offset:start + offset + 4] |
1902 | + offset += 4 |
1903 | + |
1904 | + if len(buf) - (start + offset) < length: |
1905 | + break |
1906 | + |
1907 | + data = buf[start + offset:start + offset + length] |
1908 | + |
1909 | + if masked: |
1910 | + data = _mask(data, key) |
1911 | + |
1912 | + if opcode == _CONTROLS.CLOSE: |
1913 | + if len(data) >= 2: |
1914 | + # Gotta unpack the opcode and return usable data here. |
1915 | + data = unpack(">H", data[:2])[0], data[2:] |
1916 | + else: |
1917 | + # No reason given; use generic data. |
1918 | + data = 1000, "No reason given" |
1919 | + |
1920 | + frames.append((opcode, data)) |
1921 | + start += offset + length |
1922 | + |
1923 | + return frames, buf[start:] |
1924 | + |
1925 | + |
1926 | +class _WebSocketsProtocol(ProtocolWrapper): |
1927 | + """ |
1928 | + Protocol which wraps another protocol to provide a WebSockets transport |
1929 | + layer. |
1930 | + """ |
1931 | + |
1932 | + buf = "" |
1933 | + codec = None |
1934 | + |
1935 | + def __init__(self, *args, **kwargs): |
1936 | + ProtocolWrapper.__init__(self, *args, **kwargs) |
1937 | + self._pending_frames = [] |
1938 | + |
1939 | + def connectionMade(self): |
1940 | + ProtocolWrapper.connectionMade(self) |
1941 | + log.msg("Opening connection with %s" % self.transport.getPeer()) |
1942 | + |
1943 | + def parseFrames(self): |
1944 | + """ |
1945 | + Find frames in incoming data and pass them to the underlying protocol. |
1946 | + """ |
1947 | + |
1948 | + try: |
1949 | + frames, self.buf = _parse_hybi07_frames(self.buf) |
1950 | + except _WSException: |
1951 | + # Couldn't parse all the frames, something went wrong, let's bail. |
1952 | + log.err() |
1953 | + self.loseConnection() |
1954 | + return |
1955 | + |
1956 | + for frame in frames: |
1957 | + opcode, data = frame |
1958 | + if opcode == _CONTROLS.NORMAL: |
1959 | + # Business as usual. Decode the frame, if we have a decoder. |
1960 | + if self.codec: |
1961 | + data = _decoders[self.codec](data) |
1962 | + # Pass the frame to the underlying protocol. |
1963 | + ProtocolWrapper.dataReceived(self, data) |
1964 | + elif opcode == _CONTROLS.CLOSE: |
1965 | + # The other side wants us to close. I wonder why? |
1966 | + reason, text = data |
1967 | + log.msg("Closing connection: %r (%d)" % (text, reason)) |
1968 | + |
1969 | + # Close the connection. |
1970 | + self.loseConnection() |
1971 | + return |
1972 | + elif opcode == _CONTROLS.PING: |
1973 | + # 5.5.2 PINGs must be responded to with PONGs. |
1974 | + # 5.5.3 PONGs must contain the data that was sent with the |
1975 | + # provoking PING. |
1976 | + self.transport.write(_make_hybi07_frame(data, |
1977 | + _opcode=_CONTROLS.PONG)) |
1978 | + |
1979 | + def sendFrames(self): |
1980 | + """ |
1981 | + Send all pending frames. |
1982 | + """ |
1983 | + |
1984 | + for frame in self._pending_frames: |
1985 | + # Encode the frame before sending it. |
1986 | + if self.codec: |
1987 | + frame = _encoders[self.codec](frame) |
1988 | + packet = _make_hybi07_frame(frame) |
1989 | + self.transport.write(packet) |
1990 | + self._pending_frames = [] |
1991 | + |
1992 | + def dataReceived(self, data): |
1993 | + self.buf += data |
1994 | + |
1995 | + self.parseFrames() |
1996 | + |
1997 | + # Kick any pending frames. This is needed because frames might have |
1998 | + # started piling up early; we can get write()s from our protocol above |
1999 | + # when they makeConnection() immediately, before our browser client |
2000 | + # actually sends any data. In those cases, we need to manually kick |
2001 | + # pending frames. |
2002 | + if self._pending_frames: |
2003 | + self.sendFrames() |
2004 | + |
2005 | + def write(self, data): |
2006 | + """ |
2007 | + Write to the transport. |
2008 | + |
2009 | + This method will only be called by the underlying protocol. |
2010 | + """ |
2011 | + |
2012 | + self._pending_frames.append(data) |
2013 | + self.sendFrames() |
2014 | + |
2015 | + def writeSequence(self, data): |
2016 | + """ |
2017 | + Write a sequence of data to the transport. |
2018 | + |
2019 | + This method will only be called by the underlying protocol. |
2020 | + """ |
2021 | + |
2022 | + self._pending_frames.extend(data) |
2023 | + self.sendFrames() |
2024 | + |
2025 | + def loseConnection(self): |
2026 | + """ |
2027 | + Close the connection. |
2028 | + |
2029 | + This includes telling the other side we're closing the connection. |
2030 | + |
2031 | + If the other side didn't signal that the connection is being closed, |
2032 | + then we might not see their last message, but since their last message |
2033 | + should, according to the spec, be a simple acknowledgement, it |
2034 | + shouldn't be a problem. |
2035 | + """ |
2036 | + |
2037 | + # Send a closing frame. It's only polite. (And might keep the browser |
2038 | + # from hanging.) |
2039 | + if not self.disconnecting: |
2040 | + frame = _make_hybi07_frame("", _opcode=_CONTROLS.CLOSE) |
2041 | + self.transport.write(frame) |
2042 | + |
2043 | + ProtocolWrapper.loseConnection(self) |
2044 | + |
2045 | + |
2046 | +class _WebSocketsFactory(WrappingFactory): |
2047 | + """ |
2048 | + Factory which wraps another factory to provide WebSockets frames for all |
2049 | + of its protocols. |
2050 | + |
2051 | + This factory does not provide the HTTP headers required to perform a |
2052 | + WebSockets handshake; see C{WebSocketsResource}. |
2053 | + """ |
2054 | + |
2055 | + protocol = _WebSocketsProtocol |
2056 | + |
2057 | + |
2058 | +class WebSocketsResource(object): |
2059 | + """ |
2060 | + A resource for serving a protocol through WebSockets. |
2061 | + |
2062 | + This class wraps a factory and connects it to WebSockets clients. Each |
2063 | + connecting client will be connected to a new protocol of the factory. |
2064 | + |
2065 | + Due to unresolved questions of logistics, this resource cannot have |
2066 | + children. |
2067 | + |
2068 | + @since 12.2 |
2069 | + """ |
2070 | + |
2071 | + implements(IResource) |
2072 | + |
2073 | + isLeaf = True |
2074 | + |
2075 | + def __init__(self, factory): |
2076 | + self._factory = _WebSocketsFactory(factory) |
2077 | + |
2078 | + def getChildWithDefault(self, name, request): |
2079 | + return NoResource("No such child resource.") |
2080 | + |
2081 | + def putChild(self, path, child): |
2082 | + pass |
2083 | + |
2084 | + def render(self, request): |
2085 | + """ |
2086 | + Render a request. |
2087 | + |
2088 | + We're not actually rendering a request. We are secretly going to |
2089 | + handle a WebSockets connection instead. |
2090 | + """ |
2091 | + ws_log.info("render ws") |
2092 | + # If we fail at all, we're gonna fail with 400 and no response. |
2093 | + # You might want to pop open the RFC and read along. |
2094 | + failed = False |
2095 | + if request.method != "GET": |
2096 | + # 4.2.1.1 GET is required. |
2097 | + ws_log.info("fail: not get") |
2098 | + failed = True |
2099 | + |
2100 | + upgrade = request.getHeader("Upgrade") |
2101 | + if upgrade is None or "websocket" not in upgrade.lower(): |
2102 | + # 4.2.1.3 Upgrade: WebSocket is required. |
2103 | + failed = True |
2104 | + ws_log.info("fail: no upgrade ws") |
2105 | + |
2106 | + connection = request.getHeader("Connection") |
2107 | + if connection is None or "upgrade" not in connection.lower(): |
2108 | + # 4.2.1.4 Connection: Upgrade is required. |
2109 | + failed = True |
2110 | + ws_log.info("fail: no upgrade") |
2111 | + |
2112 | + key = request.getHeader("Sec-WebSocket-Key") |
2113 | + if key is None: |
2114 | + # 4.2.1.5 The challenge key is required. |
2115 | + failed = True |
2116 | + ws_log.info("fail: no ws challenge key") |
2117 | + |
2118 | + version = request.getHeader("Sec-WebSocket-Version") |
2119 | + if version != "13": |
2120 | + # 4.2.1.6 Only version 13 works. |
2121 | + failed = True |
2122 | + ws_log.info("fail: different ws version %s" % version) |
2123 | + |
2124 | + # 4.4 Forward-compatible version checking. |
2125 | + request.setHeader("Sec-WebSocket-Version", "13") |
2126 | + |
2127 | + # Check whether a codec is needed. WS calls this a "protocol" for |
2128 | + # reasons I cannot fathom. The specification permits multiple, |
2129 | + # comma-separated codecs to be listed, but this functionality isn't |
2130 | + # used in the wild. (If that ever changes, we'll have already added |
2131 | + # the requisite codecs here anyway.) The main reason why we check for |
2132 | + # codecs at all is that older draft versions of WebSockets used base64 |
2133 | + # encoding to work around the inability to send \x00 bytes, and those |
2134 | + # runtimes would request base64 encoding during the handshake. We |
2135 | + # stand prepared to engage that behavior should any of those runtimes |
2136 | + # start supporting RFC WebSockets. |
2137 | + # |
2138 | + # We probably should remove this altogether, but I'd rather leave it |
2139 | + # because it will prove to be a useful reference if/when extensions |
2140 | + # are added, and it *does* work as advertised. |
2141 | + codec = request.getHeader("Sec-WebSocket-Protocol") |
2142 | + |
2143 | + if codec == 'undefined': |
2144 | + codec = None |
2145 | + |
2146 | + if codec: |
2147 | + if codec not in _encoders or codec not in _decoders: |
2148 | + log.msg("Codec %s is not implemented" % codec) |
2149 | + #failed = True |
2150 | + ws_log.info('codec requested %s' % codec) |
2151 | + codec = None |
2152 | + |
2153 | + if failed: |
2154 | + ws_log.info("failed, closing") |
2155 | + request.setResponseCode(400) |
2156 | + return "" |
2157 | + |
2158 | + # We are going to finish this handshake. We will return a valid status |
2159 | + # code. |
2160 | + # 4.2.2.5.1 101 Switching Protocols |
2161 | + request.setResponseCode(101) |
2162 | + # 4.2.2.5.2 Upgrade: websocket |
2163 | + request.setHeader("Upgrade", "WebSocket") |
2164 | + # 4.2.2.5.3 Connection: Upgrade |
2165 | + request.setHeader("Connection", "Upgrade") |
2166 | + # 4.2.2.5.4 Response to the key challenge |
2167 | + request.setHeader("Sec-WebSocket-Accept", _make_accept(key)) |
2168 | + # 4.2.2.5.5 Optional codec declaration |
2169 | + if codec: |
2170 | + request.setHeader("Sec-WebSocket-Protocol", codec) |
2171 | + |
2172 | + # Create the protocol. This could fail, in which case we deliver an |
2173 | + # error status. Status 502 was decreed by glyph; blame him. |
2174 | + protocol = self._factory.buildProtocol(request.transport.getPeer()) |
2175 | + if not protocol: |
2176 | + request.setResponseCode(502) |
2177 | + return "" |
2178 | + if codec: |
2179 | + protocol.codec = codec |
2180 | + |
2181 | + # Provoke request into flushing headers and finishing the handshake. |
2182 | + request.write("") |
2183 | + |
2184 | + # And now take matters into our own hands. We shall manage the |
2185 | + # transport's lifecycle. |
2186 | + transport, request.transport = request.transport, None |
2187 | + |
2188 | + # Connect the transport to our factory, and make things go. We need to |
2189 | + # do some stupid stuff here; see #3204, which could fix it. |
2190 | + if request.isSecure(): |
2191 | + # Secure connections means one more wrapping: TLSMemoryBIOProtocol. |
2192 | + transport.protocol.wrappedProtocol = protocol |
2193 | + else: |
2194 | + transport.protocol = protocol |
2195 | + protocol.makeConnection(transport) |
2196 | + |
2197 | + return NOT_DONE_YET |
2198 | |
2199 | === modified file 'juju/providers/common/cloudinit.py' |
2200 | --- juju/providers/common/cloudinit.py 2013-01-15 19:53:21 +0000 |
2201 | +++ juju/providers/common/cloudinit.py 2013-01-17 11:02:31 +0000 |
2202 | @@ -81,6 +81,17 @@ |
2203 | return service.get_cloud_init_commands() |
2204 | |
2205 | |
2206 | +def _apiserver_scripts(zookeeper_hosts): |
2207 | + service = UpstartService("juju-api-agent") |
2208 | + service.set_description("Juju api agent") |
2209 | + service.set_environ({"JUJU_ZOOKEEPER": zookeeper_hosts}) |
2210 | + service.set_command( |
2211 | + "python -m juju.agents.api --nodaemon " |
2212 | + "--logfile /var/log/juju/api-agent.log " |
2213 | + "--session-file /var/run/juju/api-agent.zksession") |
2214 | + return service.get_cloud_init_commands() |
2215 | + |
2216 | + |
2217 | def _provision_scripts(zookeeper_hosts): |
2218 | service = UpstartService("juju-provision-agent") |
2219 | service.set_description("Juju provisioning agent") |
2220 | @@ -411,6 +422,7 @@ |
2221 | self._env_id)) |
2222 | if self._provision: |
2223 | scripts.extend(_provision_scripts(self._join_zookeeper_hosts())) |
2224 | + scripts.extend(_apiserver_scripts(self._join_zookeeper_hosts())) |
2225 | return scripts |
2226 | |
2227 | def _collect_machine_data(self): |
2228 | |
2229 | === modified file 'juju/providers/common/tests/data/cloud_init_bootstrap' |
2230 | --- juju/providers/common/tests/data/cloud_init_bootstrap 2012-08-23 16:14:42 +0000 |
2231 | +++ juju/providers/common/tests/data/cloud_init_bootstrap 2013-01-17 11:02:31 +0000 |
2232 | @@ -57,5 +57,28 @@ |
2233 | |
2234 | EOF |
2235 | |
2236 | - ', /sbin/start juju-provision-agent] |
2237 | + ', /sbin/start juju-provision-agent, 'cat >> /etc/init/juju-api-agent.conf <<EOF |
2238 | + |
2239 | + description "Juju api agent" |
2240 | + |
2241 | + author "Juju Team <juju@lists.ubuntu.com>" |
2242 | + |
2243 | + |
2244 | + start on runlevel [2345] |
2245 | + |
2246 | + stop on runlevel [!2345] |
2247 | + |
2248 | + respawn |
2249 | + |
2250 | + |
2251 | + env JUJU_ZOOKEEPER="localhost:2181" |
2252 | + |
2253 | + |
2254 | + exec python -m juju.agents.api --nodaemon --logfile /var/log/juju/api-agent.log |
2255 | + --session-file /var/run/juju/api-agent.zksession >> /tmp/juju-api-agent.output |
2256 | + 2>&1 |
2257 | + |
2258 | + EOF |
2259 | + |
2260 | + ', /sbin/start juju-api-agent] |
2261 | ssh_authorized_keys: [chubb] |
2262 | |
2263 | === modified file 'juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers' |
2264 | --- juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2012-08-23 16:14:42 +0000 |
2265 | +++ juju/providers/common/tests/data/cloud_init_bootstrap_zookeepers 2013-01-17 11:02:31 +0000 |
2266 | @@ -57,5 +57,28 @@ |
2267 | |
2268 | EOF |
2269 | |
2270 | - ', /sbin/start juju-provision-agent] |
2271 | + ', /sbin/start juju-provision-agent, 'cat >> /etc/init/juju-api-agent.conf <<EOF |
2272 | + |
2273 | + description "Juju api agent" |
2274 | + |
2275 | + author "Juju Team <juju@lists.ubuntu.com>" |
2276 | + |
2277 | + |
2278 | + start on runlevel [2345] |
2279 | + |
2280 | + stop on runlevel [!2345] |
2281 | + |
2282 | + respawn |
2283 | + |
2284 | + |
2285 | + env JUJU_ZOOKEEPER="cotswold:2181,longleat:2181,localhost:2181" |
2286 | + |
2287 | + |
2288 | + exec python -m juju.agents.api --nodaemon --logfile /var/log/juju/api-agent.log |
2289 | + --session-file /var/run/juju/api-agent.zksession >> /tmp/juju-api-agent.output |
2290 | + 2>&1 |
2291 | + |
2292 | + EOF |
2293 | + |
2294 | + ', /sbin/start juju-api-agent] |
2295 | ssh_authorized_keys: [chubb] |
2296 | |
2297 | === modified file 'juju/providers/common/tests/test_cloudinit.py' |
2298 | --- juju/providers/common/tests/test_cloudinit.py 2012-10-09 18:55:55 +0000 |
2299 | +++ juju/providers/common/tests/test_cloudinit.py 2013-01-17 11:02:31 +0000 |
2300 | @@ -137,7 +137,8 @@ |
2301 | |
2302 | def test_render_bootstrap_with_zookeepers(self): |
2303 | self.assert_render( |
2304 | - self.construct_bootstrap(True), "cloud_init_bootstrap_zookeepers") |
2305 | + self.construct_bootstrap(True), |
2306 | + "cloud_init_bootstrap_zookeepers") |
2307 | |
2308 | def test_render_no_machine_id(self): |
2309 | self.patch(juju, "__file__", "/not/installed/under/usr") |
2310 | |
2311 | === modified file 'juju/providers/dummy.py' |
2312 | --- juju/providers/dummy.py 2012-04-06 16:35:31 +0000 |
2313 | +++ juju/providers/dummy.py 2013-01-17 11:02:31 +0000 |
2314 | @@ -66,9 +66,9 @@ |
2315 | os.environ.get("ZOOKEEPER_ADDRESS", "127.0.0.1:2181"), |
2316 | session_timeout=1000).connect() |
2317 | |
2318 | - def get_machines(self, instance_ids=()): |
2319 | + def get_machines(self, instance_ids=None): |
2320 | """List all the machine running in the provider.""" |
2321 | - if not instance_ids: |
2322 | + if instance_ids is None: |
2323 | return succeed(self._machines[:]) |
2324 | |
2325 | machines_by_id = dict(((m.instance_id, m) for m in self._machines)) |
2326 | |
2327 | === modified file 'juju/providers/local/__init__.py' |
2328 | --- juju/providers/local/__init__.py 2012-09-28 06:32:46 +0000 |
2329 | +++ juju/providers/local/__init__.py 2013-01-17 11:02:31 +0000 |
2330 | @@ -12,7 +12,7 @@ |
2331 | from juju.providers.common.connect import ZookeeperConnect |
2332 | from juju.providers.common.utils import get_user_authorized_keys |
2333 | |
2334 | -from juju.providers.local.agent import ManagedMachineAgent |
2335 | +from juju.providers.local.agent import ManagedMachineAgent, APIAgent |
2336 | from juju.providers.local.files import StorageServer, LocalStorage |
2337 | from juju.providers.local.machine import LocalMachine |
2338 | from juju.providers.local.network import Network |
2339 | @@ -56,10 +56,11 @@ |
2340 | def _get_storage_server(self, ip='127.0.0.1'): |
2341 | return StorageServer( |
2342 | self._qualified_name, |
2343 | + self._directory, |
2344 | storage_dir=os.path.join(self._directory, "files"), |
2345 | host=ip, |
2346 | port=get_open_port(ip), |
2347 | - logfile=os.path.join(self._directory, "storage-server.log")) |
2348 | + log_file=os.path.join(self._directory, "storage-server.log")) |
2349 | |
2350 | @inlineCallbacks |
2351 | def bootstrap(self, constraints): |
2352 | @@ -87,6 +88,11 @@ |
2353 | except LookupError, e: |
2354 | raise ProviderError(str(e)) |
2355 | |
2356 | + # Get/create directory for zookeeper and files |
2357 | + zookeeper_dir = os.path.join(self._directory, "zookeeper") |
2358 | + if not os.path.exists(zookeeper_dir): |
2359 | + os.makedirs(zookeeper_dir) |
2360 | + |
2361 | # Start networking, and get an open port. |
2362 | log.info("Starting networking...") |
2363 | net = Network() |
2364 | @@ -103,7 +109,7 @@ |
2365 | os.makedirs(zookeeper_dir) |
2366 | |
2367 | # Start ZooKeeper |
2368 | - log.info("Starting ZooKeeper...") |
2369 | + log.info("Starting zookeeper (192.168.122.1:%d)..." % port) |
2370 | # Run zookeeper as the current user, unless we're being run as root |
2371 | # in which case run zookeeper as the 'zookeeper' user. |
2372 | zookeeper_user = None |
2373 | @@ -149,11 +155,21 @@ |
2374 | juju_origin=juju_origin, |
2375 | public_key=public_key, |
2376 | juju_series=self.config["default-series"]) |
2377 | - |
2378 | log.info( |
2379 | "Starting machine agent (origin: %s)... ", agent.juju_origin) |
2380 | yield agent.start() |
2381 | |
2382 | + # Startup the api server |
2383 | + log.info("Starting api server") |
2384 | + log_file = os.path.join(self._directory, "api-agent.log") |
2385 | + port = self.config.get("api-port", 80) |
2386 | + secure = self.config.get("api-secure", False) |
2387 | + keys = self.config.get("api-keys") |
2388 | + |
2389 | + agent = APIAgent( |
2390 | + self._qualified_name, self._directory, zookeeper.address, |
2391 | + int(port), secure, keys, log_file) |
2392 | + yield agent.start() |
2393 | log.info("Environment bootstrapped") |
2394 | |
2395 | def _validate_data_dir(self): |
2396 | @@ -172,6 +188,11 @@ |
2397 | log.info("Destroying unit containers...") |
2398 | yield self._destroy_containers() |
2399 | |
2400 | + # Stop the api agent |
2401 | + log.debug("Stopping api agent...") |
2402 | + agent = APIAgent(self._qualified_name, self._directory) |
2403 | + yield agent.stop() |
2404 | + |
2405 | # Stop the machine agent |
2406 | log.debug("Stopping machine agent...") |
2407 | agent = ManagedMachineAgent(self._qualified_name, |
2408 | |
2409 | === modified file 'juju/providers/local/agent.py' |
2410 | --- juju/providers/local/agent.py 2012-09-28 06:32:46 +0000 |
2411 | +++ juju/providers/local/agent.py 2013-01-17 11:02:31 +0000 |
2412 | @@ -1,11 +1,69 @@ |
2413 | import sys |
2414 | import os |
2415 | -import tempfile |
2416 | |
2417 | from juju.lib.service import TwistedDaemonService |
2418 | from juju.providers.common.cloudinit import get_default_origin, BRANCH |
2419 | |
2420 | |
2421 | +class APIAgent(object): |
2422 | + |
2423 | + agent_module = "juju.agents.api" |
2424 | + |
2425 | + def __init__( |
2426 | + self, namespace, juju_directory, zookeeper_hosts=None, port=80, |
2427 | + secure=False, keys=None, log_file=None): |
2428 | + """ |
2429 | + :param zookeeper_hosts: Zookeeper hosts to connect. |
2430 | + :param log_file: A file to use for the agent logs. |
2431 | + :param port: Port to use for the API endpoint. |
2432 | + :param juju_unit_namespace: The machine agent will create units with |
2433 | + a known a prefix to allow for multiple users and multiple |
2434 | + environments to create containers. The namespace should be |
2435 | + unique per user and per environment. |
2436 | + """ |
2437 | + |
2438 | + assert juju_directory |
2439 | + self._pid_file = os.path.join( |
2440 | + juju_directory, "%s-api-agent.pid" % namespace) |
2441 | + |
2442 | + self._service = TwistedDaemonService( |
2443 | + "juju-%s-api-agent" % namespace, |
2444 | + self._pid_file, log_file, use_sudo=True) |
2445 | + self._service.set_description( |
2446 | + "Juju api agent for %s" % namespace) |
2447 | + |
2448 | + env = {"JUJU_ZOOKEEPER": zookeeper_hosts, |
2449 | + "JUJU_HOME": juju_directory, |
2450 | + "PYTHONPATH": ":".join(sys.path)} |
2451 | + self._service.set_environ(env) |
2452 | + self._service_args = [ |
2453 | + "/usr/bin/python", "-m", self.agent_module, |
2454 | + "--pidfile", self._pid_file, |
2455 | + "--logfile", log_file, |
2456 | + "--port", str(port), |
2457 | + "--session-file", |
2458 | + "/var/run/juju/%s-machine-agent.zksession" % namespace] |
2459 | + if secure: |
2460 | + self._service_args.extend(['--secure', '--keys', keys]) |
2461 | + |
2462 | + @property |
2463 | + def juju_origin(self): |
2464 | + return self._juju_origin |
2465 | + |
2466 | + def start(self): |
2467 | + """Start the machine agent.""" |
2468 | + self._service.set_command(self._service_args) |
2469 | + return self._service.start() |
2470 | + |
2471 | + def stop(self): |
2472 | + """Stop the machine agent.""" |
2473 | + return self._service.destroy() |
2474 | + |
2475 | + def is_running(self): |
2476 | + """Boolean value, true if the machine agent is running.""" |
2477 | + return self._service.is_running() |
2478 | + |
2479 | + |
2480 | class ManagedMachineAgent(object): |
2481 | |
2482 | agent_module = "juju.agents.machine" |
2483 | @@ -49,12 +107,11 @@ |
2484 | self._service = TwistedDaemonService( |
2485 | "juju-%s-machine-agent" % juju_unit_namespace, |
2486 | pidfile, |
2487 | + log_file, |
2488 | use_sudo=True) |
2489 | self._service.set_description( |
2490 | "Juju machine agent for %s" % juju_unit_namespace) |
2491 | self._service.set_environ(env) |
2492 | - self._service.output_path = log_file |
2493 | - |
2494 | self._service_args = [ |
2495 | "/usr/bin/python", "-m", self.agent_module, |
2496 | "--logfile", log_file, |
2497 | |
2498 | === modified file 'juju/providers/local/files.py' |
2499 | --- juju/providers/local/files.py 2012-09-28 06:13:47 +0000 |
2500 | +++ juju/providers/local/files.py 2013-01-17 11:02:31 +0000 |
2501 | @@ -5,7 +5,7 @@ |
2502 | from twisted.internet.error import ConnectionRefusedError |
2503 | from twisted.web.client import getPage |
2504 | |
2505 | -from juju.errors import ProviderError, FileNotFound |
2506 | +from juju.errors import ProviderError, FileNotFound, ServiceError |
2507 | from juju.lib import serializer |
2508 | from juju.lib.service import TwistedDaemonService |
2509 | from juju.providers.common.files import FileStorage |
2510 | @@ -16,36 +16,35 @@ |
2511 | |
2512 | class StorageServer(object): |
2513 | |
2514 | - def __init__(self, juju_unit_namespace, storage_dir=None, |
2515 | - host=None, port=None, logfile=None): |
2516 | + def __init__(self, juju_unit_namespace, juju_directory, |
2517 | + storage_dir=None, host=None, port=None, log_file=None): |
2518 | """Management facade for a web server on top of the provider storage. |
2519 | |
2520 | :param juju_unit_namespace: For disambiguation. |
2521 | :param host: Host interface to bind to. |
2522 | :param port: Port to bind to. |
2523 | - :param logfile: Path to store log output. |
2524 | + :param log_file: Path to store log output. |
2525 | """ |
2526 | if storage_dir: |
2527 | storage_dir = os.path.abspath(storage_dir) |
2528 | self._storage_dir = storage_dir |
2529 | self._host = host |
2530 | self._port = port |
2531 | - self._logfile = logfile |
2532 | - if storage_dir: |
2533 | - self._pidfile = os.path.abspath( |
2534 | - os.path.join(storage_dir, '..', 'storage-server.pid')) |
2535 | - else: |
2536 | - self._pidfile = os.path.join('/tmp', 'storage-server.pid') |
2537 | + self._log_file = log_file |
2538 | + |
2539 | + self._pid_file = os.path.join( |
2540 | + juju_directory, "%s-file-server.pid" % juju_unit_namespace) |
2541 | |
2542 | self._service = TwistedDaemonService( |
2543 | - "juju-%s-file-storage" % juju_unit_namespace, pidfile=self._pidfile, |
2544 | - use_sudo=False) |
2545 | + "juju-%s-file-storage" % juju_unit_namespace, |
2546 | + pidfile=self._pid_file, |
2547 | + output_path=self._log_file, use_sudo=False) |
2548 | self._service.set_description( |
2549 | "Juju file storage for %s" % juju_unit_namespace) |
2550 | self._service_args = [ |
2551 | "twistd", |
2552 | - "--pidfile", self._pidfile, |
2553 | - "--logfile", logfile, |
2554 | + "--pidfile", self._pid_file, |
2555 | + "--logfile", log_file, |
2556 | "-d", self._storage_dir, |
2557 | "web", |
2558 | "--port", "tcp:%s:interface=%s" % (self._port, self._host), |
2559 | @@ -69,10 +68,12 @@ |
2560 | assert self._storage_dir, "no storage_dir set" |
2561 | assert self._host, "no host set" |
2562 | assert self._port, "no port set" |
2563 | - assert None not in self._service_args, "unset params" |
2564 | + |
2565 | + assert None not in self._service_args, "unset params %s" % ( |
2566 | + " ".join(self._service_args)) |
2567 | assert os.path.exists(self._storage_dir), "Invalid storage directory" |
2568 | try: |
2569 | - with open(self._logfile, "a"): |
2570 | + with open(self._log_file, "a"): |
2571 | pass |
2572 | except IOError: |
2573 | raise AssertionError("logfile not writable by this user") |
2574 | @@ -86,6 +87,24 @@ |
2575 | self._service.set_command(self._service_args) |
2576 | yield self._service.start() |
2577 | |
2578 | + # Capture the error for the user. |
2579 | + if not self._service.is_running(): |
2580 | + content = self._capture_error() |
2581 | + raise ServiceError( |
2582 | + "Failed to start file-storage server: got output:\n" |
2583 | + "%s" % content) |
2584 | + |
2585 | + def _capture_error(self): |
2586 | + if os.path.exists(self._log_file): |
2587 | + with open(self._log_file) as fh: |
2588 | + content = fh.read() |
2589 | + else: |
2590 | + content = "" |
2591 | + return content |
2592 | + |
2593 | + def is_running(self): |
2594 | + return self._service.is_running() |
2595 | + |
2596 | def get_pid(self): |
2597 | return self._service.get_pid() |
2598 | |
2599 | |
2600 | === modified file 'juju/providers/local/tests/test_agent.py' |
2601 | --- juju/providers/local/tests/test_agent.py 2012-10-05 23:40:18 +0000 |
2602 | +++ juju/providers/local/tests/test_agent.py 2013-01-17 11:02:31 +0000 |
2603 | @@ -1,6 +1,5 @@ |
2604 | import os |
2605 | import sys |
2606 | -import tempfile |
2607 | import subprocess |
2608 | |
2609 | from twisted.internet.defer import inlineCallbacks |
2610 | @@ -35,12 +34,6 @@ |
2611 | juju_directory=juju_directory, |
2612 | log_file=log_file, |
2613 | juju_origin="lp:juju/trunk") |
2614 | - |
2615 | - try: |
2616 | - os.remove(agent._service.output_path) |
2617 | - except OSError: |
2618 | - pass # just make sure it's not there, so the .start() |
2619 | - # doesn't insert a spurious rm |
2620 | yield agent.start() |
2621 | |
2622 | start = subprocess_calls[0] |
2623 | @@ -66,7 +59,11 @@ |
2624 | @inlineCallbacks |
2625 | def test_managed_agent_root(self): |
2626 | juju_directory = self.makeDir() |
2627 | - log_file = tempfile.mktemp() |
2628 | + os.mkdir(os.path.join(juju_directory, "charms")) |
2629 | + os.mkdir(os.path.join(juju_directory, "state")) |
2630 | + os.mkdir(os.path.join(juju_directory, "units")) |
2631 | + |
2632 | + log_file = self.makeFile() |
2633 | |
2634 | # The pid file and log file get written as root |
2635 | def cleanup_root_file(cleanup_file): |
2636 | @@ -84,13 +81,15 @@ |
2637 | agent.agent_module = "juju.agents.dummy" |
2638 | self.assertFalse((yield agent.is_running())) |
2639 | yield agent.start() |
2640 | - # Give a moment for the process to start and write its config |
2641 | + # Give a moment for the process to start and write pid. |
2642 | yield self.sleep(0.1) |
2643 | self.assertTrue((yield agent.is_running())) |
2644 | |
2645 | - # running start again is fine, detects the process is running |
2646 | + # Running start again is fine, detects the process is running |
2647 | yield agent.start() |
2648 | yield agent.stop() |
2649 | + # A moment's peace to die. |
2650 | + yield self.sleep(0.1) |
2651 | self.assertFalse((yield agent.is_running())) |
2652 | |
2653 | # running stop again is fine, detects the process is stopped. |
2654 | |
2655 | === modified file 'juju/providers/local/tests/test_files.py' |
2656 | --- juju/providers/local/tests/test_files.py 2012-09-28 06:13:47 +0000 |
2657 | +++ juju/providers/local/tests/test_files.py 2013-01-17 11:02:31 +0000 |
2658 | @@ -1,7 +1,6 @@ |
2659 | import os |
2660 | import signal |
2661 | -from StringIO import StringIO |
2662 | -import subprocess |
2663 | +from StringIO import StringIO |
2664 | |
2665 | from twisted.internet.defer import inlineCallbacks, succeed |
2666 | from twisted.web.client import getPage |
2667 | @@ -22,12 +21,17 @@ |
2668 | @inlineCallbacks |
2669 | def setUp(self): |
2670 | yield super(WebFileStorageTest, self).setUp() |
2671 | - self._storage_path = self.makeDir() |
2672 | - self._logfile = self.makeFile() |
2673 | - self._storage = LocalStorage(self._storage_path) |
2674 | + self._juju_dir = self.makeDir() |
2675 | + self._storage_path = os.path.join(self._juju_dir, "files") |
2676 | + os.mkdir(self._storage_path) |
2677 | + self._log_file = self.makeFile() |
2678 | + self._storage = LocalStorage( |
2679 | + self._storage_path) |
2680 | self._port = get_open_port() |
2681 | self._server = StorageServer( |
2682 | - "ns1", self._storage_path, "localhost", self._port, self._logfile) |
2683 | + "ns1", self._juju_dir, |
2684 | + self._storage_path, "localhost", |
2685 | + self._port, self._log_file) |
2686 | |
2687 | @inlineCallbacks |
2688 | def wait_for_server(self, server): |
2689 | @@ -50,8 +54,9 @@ |
2690 | twisted = self.mocker.patch(TwistedDaemonService) |
2691 | twisted.start() |
2692 | self.mocker.result(succeed(True)) |
2693 | + twisted.is_running() |
2694 | + self.mocker.result(True) |
2695 | self.mocker.replay() |
2696 | - |
2697 | yield self._server.start() |
2698 | |
2699 | @uses_sudo |
2700 | @@ -59,28 +64,34 @@ |
2701 | def test_start_stop(self): |
2702 | yield self._storage.put("abc", StringIO("hello world")) |
2703 | yield self._server.start() |
2704 | - # Starting multiple times is fine. |
2705 | - yield self._server.start() |
2706 | + |
2707 | storage_url = yield self._storage.get_url("abc") |
2708 | |
2709 | # It might not have started actually accepting connections yet... |
2710 | yield self.wait_for_server(self._server) |
2711 | self.assertEqual((yield getPage(storage_url)), "hello world") |
2712 | |
2713 | + # Starting multiple times is fine, existing process detected. |
2714 | + yield self._server.start() |
2715 | + |
2716 | # Check that it can be killed by the current user (ie, is not running |
2717 | # as root) and still comes back up |
2718 | old_pid = yield self._server.get_pid() |
2719 | os.kill(old_pid, signal.SIGKILL) |
2720 | - new_pid = yield self._server.get_pid() |
2721 | - self.assertNotEquals(old_pid, new_pid) |
2722 | - |
2723 | + |
2724 | + # Give it a moment to die already. |
2725 | + yield self.sleep(0.1) |
2726 | + |
2727 | + self.assertFalse(self._server.is_running()) |
2728 | + |
2729 | + # For when the return to upstart.. |
2730 | # Give it a moment to actually start serving again |
2731 | - yield self.wait_for_server(self._server) |
2732 | - self.assertEqual((yield getPage(storage_url)), "hello world") |
2733 | + #yield self.wait_for_server(self._server) |
2734 | + #self.assertEqual((yield getPage(storage_url)), "hello world") |
2735 | |
2736 | - yield self._server.stop() |
2737 | # Stopping multiple times is fine too. |
2738 | yield self._server.stop() |
2739 | + yield self._server.stop() |
2740 | |
2741 | @uses_sudo |
2742 | @inlineCallbacks |
2743 | @@ -91,7 +102,8 @@ |
2744 | yield self._storage.put("some-path", StringIO("original")) |
2745 | |
2746 | alt_server = StorageServer( |
2747 | - "ns2", alt_storage_path, "localhost", get_open_port(), |
2748 | + "ns2", self._juju_dir, |
2749 | + alt_storage_path, "localhost", get_open_port(), |
2750 | self.makeFile()) |
2751 | yield alt_server.start() |
2752 | yield self._server.start() |
2753 | @@ -113,13 +125,13 @@ |
2754 | def test_capture_errors(self): |
2755 | self._port = get_open_port() |
2756 | self._server = StorageServer( |
2757 | - "borken", self._storage_path, "lol borken", self._port, |
2758 | - self._logfile) |
2759 | + "borken", self._juju_dir, |
2760 | + self._storage_path, "lol broken example com", self._port, |
2761 | + self._log_file) |
2762 | d = self._server.start() |
2763 | e = yield self.assertFailure(d, ServiceError) |
2764 | self.assertTrue(str(e).startswith( |
2765 | - "Failed to start job juju-borken-file-storage; got output:\n")) |
2766 | - self.assertIn("Wrong number of arguments", str(e)) |
2767 | + "Failed to start file-storage server: got output:")) |
2768 | yield self._server.stop() |
2769 | |
2770 | |
2771 | |
2772 | === modified file 'juju/providers/local/tests/test_provider.py' |
2773 | --- juju/providers/local/tests/test_provider.py 2012-09-26 22:30:27 +0000 |
2774 | +++ juju/providers/local/tests/test_provider.py 2013-01-17 11:02:31 +0000 |
2775 | @@ -14,7 +14,7 @@ |
2776 | from juju.machine.constraints import ConstraintSet |
2777 | from juju.providers.local import MachineProvider |
2778 | from juju.providers import local |
2779 | -from juju.providers.local.agent import ManagedMachineAgent |
2780 | +from juju.providers.local.agent import ManagedMachineAgent, APIAgent |
2781 | from juju.providers.local.files import StorageServer |
2782 | from juju.providers.local.network import Network |
2783 | from juju.lib import lxc as lxc_lib |
2784 | @@ -77,10 +77,13 @@ |
2785 | |
2786 | mock_zookeeper.address |
2787 | self.mocker.result(get_test_zookeeper_address()) |
2788 | - self.mocker.count(3) |
2789 | + self.mocker.count(4) |
2790 | |
2791 | mock_agent = self.mocker.patch(ManagedMachineAgent) |
2792 | mock_agent.start() |
2793 | + |
2794 | + mock_agent = self.mocker.patch(APIAgent) |
2795 | + mock_agent.start() |
2796 | self.mocker.result(succeed(True)) |
2797 | |
2798 | def test_provider_type(self): |
2799 | @@ -101,7 +104,7 @@ |
2800 | sorted(children)) |
2801 | output = self.output.getvalue() |
2802 | self.assertIn("Starting networking...", output) |
2803 | - self.assertIn("Starting ZooKeeper...", output) |
2804 | + self.assertIn("Starting zookeeper", output) |
2805 | self.assertIn("Initializing state...", output) |
2806 | self.assertIn("Starting storage server", output) |
2807 | self.assertIn("Starting machine agent", output) |
2808 | @@ -166,6 +169,9 @@ |
2809 | mock_agent = self.mocker.patch(ManagedMachineAgent) |
2810 | mock_agent.stop() |
2811 | |
2812 | + mock_agent = self.mocker.patch(APIAgent) |
2813 | + mock_agent.stop() |
2814 | + |
2815 | mock_server = self.mocker.patch(StorageServer) |
2816 | mock_server.stop() |
2817 | |
2818 | |
2819 | === modified file 'juju/providers/tests/test_dummy.py' |
2820 | --- juju/providers/tests/test_dummy.py 2012-03-29 01:37:57 +0000 |
2821 | +++ juju/providers/tests/test_dummy.py 2013-01-17 11:02:31 +0000 |
2822 | @@ -15,7 +15,9 @@ |
2823 | |
2824 | def setUp(self): |
2825 | super(DummyProviderTest, self).setUp() |
2826 | - self.provider = MachineProvider("foo", {"peter": "rabbit"}) |
2827 | + self.provider = MachineProvider( |
2828 | + "foo", |
2829 | + {"peter": "rabbit", "storage-directory": self.makeDir()}) |
2830 | zookeeper.set_debug_level(0) |
2831 | |
2832 | def test_environment_name(self): |
2833 | @@ -142,6 +144,7 @@ |
2834 | |
2835 | def test_get_serialization_data(self): |
2836 | data = self.provider.get_serialization_data() |
2837 | + data.pop('storage-directory') |
2838 | self.assertEqual( |
2839 | data, |
2840 | {"peter": "rabbit", |
2841 | |
2842 | === added directory 'juju/rapi' |
2843 | === added file 'juju/rapi/__init__.py' |
2844 | --- juju/rapi/__init__.py 1970-01-01 00:00:00 +0000 |
2845 | +++ juju/rapi/__init__.py 2013-01-17 11:02:31 +0000 |
2846 | @@ -0,0 +1,1 @@ |
2847 | +# |
2848 | |
2849 | === added directory 'juju/rapi/cmd' |
2850 | === added file 'juju/rapi/cmd/__init__.py' |
2851 | --- juju/rapi/cmd/__init__.py 1970-01-01 00:00:00 +0000 |
2852 | +++ juju/rapi/cmd/__init__.py 2013-01-17 11:02:31 +0000 |
2853 | @@ -0,0 +1,1 @@ |
2854 | +# |
2855 | |
2856 | === added file 'juju/rapi/cmd/add_relation.py' |
2857 | --- juju/rapi/cmd/add_relation.py 1970-01-01 00:00:00 +0000 |
2858 | +++ juju/rapi/cmd/add_relation.py 2013-01-17 11:02:31 +0000 |
2859 | @@ -0,0 +1,43 @@ |
2860 | +from twisted.internet.defer import inlineCallbacks, returnValue |
2861 | + |
2862 | +from juju.state.errors import NoMatchingEndpoints, AmbiguousRelation |
2863 | +from juju.state.relation import RelationStateManager |
2864 | +from juju.state.service import ServiceStateManager |
2865 | + |
2866 | + |
2867 | +@inlineCallbacks |
2868 | +def add_relation(context, *descriptors): |
2869 | + |
2870 | + relation_state_manager = RelationStateManager(context.client) |
2871 | + service_state_manager = ServiceStateManager(context.client) |
2872 | + endpoint_pairs = yield service_state_manager.join_descriptors( |
2873 | + *descriptors) |
2874 | + |
2875 | + context.log.debug("Endpoint pairs: %s", endpoint_pairs) |
2876 | + |
2877 | + if len(endpoint_pairs) == 0: |
2878 | + raise NoMatchingEndpoints() |
2879 | + elif len(endpoint_pairs) > 1: |
2880 | + for pair in endpoint_pairs[1:]: |
2881 | + if not (pair[0].relation_name.startswith("juju-") or |
2882 | + pair[1].relation_name.startswith("juju-")): |
2883 | + raise AmbiguousRelation(descriptors, endpoint_pairs) |
2884 | + |
2885 | + # At this point we just have one endpoint pair. We need to pick |
2886 | + # just one of the endpoints if it's a peer endpoint, since that's |
2887 | + # our current API - join descriptors takes two descriptors, but |
2888 | + # add_relation_state takes one or two endpoints. TODO consider |
2889 | + # refactoring. |
2890 | + endpoints = endpoint_pairs[0] |
2891 | + if endpoints[0] == endpoints[1]: |
2892 | + endpoints = endpoints[0:1] |
2893 | + relation, svc_rels = yield relation_state_manager.add_relation_state( |
2894 | + *endpoints) |
2895 | + context.log.info("Added %s relation to all service units.", |
2896 | + endpoints[0].relation_type) |
2897 | + |
2898 | + yield relation.load() |
2899 | + returnValue({'id': relation.internal_id, |
2900 | + 'interface': relation.interface, |
2901 | + 'scope': relation.scope, |
2902 | + 'endpoints': relation.endpoints}) |
2903 | |
2904 | === added file 'juju/rapi/cmd/add_unit.py' |
2905 | --- juju/rapi/cmd/add_unit.py 1970-01-01 00:00:00 +0000 |
2906 | +++ juju/rapi/cmd/add_unit.py 2013-01-17 11:02:31 +0000 |
2907 | @@ -0,0 +1,24 @@ |
2908 | +from twisted.internet.defer import inlineCallbacks, returnValue |
2909 | +from juju.errors import JujuError |
2910 | +from juju.state.placement import place_unit |
2911 | +from juju.state.service import ServiceStateManager |
2912 | + |
2913 | + |
2914 | +@inlineCallbacks |
2915 | +def add_unit(context, service_name, num_units=1, placement="unassigned"): |
2916 | + service_manager = ServiceStateManager(context.client) |
2917 | + service_state = yield service_manager.get_service_state(service_name) |
2918 | + if (yield service_state.is_subordinate()): |
2919 | + raise JujuError("Subordinate services acquire units from " |
2920 | + "their principal service.") |
2921 | + |
2922 | + units_ids = [] |
2923 | + for i in range(num_units): |
2924 | + unit_state = yield service_state.add_unit_state() |
2925 | + yield place_unit(context.client, placement, unit_state) |
2926 | + context.log.info( |
2927 | + "Unit %r added to service %r", |
2928 | + unit_state.unit_name, service_state.service_name) |
2929 | + units_ids.append(unit_state.unit_name) |
2930 | + |
2931 | + returnValue(units_ids) |
2932 | |
2933 | === added file 'juju/rapi/cmd/config_get.py' |
2934 | --- juju/rapi/cmd/config_get.py 1970-01-01 00:00:00 +0000 |
2935 | +++ juju/rapi/cmd/config_get.py 2013-01-17 11:02:31 +0000 |
2936 | @@ -0,0 +1,37 @@ |
2937 | +from twisted.internet.defer import inlineCallbacks, returnValue |
2938 | +from juju.state.service import ServiceStateManager |
2939 | + |
2940 | + |
2941 | +@inlineCallbacks |
2942 | +def config_get(context, service_name): |
2943 | + # Get the service |
2944 | + service_manager = ServiceStateManager(context.client) |
2945 | + service = yield service_manager.get_service_state(service_name) |
2946 | + |
2947 | + # Retrieve schema |
2948 | + charm = yield service.get_charm_state() |
2949 | + schema = yield charm.get_config() |
2950 | + schema_dict = schema.as_dict() |
2951 | + display_dict = {"service": service.service_name, |
2952 | + "charm": (yield service.get_charm_id()), |
2953 | + "settings": schema_dict} |
2954 | + |
2955 | + # Get current settings |
2956 | + settings = yield service.get_config() |
2957 | + settings = dict(settings.items()) |
2958 | + |
2959 | + # Merge current settings into schema/display dict |
2960 | + for k, v in schema_dict.items(): |
2961 | + # Display defaults for unset values. |
2962 | + if k in settings: |
2963 | + v['value'] = settings[k] |
2964 | + else: |
2965 | + v['value'] = "-Not set-" |
2966 | + |
2967 | + if 'default' in v: |
2968 | + if v['default'] == settings[k]: |
2969 | + v['default'] = True |
2970 | + else: |
2971 | + del v['default'] |
2972 | + |
2973 | + returnValue(display_dict) |
2974 | |
2975 | === added file 'juju/rapi/cmd/config_set.py' |
2976 | --- juju/rapi/cmd/config_set.py 1970-01-01 00:00:00 +0000 |
2977 | +++ juju/rapi/cmd/config_set.py 2013-01-17 11:02:31 +0000 |
2978 | @@ -0,0 +1,35 @@ |
2979 | +from twisted.internet.defer import inlineCallbacks, returnValue |
2980 | + |
2981 | +from juju.state.service import ServiceStateManager |
2982 | +from juju.lib.format import get_charm_formatter |
2983 | +from juju.lib.serializer import yaml_load |
2984 | + |
2985 | + |
2986 | +@inlineCallbacks |
2987 | +def config_set(context, service_name, service_options, data=None): |
2988 | + # Get the service and the charm |
2989 | + service_manager = ServiceStateManager(context.client) |
2990 | + service = yield service_manager.get_service_state(service_name) |
2991 | + charm = yield service.get_charm_state() |
2992 | + charm_format = (yield charm.get_metadata()).format |
2993 | + formatter = get_charm_formatter(charm_format) |
2994 | + |
2995 | + # Use the charm's ConfigOptions instance to validate the |
2996 | + # arguments to config_set. Invalid options passed to this method |
2997 | + # will thrown an exception. |
2998 | + if not service_options and data: |
2999 | + options = yaml_load(data) |
3000 | + elif isinstance(service_options, dict): |
3001 | + options = service_options |
3002 | + else: |
3003 | + options = formatter.parse_keyvalue_pairs(service_options) |
3004 | + |
3005 | + config = yield charm.get_config() |
3006 | + options = config.validate(options) |
3007 | + |
3008 | + # Apply the change |
3009 | + state = yield service.get_config() |
3010 | + state.update(options) |
3011 | + yield state.write() |
3012 | + |
3013 | + returnValue(True) |
3014 | |
3015 | === added file 'juju/rapi/cmd/constraints_get.py' |
3016 | --- juju/rapi/cmd/constraints_get.py 1970-01-01 00:00:00 +0000 |
3017 | +++ juju/rapi/cmd/constraints_get.py 2013-01-17 11:02:31 +0000 |
3018 | @@ -0,0 +1,33 @@ |
3019 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3020 | + |
3021 | +from juju.state.environment import EnvironmentStateManager |
3022 | +from juju.state.machine import MachineStateManager |
3023 | +from juju.state.service import ServiceStateManager |
3024 | + |
3025 | + |
3026 | +@inlineCallbacks |
3027 | +def constraints_get(context, entity_names=()): |
3028 | + if not entity_names: |
3029 | + esm = EnvironmentStateManager(context.client) |
3030 | + context.log.info("Fetching constraints for environment") |
3031 | + constraints = yield esm.get_constraints() |
3032 | + returnValue(dict(constraints)) |
3033 | + |
3034 | + msm = MachineStateManager(context.client) |
3035 | + ssm = ServiceStateManager(context.client) |
3036 | + result = {} |
3037 | + |
3038 | + for name in entity_names: |
3039 | + if name.isdigit(): |
3040 | + kind = "machine" |
3041 | + entity = yield msm.get_machine_state(name) |
3042 | + elif "/" in name: |
3043 | + kind = "service unit" |
3044 | + entity = yield ssm.get_unit_state(name) |
3045 | + else: |
3046 | + kind = "service" |
3047 | + entity = yield ssm.get_service_state(name) |
3048 | + context.log.info("Fetching constraints for %s %s", kind, name) |
3049 | + constraints = yield entity.get_constraints() |
3050 | + result[name] = dict(constraints) |
3051 | + returnValue(result) |
3052 | |
3053 | === added file 'juju/rapi/cmd/constraints_set.py' |
3054 | --- juju/rapi/cmd/constraints_set.py 1970-01-01 00:00:00 +0000 |
3055 | +++ juju/rapi/cmd/constraints_set.py 2013-01-17 11:02:31 +0000 |
3056 | @@ -0,0 +1,26 @@ |
3057 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3058 | + |
3059 | +from juju.state.environment import EnvironmentStateManager |
3060 | +from juju.state.service import ServiceStateManager |
3061 | + |
3062 | + |
3063 | +@inlineCallbacks |
3064 | +def constraints_set(context, service_name, constraint_strs): |
3065 | + # XXX / TODO : provider constraints require privileged access to |
3066 | + # the provider, which the REST api shouldn't have ideally. |
3067 | + |
3068 | + # Perhaps we could cache serialize provider constraints with |
3069 | + # periodic update. Also comes up do to frequence of constraint set |
3070 | + # access for deployment. |
3071 | + constraint_set = yield context.provider.get_constraint_set() |
3072 | + constraints = constraint_set.parse(constraint_strs) |
3073 | + |
3074 | + if service_name is None: |
3075 | + esm = EnvironmentStateManager(context.client) |
3076 | + yield esm.set_constraints(constraints) |
3077 | + else: |
3078 | + ssm = ServiceStateManager(context.client) |
3079 | + service = yield ssm.get_service_state(service_name) |
3080 | + yield service.set_constraints(constraints) |
3081 | + |
3082 | + returnValue(True) |
3083 | |
3084 | === added file 'juju/rapi/cmd/debug_hooks.py' |
3085 | --- juju/rapi/cmd/debug_hooks.py 1970-01-01 00:00:00 +0000 |
3086 | +++ juju/rapi/cmd/debug_hooks.py 2013-01-17 11:02:31 +0000 |
3087 | @@ -0,0 +1,72 @@ |
3088 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3089 | + |
3090 | +from juju.charm.errors import InvalidCharmHook |
3091 | +from juju.state.charm import CharmStateManager |
3092 | +from juju.state.service import ServiceStateManager |
3093 | + |
3094 | + |
3095 | +@inlineCallbacks |
3096 | +def debug_hooks(context, unit_name, hook_names): |
3097 | + |
3098 | + manager = ServiceStateManager(context.client) |
3099 | + unit = yield manager.get_unit_state(unit_name) |
3100 | + |
3101 | + # Verify hook name |
3102 | + if hook_names != ["*"]: |
3103 | + context.log.debug("Verifying hook names...") |
3104 | + yield validate_hooks(context.client, unit, hook_names) |
3105 | + |
3106 | + # Enable debug hooks |
3107 | + context.log.debug( |
3108 | + "Enabling hook debug on unit (%r)..." % unit_name) |
3109 | + yield unit.enable_hook_debug(hook_names) |
3110 | + |
3111 | + # Ensure the unit is up. |
3112 | + context.log.info("Waiting for unit") |
3113 | + |
3114 | + # Wait and verify the agent is running. |
3115 | + while 1: |
3116 | + exists_d, watch_d = unit.watch_agent() |
3117 | + exists = yield exists_d |
3118 | + if exists: |
3119 | + context.log.info("Unit running") |
3120 | + break |
3121 | + yield watch_d |
3122 | + |
3123 | + |
3124 | +@inlineCallbacks |
3125 | +def validate_hooks(client, unit_state, hook_names): |
3126 | + |
3127 | + # Assemble a list of valid hooks for the charm. |
3128 | + valid_hooks = ["start", "stop", "install", "config-changed"] |
3129 | + service_manager = ServiceStateManager(client) |
3130 | + endpoints = yield service_manager.get_relation_endpoints( |
3131 | + unit_state.service_name) |
3132 | + endpoint_names = [ep.relation_name for ep in endpoints] |
3133 | + for endpoint_name in endpoint_names: |
3134 | + valid_hooks.extend([ |
3135 | + endpoint_name + "-relation-joined", |
3136 | + endpoint_name + "-relation-changed", |
3137 | + endpoint_name + "-relation-departed", |
3138 | + endpoint_name + "-relation-broken", |
3139 | + ]) |
3140 | + |
3141 | + # Verify the debug names. |
3142 | + for hook_name in hook_names: |
3143 | + if hook_name in valid_hooks: |
3144 | + continue |
3145 | + break |
3146 | + else: |
3147 | + returnValue(True) |
3148 | + |
3149 | + # We dereference to the charm to give a fully qualified error |
3150 | + # message. I wish this was a little easier to dereference, the |
3151 | + # service_manager.get_relation_endpoints effectively does this |
3152 | + # already. |
3153 | + service_manager = ServiceStateManager(client) |
3154 | + service_state = yield service_manager.get_service_state( |
3155 | + unit_state.service_name) |
3156 | + charm_id = yield service_state.get_charm_id() |
3157 | + charm_manager = CharmStateManager(client) |
3158 | + charm = yield charm_manager.get_charm_state(charm_id) |
3159 | + raise InvalidCharmHook(charm.id, hook_name) |
3160 | |
3161 | === added file 'juju/rapi/cmd/deploy.py' |
3162 | --- juju/rapi/cmd/deploy.py 1970-01-01 00:00:00 +0000 |
3163 | +++ juju/rapi/cmd/deploy.py 2013-01-17 11:02:31 +0000 |
3164 | @@ -0,0 +1,102 @@ |
3165 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3166 | + |
3167 | +from juju.charm.errors import CharmNotFound, RepositoryNotFound |
3168 | +from juju.charm.publisher import CharmPublisher |
3169 | +from juju.charm.repository import resolve |
3170 | +from juju.lib import serializer |
3171 | + |
3172 | +from juju.state.endpoint import RelationEndpoint |
3173 | +from juju.state.placement import place_unit |
3174 | +from juju.state.relation import RelationStateManager |
3175 | +from juju.state.service import ServiceStateManager |
3176 | + |
3177 | + |
3178 | +@inlineCallbacks |
3179 | +def deploy(context, |
3180 | + charm_name, |
3181 | + service_name=None, |
3182 | + constraint_strs=(), |
3183 | + config=None, config_raw=None, num_units=1): |
3184 | + """Deploy a charm within an environment. |
3185 | + |
3186 | + This will publish the charm to the environment, creating |
3187 | + a service from the charm, and get it set to be launched |
3188 | + on a new machine. If --repository is not specified, it |
3189 | + will be taken from the environment variable JUJU_REPOSITORY. |
3190 | + """ |
3191 | + |
3192 | + # Locate charm, no local repos yet. |
3193 | + try: |
3194 | + store, charm_url = resolve( |
3195 | + charm_name, "/dev/null", context.default_series) |
3196 | + except RepositoryNotFound: |
3197 | + raise CharmNotFound("charm store", charm_name) |
3198 | + |
3199 | + store.cache_path = context.charm_cache_dir |
3200 | + |
3201 | + charm = yield store.find(charm_url) |
3202 | + |
3203 | + # Validate config options prior to deployment attempt |
3204 | + service_name = service_name or charm_url.name |
3205 | + service_options = {} |
3206 | + if config_raw: |
3207 | + config = serializer.yaml_load(config_raw) |
3208 | + if config: |
3209 | + service_options = charm.config.validate(config) |
3210 | + |
3211 | + charm_id = str(charm_url.with_revision(charm.get_revision())) |
3212 | + context.log.info("Deploying service %s using charm %s" % ( |
3213 | + service_name, charm_id)) |
3214 | + constraint_set = yield context.provider.get_constraint_set() |
3215 | + placement_policy = context.provider.get_placement_policy() |
3216 | + constraints = constraint_set.parse(constraint_strs) |
3217 | + |
3218 | + # Publish the charm to juju |
3219 | + storage = yield context.provider.get_file_storage() |
3220 | + publisher = CharmPublisher(context.client, storage) |
3221 | + yield publisher.add_charm(charm_id, charm) |
3222 | + result = yield publisher.publish() |
3223 | + |
3224 | + # In future we might have multiple charms be published at |
3225 | + # the same time. For now, extract the charm_state from the |
3226 | + # list. |
3227 | + charm_state = result[0] |
3228 | + |
3229 | + # Create the service state |
3230 | + service_manager = ServiceStateManager(context.client) |
3231 | + service_state = yield service_manager.add_service_state( |
3232 | + service_name, charm_state, constraints) |
3233 | + |
3234 | + # Use the charm's ConfigOptions instance to validate service |
3235 | + # options.. Invalid options passed will thrown an exception |
3236 | + # and prevent the deploy. |
3237 | + state = yield service_state.get_config() |
3238 | + charm_config = yield charm_state.get_config() |
3239 | + |
3240 | + # Return the validated options with the defaults included. |
3241 | + service_options = charm_config.validate(service_options) |
3242 | + state.update(service_options) |
3243 | + yield state.write() |
3244 | + |
3245 | + # Create desired number of service units |
3246 | + if (yield service_state.is_subordinate()): |
3247 | + context.log.info("Subordinate %r awaiting relationship " |
3248 | + "to principal for deployment.", service_name) |
3249 | + else: |
3250 | + for i in xrange(num_units): |
3251 | + unit_state = yield service_state.add_unit_state() |
3252 | + yield place_unit( |
3253 | + context.client, placement_policy, unit_state) |
3254 | + |
3255 | + # Check if we have any peer relations to establish |
3256 | + if charm.metadata.peers: |
3257 | + relation_manager = RelationStateManager(context.client) |
3258 | + for peer_name, peer_info in charm.metadata.peers.items(): |
3259 | + yield relation_manager.add_relation_state( |
3260 | + RelationEndpoint(service_name, |
3261 | + peer_info["interface"], |
3262 | + peer_name, |
3263 | + "peer")) |
3264 | + |
3265 | + context.log.info("Charm deployed as service: %r", service_name) |
3266 | + returnValue(True) |
3267 | |
3268 | === added file 'juju/rapi/cmd/destroy_service.py' |
3269 | --- juju/rapi/cmd/destroy_service.py 1970-01-01 00:00:00 +0000 |
3270 | +++ juju/rapi/cmd/destroy_service.py 2013-01-17 11:02:31 +0000 |
3271 | @@ -0,0 +1,42 @@ |
3272 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3273 | + |
3274 | +from juju.state.errors import UnsupportedSubordinateServiceRemoval |
3275 | +from juju.state.service import ServiceStateManager |
3276 | +from juju.state.relation import RelationStateManager |
3277 | + |
3278 | + |
3279 | +@inlineCallbacks |
3280 | +def destroy_service(context, service_name): |
3281 | + service_manager = ServiceStateManager(context.client) |
3282 | + service_state = yield service_manager.get_service_state(service_name) |
3283 | + |
3284 | + if (yield service_state.is_subordinate()): |
3285 | + # We can destroy the service if does not have relations. |
3286 | + # That implies that principals have already been torn |
3287 | + # down (or were never added). |
3288 | + relation_manager = RelationStateManager(context.client) |
3289 | + relations = yield relation_manager.get_relations_for_service( |
3290 | + service_state) |
3291 | + |
3292 | + if relations: |
3293 | + principal_service = None |
3294 | + # if we don't have a container we can destroy the subordinate |
3295 | + # (revisit in the future) |
3296 | + for relation in relations: |
3297 | + if relation.relation_scope != "container": |
3298 | + continue |
3299 | + services = yield relation.get_service_states() |
3300 | + remote_service = [s for s in services if s.service_name != |
3301 | + service_state.service_name][0] |
3302 | + if not (yield remote_service.is_subordinate()): |
3303 | + principal_service = remote_service |
3304 | + break |
3305 | + |
3306 | + if principal_service: |
3307 | + raise UnsupportedSubordinateServiceRemoval( |
3308 | + service_state.service_name, |
3309 | + principal_service.service_name) |
3310 | + |
3311 | + yield service_manager.remove_service_state(service_state) |
3312 | + context.log.info("Service %r destroyed.", service_state.service_name) |
3313 | + returnValue(True) |
3314 | |
3315 | === added file 'juju/rapi/cmd/export.py' |
3316 | --- juju/rapi/cmd/export.py 1970-01-01 00:00:00 +0000 |
3317 | +++ juju/rapi/cmd/export.py 2013-01-17 11:02:31 +0000 |
3318 | @@ -0,0 +1,91 @@ |
3319 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3320 | + |
3321 | +from juju.state.service import ServiceStateManager |
3322 | +from juju.state.relation import RelationStateManager |
3323 | +from juju.state.environment import EnvironmentStateManager |
3324 | + |
3325 | + |
3326 | +@inlineCallbacks |
3327 | +def export(context): |
3328 | + data = {'services': [], |
3329 | + 'relations': []} |
3330 | + |
3331 | + rels = set() |
3332 | + |
3333 | + # Get services |
3334 | + services = ServiceStateManager(context.client) |
3335 | + relations = RelationStateManager(context.client) |
3336 | + environment = EnvironmentStateManager(context.client) |
3337 | + |
3338 | + context.log.info("Exporting environment") |
3339 | + |
3340 | + for s in (yield services.get_all_service_states()): |
3341 | + units = yield s.get_unit_names() |
3342 | + charm_id = yield s.get_charm_id() |
3343 | + constraints = yield s.get_constraints() |
3344 | + config = yield s.get_config() |
3345 | + exposed = yield s.get_exposed_flag() |
3346 | + |
3347 | + context.log.info( |
3348 | + "Processing service %s / %s", s.service_name, charm_id) |
3349 | + |
3350 | + # Really!? relation-type not available via state api |
3351 | + topology = yield relations._read_topology() |
3352 | + |
3353 | + # More efficient to do this via topology, ie op per rel |
3354 | + # instead inspection of both sides by each endpoint, which is |
3355 | + # kinda of gross, need some state api support. But trying to |
3356 | + # stick to the state api as much as possible. |
3357 | + svc_rels = yield relations.get_relations_for_service(s) |
3358 | + |
3359 | + for r in svc_rels: |
3360 | + relation_type = topology.get_relation_type(r.internal_relation_id) |
3361 | + rdata = [ |
3362 | + (s.service_name, |
3363 | + relation_type, |
3364 | + r.relation_name, |
3365 | + r.relation_role, |
3366 | + r.relation_scope)] |
3367 | + if r.relation_role == "peer": |
3368 | + rels.add(tuple(rdata)) |
3369 | + continue |
3370 | + rel_services = yield r.get_service_states() |
3371 | + |
3372 | + # Get the endpoint's svc rel state. |
3373 | + found = None |
3374 | + for ep_svc in rel_services: |
3375 | + if ep_svc.service_name == s.service_name: |
3376 | + continue |
3377 | + ep_rels = yield relations.get_relations_for_service(ep_svc) |
3378 | + for ep_r in ep_rels: |
3379 | + if ep_r.internal_relation_id != r.internal_relation_id: |
3380 | + continue |
3381 | + found = (ep_svc, ep_r) |
3382 | + break |
3383 | + |
3384 | + if not found: |
3385 | + context.log.info("Couldn't decipher rel %s %s", |
3386 | + s.service_name, r) |
3387 | + continue |
3388 | + |
3389 | + ep_svc, ep_r = found |
3390 | + rdata.append( |
3391 | + (ep_svc.service_name, |
3392 | + relation_type, |
3393 | + ep_r.relation_name, |
3394 | + ep_r.relation_role, |
3395 | + ep_r.relation_scope)) |
3396 | + rdata.sort() |
3397 | + rels.add(tuple(rdata)) |
3398 | + |
3399 | + data['services'].append( |
3400 | + {'name': s.service_name, |
3401 | + 'charm': charm_id, |
3402 | + 'exposed': exposed, |
3403 | + 'unit_count': len(units), |
3404 | + 'constraints': constraints.data, |
3405 | + 'config': dict(config.items())}) |
3406 | + |
3407 | + data['relations'] = list(rels) |
3408 | + data['constraints'] = (yield environment.get_constraints()).data |
3409 | + returnValue(data) |
3410 | |
3411 | === added file 'juju/rapi/cmd/expose.py' |
3412 | --- juju/rapi/cmd/expose.py 1970-01-01 00:00:00 +0000 |
3413 | +++ juju/rapi/cmd/expose.py 2013-01-17 11:02:31 +0000 |
3414 | @@ -0,0 +1,16 @@ |
3415 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3416 | +from juju.state.service import ServiceStateManager |
3417 | + |
3418 | + |
3419 | +@inlineCallbacks |
3420 | +def expose(context, service_name): |
3421 | + """Expose a service.""" |
3422 | + service_manager = ServiceStateManager(context.client) |
3423 | + service_state = yield service_manager.get_service_state(service_name) |
3424 | + already_exposed = yield service_state.get_exposed_flag() |
3425 | + if not already_exposed: |
3426 | + yield service_state.set_exposed_flag() |
3427 | + context.log.info("Service %r was exposed.", service_name) |
3428 | + else: |
3429 | + context.log.info("Service %r was already exposed.", service_name) |
3430 | + returnValue(True) |
3431 | |
3432 | === added file 'juju/rapi/cmd/import_env.py' |
3433 | --- juju/rapi/cmd/import_env.py 1970-01-01 00:00:00 +0000 |
3434 | +++ juju/rapi/cmd/import_env.py 2013-01-17 11:02:31 +0000 |
3435 | @@ -0,0 +1,98 @@ |
3436 | +from twisted.internet.defer import inlineCallbacks |
3437 | + |
3438 | +from juju.charm.publisher import CharmPublisher |
3439 | +from juju.charm.repository import ( |
3440 | + CharmURL, RemoteCharmRepository, CS_STORE_URL) |
3441 | + |
3442 | +from juju.errors import JujuError |
3443 | + |
3444 | +from juju.state.endpoint import RelationEndpoint |
3445 | +from juju.state.service import ServiceStateManager |
3446 | +from juju.state.machine import MachineStateManager |
3447 | +from juju.state.placement import _unassigned_placement |
3448 | +from juju.state.relation import RelationStateManager |
3449 | + |
3450 | + |
3451 | +@inlineCallbacks |
3452 | +def load(context, data): |
3453 | + """ |
3454 | + |
3455 | + Check for name conflicts |
3456 | + - Upload charms |
3457 | + - Create services (+ config, constraints) |
3458 | + - Add units |
3459 | + - Add relations |
3460 | + """ |
3461 | + # Get state managers |
3462 | + services = ServiceStateManager(context.client) |
3463 | + relations = RelationStateManager(context.client) |
3464 | + machines = MachineStateManager(context.client) |
3465 | + |
3466 | + # First detect conflicts |
3467 | + existing_services = yield services.get_all_service_states() |
3468 | + env_svc_names = [e.service_name for e in existing_services] |
3469 | + import_svc_names = [i['name'] for i in data['services']] |
3470 | + conflict_svc_names = set(env_svc_names) & set(import_svc_names) |
3471 | + if conflict_svc_names: |
3472 | + raise JujuError( |
3473 | + "Import has name conflicts with existing services %s" % ( |
3474 | + ", ".join(conflict_svc_names))) |
3475 | + |
3476 | + store = RemoteCharmRepository(CS_STORE_URL) |
3477 | + |
3478 | + charms = [] |
3479 | + for s in data['services']: |
3480 | + curl = CharmURL.infer(s['charm'], context.default_series) |
3481 | + if s['charm'].startswith('local:'): |
3482 | + raise JujuError( |
3483 | + "Local charm needed but repository not specified") |
3484 | + else: |
3485 | + assert s['charm'].startswith('cs:') |
3486 | + charm = yield store.find(curl) |
3487 | + charms.append((s['charm'], charm)) |
3488 | + |
3489 | + # Upload charms to the environment |
3490 | + publisher = CharmPublisher( |
3491 | + context.client, context.provider.get_file_storage()) |
3492 | + for cid, c in charms: |
3493 | + context.log.info("Publishing charm %s", cid) |
3494 | + yield publisher.add_charm( |
3495 | + str(CharmURL.infer(cid, context.default_series)), |
3496 | + c) |
3497 | + charm_states = yield publisher.publish() |
3498 | + |
3499 | + # Index by url |
3500 | + charm_map = dict([(ci[0], cs) for ci, cs in zip(charms, charm_states)]) |
3501 | + constraint_set = yield context.provider.get_constraint_set() |
3502 | + |
3503 | + # Create services |
3504 | + for s in data['services']: |
3505 | + context.log.info( |
3506 | + "Creating service %s %s", s['name'], charm_map[s['charm']].id) |
3507 | + constraints = constraint_set.load(s['constraints']) |
3508 | + svc = yield services.add_service_state( |
3509 | + s['name'], |
3510 | + charm_map[s['charm']], |
3511 | + constraints) |
3512 | + config = yield svc.get_config() |
3513 | + config.update(s['config']) |
3514 | + yield config.write() |
3515 | + |
3516 | + # Create units |
3517 | + context.log.info("Creating units") |
3518 | + for i in range(s['unit_count']): |
3519 | + unit = yield svc.add_unit_state() |
3520 | + yield _unassigned_placement(context.client, machines, unit) |
3521 | + |
3522 | + # Add relations |
3523 | + for r in data['relations']: |
3524 | + if len(r) == 1: |
3525 | + eps = [RelationEndpoint(*r[0])] |
3526 | + else: |
3527 | + eps = [RelationEndpoint(*r[0]), RelationEndpoint(*r[1])] |
3528 | + context.log.info("Adding relation %s" % ( |
3529 | + " ".join(map( |
3530 | + lambda r: "%s:%s %s" % ( |
3531 | + r.service_name, r.relation_name, r.relation_role), |
3532 | + eps)))) |
3533 | + yield relations.add_relation_state(*eps) |
3534 | |
3535 | === added file 'juju/rapi/cmd/remove_relation.py' |
3536 | --- juju/rapi/cmd/remove_relation.py 1970-01-01 00:00:00 +0000 |
3537 | +++ juju/rapi/cmd/remove_relation.py 2013-01-17 11:02:31 +0000 |
3538 | @@ -0,0 +1,58 @@ |
3539 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3540 | + |
3541 | +from juju.state.errors import (AmbiguousRelation, NoMatchingEndpoints, |
3542 | + UnsupportedSubordinateServiceRemoval) |
3543 | +from juju.state.relation import RelationStateManager |
3544 | +from juju.state.service import ServiceStateManager |
3545 | + |
3546 | + |
3547 | +@inlineCallbacks |
3548 | +def remove_relation(context, descriptors=()): |
3549 | + |
3550 | + relation_state_manager = RelationStateManager(context.client) |
3551 | + service_state_manager = ServiceStateManager(context.client) |
3552 | + endpoint_pairs = yield service_state_manager.join_descriptors( |
3553 | + *descriptors) |
3554 | + |
3555 | + if len(endpoint_pairs) == 0: |
3556 | + raise NoMatchingEndpoints() |
3557 | + elif len(endpoint_pairs) > 1: |
3558 | + raise AmbiguousRelation(descriptors, endpoint_pairs) |
3559 | + |
3560 | + # At this point we just have one endpoint pair. We need to pick |
3561 | + # just one of the endpoints if it's a peer endpoint, since that's |
3562 | + # our current API - join descriptors takes two descriptors, but |
3563 | + # add_relation_state takes one or two endpoints. TODO consider |
3564 | + # refactoring. |
3565 | + endpoints = endpoint_pairs[0] |
3566 | + if endpoints[0] == endpoints[1]: |
3567 | + endpoints = endpoints[0:1] |
3568 | + relation_state = yield relation_state_manager.get_relation_state( |
3569 | + *endpoints) |
3570 | + |
3571 | + # Look at both endpoints, if we are dealing with a container relation |
3572 | + # decide if one end is a principal. |
3573 | + service_pair = [] # ordered such that sub, principal |
3574 | + |
3575 | + is_container = False |
3576 | + has_principal = True |
3577 | + for ep in endpoints: |
3578 | + if ep.relation_scope == "container": |
3579 | + is_container = True |
3580 | + service = yield service_state_manager.get_service_state( |
3581 | + ep.service_name) |
3582 | + if (yield service.is_subordinate()): |
3583 | + service_pair.append(service) |
3584 | + has_principal = True |
3585 | + else: |
3586 | + service_pair.insert(0, service) |
3587 | + if is_container and len(service_pair) == 2 and has_principal: |
3588 | + sub, principal = service_pair |
3589 | + raise UnsupportedSubordinateServiceRemoval(sub.service_name, |
3590 | + principal.service_name) |
3591 | + |
3592 | + yield relation_state_manager.remove_relation_state(relation_state) |
3593 | + context.log.info("Removed %s relation from all service units.", |
3594 | + endpoints[0].relation_type) |
3595 | + |
3596 | + returnValue(True) |
3597 | |
3598 | === added file 'juju/rapi/cmd/remove_unit.py' |
3599 | --- juju/rapi/cmd/remove_unit.py 1970-01-01 00:00:00 +0000 |
3600 | +++ juju/rapi/cmd/remove_unit.py 2013-01-17 11:02:31 +0000 |
3601 | @@ -0,0 +1,30 @@ |
3602 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3603 | + |
3604 | +from juju.state.errors import ( |
3605 | + UnsupportedSubordinateServiceRemoval, ServiceUnitStateNotFound) |
3606 | +from juju.state.service import ServiceStateManager, parse_service_name |
3607 | + |
3608 | + |
3609 | +@inlineCallbacks |
3610 | +def remove_unit(context, unit_names): |
3611 | + service_manager = ServiceStateManager(context.client) |
3612 | + for unit_name in unit_names: |
3613 | + service_name = parse_service_name(unit_name) |
3614 | + service_state = yield service_manager.get_service_state( |
3615 | + service_name) |
3616 | + try: |
3617 | + unit_state = yield service_state.get_unit_state(unit_name) |
3618 | + except ServiceUnitStateNotFound: |
3619 | + context.log.warning("Unit %r does not exist", unit_name) |
3620 | + continue |
3621 | + if (yield service_state.is_subordinate()): |
3622 | + container = yield unit_state.get_container() |
3623 | + raise UnsupportedSubordinateServiceRemoval( |
3624 | + unit_state.unit_name, |
3625 | + container.unit_name) |
3626 | + |
3627 | + yield service_state.remove_unit_state(unit_state) |
3628 | + context.log.info("Unit %r removed from service %r", |
3629 | + unit_state.unit_name, service_state.service_name) |
3630 | + |
3631 | + returnValue(True) |
3632 | |
3633 | === added file 'juju/rapi/cmd/resolved.py' |
3634 | --- juju/rapi/cmd/resolved.py 1970-01-01 00:00:00 +0000 |
3635 | +++ juju/rapi/cmd/resolved.py 2013-01-17 11:02:31 +0000 |
3636 | @@ -0,0 +1,70 @@ |
3637 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3638 | + |
3639 | +from juju.state.service import ServiceStateManager, RETRY_HOOKS, NO_HOOKS |
3640 | +from juju.state.relation import RelationStateManager |
3641 | +from juju.state.errors import RelationStateNotFound |
3642 | +from juju.unit.workflow import is_unit_running, is_relation_running |
3643 | + |
3644 | + |
3645 | +@inlineCallbacks |
3646 | +def resolved(context, unit_name, relation_name, retry): |
3647 | + """Mark an error as resolved in a unit or unit relation. |
3648 | + |
3649 | + If one of a unit's charm non-relation hooks returns a non-zero exit |
3650 | + status, the entire unit can be considered to be in a non-running state. |
3651 | + |
3652 | + As a resolution, the the unit can be manually returned a running state |
3653 | + via the juju resolved command. Optionally this command can also |
3654 | + rerun the failed hook. |
3655 | + |
3656 | + This resolution also applies separately to each of the unit's relations. |
3657 | + If one of the relation-hooks failed. In that case there is no |
3658 | + notion of retrying (the change is gone), but resolving will allow |
3659 | + additional relation hooks for that relation to proceed. |
3660 | + """ |
3661 | + service_manager = ServiceStateManager(context.client) |
3662 | + relation_manager = RelationStateManager(context.client) |
3663 | + |
3664 | + unit_state = yield service_manager.get_unit_state(unit_name) |
3665 | + service_state = yield service_manager.get_service_state( |
3666 | + unit_name.split("/")[0]) |
3667 | + |
3668 | + retry = retry and RETRY_HOOKS or NO_HOOKS |
3669 | + |
3670 | + if not relation_name: |
3671 | + running, workflow_state = yield is_unit_running( |
3672 | + context.client, unit_state) |
3673 | + if running: |
3674 | + context.log.info( |
3675 | + "Unit %r already running: %s", unit_name, workflow_state) |
3676 | + returnValue(False) |
3677 | + |
3678 | + yield unit_state.set_resolved(retry) |
3679 | + context.log.info("Marked unit %r as resolved", unit_name) |
3680 | + returnValue(True) |
3681 | + |
3682 | + # Check for the matching relations |
3683 | + service_relations = yield relation_manager.get_relations_for_service( |
3684 | + service_state) |
3685 | + service_relations = [ |
3686 | + sr for sr in service_relations if sr.relation_name == relation_name] |
3687 | + if not service_relations: |
3688 | + raise RelationStateNotFound() |
3689 | + |
3690 | + # Verify the relations are in need of resolution. |
3691 | + resolved_relations = {} |
3692 | + for service_relation in service_relations: |
3693 | + unit_relation = yield service_relation.get_unit_state(unit_state) |
3694 | + running, state = yield is_relation_running( |
3695 | + context.client, unit_relation) |
3696 | + if not running: |
3697 | + resolved_relations[unit_relation.internal_relation_id] = retry |
3698 | + |
3699 | + if not resolved_relations: |
3700 | + context.log.warning("Matched relations are all running") |
3701 | + returnValue(False) |
3702 | + |
3703 | + # Mark the relations as resolved. |
3704 | + yield unit_state.set_relation_resolved(resolved_relations) |
3705 | + context.log.info( |
3706 | + "Marked unit %r relation %r as resolved", unit_name, relation_name) |
3707 | |
3708 | === added file 'juju/rapi/cmd/status.py' |
3709 | --- juju/rapi/cmd/status.py 1970-01-01 00:00:00 +0000 |
3710 | +++ juju/rapi/cmd/status.py 2013-01-17 11:02:31 +0000 |
3711 | @@ -0,0 +1,471 @@ |
3712 | +from fnmatch import fnmatch |
3713 | + |
3714 | +from twisted.internet.defer import inlineCallbacks, returnValue |
3715 | + |
3716 | +from juju.errors import ProviderError |
3717 | +from juju.state.errors import UnitRelationStateNotFound |
3718 | +from juju.state.charm import CharmStateManager |
3719 | +from juju.state.machine import MachineStateManager |
3720 | +from juju.state.service import ServiceStateManager, parse_service_name |
3721 | +from juju.state.relation import RelationStateManager |
3722 | +from juju.unit.workflow import WorkflowStateClient |
3723 | + |
3724 | + |
3725 | +@inlineCallbacks |
3726 | +def status(context, scope): |
3727 | + inspector = StatusInspector( |
3728 | + context.client, context.provider, context.log) |
3729 | + data = yield inspector(scope) |
3730 | + returnValue(data) |
3731 | + |
3732 | + |
3733 | +def digest_scope(scope): |
3734 | + """Parse scope used to filter status information. |
3735 | + |
3736 | + `scope`: a list of name specifiers. see collect() |
3737 | + |
3738 | + Returns a tuple of (service_filter, unit_filter). The values in |
3739 | + either filter list will be passed as a glob to fnmatch |
3740 | + """ |
3741 | + |
3742 | + services = [] |
3743 | + units = [] |
3744 | + |
3745 | + if scope is not None: |
3746 | + for value in scope: |
3747 | + if "/" in value: |
3748 | + units.append(value) |
3749 | + else: |
3750 | + services.append(value) |
3751 | + |
3752 | + return (services, units) |
3753 | + |
3754 | + |
3755 | +class StatusInspector(object): |
3756 | + def __init__(self, client, provider, log): |
3757 | + """ |
3758 | + Status inspector. |
3759 | + |
3760 | + `client`: ZK client connection |
3761 | + `provider`: machine provider for the environment |
3762 | + `log`: a Python stdlib logger. |
3763 | + |
3764 | + """ |
3765 | + self.client = client |
3766 | + self.provider = provider |
3767 | + self.log = log |
3768 | + |
3769 | + self.service_manager = ServiceStateManager(client) |
3770 | + self.relation_manager = RelationStateManager(client) |
3771 | + self.machine_manager = MachineStateManager(client) |
3772 | + self.charm_manager = CharmStateManager(client) |
3773 | + self._reset() |
3774 | + |
3775 | + def _reset(self, scope=None): |
3776 | + # init per-run state |
3777 | + # self.state is assembled by the various process methods |
3778 | + # intermediate access to state is made more convenient |
3779 | + # using these references to its internals. |
3780 | + self.service_data = {} # service name: service info |
3781 | + self.machine_data = {} # machine id: machine state |
3782 | + self.unit_data = {} # unit_name :unit_info |
3783 | + |
3784 | + # used in collecting subordinate (which are added to state in a two |
3785 | + # phase pass) |
3786 | + self.subordinates = {} # service : set(principal service names) |
3787 | + |
3788 | + self.state = dict(services=self.service_data, |
3789 | + machines=self.machine_data) |
3790 | + |
3791 | + # Filtering info |
3792 | + self.seen_machines = set() |
3793 | + self.filter_services, self.filter_units = digest_scope(scope) |
3794 | + |
3795 | + @inlineCallbacks |
3796 | + def __call__(self, scope=None): |
3797 | + """Extract status information into nested dicts for rendering. |
3798 | + |
3799 | + `scope`: an optional list of name specifiers. Globbing based wildcards |
3800 | + supported. Defaults to all units, services and relations. |
3801 | + |
3802 | + """ |
3803 | + self._reset(scope) |
3804 | + |
3805 | + # Pass 1 Gather Data (including principals and subordinates) |
3806 | + # this builds unit info and container relationships |
3807 | + # which is assembled in pass 2 below |
3808 | + yield self._process_services() |
3809 | + |
3810 | + # Pass 2: Nest information according to principal/subordinates |
3811 | + # rules |
3812 | + self._process_subordinates() |
3813 | + |
3814 | + yield self._process_machines() |
3815 | + |
3816 | + returnValue(self.state) |
3817 | + |
3818 | + @inlineCallbacks |
3819 | + def _process_services(self): |
3820 | + """ |
3821 | + For each service gather the following information:: |
3822 | + |
3823 | + <service name>: |
3824 | + charm: <charm name> |
3825 | + exposed: <expose boolean> |
3826 | + relations: |
3827 | + <relation info -- see _process_relations> |
3828 | + units: |
3829 | + <unit info -- see _process_units> |
3830 | + """ |
3831 | + services = yield self.service_manager.get_all_service_states() |
3832 | + for service in services: |
3833 | + if len(self.filter_services): |
3834 | + found = False |
3835 | + for filter_service in self.filter_services: |
3836 | + if fnmatch(service.service_name, filter_service): |
3837 | + found = True |
3838 | + break |
3839 | + if not found: |
3840 | + continue |
3841 | + yield self._process_service(service) |
3842 | + |
3843 | + @inlineCallbacks |
3844 | + def _process_service(self, service): |
3845 | + """ |
3846 | + Gather the service info (described in _process_services). |
3847 | + |
3848 | + `service`: ServiceState instance |
3849 | + """ |
3850 | + |
3851 | + relation_data = {} |
3852 | + service_data = self.service_data |
3853 | + |
3854 | + charm_id = yield service.get_charm_id() |
3855 | + charm = yield self.charm_manager.get_charm_state(charm_id) |
3856 | + |
3857 | + service_data[service.service_name] = ( |
3858 | + dict(units={}, |
3859 | + charm=charm.id, |
3860 | + relations=relation_data)) |
3861 | + |
3862 | + if (yield service.is_subordinate()): |
3863 | + service_data[service.service_name]["subordinate"] = True |
3864 | + |
3865 | + yield self._process_expose(service) |
3866 | + |
3867 | + relations, rel_svc_map = yield self._process_relation_map( |
3868 | + service) |
3869 | + |
3870 | + unit_matched = yield self._process_units(service, |
3871 | + relations, |
3872 | + rel_svc_map) |
3873 | + |
3874 | + # after filtering units check if any matched or remove the |
3875 | + # service from the output |
3876 | + if self.filter_units and not unit_matched: |
3877 | + del service_data[service.service_name] |
3878 | + return |
3879 | + |
3880 | + yield self._process_relations(service, relations, rel_svc_map) |
3881 | + |
3882 | + @inlineCallbacks |
3883 | + def _process_units(self, service, relations, rel_svc_map): |
3884 | + """ |
3885 | + Gather unit information for a service:: |
3886 | + |
3887 | + <unit name>: |
3888 | + agent-state: <started|pendding|etc> |
3889 | + machine: <machine id> |
3890 | + open-ports: ["port/protocol", ...] |
3891 | + public-address: <public dns name or ip> |
3892 | + subordinates: |
3893 | + <optional nested units of subordinate services> |
3894 | + |
3895 | + |
3896 | + `service`: ServiceState intance |
3897 | + `relations`: list of ServiceRelationState instance for this service |
3898 | + `rel_svc_map`: maps relation internal ids to the remote endpoint |
3899 | + service name. This references the name of the remote |
3900 | + endpoint and so is generated per service. |
3901 | + """ |
3902 | + units = yield service.get_all_unit_states() |
3903 | + unit_matched = False |
3904 | + |
3905 | + for unit in units: |
3906 | + if len(self.filter_units): |
3907 | + found = False |
3908 | + for filter_unit in self.filter_units: |
3909 | + if fnmatch(unit.unit_name, filter_unit): |
3910 | + found = True |
3911 | + break |
3912 | + if not found: |
3913 | + continue |
3914 | + yield self._process_unit(service, unit, relations, rel_svc_map) |
3915 | + unit_matched = True |
3916 | + returnValue(unit_matched) |
3917 | + |
3918 | + @inlineCallbacks |
3919 | + def _process_unit(self, service, unit, relations, rel_svc_map): |
3920 | + """ Generate unit info for a single unit of a single service. |
3921 | + |
3922 | + `unit`: ServiceUnitState |
3923 | + see `_process_units` for an explanation of other arguments. |
3924 | + |
3925 | + """ |
3926 | + u = self.unit_data[unit.unit_name] = dict() |
3927 | + container = yield unit.get_container() |
3928 | + |
3929 | + if container: |
3930 | + u["container"] = container.unit_name |
3931 | + self.subordinates.setdefault(unit.service_name, |
3932 | + set()).add(container.service_name) |
3933 | + |
3934 | + machine_id = yield unit.get_assigned_machine_id() |
3935 | + u["machine"] = machine_id |
3936 | + unit_workflow_client = WorkflowStateClient(self.client, unit) |
3937 | + unit_state = yield unit_workflow_client.get_state() |
3938 | + if not unit_state: |
3939 | + u["agent-state"] = "pending" |
3940 | + else: |
3941 | + unit_connected = yield unit.has_agent() |
3942 | + u["agent-state"] = unit_state.replace("_", "-") \ |
3943 | + if unit_connected else "down" |
3944 | + |
3945 | + exposed = self.service_data[service.service_name].get("exposed") |
3946 | + open_ports = yield unit.get_open_ports() |
3947 | + if exposed: |
3948 | + u["open-ports"] = ["{port}/{proto}".format(**port_info) |
3949 | + for port_info in open_ports] |
3950 | + elif open_ports: |
3951 | + # Ensure a hint is provided that there are open ports if |
3952 | + # not exposed by setting the key in the output |
3953 | + self.service_data[service.service_name]["exposed"] = False |
3954 | + |
3955 | + u["public-address"] = yield unit.get_public_address() |
3956 | + |
3957 | + # indicate we should include information about this |
3958 | + # machine later |
3959 | + self.seen_machines.add(machine_id) |
3960 | + |
3961 | + # collect info on each relation for the service unit |
3962 | + yield self._process_unit_relations(service, unit, |
3963 | + relations, rel_svc_map) |
3964 | + |
3965 | + @inlineCallbacks |
3966 | + def _process_relation_map(self, service): |
3967 | + """Generate a mapping from a services relations to the service name of |
3968 | + the remote endpoints. |
3969 | + |
3970 | + returns: ([ServiceRelationState, ...], mapping) |
3971 | + """ |
3972 | + relation_data = self.service_data[service.service_name]["relations"] |
3973 | + relation_mgr = self.relation_manager |
3974 | + relations = yield relation_mgr.get_relations_for_service(service) |
3975 | + rel_svc_map = {} |
3976 | + |
3977 | + for relation in relations: |
3978 | + rel_services = yield relation.get_service_states() |
3979 | + |
3980 | + # A single related service implies a peer relation. More |
3981 | + # imply a bi-directional provides/requires relationship. |
3982 | + # In the later case we omit the local side of the relation |
3983 | + # when reporting. |
3984 | + if len(rel_services) > 1: |
3985 | + # Filter out self from multi-service relations. |
3986 | + rel_services = [ |
3987 | + rsn for rsn in rel_services if rsn.service_name != |
3988 | + service.service_name] |
3989 | + |
3990 | + if len(rel_services) > 1: |
3991 | + raise ValueError("Unexpected relationship with more " |
3992 | + "than 2 endpoints") |
3993 | + |
3994 | + rel_service = rel_services[0] |
3995 | + relation_data.setdefault(relation.relation_name, set()).add( |
3996 | + rel_service.service_name) |
3997 | + rel_svc_map[relation.internal_relation_id] = ( |
3998 | + rel_service.service_name) |
3999 | + |
4000 | + returnValue((relations, rel_svc_map)) |
4001 | + |
4002 | + @inlineCallbacks |
4003 | + def _process_relations(self, service, relations, rel_svc_map): |
4004 | + """Generate relation information for a given service |
4005 | + |
4006 | + Each service with relations will have a relations dict |
4007 | + nested under it with one or more relations described:: |
4008 | + |
4009 | + relations: |
4010 | + <relation name>: |
4011 | + - <remote service name> |
4012 | + |
4013 | + """ |
4014 | + relation_data = self.service_data[service.service_name]["relations"] |
4015 | + |
4016 | + for relation in relations: |
4017 | + rel_services = yield relation.get_service_states() |
4018 | + |
4019 | + # A single related service implies a peer relation. More |
4020 | + # imply a bi-directional provides/requires relationship. |
4021 | + # In the later case we omit the local side of the relation |
4022 | + # when reporting. |
4023 | + if len(rel_services) > 1: |
4024 | + # Filter out self from multi-service relations. |
4025 | + rel_services = [ |
4026 | + rsn for rsn in rel_services if rsn.service_name != |
4027 | + service.service_name] |
4028 | + |
4029 | + if len(rel_services) > 1: |
4030 | + raise ValueError("Unexpected relationship with more " |
4031 | + "than 2 endpoints") |
4032 | + |
4033 | + rel_service = rel_services[0] |
4034 | + relation_data.setdefault( |
4035 | + relation.relation_name, set()).add( |
4036 | + rel_service.service_name) |
4037 | + rel_svc_map[relation.internal_relation_id] = ( |
4038 | + rel_service.service_name) |
4039 | + |
4040 | + # Normalize the sets back to lists |
4041 | + for r in relation_data: |
4042 | + relation_data[r] = sorted(relation_data[r]) |
4043 | + |
4044 | + @inlineCallbacks |
4045 | + def _process_unit_relations(self, service, unit, relations, rel_svc_map): |
4046 | + """Collect UnitRelationState information per relation and per unit. |
4047 | + |
4048 | + Includes information under each unit for its relations including |
4049 | + its relation state and information about any possible errors. |
4050 | + |
4051 | + see `_process_relations` for argument information |
4052 | + """ |
4053 | + u = self.unit_data[unit.unit_name] |
4054 | + relation_errors = {} |
4055 | + |
4056 | + for relation in relations: |
4057 | + try: |
4058 | + relation_unit = yield relation.get_unit_state(unit) |
4059 | + except UnitRelationStateNotFound: |
4060 | + # This exception will occur when relations are |
4061 | + # established between services without service |
4062 | + # units, and therefore never have any |
4063 | + # corresponding service relation units. |
4064 | + # UPDATE: common with subordinate services, and |
4065 | + # some testing scenarios. |
4066 | + continue |
4067 | + relation_workflow_client = WorkflowStateClient( |
4068 | + self.client, relation_unit) |
4069 | + workflow_state = yield relation_workflow_client.get_state() |
4070 | + |
4071 | + rel_svc_name = rel_svc_map.get(relation.internal_relation_id) |
4072 | + if rel_svc_name and workflow_state not in ("up", None): |
4073 | + relation_errors.setdefault( |
4074 | + relation.relation_name, set()).add(rel_svc_name) |
4075 | + |
4076 | + if relation_errors: |
4077 | + # Normalize sets and store. |
4078 | + u["relation-errors"] = dict( |
4079 | + [(r, sorted(relation_errors[r])) for r in relation_errors]) |
4080 | + |
4081 | + def _process_subordinates(self): |
4082 | + """Properly nest subordinate units under their principal service's |
4083 | + unit nodes. Services and units are generated in one pass, then |
4084 | + iterated by this method to structure the output data to reflect |
4085 | + actual unit containment. |
4086 | + |
4087 | + Subordinate units will include the follow:: |
4088 | + subordinate: true |
4089 | + subordinate-to: |
4090 | + - <principal service names> |
4091 | + |
4092 | + Principal services that have subordinates will include:: |
4093 | + |
4094 | + subordinates: |
4095 | + <subordinate unit name>: |
4096 | + agent-state: <agent state> |
4097 | + """ |
4098 | + service_data = self.service_data |
4099 | + |
4100 | + for unit_name, u in self.unit_data.iteritems(): |
4101 | + container = u.get("container") |
4102 | + if container: |
4103 | + d = self.unit_data[container].setdefault("subordinates", {}) |
4104 | + d[unit_name] = u |
4105 | + # remove key that don't appear in output or come from container |
4106 | + for key in ("container", "machine", "public-address"): |
4107 | + u.pop(key, None) |
4108 | + else: |
4109 | + service_name = parse_service_name(unit_name) |
4110 | + service_data[service_name]["units"][unit_name] = u |
4111 | + |
4112 | + for sub_service, principal_services in self.subordinates.iteritems(): |
4113 | + service_data[sub_service]["subordinate-to"] = sorted( |
4114 | + principal_services) |
4115 | + service_data[sub_service].pop("units", None) |
4116 | + |
4117 | + @inlineCallbacks |
4118 | + def _process_expose(self, service): |
4119 | + """Indicate if a service is exposed or not.""" |
4120 | + exposed = yield service.get_exposed_flag() |
4121 | + if exposed: |
4122 | + self.service_data[service.service_name].update(exposed=exposed) |
4123 | + returnValue(exposed) |
4124 | + |
4125 | + @inlineCallbacks |
4126 | + def _process_machines(self): |
4127 | + """Gather machine information. |
4128 | + |
4129 | + machines: |
4130 | + <machine id>: |
4131 | + agent-state: <agent state> |
4132 | + dns-name: <dns name> |
4133 | + instance-id: <provider specific instance id> |
4134 | + instance-state: <instance state> |
4135 | + """ |
4136 | + |
4137 | + machines = yield self.machine_manager.get_all_machine_states() |
4138 | + for machine_state in machines: |
4139 | + if (self.filter_services or self.filter_units) and \ |
4140 | + machine_state.id not in self.seen_machines: |
4141 | + continue |
4142 | + yield self._process_machine(machine_state) |
4143 | + |
4144 | + @inlineCallbacks |
4145 | + def _process_machine(self, machine_state): |
4146 | + """ |
4147 | + `machine_state`: MachineState instance |
4148 | + """ |
4149 | + instance_id = yield machine_state.get_instance_id() |
4150 | + m = {"instance-id": instance_id \ |
4151 | + if instance_id is not None else "pending"} |
4152 | + if instance_id is not None: |
4153 | + try: |
4154 | + pm = yield self.provider.get_machine(instance_id) |
4155 | + m["dns-name"] = pm.dns_name |
4156 | + m["instance-state"] = pm.state |
4157 | + if (yield machine_state.has_agent()): |
4158 | + # if the agent's connected, we're fine |
4159 | + m["agent-state"] = "running" |
4160 | + else: |
4161 | + units = ( |
4162 | + yield machine_state.get_all_service_unit_states()) |
4163 | + for unit in units: |
4164 | + unit_workflow_client = WorkflowStateClient( |
4165 | + self.client, unit) |
4166 | + if (yield unit_workflow_client.get_state()): |
4167 | + # for unit to have a state, its agent must |
4168 | + # have run, which implies the machine agent |
4169 | + # must have been running correctly at some |
4170 | + # point in the past |
4171 | + m["agent-state"] = "down" |
4172 | + break |
4173 | + else: |
4174 | + # otherwise we're probably just still waiting |
4175 | + m["agent-state"] = "not-started" |
4176 | + except ProviderError: |
4177 | + # The provider doesn't have machine information |
4178 | + self.log.exception( |
4179 | + "Machine provider information missing: machine %s" % ( |
4180 | + machine_state.id)) |
4181 | + |
4182 | + self.machine_data[machine_state.id] = m |
4183 | |
4184 | === added file 'juju/rapi/cmd/terminate_machine.py' |
4185 | --- juju/rapi/cmd/terminate_machine.py 1970-01-01 00:00:00 +0000 |
4186 | +++ juju/rapi/cmd/terminate_machine.py 2013-01-17 11:02:31 +0000 |
4187 | @@ -0,0 +1,41 @@ |
4188 | +from twisted.internet.defer import inlineCallbacks, returnValue |
4189 | + |
4190 | +from juju.errors import CannotTerminateMachine |
4191 | +from juju.state.errors import MachineStateNotFound |
4192 | +from juju.state.machine import MachineStateManager |
4193 | + |
4194 | + |
4195 | +@inlineCallbacks |
4196 | +def terminate_machine(context, machine_ids): |
4197 | + """Terminates the machines in `machine_ids`. |
4198 | + |
4199 | + Like the underlying code in MachineStateManager, it's permissible |
4200 | + if the machine ID is already terminated or even never running. If |
4201 | + we determine this is not desired behavior, presumably propagate |
4202 | + that back to the state manager. |
4203 | + |
4204 | + XXX However, we currently special case support of not terminating |
4205 | + the "root" machine, that is the one running the provisioning |
4206 | + agent. At some point, this will be managed like any other service, |
4207 | + but until then it seems best to ensure it's not terminated at this |
4208 | + level. |
4209 | + """ |
4210 | + terminated_machine_ids = [] |
4211 | + machine_state_manager = MachineStateManager(context.client) |
4212 | + |
4213 | + for machine_id in machine_ids: |
4214 | + if machine_id == 0: |
4215 | + raise CannotTerminateMachine( |
4216 | + 0, "environment would be destroyed") |
4217 | + removed = yield machine_state_manager.remove_machine_state( |
4218 | + machine_id) |
4219 | + if not removed: |
4220 | + raise MachineStateNotFound(machine_id) |
4221 | + terminated_machine_ids.append(machine_id) |
4222 | + |
4223 | + if terminated_machine_ids: |
4224 | + context.log.info( |
4225 | + "Machines terminated: %s", |
4226 | + ", ".join(str(id) for id in terminated_machine_ids)) |
4227 | + |
4228 | + returnValue(True) |
4229 | |
4230 | === added directory 'juju/rapi/cmd/tests' |
4231 | === added file 'juju/rapi/cmd/tests/__init__.py' |
4232 | --- juju/rapi/cmd/tests/__init__.py 1970-01-01 00:00:00 +0000 |
4233 | +++ juju/rapi/cmd/tests/__init__.py 2013-01-17 11:02:31 +0000 |
4234 | @@ -0,0 +1,1 @@ |
4235 | +# |
4236 | |
4237 | === added file 'juju/rapi/cmd/unexpose.py' |
4238 | --- juju/rapi/cmd/unexpose.py 1970-01-01 00:00:00 +0000 |
4239 | +++ juju/rapi/cmd/unexpose.py 2013-01-17 11:02:31 +0000 |
4240 | @@ -0,0 +1,16 @@ |
4241 | +from twisted.internet.defer import inlineCallbacks, returnValue |
4242 | +from juju.state.service import ServiceStateManager |
4243 | + |
4244 | + |
4245 | +@inlineCallbacks |
4246 | +def unexpose(context, service_name): |
4247 | + """Unexpose a service.""" |
4248 | + service_manager = ServiceStateManager(context.client) |
4249 | + service_state = yield service_manager.get_service_state(service_name) |
4250 | + already_exposed = yield service_state.get_exposed_flag() |
4251 | + if already_exposed: |
4252 | + yield service_state.clear_exposed_flag() |
4253 | + context.log.info("Service %r was unexposed.", service_name) |
4254 | + else: |
4255 | + context.log.info("Service %r was not exposed.", service_name) |
4256 | + returnValue(True) |
4257 | |
4258 | === added file 'juju/rapi/context.py' |
4259 | --- juju/rapi/context.py 1970-01-01 00:00:00 +0000 |
4260 | +++ juju/rapi/context.py 2013-01-17 11:02:31 +0000 |
4261 | @@ -0,0 +1,332 @@ |
4262 | +from StringIO import StringIO |
4263 | +from twisted.internet.defer import ( |
4264 | + maybeDeferred, succeed, returnValue, inlineCallbacks) |
4265 | + |
4266 | +from juju.rapi.cmd import add_relation |
4267 | +from juju.rapi.cmd import add_unit |
4268 | +from juju.rapi.cmd import config_get |
4269 | +from juju.rapi.cmd import config_set |
4270 | +from juju.rapi.cmd import constraints_set |
4271 | +from juju.rapi.cmd import constraints_get |
4272 | +from juju.rapi.cmd import deploy |
4273 | +from juju.rapi.cmd import debug_hooks |
4274 | +from juju.rapi.cmd import destroy_service |
4275 | +from juju.rapi.cmd import expose |
4276 | +from juju.rapi.cmd import export |
4277 | +from juju.rapi.cmd import import_env |
4278 | +from juju.rapi.cmd import remove_relation |
4279 | +from juju.rapi.cmd import remove_unit |
4280 | +from juju.rapi.cmd import resolved |
4281 | +from juju.rapi.cmd import status |
4282 | +from juju.rapi.cmd import terminate_machine |
4283 | +from juju.rapi.cmd import unexpose |
4284 | + |
4285 | +from juju.rapi import rest |
4286 | + |
4287 | +from juju.state.annotation import AnnotationManager |
4288 | +from juju.state.security import Principal |
4289 | + |
4290 | +import logging |
4291 | +import zookeeper |
4292 | + |
4293 | +log = logging.getLogger('juju.rapi.context') |
4294 | + |
4295 | + |
4296 | +class LogBuffer(object): |
4297 | + |
4298 | + def __init__(self): |
4299 | + self.msgs = [] |
4300 | + |
4301 | + def debug(self, msg, *args): |
4302 | + self.msgs.append(('debug', msg % args)) |
4303 | + |
4304 | + def error(self, msg, *args): |
4305 | + self.msgs.append(('error', msg % args)) |
4306 | + |
4307 | + def exception(self, msg, *args): |
4308 | + self.msgs.append(('error', msg % args)) |
4309 | + |
4310 | + def info(self, msg, *args): |
4311 | + self.msgs.append(('info', msg % args)) |
4312 | + |
4313 | + def warning(self, msg, *args): |
4314 | + self.msgs.append(('warning', msg % args)) |
4315 | + |
4316 | + def reset(self): |
4317 | + msgs = list(self.msgs) |
4318 | + self.msgs = [] |
4319 | + return msgs |
4320 | + |
4321 | + |
4322 | +class APIContext(object): |
4323 | + |
4324 | + default_series = "precise" |
4325 | + charm_cache_dir = "/tmp" |
4326 | + |
4327 | + def __init__(self, client, provider): |
4328 | + self.client = client |
4329 | + self.provider = provider |
4330 | + self.log = LogBuffer() |
4331 | + |
4332 | + def connect(self, *args, **kw): |
4333 | + if self.client.connected: |
4334 | + return succeed(self) |
4335 | + d = self.client.connect(*args, **kw) |
4336 | + d.addCallback(lambda x: self) |
4337 | + return d |
4338 | + |
4339 | + def close(self): |
4340 | + if self.client.connected: |
4341 | + self.client.close() |
4342 | + |
4343 | + def _invoke(self, func, *args, **kw): |
4344 | + d = maybeDeferred(func, *args, **kw) |
4345 | + return d.addCallback( |
4346 | + lambda r: {'result': r, 'log': self.log.reset()} |
4347 | + ).addErrback(self._on_error) |
4348 | + |
4349 | + def _on_error(self, failure): |
4350 | + io = StringIO() |
4351 | + failure.printTraceback(file=io) |
4352 | + return {'err': True, 'log': self.log.reset(), |
4353 | + 'failure': io.getvalue()} |
4354 | + |
4355 | + def add_relation(self, endpoint_a, endpoint_b): |
4356 | + """Add a relation to a service(s). |
4357 | + """ |
4358 | + return self._invoke( |
4359 | + add_relation.add_relation, |
4360 | + self, |
4361 | + endpoint_a, |
4362 | + endpoint_b) |
4363 | + |
4364 | + def add_unit(self, service_name, num_units): |
4365 | + """Add a unit to a service. |
4366 | + """ |
4367 | + return self._invoke( |
4368 | + add_unit.add_unit, |
4369 | + self, |
4370 | + service_name, |
4371 | + num_units) |
4372 | + |
4373 | + def get_annotations(self, entity): |
4374 | + """Retrieve annotations for an entity. |
4375 | + """ |
4376 | + return self._invoke(self._get_annotations, entity) |
4377 | + |
4378 | + def _get_annotations(self, entity): |
4379 | + return AnnotationManager(self.client).get(entity) |
4380 | + |
4381 | + def get_service(self, service_name): |
4382 | + """Get information about the named service.""" |
4383 | + return self._invoke( |
4384 | + rest.get_service, |
4385 | + self, |
4386 | + service_name) |
4387 | + |
4388 | + def get_endpoints(self, service_names=()): |
4389 | + """Return available relation endpoints for the given services.""" |
4390 | + return self._invoke( |
4391 | + rest.get_endpoints, |
4392 | + self, |
4393 | + service_names) |
4394 | + |
4395 | + def get_charm(self, charm_url): |
4396 | + """Get information about the named charm from the env.""" |
4397 | + return self._invoke( |
4398 | + rest.get_charm, |
4399 | + self, |
4400 | + charm_url) |
4401 | + |
4402 | + def get_constraints(self, named_entities=()): |
4403 | + """Get the constraints of the named entities. |
4404 | + |
4405 | + If no entities are specified, return the constraints |
4406 | + on the environment. |
4407 | + """ |
4408 | + return self._invoke( |
4409 | + constraints_get.constraints_get, |
4410 | + self, |
4411 | + named_entities) |
4412 | + |
4413 | + def get_config(self, service_name): |
4414 | + """Get configuration of a service. |
4415 | + """ |
4416 | + return self._invoke( |
4417 | + config_get.config_get, |
4418 | + self, |
4419 | + service_name) |
4420 | + |
4421 | + def deploy(self, |
4422 | + charm_url, |
4423 | + service_name=None, |
4424 | + charm_bundle=None, |
4425 | + constraints=(), |
4426 | + config=None, |
4427 | + config_raw=None, |
4428 | + num_units=1): |
4429 | + """Deploy a charm as a service |
4430 | + """ |
4431 | + return self._invoke( |
4432 | + deploy.deploy, |
4433 | + self, |
4434 | + charm_url, |
4435 | + service_name, |
4436 | + constraints, |
4437 | + config, |
4438 | + config_raw, |
4439 | + num_units) |
4440 | + |
4441 | + def debug_hooks(self, unit_name, hook_names=()): |
4442 | + """Enable hook debug on a unit. |
4443 | + """ |
4444 | + return self._invoke( |
4445 | + debug_hooks.debug_hooks, |
4446 | + self, |
4447 | + unit_name, |
4448 | + hook_names) |
4449 | + |
4450 | + def destroy_service(self, service_name): |
4451 | + """Destroy the named service. |
4452 | + """ |
4453 | + return self._invoke( |
4454 | + destroy_service.destroy_service, |
4455 | + self, |
4456 | + service_name) |
4457 | + |
4458 | + def export(self): |
4459 | + """Export an environment. |
4460 | + """ |
4461 | + return self._invoke( |
4462 | + export.export, |
4463 | + self) |
4464 | + |
4465 | + def expose(self, service_name): |
4466 | + """Expose a service for external access. |
4467 | + """ |
4468 | + return self._invoke( |
4469 | + expose.expose, |
4470 | + self, |
4471 | + service_name) |
4472 | + |
4473 | + def import_env(self, data): |
4474 | + """Import an environment from data serialization. |
4475 | + """ |
4476 | + return self._invoke( |
4477 | + import_env.import_env, |
4478 | + self, |
4479 | + data) |
4480 | + |
4481 | + def login(self, user, password): |
4482 | + return self._invoke( |
4483 | + self._login, |
4484 | + user, |
4485 | + password) |
4486 | + |
4487 | + @inlineCallbacks |
4488 | + def _login(self, user, password): |
4489 | + principal = Principal(user, password) |
4490 | + yield principal.attach(self.client) |
4491 | + try: |
4492 | + yield self.client.get("/login") |
4493 | + except (zookeeper.NoAuthException, zookeeper.AuthFailedException): |
4494 | + self.log.error("Invalid credentials") |
4495 | + raise |
4496 | + else: |
4497 | + self.log.info("Login success") |
4498 | + returnValue(True) |
4499 | + |
4500 | + def remove_annotations(self, entity, keys=()): |
4501 | + return self._invoke(self._remove_annotations, entity, keys) |
4502 | + |
4503 | + def _remove_annotations(self, entity, keys): |
4504 | + return AnnotationManager(self.client).remove(entity, keys) |
4505 | + |
4506 | + def remove_relation(self, endpoint_a, endpoint_b): |
4507 | + """Remove a relation from a service(s). |
4508 | + """ |
4509 | + return self._invoke( |
4510 | + remove_relation.remove_relation, |
4511 | + self, |
4512 | + (endpoint_a, endpoint_b)) |
4513 | + |
4514 | + def remove_units(self, unit_names): |
4515 | + """Remove the given set of units. |
4516 | + """ |
4517 | + return self._invoke( |
4518 | + remove_unit.remove_unit, |
4519 | + self, |
4520 | + unit_names) |
4521 | + |
4522 | + def resolved(self, unit_name, relation_name=None, retry=False): |
4523 | + """Mark an error as fixed. |
4524 | + """ |
4525 | + return self._invoke( |
4526 | + resolved.resolved, |
4527 | + self, |
4528 | + unit_name, |
4529 | + relation_name, |
4530 | + retry) |
4531 | + |
4532 | + def set_config(self, service_name, config, data=None): |
4533 | + """Set a service's configuration. |
4534 | + """ |
4535 | + return self._invoke( |
4536 | + config_set.config_set, |
4537 | + self, |
4538 | + service_name, |
4539 | + config, |
4540 | + data) |
4541 | + |
4542 | + def set_constraints(self, service_name, constraints): |
4543 | + """Set constraints on the environment or service. |
4544 | + """ |
4545 | + return self._invoke( |
4546 | + constraints_set.constraints_set, |
4547 | + self, |
4548 | + service_name, |
4549 | + constraints) |
4550 | + |
4551 | + def status(self, scope=None): |
4552 | + """Get information about the environment and its contained items. |
4553 | + """ |
4554 | + return self._invoke( |
4555 | + status.status, |
4556 | + self, |
4557 | + scope) |
4558 | + |
4559 | + def terminate_machine(self, machine_ids): |
4560 | + """Terminate a machine. |
4561 | + """ |
4562 | + return self._invoke( |
4563 | + terminate_machine.terminate_machine, |
4564 | + self, |
4565 | + machine_ids) |
4566 | + |
4567 | + def unexpose(self, service_name): |
4568 | + """Remove external access from a service. |
4569 | + """ |
4570 | + return self._invoke( |
4571 | + unexpose.unexpose, |
4572 | + self, |
4573 | + service_name) |
4574 | + |
4575 | + def update_annotations(self, entity, data): |
4576 | + """Annotate an entity by name. |
4577 | + |
4578 | + An entity is either a machine, unit, service, or the environment. |
4579 | + """ |
4580 | + return self._invoke(self._update_annotations, entity, data) |
4581 | + |
4582 | + def _update_annotations(self, entity, data): |
4583 | + return AnnotationManager(self.client).set(entity, data) |
4584 | + |
4585 | + def upgrade_charm(self, service_name, local_charm=None): |
4586 | + """Upgrade a charm. |
4587 | + """ |
4588 | + raise NotImplementedError() |
4589 | + |
4590 | + def resolve_address(self, entity): |
4591 | + """Given an entity return its public address. |
4592 | + """ |
4593 | + raise NotImplementedError() |
4594 | |
4595 | === added file 'juju/rapi/delta.py' |
4596 | --- juju/rapi/delta.py 1970-01-01 00:00:00 +0000 |
4597 | +++ juju/rapi/delta.py 2013-01-17 11:02:31 +0000 |
4598 | @@ -0,0 +1,275 @@ |
4599 | +""" |
4600 | +Object Stream Delta |
4601 | + - status parsing atm |
4602 | + - topology usage for relation comparison |
4603 | +""" |
4604 | +import copy |
4605 | +import logging |
4606 | +from twisted.internet.defer import inlineCallbacks, returnValue |
4607 | + |
4608 | +from juju.state.annotation import AnnotationManager |
4609 | +# For unambigious relations deltas/identities. |
4610 | +from juju.state.topology import InternalTopology |
4611 | + |
4612 | + |
4613 | +log = logging.getLogger("juju.rapi.delta") |
4614 | + |
4615 | + |
4616 | +def _unit_key(name): |
4617 | + name, number = name.split('/', 1) |
4618 | + return (name, int(number)) |
4619 | + |
4620 | + |
4621 | +class DeltaStreamManager(object): |
4622 | + |
4623 | + def __init__(self, context): |
4624 | + self.context = context |
4625 | + self.previous = { |
4626 | + 'services': {}, |
4627 | + 'machines': {}, |
4628 | + 'topology': None} |
4629 | + self.streams = [] # protocol instances |
4630 | + self.running = False |
4631 | + self.annotations = AnnotationManager(context.client) |
4632 | + self.annotation_stream = self.annotations.stream() |
4633 | + |
4634 | + def start(self): |
4635 | + if self.running: |
4636 | + return |
4637 | + |
4638 | + d = self.context.connect() |
4639 | + |
4640 | + def _start(ctx): |
4641 | + self.running = True |
4642 | + return self.pump() |
4643 | + |
4644 | + return d.addCallback(_start) |
4645 | + |
4646 | + def stop(self): |
4647 | + self.running = False |
4648 | + self.context.close() |
4649 | + |
4650 | + @inlineCallbacks |
4651 | + def pump(self, *ignored): |
4652 | + """Process state changes into notifications. |
4653 | + """ |
4654 | + if not self.running: |
4655 | + return |
4656 | + |
4657 | + try: |
4658 | + current = (yield self.context.status())['result'] |
4659 | + self._annotate( |
4660 | + current, (yield self.annotation_stream.next())) |
4661 | + yield self.annotations.gc() |
4662 | + except Exception, e: |
4663 | + # The delta pump must go on. |
4664 | + log.exception("An error occurred %s" % str(e)) |
4665 | + self._schedule() |
4666 | + return |
4667 | + |
4668 | + current['topology'] = yield self._get_topology() |
4669 | + |
4670 | + changes = [] |
4671 | + delta(copy.deepcopy(self.previous), |
4672 | + copy.deepcopy(current), |
4673 | + changes.append) |
4674 | + |
4675 | + self.previous = current |
4676 | + self.flush(changes) |
4677 | + self._schedule() |
4678 | + |
4679 | + def _schedule(self): |
4680 | + """Schedule the next event stream pump. |
4681 | + """ |
4682 | + if not self.running: |
4683 | + return |
4684 | + |
4685 | + from twisted.internet import reactor |
4686 | + reactor.callLater(5, self.pump) |
4687 | + |
4688 | + @inlineCallbacks |
4689 | + def _get_topology(self): |
4690 | + """Delta streams relation resolution needs this. |
4691 | + """ |
4692 | + t = InternalTopology() |
4693 | + content, stat = yield self.context.client.get('/topology') |
4694 | + t.parse(content) |
4695 | + returnValue(t) |
4696 | + |
4697 | + def _annotate(self, status, annotations): |
4698 | + for s in status['services']: |
4699 | + if s in annotations: |
4700 | + status['services'][s]['annotations'] = annotations[s] |
4701 | + |
4702 | + for u in status['services'][s].get('units', ()): |
4703 | + if u in annotations: |
4704 | + status['services'][s]['units'][u]['annotations'] = \ |
4705 | + annotations[u] |
4706 | + for m in status['machines']: |
4707 | + if m in annotations: |
4708 | + status['machines'][m]['annotations'] = annotations[m] |
4709 | + |
4710 | + # Top level environment annotations |
4711 | + status['annotations'] = annotations.get('env', {}) |
4712 | + |
4713 | + def add(self, p): |
4714 | + """Add a stream/protocol |
4715 | + """ |
4716 | + self.streams.append(p) |
4717 | + |
4718 | + def remove(self, p): |
4719 | + """Add a stream/protocol |
4720 | + """ |
4721 | + if p in self.streams: |
4722 | + self.streams.remove(p) |
4723 | + |
4724 | + def get_current(self): |
4725 | + """Return current state as list of changes.""" |
4726 | + changes = [] |
4727 | + delta( |
4728 | + {'machines': {}, 'services': {}, 'topology': None}, |
4729 | + copy.deepcopy(self.previous), changes.append) |
4730 | + return changes |
4731 | + |
4732 | + def get_status(self): |
4733 | + """Return the current status output.""" |
4734 | + status = dict(self.previous) |
4735 | + del status['topology'] |
4736 | + return status |
4737 | + |
4738 | + def flush(self, delta): |
4739 | + if not delta: |
4740 | + return None |
4741 | + for s in self.streams: |
4742 | + s.publish_changes(delta) |
4743 | + |
4744 | + |
4745 | +def delta(prev, cur, stream): |
4746 | + """ |
4747 | + Given two topology augmented status dictionaries, generate a |
4748 | + stream of object events corresponding to the delta between the |
4749 | + two states. |
4750 | + |
4751 | + Logical order matters wrt to the event stream. |
4752 | + |
4753 | + Bring Up / Bring down roughly reverse. |
4754 | + -------- |
4755 | + Services |
4756 | + Relations |
4757 | + Machines |
4758 | + Units |
4759 | + |
4760 | + |
4761 | + TODO: A more efficient alternative to 1-1 delta changes, is |
4762 | + assuming intelligence on the client, and only sending logical |
4763 | + changes, ala sending remove service automatically removes units. |
4764 | + ie. operational transform. |
4765 | + """ |
4766 | + |
4767 | + ## Services |
4768 | + cur_svc_ids = set(cur['services']) |
4769 | + prev_svc_ids = set(prev['services']) |
4770 | + |
4771 | + ## Relations |
4772 | + if not prev.get('topology'): |
4773 | + prev_rel_ids = set() |
4774 | + else: |
4775 | + prev_rel_ids = set(prev['topology'].get_relations()) |
4776 | + |
4777 | + if cur['topology']: |
4778 | + cur_rel_ids = set(cur['topology'].get_relations()) |
4779 | + else: |
4780 | + cur_rel_ids = set() |
4781 | + |
4782 | + ## Units |
4783 | + prev_units = {} |
4784 | + for s in prev['services']: |
4785 | + units = prev['services'][s].pop('units', ()) |
4786 | + prev_units.update(units) |
4787 | + del units |
4788 | + |
4789 | + cur_units = {} |
4790 | + for s in cur['services']: |
4791 | + units = cur['services'][s].pop('units', ()) |
4792 | + cur_units.update(units) |
4793 | + del units |
4794 | + |
4795 | + ## Machines |
4796 | + prev_machine_ids = set(prev['machines'].keys()) |
4797 | + cur_machine_ids = set(cur['machines'].keys()) |
4798 | + |
4799 | + # svc add |
4800 | + for sid in cur_svc_ids - prev_svc_ids: |
4801 | + svc = dict(cur['services'][sid]) |
4802 | + svc.pop('units', None) |
4803 | + svc.pop('relations', None) |
4804 | + svc['id'] = sid |
4805 | + stream(('service', 'add', svc)) |
4806 | + |
4807 | + # machine change |
4808 | + for mid in cur_machine_ids.intersection(prev_machine_ids): |
4809 | + if cur['machines'][mid] == prev['machines'][mid]: |
4810 | + continue |
4811 | + machine = cur['machines'][mid] |
4812 | + machine['id'] = mid |
4813 | + stream(('machine', 'change', machine)) |
4814 | + |
4815 | + # relation add |
4816 | + for rid in cur_rel_ids - prev_rel_ids: |
4817 | + scope = cur['topology'].get_relation_scope(rid) |
4818 | + interface = cur['topology'].get_relation_type(rid) |
4819 | + services = cur['topology'].get_relation_services(rid) |
4820 | + |
4821 | + services = [ |
4822 | + (cur['topology'].get_service_name(_sid), |
4823 | + services[_sid]) for _sid in services.keys()] |
4824 | + relation = { |
4825 | + 'id': rid, |
4826 | + 'interface': interface, |
4827 | + 'scope': scope, |
4828 | + 'endpoints': services} |
4829 | + stream(('relation', 'add', relation)) |
4830 | + |
4831 | + for mid in cur_machine_ids - prev_machine_ids: |
4832 | + machine = cur['machines'][mid] |
4833 | + machine['id'] = mid |
4834 | + stream(('machine', 'add', machine)) |
4835 | + |
4836 | + # We want the added units to be sent in ascending order by number. |
4837 | + for uid in sorted(set(cur_units) - set(prev_units), key=_unit_key): |
4838 | + unit = cur_units[uid] |
4839 | + unit['id'] = uid |
4840 | + stream(('unit', 'add', unit)) |
4841 | + |
4842 | + for uid in (set(cur_units).intersection(set(prev_units))): |
4843 | + if cur_units[uid] == prev_units[uid]: |
4844 | + continue |
4845 | + unit = cur_units[uid] |
4846 | + unit['id'] = uid |
4847 | + stream(('unit', 'change', unit)) |
4848 | + |
4849 | + for uid in (set(prev_units) - set(cur_units)): |
4850 | + stream(('unit', 'remove', uid)) |
4851 | + |
4852 | + for mid in prev_machine_ids - cur_machine_ids: |
4853 | + stream(('machine', 'remove', mid)) |
4854 | + |
4855 | + for rid in prev_rel_ids - cur_rel_ids: |
4856 | + # with svc ids |
4857 | + stream(('relation', 'remove', rid)) |
4858 | + |
4859 | + for sid in prev_svc_ids - cur_svc_ids: |
4860 | + stream(('service', 'remove', sid)) |
4861 | + |
4862 | + for sid in cur_svc_ids.intersection(prev_svc_ids): |
4863 | + svc = dict(cur['services'][sid]) |
4864 | + svc.pop('units', None) |
4865 | + svc.pop('relations', None) |
4866 | + |
4867 | + prev_svc = dict(prev['services'][sid]) |
4868 | + prev_svc.pop('units', None) |
4869 | + prev_svc.pop('relations', None) |
4870 | + |
4871 | + if svc != prev_svc: |
4872 | + svc['id'] = sid |
4873 | + stream(('service', 'change', svc)) |
4874 | |
4875 | === added file 'juju/rapi/rest.py' |
4876 | --- juju/rapi/rest.py 1970-01-01 00:00:00 +0000 |
4877 | +++ juju/rapi/rest.py 2013-01-17 11:02:31 +0000 |
4878 | @@ -0,0 +1,121 @@ |
4879 | +""" |
4880 | + |
4881 | +additional context commands for state representation. |
4882 | +""" |
4883 | + |
4884 | +from twisted.internet.defer import inlineCallbacks, returnValue |
4885 | + |
4886 | +from juju.state.charm import CharmStateManager |
4887 | +from juju.state.service import ServiceStateManager |
4888 | +from juju.state.relation import RelationStateManager |
4889 | + |
4890 | + |
4891 | +@inlineCallbacks |
4892 | +def get_charm(context, charm_id): |
4893 | + """ |
4894 | + GET /charms/:id |
4895 | + |
4896 | + Charms have embedded '/' |
4897 | + |
4898 | + Return back charm metadata. |
4899 | + """ |
4900 | + |
4901 | + charms = CharmStateManager(context.client) |
4902 | + charm = yield charms.get_charm_state(charm_id) |
4903 | + config = yield charm.get_config() |
4904 | + md = yield charm.get_metadata() |
4905 | + |
4906 | + returnValue({ |
4907 | + 'name': charm.name, |
4908 | + 'revision': charm.revision, |
4909 | + 'url': charm.id, |
4910 | + 'config': {'options': config.as_dict()}, |
4911 | + 'provides': md.provides, |
4912 | + 'requires': md.requires, |
4913 | + 'peers': md.peers, |
4914 | + 'subordinate': md.is_subordinate, |
4915 | + }) |
4916 | + |
4917 | + |
4918 | +@inlineCallbacks |
4919 | +def get_service(context, service_name): |
4920 | + """ |
4921 | + get('/service/foobar') |
4922 | + set('/service/foobar/', value) |
4923 | + |
4924 | + GET /service/:id |
4925 | + returns: |
4926 | + |
4927 | + - Service Info |
4928 | + - Config |
4929 | + - Constraints |
4930 | + - Relations |
4931 | + - Charm identifier. |
4932 | + |
4933 | + Set Service -> (context api set config, set constraints) |
4934 | + """ |
4935 | + services = ServiceStateManager(context.client) |
4936 | + service = yield services.get_service_state(service_name) |
4937 | + config = yield service.get_config() |
4938 | + constraints = yield service.get_constraints() |
4939 | + charm_id = yield service.get_charm_id() |
4940 | + relations = RelationStateManager(context.client) |
4941 | + exposed = yield service.get_exposed_flag() |
4942 | + |
4943 | + rels = [] |
4944 | + for r in (yield relations.get_relations_for_service(service)): |
4945 | + endpoint = None |
4946 | + if r.relation_role != "peer": |
4947 | + for s in (yield r.get_service_states()): |
4948 | + if s.service_name != service.service_name: |
4949 | + endpoint = s.service_name |
4950 | + rels.append( |
4951 | + dict(name=r.relation_name, |
4952 | + ident=r.relation_ident, |
4953 | + role=r.relation_role, |
4954 | + scope=r.relation_scope, |
4955 | + endpoint=endpoint)) |
4956 | + |
4957 | + returnValue({ |
4958 | + 'name': service.service_name, |
4959 | + 'charm': charm_id, |
4960 | + 'config': dict(config), |
4961 | + 'exposed': exposed, |
4962 | + 'constraints': constraints.data, |
4963 | + 'rels': rels |
4964 | + }) |
4965 | + |
4966 | + |
4967 | +@inlineCallbacks |
4968 | +def get_endpoints(context, service_names=()): |
4969 | + service_manager = ServiceStateManager(context.client) |
4970 | + if not service_names: |
4971 | + services = yield service_manager.get_all_service_states() |
4972 | + else: |
4973 | + services = [] |
4974 | + for s in service_names: |
4975 | + services.append( |
4976 | + (yield service_manager.get_service_state(s))) |
4977 | + |
4978 | + endpoint_mapping = {} |
4979 | + |
4980 | + for svc in services: |
4981 | + charm = yield svc.get_charm_state() |
4982 | + metadata = yield charm.get_metadata() |
4983 | + endpoint_mapping[svc.service_name] = svc_rel_meta = {} |
4984 | + svc_rel_meta['provides'] = _flatter(metadata.provides) |
4985 | + svc_rel_meta['requires'] = _flatter(metadata.requires) |
4986 | + |
4987 | + returnValue(endpoint_mapping) |
4988 | + |
4989 | + |
4990 | +def _flatter(rel_meta): |
4991 | + flattened = [] |
4992 | + if not rel_meta: |
4993 | + return flattened |
4994 | + |
4995 | + for k in rel_meta: |
4996 | + rel = dict(rel_meta[k]) |
4997 | + rel['name'] = k |
4998 | + flattened.append(rel) |
4999 | + return flattened |
5000 |
The diff has been truncated for viewing.