Merge lp:~hazmat/pyjuju/unit-with-addresses into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 400
Merged at revision: 373
Proposed branch: lp:~hazmat/pyjuju/unit-with-addresses
Merge into: lp:pyjuju
Prerequisite: lp:~bcsaller/pyjuju/lxc-omega
Diff against target: 482 lines (+333/-7)
9 files modified
juju/agents/tests/test_unit.py (+17/-0)
juju/agents/unit.py (+8/-0)
juju/environment/config.py (+1/-0)
juju/providers/lxc/__init__.py (+7/-0)
juju/providers/lxc/tests/test_provider.py (+5/-5)
juju/state/service.py (+60/-1)
juju/state/tests/test_service.py (+23/-1)
juju/unit/address.py (+86/-0)
juju/unit/tests/test_address.py (+126/-0)
To merge this branch: bzr merge lp:~hazmat/pyjuju/unit-with-addresses
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+76917@code.launchpad.net

Description of the change

Service units should know their public and private addresses.

This adds a two additional accessor/mutators to units for public/private
addresses, respectively. As well as provider specific implementation of address
detection, and unit agent initialization of those addresses on the unit state.

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

This looks very nice too, thanks!

Just a couple of details:

[1]

+ elif provider_type == "lxc":

Please do not forget to rename the provider type to "local"
before making this public.

[2]

+ output = subprocess.check_output(["hostname", "-f"])

Why is this different for orchestra? Won't this fail if thethe release.
machine doesn't have a properly assigned domain name? I suspect
it will, and suggest using the same implementation of the local
case there as well.

review: Approve
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

Excerpts from Gustavo Niemeyer's message of Mon Sep 26 13:15:39 UTC 2011:
> Review: Approve
>
> This looks very nice too, thanks!
>
> Just a couple of details:
>
>
> [1]
>
> + elif provider_type == "lxc":
>
> Please do not forget to rename the provider type to "local"
> before making this public.

sounds good, i'll put another branch in for the rename.

>
> [2]
>
> + output = subprocess.check_output(["hostname", "-f"])
>
> Why is this different for orchestra? Won't this fail if thethe release.
> machine doesn't have a properly assigned domain name? I suspect
> it will, and suggest using the same implementation of the local
> case there as well.
>

orchestra asserts that hostname is a valid fqdn. after long discussion on irc,
it seems that this is the most appropriate thing, and helps us get the
equivalent lines out of charms.

cheers,

Kapil

401. By Kapil Thangavelu

remove stray pdb

402. By Kapil Thangavelu

Merged lxc-omega-merge into unit-with-addresses.

403. By Kapil Thangavelu

Merged lxc-omega-merge into unit-with-addresses.

404. By Kapil Thangavelu

