Merge lp:~hazmat/pyjuju/local-provider into lp:pyjuju

Proposed by Kapil Thangavelu
Status: Merged
Approved by: Gustavo Niemeyer
Approved revision: 375
Merged at revision: 356
Proposed branch: lp:~hazmat/pyjuju/local-provider
Merge into: lp:pyjuju
Prerequisite: lp:~hazmat/pyjuju/managed-agent
Diff against target: 649 lines (+524/-9)
11 files modified
juju/lib/zk.py (+3/-1)
juju/providers/common/connect.py (+2/-2)
juju/providers/lxc/__init__.py (+210/-1)
juju/providers/lxc/agent.py (+4/-3)
juju/providers/lxc/container.py (+29/-0)
juju/providers/lxc/pkg.py (+18/-0)
juju/providers/lxc/tests/test_container.py (+38/-0)
juju/providers/lxc/tests/test_pkg.py (+27/-0)
juju/providers/lxc/tests/test_provider.py (+177/-0)
juju/state/tests/test_utils.py (+13/-0)
juju/state/utils.py (+3/-2)
To merge this branch: bzr merge lp:~hazmat/pyjuju/local-provider
Reviewer Review Type Date Requested Status
Gustavo Niemeyer Approve
Review via email: mp+76299@code.launchpad.net

This proposal supersedes a proposal from 2011-09-19.

Description of the change

Local/lxc for juju development/deployment on a single machine

Implements a local machine provider using lxc.

Resubmit with pre-requisite

To post a comment you must log in.
lp:~hazmat/pyjuju/local-provider updated
370. By Kapil Thangavelu

ensure wait for agent start, state save, and log file present

371. By Kapil Thangavelu

setup log file for the agent

372. By Kapil Thangavelu

fully initialize the managed agent with all arges on bootstrap

373. By Kapil Thangavelu

additional logging

374. By Kapil Thangavelu

correct pid file usage

Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :
Download full text (6.5 KiB)

Thanks Kapil. Good stuff in record time.

[1]

Tests fail due to missing dependencies in the environment.

===============================================================================
[ERROR]
Traceback (most recent call last):
  File "/usr/lib/python2.7/dist-packages/twisted/internet/defer.py", line 1020, in _inlineCallbacks
    result = g.send(result)
  File "/home/niemeyer/src/juju/hazmat/local-provider/juju/providers/lxc/__init__.py", line 53, in bootstrap
    ", ".join(sorted(list(missing)))))
juju.errors.ProviderError: Missing packages apt-cacher-ng, libvirt-bin, lxc, zookeeperd

Not sure if we should just say "install the packages" as the answer for
that one, though.

[2]

+ # Install required packages

It's checking for, rather than installing. (which is fine, message just
needs fixing)

[3]

Another one:

  File "/home/niemeyer/src/juju/hazmat/local-provider/juju/providers/lxc/__init__.py", line 64, in bootstrap
    net_attributes = yield net.get_attributes()
  File "/usr/lib/python2.7/dist-packages/twisted/python/threadpool.py", line 207, in _worker
    result = context.call(ctx, function, *args, **kwargs)
  File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 59, in callWithContext
    return self.currentContext().callWithContext(ctx, func, *args, **kw)
  File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 37, in callWithContext
    return func(*args,**kw)
  File "/home/niemeyer/src/juju/hazmat/local-provider/juju/providers/lxc/network.py", line 135, in get_network_attributes
    output = subprocess.check_output(["virsh", "net-dumpxml", name])
  File "/usr/lib/python2.7/subprocess.py", line 537, in check_output
    raise CalledProcessError(retcode, cmd, output=output)
subprocess.CalledProcessError: Command '['virsh', 'net-dumpxml', 'default']' returned non-zero exit status 1

juju.providers.lxc.tests.test_provider.LocalProviderTest.test_bootstrap

Apparently virsh is being called for real in the system for tests to pass.

[4]

+ log.debug("Starting zookeeper...")

These debug messages feel like good info for the user. I suggest making them
visible through log.info instead.

[5]

+ # Default to running zookeeper as the user, unless the user is root.
+ zookeeper_user = os.geteuid() == 0 and "zookeeper" or None

This shorthand version is actually not that short and makes things
significantly less straightforward to parse when reading.

Compare it to:

    zookeeper_user = None
    if os.getuid() == 0:
        zookeeper_user = "zookeeper"

Please also tweak the comment if possible (or remove since the version above
is clear). The comment is confusing since it's running as the zookeeper user
if the user is root, and it's easy to read it as stating the exact opposite.

[6]

+ log.info("Destroying unit containers..")
(...)
+ log.debug("Stopping zookeeper..")
(...)
+ log.info("Environment bootstrapped")

There's some inconsisteny in the logging. Would be nice to have
the messages ending consistently in "..." or ".".

[7]

+ "local") # "lxc")

The comment looks like a brainstorm left over.

[8]

+ """
+ Shutdown the m...

Read more...

review: Approve
lp:~hazmat/pyjuju/local-provider updated
375. By Kapil Thangavelu

address review comments, local provider tests don't require pkg install

Revision history for this message
Kapil Thangavelu (hazmat) wrote :
Download full text (8.9 KiB)

Excerpts from Gustavo Niemeyer's message of Thu Sep 22 16:03:25 UTC 2011:
> Review: Approve
>
> Thanks Kapil. Good stuff in record time.
>

Thanks for the review!

>
> [1]
>
> Tests fail due to missing dependencies in the environment.
>
> ===============================================================================
> [ERROR]
> Traceback (most recent call last):
> File "/usr/lib/python2.7/dist-packages/twisted/internet/defer.py", line 1020, in _inlineCallbacks
> result = g.send(result)
> File "/home/niemeyer/src/juju/hazmat/local-provider/juju/providers/lxc/__init__.py", line 53, in bootstrap
> ", ".join(sorted(list(missing)))))
> juju.errors.ProviderError: Missing packages apt-cacher-ng, libvirt-bin, lxc, zookeeperd
>
> Not sure if we should just say "install the packages" as the answer for
> that one, though.
>

I'll move this to a package level global and patch it for testing.

>
> [2]
>
> + # Install required packages
>
> It's checking for, rather than installing. (which is fine, message just
> needs fixing)
>
>
> [3]
>
> Another one:
>
> File "/home/niemeyer/src/juju/hazmat/local-provider/juju/providers/lxc/__init__.py", line 64, in bootstrap
> net_attributes = yield net.get_attributes()
> File "/usr/lib/python2.7/dist-packages/twisted/python/threadpool.py", line 207, in _worker
> result = context.call(ctx, function, *args, **kwargs)
> File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 59, in callWithContext
> return self.currentContext().callWithContext(ctx, func, *args, **kw)
> File "/usr/lib/python2.7/dist-packages/twisted/python/context.py", line 37, in callWithContext
> return func(*args,**kw)
> File "/home/niemeyer/src/juju/hazmat/local-provider/juju/providers/lxc/network.py", line 135, in get_network_attributes
> output = subprocess.check_output(["virsh", "net-dumpxml", name])
> File "/usr/lib/python2.7/subprocess.py", line 537, in check_output
> raise CalledProcessError(retcode, cmd, output=output)
> subprocess.CalledProcessError: Command '['virsh', 'net-dumpxml', 'default']' returned non-zero exit status 1
>
> juju.providers.lxc.tests.test_provider.LocalProviderTest.test_bootstrap
>
> Apparently virsh is being called for real in the system for tests to pass.
>

fixed, nice catch, i should do some runs without the required packages.

>
> [4]
>
> + log.debug("Starting zookeeper...")
>
> These debug messages feel like good info for the user. I suggest making them
> visible through log.info instead.
>

Done.

> [5]
>
> + # Default to running zookeeper as the user, unless the user is root.
> + zookeeper_user = os.geteuid() == 0 and "zookeeper" or None
>
> This shorthand version is actually not that short and makes things
> significantly less straightforward to parse when reading.
>
> Compare it to:
>
> zookeeper_user = None
> if os.getuid() == 0:
> zookeeper_user = "zookeeper"
>
> Please also tweak the comment if possible (or remove since the version above
> is clear). The comment is confusing since it's running as the zookeeper user
> if the user is root, and it's easy to read it as stating the...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/lib/zk.py'
2--- juju/lib/zk.py 2011-09-16 00:36:31 +0000
3+++ juju/lib/zk.py 2011-09-22 19:36:25 +0000
4@@ -49,7 +49,7 @@
5
6 class Zookeeper(object):
7
8- def __init__(self, run_dir, port, host=None,
9+ def __init__(self, run_dir, port=None, host=None,
10 zk_location="system", user=None, group=None,
11 use_deferred=True):
12 """
13@@ -75,6 +75,8 @@
14 self._use_deferred = use_deferred
15
16 def start(self):
17+ assert self._port is not None
18+
19 if self._use_deferred:
20 return deferToThread(self._start)
21 return self._start()
22
23=== modified file 'juju/providers/common/connect.py'
24--- juju/providers/common/connect.py 2011-09-15 18:50:23 +0000
25+++ juju/providers/common/connect.py 2011-09-22 19:36:25 +0000
26@@ -31,7 +31,7 @@
27 candidates = yield self._provider.get_zookeeper_machines()
28 chosen = yield self._pick_machine(candidates)
29 client = yield self._connect_to_machine(chosen, share)
30- yield self._wait_for_initialization(client)
31+ yield self.wait_for_initialization(client)
32 returnValue(client)
33
34 def _pick_machine(self, machines):
35@@ -55,7 +55,7 @@
36 % (machine.instance_id, str(failure.value)))
37
38 @inlineCallbacks
39- def _wait_for_initialization(self, client):
40+ def wait_for_initialization(self, client):
41 exists_d, watch_d = client.exists_and_watch("/initialized")
42 exists = yield exists_d
43 if not exists:
44
45=== modified file 'juju/providers/lxc/__init__.py'
46--- juju/providers/lxc/__init__.py 2011-08-30 21:10:46 +0000
47+++ juju/providers/lxc/__init__.py 2011-09-22 19:36:25 +0000
48@@ -1,1 +1,210 @@
49-#
50+import os
51+import logging
52+
53+from twisted.internet.defer import succeed, fail, inlineCallbacks, returnValue
54+
55+from txzookeeper import ZookeeperClient
56+
57+from juju.errors import ProviderError, EnvironmentNotFound
58+from juju.lib.lxc import LXCContainer
59+from juju.lib.zk import Zookeeper
60+from juju.providers.common.base import MachineProviderBase
61+from juju.providers.common.files import FileStorage
62+from juju.providers.common.connect import ZookeeperConnect
63+from juju.providers.lxc.agent import ManagedMachineAgent
64+from juju.providers.lxc.container import get_containers
65+from juju.providers.lxc.machine import LocalMachine
66+from juju.providers.lxc.network import Network
67+from juju.providers.lxc.pkg import check_packages
68+from juju.state.auth import make_identity
69+from juju.state.initialize import StateHierarchy
70+from juju.state.utils import get_open_port
71+
72+log = logging.getLogger("juju.local-dev")
73+
74+REQUIRED_PACKAGES = ["zookeeperd", "libvirt-bin", "lxc", "apt-cacher-ng"]
75+
76+
77+class MachineProvider(MachineProviderBase):
78+ """LXC/Ubuntu local provider.
79+
80+ Only the host machine is utilized.
81+ """
82+ def __init__(self, environment_name, config):
83+ super(MachineProvider, self).__init__(environment_name, config)
84+ self._qualified_name = self._get_qualified_name()
85+ self._directory = os.path.join(
86+ self.config.get("data-dir", "/var/lib/juju/"),
87+ self._qualified_name)
88+
89+ @inlineCallbacks
90+ def bootstrap(self):
91+ """Bootstrap a local development environment.
92+ """
93+ # Check for existing environment
94+ state = yield self.load_state()
95+ if state is not False:
96+ raise ProviderError("Environment already bootstrapped")
97+
98+ # Check for required packages
99+ log.info("Checking for required packages...")
100+ missing = check_packages(*REQUIRED_PACKAGES)
101+ if missing:
102+ raise ProviderError("Missing packages %s" % (
103+ ", ".join(sorted(list(missing)))))
104+
105+ # Get/create directory for zookeeper and files
106+ zookeeper_dir = os.path.join(self._directory, "zookeeper")
107+ if not os.path.exists(zookeeper_dir):
108+ os.makedirs(zookeeper_dir)
109+
110+ # Start networking, and get an open port.
111+ log.info("Starting networking...")
112+ net = Network("default", subnet=122)
113+ # Start is a noop if its already started, which it is by default,
114+ # per libvirt-bin package installation
115+ yield net.start()
116+ net_attributes = yield net.get_attributes()
117+ port = get_open_port(net_attributes["ip"]["address"])
118+
119+ # Start zookeeper
120+ log.info("Starting zookeeper...")
121+ # Run zookeeper as the current user, unless we're being run as root
122+ # in which case run zookeeper as the 'zookeeper' user.
123+ zookeeper_user = None
124+ if os.geteuid() == 0:
125+ zookeeper_user = "zookeeper"
126+ zookeeper = Zookeeper(zookeeper_dir,
127+ port=port,
128+ host=net_attributes["ip"]["address"],
129+ user=zookeeper_user, group=zookeeper_user)
130+
131+ yield zookeeper.start()
132+
133+ # Save the zookeeper start to provider storage.
134+ yield self.save_state({"zookeeper-instances": ["local"],
135+ "zookeeper-address": zookeeper.address})
136+
137+ # Initialize the zookeeper state
138+ log.debug("Initializing state...")
139+ admin_identity = make_identity(
140+ "admin:%s" % self.config["admin-secret"])
141+ client = ZookeeperClient(zookeeper.address)
142+ yield client.connect()
143+ hierarchy = StateHierarchy(client, admin_identity, "local")
144+ yield hierarchy.initialize()
145+
146+ # Startup the machine agent
147+ log.info("Starting machine agent...")
148+ pid_file = os.path.join(self._directory, "machine-agent.pid")
149+ log_file = os.path.join(self._directory, "machine-agent.log")
150+ agent = ManagedMachineAgent(pid_file,
151+ zookeeper_hosts=zookeeper.address,
152+ machine_id="0",
153+ juju_directory=self._directory,
154+ log_file=log_file)
155+ yield agent.start()
156+
157+ log.info("Environment bootstrapped")
158+
159+ @inlineCallbacks
160+ def destroy_environment(self):
161+ """Shutdown the machine environment.
162+ """
163+ # Stop all the containers
164+ log.info("Destroying unit containers...")
165+ yield self._destroy_containers()
166+
167+ # Stop the machine agent
168+ log.debug("Stopping machine agent...")
169+ pid_file = os.path.join(self._directory, "machine-agent.pid")
170+ agent = ManagedMachineAgent(pid_file)
171+ yield agent.stop()
172+
173+ # Stop zookeeper
174+ log.debug("Stopping zookeeper...")
175+ zookeeper_dir = os.path.join(self._directory, "zookeeper")
176+ zookeeper = Zookeeper(zookeeper_dir, None)
177+ yield zookeeper.stop()
178+
179+ # Clean out local state
180+ yield self.save_state(False)
181+
182+ # Don't stop the network since we're using the default libvirt
183+ log.debug("Environment destroyed.")
184+
185+ @inlineCallbacks
186+ def _destroy_containers(self):
187+ container_map = yield get_containers(self._qualified_name)
188+ for container_name in container_map:
189+ container = LXCContainer(container_name)
190+ if container_map[container.container_name]:
191+ yield container.stop()
192+ yield container.destroy()
193+
194+ @inlineCallbacks
195+ def connect(self, share=False):
196+ """Connect to juju's zookeeper.
197+ """
198+ state = yield self.load_state()
199+ if not state:
200+ raise EnvironmentNotFound()
201+ client = yield ZookeeperClient(state["zookeeper-address"]).connect()
202+ yield ZookeeperConnect(self).wait_for_initialization(client)
203+ returnValue(client)
204+
205+ def get_file_storage(self):
206+ """Retrieve the provider C{FileStorage} abstraction.
207+ """
208+ storage_path = self.config.get(
209+ "storage-dir", os.path.join(self._directory, "files"))
210+ if not os.path.exists(storage_path):
211+ try:
212+ os.makedirs(storage_path)
213+ except OSError:
214+ raise ProviderError(
215+ "Unable to create file storage for environment")
216+ return FileStorage(storage_path)
217+
218+ def start_machine(self, machine_data, master=False):
219+ """Start a machine in the provider.
220+
221+ @param machine_data: a dictionary of data to pass along to the newly
222+ launched machine.
223+
224+ @param master: if True, machine will initialize the juju admin
225+ and run a provisioning agent.
226+ """
227+ return fail(ProviderError("Only local machine available"))
228+
229+ def shutdown_machine(self, machine_id):
230+ return fail(ProviderError(
231+ "Not enabled for local dev, use remove-unit"))
232+
233+ def get_machines(self, instance_ids=()):
234+ """List machines running in the provider.
235+
236+ @param instance_ids: ids of instances you want to get. Leave blank
237+ to list all machines owned by juju.
238+
239+ @return: a list of LocalMachine, always contains one item.
240+
241+ @raise: MachinesNotFound
242+ """
243+ if instance_ids and instance_ids != [LocalMachine.instance_id]:
244+ raise ProviderError("Only local machine available")
245+ return succeed([LocalMachine()])
246+
247+ def _get_qualified_name(self):
248+ """Get a qualified environment name for use by local dev resources
249+ """
250+ # Ensure we namespace resources by user.
251+ user = os.environ.get("USER")
252+
253+ # We need sudo access for lxc (till user namespaces), use the actual
254+ # user.
255+ if user == "root":
256+ sudo_user = os.environ.get("SUDO_USER")
257+ if sudo_user:
258+ user = sudo_user
259+ return "%s-%s" % (user, self.environment_name)
260
261=== modified file 'juju/providers/lxc/agent.py'
262--- juju/providers/lxc/agent.py 2011-09-17 00:07:16 +0000
263+++ juju/providers/lxc/agent.py 2011-09-22 19:36:25 +0000
264@@ -22,16 +22,16 @@
265 :param juju_directory: The directory to use for all state and logs.
266 """
267 self._pid_file = pid_file
268- self._log_file = log_file
269 self._machine_id = machine_id
270 self._zookeeper_hosts = zookeeper_hosts
271 self._juju_directory = juju_directory
272+ self._log_file = log_file
273
274 @inlineCallbacks
275 def start(self):
276 """Start the machine agent.
277 """
278- assert self._zookeeper_hosts
279+ assert self._zookeeper_hosts and self._log_file
280
281 if (yield self.is_running()):
282 return
283@@ -44,7 +44,8 @@
284 "JUJU_HOME=%s" % self._juju_directory,
285 "PYTHONPATH=%s" % ":".join(sys.path),
286 sys.executable, "-m", self.agent_module,
287- "-n", "--pidfile", self._pid_file, "--logfile", self._log_file]
288+ "-n", "--pidfile", self._pid_file,
289+ "--logfile", self._log_file]
290
291 yield deferToThread(subprocess.check_call, args)
292
293
294=== added file 'juju/providers/lxc/container.py'
295--- juju/providers/lxc/container.py 1970-01-01 00:00:00 +0000
296+++ juju/providers/lxc/container.py 2011-09-22 19:36:25 +0000
297@@ -0,0 +1,29 @@
298+from twisted.internet.threads import deferToThread
299+
300+import subprocess
301+
302+
303+def get_containers(prefix=None):
304+ """Return a dictionary of containers key names to runtime boolean value.
305+
306+ :param prefix: Optionally specify a prefix that the container should
307+ match any returned containers.
308+ """
309+ return deferToThread(_list_containers, prefix)
310+
311+
312+def _list_containers(prefix):
313+ output = subprocess.check_output(["lxc-ls"])
314+
315+ containers = {}
316+ for i in filter(None, output.split("\n")):
317+ if i in containers:
318+ containers[i] = True
319+ else:
320+ containers[i] = False
321+
322+ if prefix:
323+ remove = [k for k in containers.keys() if not k.startswith(prefix)]
324+ map(containers.pop, remove)
325+
326+ return containers
327
328=== added file 'juju/providers/lxc/pkg.py'
329--- juju/providers/lxc/pkg.py 1970-01-01 00:00:00 +0000
330+++ juju/providers/lxc/pkg.py 2011-09-22 19:36:25 +0000
331@@ -0,0 +1,18 @@
332+import apt
333+
334+
335+def check_packages(*packages):
336+ """Given a list of packages, return the packages which are not installed.
337+ """
338+ cache = apt.Cache()
339+ missing = set()
340+ for pkg_name in packages:
341+ try:
342+ pkg = cache[pkg_name]
343+ except KeyError:
344+ missing.add(pkg_name)
345+ continue
346+ if pkg.is_installed:
347+ continue
348+ missing.add(pkg_name)
349+ return missing
350
351=== added file 'juju/providers/lxc/tests/test_container.py'
352--- juju/providers/lxc/tests/test_container.py 1970-01-01 00:00:00 +0000
353+++ juju/providers/lxc/tests/test_container.py 2011-09-22 19:36:25 +0000
354@@ -0,0 +1,38 @@
355+import subprocess
356+
357+from twisted.internet.defer import inlineCallbacks
358+
359+from juju.lib.testing import TestCase
360+from juju.providers.lxc.container import get_containers
361+
362+lxc_output_sample = "\
363+calendarserver\ncaprica\ngemini\nreconnoiter\nvirgo\ncalendarserver\n"
364+
365+
366+class GetContainersTest(TestCase):
367+
368+ @inlineCallbacks
369+ def test_get_containers(self):
370+ lxc_ls_mock = self.mocker.mock()
371+ self.patch(subprocess, "check_output", lxc_ls_mock)
372+ lxc_ls_mock(["lxc-ls"])
373+ self.mocker.result(lxc_output_sample)
374+ self.mocker.replay()
375+
376+ container_map = yield get_containers()
377+ self.assertEqual(
378+ dict(caprica=False, gemini=False, reconnoiter=False, virgo=False,
379+ calendarserver=True),
380+ container_map)
381+
382+ @inlineCallbacks
383+ def test_get_containers_with_prefix(self):
384+ lxc_ls_mock = self.mocker.mock()
385+ self.patch(subprocess, "check_output", lxc_ls_mock)
386+ lxc_ls_mock(["lxc-ls"])
387+ self.mocker.result(lxc_output_sample)
388+ self.mocker.replay()
389+
390+ container_map = yield get_containers("ca")
391+ self.assertEqual(
392+ dict(calendarserver=True, caprica=False), container_map)
393
394=== added file 'juju/providers/lxc/tests/test_pkg.py'
395--- juju/providers/lxc/tests/test_pkg.py 1970-01-01 00:00:00 +0000
396+++ juju/providers/lxc/tests/test_pkg.py 2011-09-22 19:36:25 +0000
397@@ -0,0 +1,27 @@
398+import apt
399+from juju.lib.testing import TestCase
400+from juju.providers.lxc.pkg import check_packages
401+
402+
403+class PackageInstallTest(TestCase):
404+
405+ def test_package_reports_missing(self):
406+ pkg_name = "tryton-modules-sale-opportunity"
407+ missing_pkg = self.mocker.mock()
408+ mock_cache = self.mocker.patch(apt.Cache)
409+ mock_cache[pkg_name]
410+ self.mocker.result(missing_pkg)
411+ missing_pkg.is_installed
412+ self.mocker.result(False)
413+ self.mocker.replay()
414+ self.assertEqual(
415+ check_packages(pkg_name), set([pkg_name]))
416+
417+ def test_package_reports_installed(self):
418+ self.assertEqual(
419+ check_packages("python-apt"), set())
420+
421+ def test_package_handles_unknown(self):
422+ self.assertEqual(
423+ check_packages("global-gook"),
424+ set(["global-gook"]))
425
426=== added file 'juju/providers/lxc/tests/test_provider.py'
427--- juju/providers/lxc/tests/test_provider.py 1970-01-01 00:00:00 +0000
428+++ juju/providers/lxc/tests/test_provider.py 2011-09-22 19:36:25 +0000
429@@ -0,0 +1,177 @@
430+import logging
431+import os
432+import pwd
433+import subprocess
434+from StringIO import StringIO
435+import zookeeper
436+
437+from twisted.internet.defer import succeed, inlineCallbacks
438+
439+from txzookeeper.tests.utils import deleteTree
440+
441+from juju.errors import ProviderError, EnvironmentNotFound
442+from juju.lib.lxc import LXCContainer
443+from juju.lib.testing import TestCase
444+from juju.tests.common import get_test_zookeeper_address
445+from juju.lib.zk import Zookeeper
446+from juju.providers.lxc import MachineProvider
447+from juju.providers import lxc
448+from juju.providers.lxc.agent import ManagedMachineAgent
449+from juju.providers.lxc.network import Network
450+
451+
452+class LocalProviderTest(TestCase):
453+
454+ @inlineCallbacks
455+ def setUp(self):
456+ self.provider = MachineProvider(
457+ "local-test", {
458+ "admin-secret": "admin:abc", "data-dir": self.makeDir()})
459+ self.output = self.capture_logging(
460+ "juju.local-dev", level=logging.DEBUG)
461+ zookeeper.set_debug_level(0)
462+ self.client = yield self.get_zookeeper_client().connect()
463+
464+ def tearDown(self):
465+ deleteTree("/", self.client.handle)
466+ self.client.close()
467+
468+ @property
469+ def qualified_name(self):
470+ user_name = pwd.getpwuid(os.getuid()).pw_name
471+ return "%s-%s" % (user_name, self.provider.environment_name)
472+
473+ def assertDir(self, *path_parts):
474+ path = os.path.join(*path_parts)
475+ self.assertTrue(os.path.isdir(path))
476+
477+ def bootstrap_mock(self):
478+ self.patch(lxc, "REQUIRED_PACKAGES", [])
479+ mock_network = self.mocker.patch(Network)
480+ mock_network.start()
481+ self.mocker.result(succeed(True))
482+ mock_network.get_attributes()
483+ self.mocker.result({"ip": {"address": "127.0.0.1"}})
484+
485+ mock_zookeeper = self.mocker.patch(Zookeeper)
486+ mock_zookeeper.start()
487+ self.mocker.result(succeed(True))
488+
489+ mock_zookeeper.address
490+ self.mocker.result(get_test_zookeeper_address())
491+ self.mocker.count(3)
492+
493+ mock_agent = self.mocker.patch(ManagedMachineAgent)
494+ mock_agent.start()
495+ self.mocker.result(succeed(True))
496+
497+ @inlineCallbacks
498+ def test_bootstrap(self):
499+ self.bootstrap_mock()
500+ self.mocker.replay()
501+
502+ yield self.provider.bootstrap()
503+
504+ children = yield self.client.get_children("/")
505+ self.assertEqual(
506+ sorted(['services', 'charms', 'relations', 'zookeeper',
507+ 'initialized', 'topology', 'machines', 'units']),
508+ sorted(children))
509+ output = self.output.getvalue()
510+ self.assertIn("Starting networking...", output)
511+ self.assertIn("Starting zookeeper...", output)
512+ self.assertIn("Initializing state...", output)
513+ self.assertIn("Starting machine agent...", output)
514+ self.assertIn("Environment bootstrapped", output)
515+
516+ self.assertDir(
517+ self.provider.config["data-dir"], self.qualified_name, "files")
518+ self.assertDir(
519+ self.provider.config["data-dir"], self.qualified_name, "zookeeper")
520+
521+ self.assertEqual((yield self.provider.load_state()),
522+ {"zookeeper-address": get_test_zookeeper_address(),
523+ "zookeeper-instances": ["local"]})
524+
525+ @inlineCallbacks
526+ def test_boostrap_previously_bootstrapped(self):
527+ """Any local state is a sign that we had a previous bootstrap.
528+ """
529+ yield self.provider.save_state({"xyz": 1})
530+ error = yield self.assertFailure(
531+ self.provider.bootstrap(), ProviderError)
532+ self.assertEqual(str(error), "Environment already bootstrapped")
533+
534+ @inlineCallbacks
535+ def test_destroy_environment(self):
536+ """Destroying a local environment kills units, zk, and machine agent.
537+ """
538+ user_name = pwd.getpwuid(os.getuid()).pw_name
539+ # Mock container destruction, including stopping running contianers.
540+ lxc_ls_mock = self.mocker.mock()
541+ self.patch(subprocess, "check_output", lxc_ls_mock)
542+ lxc_ls_mock(["lxc-ls"])
543+ self.mocker.result(
544+ "%(name)s-local-test-unit-1\n%(name)s-local-test-unit-2\n"
545+ "%(name)s-local-test-unit-3\n%(name)s-local-test-unit-1\n"
546+ "%(name)s-local-test-unit-2\n" % dict(name=user_name))
547+
548+ mock_container = self.mocker.patch(LXCContainer)
549+ mock_container.stop()
550+ self.mocker.count(2)
551+ mock_container.destroy()
552+ self.mocker.count(3)
553+
554+ mock_agent = self.mocker.patch(ManagedMachineAgent)
555+ mock_agent.stop()
556+
557+ mock_zookeeper = self.mocker.patch(Zookeeper)
558+ mock_zookeeper.stop()
559+ yield self.provider.save_state({"i-exist": 1})
560+ self.mocker.replay()
561+
562+ yield self.provider.destroy_environment()
563+
564+ output = self.output.getvalue()
565+
566+ self.assertIn("Destroying unit containers..", output)
567+ self.assertIn("Stopping machine agent..", output)
568+ self.assertIn("Stopping zookeeper..", output)
569+ self.assertIn("Environment destroyed", output)
570+
571+ self.assertEqual((yield self.provider.load_state()), False)
572+
573+ @inlineCallbacks
574+ def test_connect(self):
575+ self.bootstrap_mock()
576+ self.mocker.replay()
577+ yield self.provider.bootstrap()
578+ client = yield self.provider.connect()
579+ self.assertTrue((yield client.exists("/initialized")))
580+
581+ def test_connect_sans_environment(self):
582+ return self.assertFailure(self.provider.connect(), EnvironmentNotFound)
583+
584+ def test_shutdown_machine(self):
585+ return self.assertFailure(
586+ self.provider.shutdown_machine("local"), ProviderError)
587+
588+ def test_start_machine(self):
589+ return self.assertFailure(
590+ self.provider.start_machine({}), ProviderError)
591+
592+ @inlineCallbacks
593+ def test_get_machines(self):
594+ machines = yield self.provider.get_machines()
595+ self.assertEqual(len(machines), 1)
596+ self.assertEqual(machines[0].instance_id, "local")
597+ self.assertEqual(machines[0].dns_name, "localhost")
598+
599+ @inlineCallbacks
600+ def test_get_file_storage(self):
601+ storage = self.provider.get_file_storage()
602+ content = StringIO("put")
603+ yield storage.put("abc", content)
604+ data_dir = self.provider.config["data-dir"]
605+ self.assertTrue("abc" in os.listdir(
606+ os.path.join(data_dir, self.qualified_name, "files")))
607
608=== modified file 'juju/state/tests/test_utils.py'
609--- juju/state/tests/test_utils.py 2011-09-15 18:50:23 +0000
610+++ juju/state/tests/test_utils.py 2011-09-22 19:36:25 +0000
611@@ -244,6 +244,19 @@
612 sock.close()
613 del sock
614
615+ def test_get_open_port_with_host(self):
616+ port = get_open_port("localhost")
617+ self.assertTrue(isinstance(port, int))
618+
619+ sock = socket.socket(
620+ socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP)
621+
622+ # would raise an error if we got it wrong.
623+ sock.bind(("127.0.0.1", port))
624+ sock.listen(1)
625+ sock.close()
626+ del sock
627+
628
629 class ChangeItemsTest(TestCase):
630 """Tests the formatting of change items.
631
632=== modified file 'juju/state/utils.py'
633--- juju/state/utils.py 2011-09-15 18:50:23 +0000
634+++ juju/state/utils.py 2011-09-22 19:36:25 +0000
635@@ -83,11 +83,12 @@
636 yield client.delete(path)
637
638
639-def get_open_port():
640+def get_open_port(host=""):
641 """Get an open port on the machine.
642 """
643 temp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
644- temp_sock.bind(("", 0))
645+ temp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
646+ temp_sock.bind((host, 0))
647 port = temp_sock.getsockname()[1]
648 temp_sock.close()
649 del temp_sock

Subscribers

People subscribed via source and target branches

to status/vote changes: