class ManagedMachineAgent(object): agent_module = "juju.agents.machine" def __init__( self, juju_unit_namespace, zookeeper_hosts=None, machine_id="0", log_file=None, juju_directory="/var/lib/juju", public_key=None, juju_origin="ppa", juju_series=None): """ :param juju_series: The release series to use (maverick, natty, etc). :param machine_id: machine id for the local machine. :param zookeeper_hosts: Zookeeper hosts to connect. :param log_file: A file to use for the agent logs. :param juju_directory: The directory to use for all state and logs. :param juju_unit_namespace: The machine agent will create units with a known a prefix to allow for multiple users and multiple environments to create containers. The namespace should be unique per user and per environment. :param public_key: An SSH public key (string) that will be used in the container for access. """ self._juju_origin = juju_origin if self._juju_origin is None: origin, source = get_default_origin() if origin == BRANCH: origin = source self._juju_origin = origin env = {"JUJU_MACHINE_ID": machine_id, "JUJU_ZOOKEEPER": zookeeper_hosts, "JUJU_HOME": juju_directory, "JUJU_ORIGIN": self._juju_origin, "JUJU_UNIT_NS": juju_unit_namespace, "JUJU_SERIES": juju_series, "PYTHONPATH": ":".join(sys.path)} if public_key: env["JUJU_PUBLIC_KEY"] = public_key self._service = UpstartService( "juju-%s-machine-agent" % juju_unit_namespace, use_sudo=True) self._service.set_description( "Juju machine agent for %s" % juju_unit_namespace) self._service.set_environ(env) self._service_args = [ "/usr/bin/python", "-m", self.agent_module, "--nodaemon", "--logfile", log_file, "--session-file", "/var/run/juju/%s-machine-agent.zksession" % juju_unit_namespace] @property def juju_origin(self): return self._juju_origin def start(self): """Start the machine agent.""" self._service.set_command(" ".join(self._service_args)) return self._service.start() def stop(self): """Stop the machine agent.""" return self._service.destroy() def is_running(self): """Boolean value, true if the machine agent is running.""" return self._service.is_running()
class StorageServer(object): def __init__(self, juju_unit_namespace, storage_dir=None, host=None, port=None, logfile=None): """Management facade for a web server on top of the provider storage. :param juju_unit_namespace: For disambiguation. :param host: Host interface to bind to. :param port: Port to bind to. :param logfile: Path to store log output. """ if storage_dir: storage_dir = os.path.abspath(storage_dir) self._storage_dir = storage_dir self._host = host self._port = port self._logfile = logfile self._service = UpstartService( "juju-%s-file-storage" % juju_unit_namespace, use_sudo=True) self._service.set_description( "Juju file storage for %s" % juju_unit_namespace) self._service_args = [ "twistd", "--nodaemon", "--uid", str(os.getuid()), "--gid", str(os.getgid()), "--logfile", logfile, "--pidfile=", "-d", self._storage_dir, "web", "--port", "tcp:%s:interface=%s" % (self._port, self._host), "--path", self._storage_dir] @inlineCallbacks def is_serving(self): try: storage = LocalStorage(self._storage_dir) yield getPage((yield storage.get_url(SERVER_URL_KEY))) returnValue(True) except ConnectionRefusedError: returnValue(False) @inlineCallbacks def start(self): """Start the storage server. Also stores the storage server url directly into provider storage. """ assert self._storage_dir, "no storage_dir set" assert self._host, "no host set" assert self._port, "no port set" assert None not in self._service_args, "unset params" assert os.path.exists(self._storage_dir), "Invalid storage directory" try: with open(self._logfile, "a"): pass except IOError: raise AssertionError("logfile not writable by this user") storage = LocalStorage(self._storage_dir) yield storage.put( SERVER_URL_KEY, StringIO(yaml.safe_dump( {"storage-url": "http://%s:%s/" % (self._host, self._port)}))) self._service.set_command(" ".join(self._service_args)) yield self._service.start() def get_pid(self): return self._service.get_pid() def stop(self): """Stop the storage server.""" return self._service.destroy()
class UpstartServiceTest(TestCase): @inlineCallbacks def setUp(self): yield super(UpstartServiceTest, self).setUp() self.init_dir = self.makeDir() self.conf = os.path.join(self.init_dir, "some-name.conf") self.output = "/tmp/some-name.output" self.patch(UpstartService, "init_dir", self.init_dir) self.service = UpstartService("some-name") def setup_service(self): self.service.set_description("a wretched hive of scum and villainy") self.service.set_command("/bin/imagination-failure --no-ideas") self.service.set_environ({"LIGHTSABER": "civilised weapon"}) def setup_mock(self): self.check_call = self.mocker.replace("subprocess.check_call") self.getProcessOutput = self.mocker.replace( "twisted.internet.utils.getProcessOutput") def mock_status(self, result): self.getProcessOutput("/sbin/status", ["some-name"]) self.mocker.result(result) def mock_call(self, args, output=None): self.check_call(args, KWARGS) if output is None: self.mocker.result(0) else: def write(ANY, **_): with open(self.output, "w") as f: f.write(output) self.mocker.call(write) def mock_start(self, output=None): self.mock_call(("/sbin/start", "some-name"), output) def mock_stop(self): self.mock_call(("/sbin/stop", "some-name")) def mock_check_success(self): for _ in range(5): self.mock_status(succeed("blah start/running blah 12345")) def mock_check_unstable(self): for _ in range(4): self.mock_status(succeed("blah start/running blah 12345")) self.mock_status(succeed("blah start/running blah 12346")) def mock_check_not_running(self): self.mock_status(succeed("blah")) def write_dummy_conf(self): with open(self.conf, "w") as f: f.write("dummy") def assert_dummy_conf(self): with open(self.conf) as f: self.assertEquals(f.read(), "dummy") def assert_no_conf(self): self.assertFalse(os.path.exists(self.conf)) def assert_conf(self, name="test_standard_install"): with open(os.path.join(DATA_DIR, name)) as expected: with open(self.conf) as actual: self.assertEquals(actual.read(), expected.read()) def test_is_installed(self): """Check is_installed depends on conf file existence""" self.assertFalse(self.service.is_installed()) self.write_dummy_conf() self.assertTrue(self.service.is_installed()) def test_init_dir(self): """ Check is_installed still works when init_dir specified explicitly """ self.patch(UpstartService, "init_dir", "/BAD/PATH") self.service = UpstartService("some-name", init_dir=self.init_dir) self.setup_service() self.assertFalse(self.service.is_installed()) self.write_dummy_conf() self.assertTrue(self.service.is_installed()) @inlineCallbacks def test_is_running(self): """ Check is_running interprets status output (when service is installed) """ self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mock_status(succeed("blah blob/gibbering blah")) self.mock_status(succeed("blah start/running blah 12345")) self.mocker.replay() # Won't hit status; conf is not installed self.assertFalse((yield self.service.is_running())) self.write_dummy_conf() # These 3 calls correspond to the first 3 mock_status calls above self.assertFalse((yield self.service.is_running())) self.assertFalse((yield self.service.is_running())) self.assertTrue((yield self.service.is_running())) @inlineCallbacks def test_is_stable_yes(self): self.setup_mock() self.mock_check_success() self.mocker.replay() self.write_dummy_conf() self.assertTrue((yield self.service.is_stable())) @inlineCallbacks def test_is_stable_no(self): self.setup_mock() self.mock_check_unstable() self.mocker.replay() self.write_dummy_conf() self.assertFalse((yield self.service.is_stable())) @inlineCallbacks def test_is_stable_not_running(self): self.setup_mock() self.mock_check_not_running() self.mocker.replay() self.write_dummy_conf() self.assertFalse((yield self.service.is_stable())) @inlineCallbacks def test_is_stable_not_even_installed(self): self.assertFalse((yield self.service.is_stable())) @inlineCallbacks def test_get_pid(self): """ Check get_pid interprets status output (when service is installed) """ self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mock_status(succeed("blah blob/gibbering blah")) self.mock_status(succeed("blah start/running blah 12345")) self.mocker.replay() # Won't hit status; conf is not installed self.assertEquals((yield self.service.get_pid()), None) self.write_dummy_conf() # These 3 calls correspond to the first 3 mock_status calls above self.assertEquals((yield self.service.get_pid()), None) self.assertEquals((yield self.service.get_pid()), None) self.assertEquals((yield self.service.get_pid()), 12345) @inlineCallbacks def test_basic_install(self): """Check a simple UpstartService writes expected conf file""" e = yield self.assertFailure(self.service.install(), ServiceError) self.assertEquals(str(e), "Cannot render .conf: no description set") self.service.set_description("uninteresting service") e = yield self.assertFailure(self.service.install(), ServiceError) self.assertEquals(str(e), "Cannot render .conf: no command set") self.service.set_command("/bin/false") yield self.service.install() self.assert_conf("test_basic_install") @inlineCallbacks def test_less_basic_install(self): """Check conf for a different UpstartService (which sets an env var)""" self.service.set_description("pew pew pew blam") self.service.set_command("/bin/deathstar --ignore-ewoks endor") self.service.set_environ({"FOO": "bar baz qux", "PEW": "pew"}) self.service.set_output_path("/somewhere/else") yield self.service.install() self.assert_conf("test_less_basic_install") def test_install_via_script(self): """Check that the output-as-script form does the right thing""" self.setup_service() install, start = self.service.get_cloud_init_commands() os.system(install) self.assert_conf() self.assertEquals(start, "/sbin/start some-name") @inlineCallbacks def test_start_not_installed(self): """Check that .start() also installs if necessary""" self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mock_start() self.mock_check_success() self.mocker.replay() self.setup_service() yield self.service.start() self.assert_conf() @inlineCallbacks def test_start_not_started_stable(self): """Check that .start() starts if stopped, and checks for stable pid""" self.write_dummy_conf() self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mock_start("ignored") self.mock_check_success() self.mocker.replay() self.setup_service() yield self.service.start() self.assert_dummy_conf() @inlineCallbacks def test_start_not_started_unstable(self): """Check that .start() starts if stopped, and raises on unstable pid""" self.write_dummy_conf() self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mock_start("kangaroo") self.mock_check_unstable() self.mocker.replay() self.setup_service() e = yield self.assertFailure(self.service.start(), ServiceError) self.assertEquals( str(e), "Failed to start job some-name; got output:\nkangaroo") self.assert_dummy_conf() @inlineCallbacks def test_start_not_started_failure(self): """Check that .start() starts if stopped, and raises on no pid""" self.write_dummy_conf() self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mock_start() self.mock_check_not_running() self.mocker.replay() self.setup_service() e = yield self.assertFailure(self.service.start(), ServiceError) self.assertEquals( str(e), "Failed to start job some-name; no output detected") self.assert_dummy_conf() @inlineCallbacks def test_start_started(self): """Check that .start() does nothing if already running""" self.write_dummy_conf() self.setup_mock() self.mock_status(succeed("blah start/running blah 12345")) self.mocker.replay() self.setup_service() yield self.service.start() self.assert_dummy_conf() @inlineCallbacks def test_destroy_not_installed(self): """Check .destroy() does nothing if not installed""" yield self.service.destroy() self.assert_no_conf() @inlineCallbacks def test_destroy_not_started(self): """Check .destroy just deletes conf if not running""" self.write_dummy_conf() self.setup_mock() self.mock_status(succeed("blah stop/waiting blah")) self.mocker.replay() yield self.service.destroy() self.assert_no_conf() @inlineCallbacks def test_destroy_started(self): """Check .destroy() stops running service and deletes conf file""" self.write_dummy_conf() self.setup_mock() self.mock_status(succeed("blah start/running blah 54321")) self.mock_stop() self.mocker.replay() yield self.service.destroy() self.assert_no_conf() @inlineCallbacks def test_use_sudo(self): """Check that expected commands are generated when use_sudo is set""" self.setup_mock() self.service = UpstartService("some-name", use_sudo=True) self.setup_service() with open(self.output, "w") as f: f.write("clear this file out...") def verify_cp(args, **kwargs): sudo, cp, src, dst = args self.assertEquals(sudo, "sudo") self.assertEquals(cp, "cp") with open(os.path.join(DATA_DIR, "test_standard_install")) as exp: with open(src) as actual: self.assertEquals(actual.read(), exp.read()) self.assertEquals(dst, self.conf) self.write_dummy_conf() self.check_call(ANY, KWARGS) self.mocker.call(verify_cp) self.mock_call(("sudo", "rm", self.output)) self.mock_call(("sudo", "chmod", "644", self.conf)) self.mock_status(succeed("blah stop/waiting blah")) self.mock_call(("sudo", "/sbin/start", "some-name")) # 5 for initial stability check; 1 for final do-we-need-to-stop check for _ in range(6): self.mock_status(succeed("blah start/running blah 12345")) self.mock_call(("sudo", "/sbin/stop", "some-name")) self.mock_call(("sudo", "rm", self.conf)) self.mock_call(("sudo", "rm", self.output)) self.mocker.replay() yield self.service.start() yield self.service.destroy()
class UnitMachineDeployment(object): """ Deploy a unit directly onto a machine. A service unit deployed directly to a machine has full access to the machine resources. Units deployed in such a manner have no isolation from other units on the machine, and may leave artifacts on the machine even upon service destruction. """ unit_agent_module = "juju.agents.unit" def __init__(self, unit_name, juju_home): assert ".." not in unit_name, "Invalid Unit Name" self.unit_name = unit_name self.juju_home = juju_home self.unit_path_name = unit_name.replace("/", "-") self.directory = os.path.join( self.juju_home, "units", self.unit_path_name) self.service = UpstartService( # NOTE: we need use_sudo to work correctly during tests that # launch actual processes (rather than just mocking/trusting). "juju-%s" % self.unit_path_name, use_sudo=True) @inlineCallbacks def start(self, machine_id, zookeeper_hosts, bundle): """Start a service unit agent.""" self.unpack_charm(bundle) self.service.set_description( "Juju unit agent for %s" % self.unit_name) self.service.set_environ(_get_environment( self.unit_name, self.juju_home, machine_id, zookeeper_hosts)) self.service.set_command(" ".join(( "/usr/bin/python", "-m", self.unit_agent_module, "--nodaemon", "--logfile", os.path.join(self.directory, "charm.log"), "--session-file", "/var/run/juju/unit-%s-agent.zksession" % self.unit_path_name))) try: yield self.service.start() except ServiceError as e: raise UnitDeploymentError(str(e)) @inlineCallbacks def destroy(self): """Forcibly terminate a service unit agent, and clean disk state. This will destroy/unmount any state on disk. """ yield self.service.destroy() if os.path.exists(self.directory): shutil.rmtree(self.directory) def get_pid(self): """Get the service unit's process id.""" return self.service.get_pid() def is_running(self): """Is the service unit running.""" return self.service.is_running() def unpack_charm(self, charm): """Unpack a charm to the service units directory.""" if not isinstance(charm, CharmBundle): raise UnitDeploymentError( "Invalid charm for deployment: %s" % charm.path) charm.extract_to(os.path.join(self.directory, "charm"))