merge trunk and resolve conflict

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/agents/tests/test_unit.py'
2--- juju/agents/tests/test_unit.py 2011-09-29 00:33:59 +0000
3+++ juju/agents/tests/test_unit.py 2011-09-30 02:57:25 +0000
4@@ -11,6 +11,7 @@
5 from juju.charm import get_charm_from_path
6 from juju.charm.url import CharmURL
7 from juju.errors import JujuError
8+from juju.state.environment import GlobalSettingsStateManager
9 from juju.state.errors import ServiceStateNotFound
10 from juju.state.service import NO_HOOKS, RETRY_HOOKS
11
12@@ -29,6 +30,8 @@
13 @inlineCallbacks
14 def setUp(self):
15 yield super(UnitAgentTestBase, self).setUp()
16+ settings = GlobalSettingsStateManager(self.client)
17+ yield settings.set_provider_type("dummy")
18 self.change_environment(
19 PATH=get_cli_environ_path(),
20 JUJU_UNIT_NAME="mysql/0")
21@@ -148,6 +151,18 @@
22 return self.assertFailure(agent.startService(), ServiceStateNotFound)
23
24 @inlineCallbacks
25+ def test_agent_records_address_on_startup(self):
26+ """On startup the agent will record the unit's addresses.
27+ """
28+ yield self.agent.startService()
29+ self.assertEqual(
30+ (yield self.agent.unit_state.get_public_address()),
31+ "localhost")
32+ self.assertEqual(
33+ (yield self.agent.unit_state.get_private_address()),
34+ "localhost")
35+
36+ @inlineCallbacks
37 def test_agent_agent_executes_install_and_start_hooks_on_startup(self):
38 """On initial startup, the unit agent executes install and start hooks.
39 """
40@@ -497,6 +512,8 @@
41 @inlineCallbacks
42 def setUp(self):
43 yield super(UnitAgentTestBase, self).setUp()
44+ settings = GlobalSettingsStateManager(self.client)
45+ yield settings.set_provider_type("dummy")
46 self.makeDir(path=os.path.join(self.juju_directory, "charms"))
47
48 @inlineCallbacks
49
50=== modified file 'juju/agents/unit.py'
51--- juju/agents/unit.py 2011-09-27 19:18:08 +0000
52+++ juju/agents/unit.py 2011-09-30 02:57:25 +0000
53@@ -10,6 +10,7 @@
54 from juju.hooks.protocol import UnitSettingsFactory
55 from juju.hooks.executor import HookExecutor
56
57+from juju.unit.address import get_unit_address
58 from juju.unit.lifecycle import UnitLifecycle, HOOK_SOCKET_FILE
59 from juju.unit.workflow import UnitWorkflowState
60
61@@ -84,6 +85,13 @@
62 os.path.join(self.unit_directory, HOOK_SOCKET_FILE),
63 self.api_factory)
64
65+ # Setup the unit state's address
66+ address = yield get_unit_address(self.client)
67+ yield self.unit_state.set_public_address(
68+ (yield address.get_public_address()))
69+ yield self.unit_state.set_private_address(
70+ (yield address.get_private_address()))
71+
72 # Inform the system, we're alive.
73 yield self.unit_state.connect_agent()
74
75
76=== modified file 'juju/environment/config.py'
77--- juju/environment/config.py 2011-09-27 04:29:30 +0000
78+++ juju/environment/config.py 2011-09-30 02:57:25 +0000
79@@ -57,6 +57,7 @@
80 "default-series": String()},
81 optional=["storage-url", "placement",
82 "default-series"]),
83+ "dummy": KeyDict({}),
84 "lxc": KeyDict({"admin-secret": String(),
85 "data-dir": String(),
86 "placement": Constant("local"),
87
88=== modified file 'juju/providers/lxc/__init__.py'
89--- juju/providers/lxc/__init__.py 2011-09-27 04:29:30 +0000
90+++ juju/providers/lxc/__init__.py 2011-09-30 02:57:25 +0000
91@@ -46,6 +46,13 @@
92 "Unsupported placement policy for local provider")
93 return LOCAL_POLICY
94
95+ def get_placement_policy(self):
96+ """Local dev supports only one unit placement policy."""
97+ if self.config.get("placement", LOCAL_POLICY) != LOCAL_POLICY:
98+ raise ProviderError(
99+ "Unsupported placement policy for local provider")
100+ return LOCAL_POLICY
101+
102 @property
103 def provider_type(self):
104 return "lxc"
105
106=== modified file 'juju/providers/lxc/tests/test_provider.py'
107--- juju/providers/lxc/tests/test_provider.py 2011-09-27 04:29:30 +0000
108+++ juju/providers/lxc/tests/test_provider.py 2011-09-30 02:57:25 +0000
109@@ -1,7 +1,6 @@
110 import logging
111 import os
112 import pwd
113-import subprocess
114 from StringIO import StringIO
115 import zookeeper
116
117@@ -124,10 +123,11 @@
118 lxc_ls_mock = self.mocker.mock()
119 self.patch(lxc_lib, "_cmd", lxc_ls_mock)
120 lxc_ls_mock(["lxc-ls"])
121- self.mocker.result((0,
122- "%(name)s-local-test-unit-1\n%(name)s-local-test-unit-2\n"
123- "%(name)s-local-test-unit-3\n%(name)s-local-test-unit-1\n"
124- "%(name)s-local-test-unit-2\n" % dict(name=user_name)))
125+ self.mocker.result(
126+ (0,
127+ "%(name)s-local-test-unit-1\n%(name)s-local-test-unit-2\n"
128+ "%(name)s-local-test-unit-3\n%(name)s-local-test-unit-1\n"
129+ "%(name)s-local-test-unit-2\n" % dict(name=user_name)))
130
131 mock_container = self.mocker.patch(LXCContainer)
132 mock_container.stop()
133
134=== modified file 'juju/state/service.py'
135--- juju/state/service.py 2011-09-28 23:54:04 +0000
136+++ juju/state/service.py 2011-09-30 02:57:25 +0000
137@@ -15,7 +15,7 @@
138 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
139 BadDescriptor, BadServiceStateName, NoUnusedMachines,
140 ServiceUnitDebugAlreadyEnabled, ServiceUnitResolvedAlreadyEnabled,
141- ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher)
142+ ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher, StateError)
143 from juju.state.charm import CharmStateManager
144 from juju.state.relation import ServiceRelationState, RelationStateManager
145 from juju.state.machine import _public_machine_id, MachineState
146@@ -708,6 +708,64 @@
147 return "/units/%s/agent" % self._internal_id
148
149 @inlineCallbacks
150+ def get_public_address(self):
151+ """Get the public address of the unit.
152+
153+ If the unit is unassigned, or its unit agent hasn't started this
154+ value maybe None.
155+ """
156+ unit_data, stat = yield self._client.get(
157+ "/units/%s" % self.internal_id)
158+ data = yaml.load(unit_data)
159+ returnValue(data.get("public-address"))
160+
161+ @inlineCallbacks
162+ def set_public_address(self, public_address):
163+ """A unit's public address can be utilized to access the service.
164+
165+ The service must have been exposed for the service to be reachable
166+ outside of the environment.
167+ """
168+ def update_private_address(content, stat):
169+ data = yaml.load(content)
170+ data["public-address"] = public_address
171+ return yaml.safe_dump(data)
172+
173+ yield retry_change(
174+ self._client,
175+ "/units/%s" % self.internal_id,
176+ update_private_address)
177+
178+ @inlineCallbacks
179+ def get_private_address(self):
180+ """Get the private address of the unit.
181+
182+ If the unit is unassigned, or its unit agent hasn't started this
183+ value maybe None.
184+ """
185+ unit_data, stat = yield self._client.get(
186+ "/units/%s" % self.internal_id)
187+ data = yaml.load(unit_data)
188+ returnValue(data.get("private-address"))
189+
190+ @inlineCallbacks
191+ def set_private_address(self, private_address):
192+ """A unit's address private to the environment.
193+
194+ Other service will see and utilize this address for relations.
195+ """
196+
197+ def update_private_address(content, stat):
198+ data = yaml.load(content)
199+ data["private-address"] = private_address
200+ return yaml.safe_dump(data)
201+
202+ yield retry_change(
203+ self._client,
204+ "/units/%s" % self.internal_id,
205+ update_private_address)
206+
207+ @inlineCallbacks
208 def get_charm_id(self):
209 """Get the charm identifier that the unit is currently running."""
210 unit_data, stat = yield self._client.get(
211@@ -1331,6 +1389,7 @@
212 sequence = int(sequence)
213 return service_name, sequence
214
215+
216 def parse_service_name(unit_name):
217 """Return the service name from a given unit name."""
218 try:
219
220=== modified file 'juju/state/tests/test_service.py'
221--- juju/state/tests/test_service.py 2011-09-28 22:42:59 +0000
222+++ juju/state/tests/test_service.py 2011-09-30 02:57:25 +0000
223@@ -20,7 +20,7 @@
224 ServiceUnitStateMachineAlreadyAssigned, ServiceStateNameInUse,
225 BadDescriptor, BadServiceStateName, ServiceUnitDebugAlreadyEnabled,
226 MachineStateNotFound, NoUnusedMachines, ServiceUnitResolvedAlreadyEnabled,
227- ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher)
228+ ServiceUnitRelationResolvedAlreadyEnabled, StopWatcher, StateError)
229
230
231 from juju.state.tests.common import StateTestBase
232@@ -532,6 +532,28 @@
233 self.fail("Error not raised")
234
235 @inlineCallbacks
236+ def test_get_set_public_address(self):
237+ service_state = yield self.service_state_manager.add_service_state(
238+ "wordpress", self.charm_state)
239+ unit_state = yield service_state.add_unit_state()
240+ self.assertEqual((yield unit_state.get_public_address()), None)
241+ yield unit_state.set_public_address("example.foobar.com")
242+ yield self.assertEqual(
243+ (yield unit_state.get_public_address()),
244+ "example.foobar.com")
245+
246+ @inlineCallbacks
247+ def test_get_set_private_address(self):
248+ service_state = yield self.service_state_manager.add_service_state(
249+ "wordpress", self.charm_state)
250+ unit_state = yield service_state.add_unit_state()
251+ self.assertEqual((yield unit_state.get_private_address()), None)
252+ yield unit_state.set_private_address("example.local")
253+ yield self.assertEqual(
254+ (yield unit_state.get_private_address()),
255+ "example.local")
256+
257+ @inlineCallbacks
258 def test_get_service_unit_with_changing_state(self):
259 """
260 If a service is removed during operation, get_service_unit()
261
262=== added file 'juju/unit/address.py'
263--- juju/unit/address.py 1970-01-01 00:00:00 +0000
264+++ juju/unit/address.py 2011-09-30 02:57:25 +0000
265@@ -0,0 +1,86 @@
266+"""Service units have both a public and private address.
267+"""
268+import subprocess
269+
270+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
271+from twisted.internet.threads import deferToThread
272+from twisted.web import client
273+
274+from juju.errors import JujuError
275+from juju.state.environment import GlobalSettingsStateManager
276+
277+
278+@inlineCallbacks
279+def get_unit_address(client):
280+ settings = GlobalSettingsStateManager(client)
281+ provider_type = yield settings.get_provider_type()
282+ if provider_type == "ec2":
283+ returnValue(EC2UnitAddress())
284+ elif provider_type == "lxc":
285+ returnValue(LocalUnitAddress())
286+ elif provider_type == "orchestra":
287+ returnValue(OrchestraUnitAddress())
288+ elif provider_type == "dummy":
289+ returnValue(DummyUnitAddress())
290+
291+ raise JujuError(
292+ "Unknown provider type: %r, unit addresses unknown." % provider_type)
293+
294+
295+class UnitAddress(object):
296+
297+ def get_private_address(self):
298+ raise NotImplemented()
299+
300+ def get_public_address(self):
301+ raise NotImplemented()
302+
303+
304+class DummyUnitAddress(UnitAddress):
305+
306+ def get_private_address(self):
307+ return succeed("localhost")
308+
309+ def get_public_address(self):
310+ return succeed("localhost")
311+
312+
313+class EC2UnitAddress(UnitAddress):
314+
315+ @inlineCallbacks
316+ def get_private_address(self):
317+ content = yield client.getPage(
318+ "http://169.254.169.254/latest/meta-data/local-hostname")
319+ returnValue(content.strip())
320+
321+ @inlineCallbacks
322+ def get_public_address(self):
323+ content = yield client.getPage(
324+ "http://169.254.169.254/latest/meta-data/public-hostname")
325+ returnValue(content.strip())
326+
327+
328+class LocalUnitAddress(UnitAddress):
329+
330+ def get_private_address(self):
331+ return deferToThread(self._get_address)
332+
333+ def get_public_address(self):
334+ return deferToThread(self._get_address)
335+
336+ def _get_address(self):
337+ output = subprocess.check_output(["hostname", "-I"])
338+ return output.strip().split()[0]
339+
340+
341+class OrchestraUnitAddress(UnitAddress):
342+
343+ def get_private_address(self):
344+ return deferToThread(self._get_address)
345+
346+ def get_public_address(self):
347+ return deferToThread(self._get_address)
348+
349+ def _get_address(self):
350+ output = subprocess.check_output(["hostname", "-f"])
351+ return output.strip()
352
353=== added file 'juju/unit/tests/test_address.py'
354--- juju/unit/tests/test_address.py 1970-01-01 00:00:00 +0000
355+++ juju/unit/tests/test_address.py 2011-09-30 02:57:25 +0000
356@@ -0,0 +1,126 @@
357+import subprocess
358+import zookeeper
359+
360+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
361+from twisted.web import client
362+
363+from juju.errors import JujuError
364+from juju.lib.testing import TestCase
365+from juju.unit.address import (
366+ EC2UnitAddress, LocalUnitAddress, OrchestraUnitAddress, DummyUnitAddress,
367+ get_unit_address)
368+from juju.state.environment import GlobalSettingsStateManager
369+
370+
371+class AddressTest(TestCase):
372+
373+ def setUp(self):
374+ zookeeper.set_debug_level(0)
375+ self.client = self.get_zookeeper_client()
376+ return self.client.connect()
377+
378+ @inlineCallbacks
379+ def get_address_for(self, provider_type):
380+ settings = GlobalSettingsStateManager(self.client)
381+ yield settings.set_provider_type(provider_type)
382+ address = yield get_unit_address(self.client)
383+ returnValue(address)
384+
385+ @inlineCallbacks
386+ def test_get_ec2_address(self):
387+ address = yield self.get_address_for("ec2")
388+ self.assertTrue(isinstance(address, EC2UnitAddress))
389+
390+ @inlineCallbacks
391+ def test_get_local_address(self):
392+ address = yield self.get_address_for("lxc")
393+ self.assertTrue(isinstance(address, LocalUnitAddress))
394+
395+ @inlineCallbacks
396+ def test_get_orchestra_address(self):
397+ address = yield self.get_address_for("orchestra")
398+ self.assertTrue(isinstance(address, OrchestraUnitAddress))
399+
400+ @inlineCallbacks
401+ def test_get_dummy_address(self):
402+ address = yield self.get_address_for("dummy")
403+ self.assertTrue(isinstance(address, DummyUnitAddress))
404+
405+ def test_get_unknown_address(self):
406+ return self.assertFailure(self.get_address_for("foobar"), JujuError)
407+
408+
409+class DummyAddressTest(TestCase):
410+
411+ def setUp(self):
412+ self.address = DummyUnitAddress()
413+
414+ def test_get_address(self):
415+
416+ self.assertEqual(
417+ (yield self.address.get_public_address()),
418+ "localhost")
419+
420+ self.assertEqual(
421+ (yield self.address.get_private_address()),
422+ "localhost")
423+
424+
425+class EC2AddressTest(TestCase):
426+
427+ def setUp(self):
428+ self.address = EC2UnitAddress()
429+
430+ @inlineCallbacks
431+ def test_get_address(self):
432+ urls = [
433+ "http://169.254.169.254/latest/meta-data/local-hostname",
434+ "http://169.254.169.254/latest/meta-data/public-hostname"]
435+
436+ def verify_args(url):
437+ self.assertEqual(urls.pop(0), url)
438+ return succeed("foobar\n")
439+
440+ self.patch(client, "getPage", verify_args)
441+ self.assertEqual(
442+ (yield self.address.get_private_address()), "foobar")
443+ self.assertEqual(
444+ (yield self.address.get_public_address()), "foobar")
445+
446+
447+class LocalAddressTest(TestCase):
448+
449+ def setUp(self):
450+ self.address = LocalUnitAddress()
451+
452+ @inlineCallbacks
453+ def test_get_address(self):
454+ self.patch(
455+ subprocess, "check_output",
456+ lambda args: "192.168.1.122 127.0.0.1\n")
457+ self.assertEqual(
458+ (yield self.address.get_public_address()),
459+ "192.168.1.122")
460+ self.assertEqual(
461+ (yield self.address.get_private_address()),
462+ "192.168.1.122")
463+
464+
465+class OrchestraAddressTest(TestCase):
466+
467+ def setUp(self):
468+ self.address = OrchestraUnitAddress()
469+
470+ @inlineCallbacks
471+ def test_get_address(self):
472+ self.patch(
473+ subprocess, "check_output",
474+ lambda args: "slice.foobar.domain.net\n")
475+
476+ self.assertEqual(
477+ (yield self.address.get_public_address()),
478+ "slice.foobar.domain.net")
479+
480+ self.assertEqual(
481+ (yield self.address.get_private_address()),
482+ "slice.foobar.domain.net")

Subscribers

People subscribed via source and target branches

to status/vote changes: