Merge lp:~therve/pyjuju/rapi-uuid into lp:pyjuju

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
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.

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.

Subscribers

People subscribed via source and target branches

to status/vote changes: