diff --git a/deployer/host/base.py b/deployer/host/base.py index 0b8384d..6a7a706 100644 --- a/deployer/host/base.py +++ b/deployer/host/base.py @@ -71,8 +71,8 @@ def __exit__(context, *args): def cd(self, path, expand=False): """ - Execute commands in this directory. - Nesting of cd-statements is allowed. + Execute commands in this directory. Nesting of cd-statements is + allowed. :: diff --git a/deployer/host_container.py b/deployer/host_container.py index 2c9f188..2e7c916 100644 --- a/deployer/host_container.py +++ b/deployer/host_container.py @@ -1,5 +1,5 @@ from contextlib import nested -from deployer.host import Host, HostContext +from deployer.host import Host from deployer.exceptions import ExecCommandFailed from deployer.utils import isclass, esc1 from functools import wraps @@ -9,18 +9,21 @@ class HostsContainer(object): """ - Facade to the host instances. - if you have a role, name 'www' inside the service webserver, you can do: - - - webserver.hosts.run(...) - - webserver.hosts.www.run(...) - - webserver.hosts[0].run(...) - - webserver.hosts.www[0].run(...) - - webserver.hosts.filter('www')[0].run(...) - - The host container also keeps track of HostStatus. So, if we fork a new - thread, and the HostStatus object gets modified in either thread. Clone - this HostsContainer first. + Proxy to a group of :class:`~deployer.host.base.Host` instances. + + For instance, if you have a role, name 'www' inside the container, you + could do: + + :: + + host_container.run(...) + host_container[0].run(...) + host_container.filter('www')[0].run(...) + + Typically, you get a :class:`~deployer.host_container.HostsContainer` class + by accessing the :attr:`~deployer.node.base.Env.hosts` property of an + :class:`~deployer.node.base.Env` (:class:`~deployer.node.base.Node` + wrapper.) """ def __init__(self, hosts, pty=None, logger=None, is_sandbox=False): # the hosts parameter is a dictionary, mapping roles to instances, or lists @@ -99,7 +102,7 @@ def get_hosts_as_dict(self): Return a dictionary which maps all the roles to the set of :class:`deployer.host.Host` classes for each role. """ - return { k: { h.__class__ for h in l } for k,l in self._hosts.items() } + return { k: { h.__class__ for h in l } for k, l in self._hosts.items() } def __repr__(self): return ('<%s\n' % self.__class__.__name__ + @@ -194,10 +197,12 @@ def __iter__(self): def expand_path(self, path): return [ h.expand_path(path) for h in self._all ] - @wraps(Host.run) def run(self, *a, **kw): - """ - Call ``run`` with this parameters on every host. + """run(command, sandbox=False, interactive=True, user=None, ignore_exit_status=False, initial_input=None) + + Call :func:`~deployer.host.base.Host.run` with this parameters on every + :class:`~deployer.host.base.Host` in this container. It can be executed + in parallel when we have multiple hosts. :returns: An array of all the results. """ @@ -235,10 +240,14 @@ def call(pty): else: return [ c(self._pty) for c in callables ] - @wraps(Host.sudo) def sudo(self, *args, **kwargs): - """ - Call ``sudo`` with this parameters on every host. + """sudo(command, sandbox=False, interactive=True, user=None, ignore_exit_status=False, initial_input=None) + + Call :func:`~deployer.host.base.Host.sudo` with this parameters on every + :class:`~deployer.host.base.Host` in this container. It can be executed + in parallel when we have multiple hosts. + + :returns: An array of all the results. """ kwargs['use_sudo'] = True return HostsContainer.run(self, *args, **kwargs) @@ -246,29 +255,49 @@ def sudo(self, *args, **kwargs): # sure that we don't call te overriden method in # HostContainer. - @wraps(HostContext.prefix) - def prefix(self, *a, **kw): + def prefix(self, command): """ - Call 'prefix' with this parameters on every host. + Call :func:`~deployer.host.base.HostContext.prefix` on the + :class:`~deployer.host.base.HostContext` of every host. + + :: + + with host.prefix('workon environment'): + host.run('./manage.py migrate') """ - return nested(* [ h.host_context.prefix(*a, **kw) for h in self._all ]) + return nested(* [ h.host_context.prefix(command) for h in self._all ]) - @wraps(HostContext.cd) - def cd(self, *a, **kw): + def cd(self, path, expand=False): """ - Call 'cd' with this parameters on every host. + Execute commands in this directory. Nesting of cd-statements is + allowed. + + Call :func:`~deployer.host.base.HostContext.cd` on the + :class:`~deployer.host.base.HostContext` of every host. + + :: + + with host_container.cd('directory'): + host_container.run('ls') """ - return nested(* [ h.host_context.cd(*a, **kw) for h in self._all ]) + return nested(* [ h.host_context.cd(path, expand=expand) for h in self._all ]) - @wraps(HostContext.env) - def env(self, *a, **kw): + def env(self, variable, value, escape=True): """ - Call 'env' with this parameters on every host. + Sets an environment variable. + + This calls :func:`~deployer.host.base.HostContext.env` on the + :class:`~deployer.host.base.HostContext` of every host. + + :: + + with host_container.cd('VAR', 'my-value'): + host_container.run('echo $VAR') """ - return nested(* [ h.host_context.env(*a, **kw) for h in self._all ]) + return nested(* [ h.host_context.env(variable, value, escape=escape) for h in self._all ]) def getcwd(self): - """ Call getcwd() for every host """ + """ Calls :func:`~deployer.host.base.Host.getcwd` for every host and return the result as an array. """ return [ h._host.getcwd() for h in self ] # @@ -277,7 +306,8 @@ def getcwd(self): # def exists(self, filename, use_sudo=True): """ - Returns ``True`` when this file exists on the hosts. + Returns an array of boolean values that represent whether this a file + with this name exist for each host. """ def on_host(container): return container._host.exists(filename, use_sudo=use_sudo) @@ -311,7 +341,8 @@ def is_64_bit(self): # TODO: deprecate!!! class HostContainer(HostsContainer): """ - Similar to hostsContainer, but wraps only around exactly one host. + Similar to :class:`~deployer.host_container.HostsContainer`, but wraps only + around exactly one :class:`~deployer.host.base.Host`. """ @property def _host(self): @@ -326,7 +357,7 @@ def slug(self): return self._host.slug @wraps(Host.get_file) - def get_file(self, *args,**kwargs): + def get_file(self, *args, **kwargs): kwargs['sandbox'] = self._sandbox return self._host.get_file(*args, **kwargs) @@ -369,10 +400,15 @@ def __getattr__(self, name): def expand_path(self, *a, **kw): return HostsContainer.expand_path(self, *a, **kw)[0] - @wraps(HostsContainer.exists) - def exists(self, *a, **kw): - return HostsContainer.exists(self, *a, **kw)[0] + def exists(self, filename, use_sudo=True): + """ + Returns ``True`` when this file exists on the hosts. + """ + return HostsContainer.exists(self, filename, use_sudo=use_sudo)[0] - @wraps(HostsContainer.has_command) - def has_command(self, *a, **kw): - return HostsContainer.has_command(self, *a, **kw)[0] + def has_command(self, command, use_sudo=False): + """ + Test whether this command can be found in the bash shell, by executing + a ``which`` Returns ``True`` when the command exists. + """ + return HostsContainer.has_command(self, command, use_sudo=use_sudo)[0] diff --git a/deployer/node/base.py b/deployer/node/base.py index 3de6d06..70c930b 100644 --- a/deployer/node/base.py +++ b/deployer/node/base.py @@ -205,7 +205,9 @@ def default_from_node(cls, node): Create a default environment for this node to run. It will be attached to stdin/stdout and commands will be logged to - stdout. The is the most obvious default. + stdout. The is the most obvious default to create an ``Env`` instance. + + :param node: :class:`~deployer.node.base.Node` instance """ from deployer.pseudo_terminal import Pty from deployer.loggers import LoggerInterface @@ -647,7 +649,10 @@ def __new__(cls, parent=None): @_internal def __init__(self, parent=None): + #: Reference to the parent :class:`~deployer.node.base.Node`. + #: (This is always assigned in the constructor. You should never override it.) self.parent = parent + if self._node_type in (NodeTypes.SIMPLE_ARRAY, NodeTypes.SIMPLE_ONE) and not parent: raise Exception('Cannot initialize a node of type %s without a parent' % self._node_type) @@ -801,12 +806,20 @@ class ParallelNode(Node): Multiple hosts can be given for this role, but all of them will be isolated, during execution. This allows parallel executing of functions on each 'cell'. + If you call a method on a ``ParallelNode``, it will be called one for every + host, which can be accessed through the ``host`` property. + :note: This was called `SimpleNode` before. """ __metaclass__ = ParallelNodeBase _node_type = NodeTypes.SIMPLE def host(self): + """ + This is the proxy to the active host. + + :returns: :class:`~deployer.host_container.HostContainer` instance. + """ if self._node_is_isolated: host = self.hosts.filter('host') if len(host) != 1: diff --git a/docs/index.rst b/docs/index.rst index 2d078d5..3202126 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -27,12 +27,12 @@ meant to replace anything, it's another tool for your toolbox. .. _Fabric: http://docs.fabfile.org/ .. _Saltstack: http://saltstack.com -Questions? Just `create a ticket `_ in Github for now: +Questions? Just `create a ticket`_ in Github for now: -.. _create-ticket: https://github.com/jonathanslenders/python-deployer/issues?state=open +.. _create a ticket: https://github.com/jonathanslenders/python-deployer/issues?state=open - - Read the tutorials: :ref:`Hello world ` and :ref:`Django - tutorial ` + - Read the tutorials: :ref:`Hello world ` and :ref:`Deploying an + application ` - Find the source code at `github`_. .. _github: https://github.com/jonathanslenders/python-deployer @@ -45,8 +45,8 @@ Table of contents .. toctree:: :maxdepth: 3 - pages/getting_started - examples/django-deployment + pages/hello_world + pages/django-deployment pages/architecture_of_roles_and_nodes pages/interactive_shell diff --git a/docs/examples/django-deployment.rst b/docs/pages/django-deployment.rst similarity index 99% rename from docs/examples/django-deployment.rst rename to docs/pages/django-deployment.rst index b37a551..7c55320 100644 --- a/docs/examples/django-deployment.rst +++ b/docs/pages/django-deployment.rst @@ -123,9 +123,9 @@ the repository. You can add the ``install_git``, ``git_clone`` and with self.host.cd(self.project_directory, expand=True): self.host.run("git checkout '%s'" % esc1(commit)) -Probably obvious, we have a clone and checkout function that are meant to move -to a certain directory on the server and run a shell command in there. Some -points worth noting: +Probably obvious, we have a clone and checkout function that are meant to go +to a certain directory on the server and run a shell command in there through +:func:`~deployer.host.base.Host.run`. Some points worth noting: - ``expand=True``: this means that we should do tilde-expension. You want the tilde to be replaced with the home directory. If you have an absolute path, @@ -254,6 +254,7 @@ Anyway, suppose that you have a configuration that you want to upload to """ class DjangoDeployment(Node): + ... def upload_django_settings(self): """ Upload the content of the variable 'local_settings' in the local_settings.py file. """ @@ -858,4 +859,4 @@ would overwrite it with. Also, learn about :ref:`query expressions ` and the -``parent`` variable which are very powerful. +:attr:`~deployer.node.base.Node.parent` variable which are very powerful. diff --git a/docs/pages/getting_started.rst b/docs/pages/hello_world.rst similarity index 99% rename from docs/pages/getting_started.rst rename to docs/pages/hello_world.rst index 70dcf11..03ac05d 100644 --- a/docs/pages/getting_started.rst +++ b/docs/pages/hello_world.rst @@ -1,4 +1,4 @@ -.. _getting-started: +.. _hello-world: Tutorial: Hello world ===================== diff --git a/docs/pages/host_container.rst b/docs/pages/host_container.rst index 6e98a6b..f3567e1 100644 --- a/docs/pages/host_container.rst +++ b/docs/pages/host_container.rst @@ -1,13 +1,14 @@ host_container ============== -Access to hosts from within a ``Node`` class happens through a -``HostsContainer`` proxy. This container object has also methods for reducing -the amount of hosts on which commands are executed, by filtering according to -conditions. +Access to hosts from within a :class:`~deployer.node.base.Node` class happens +through a :class:`~deployer.host_container.HostsContainer` proxy. This +container object has also methods for reducing the amount of hosts on which +commands are executed, by filtering according to conditions. -The ``hosts`` property of a node instance returns such a ``HostsContainer`` -object. +The :attr:`~deployer.node.base.Env.hosts` property of +:class:`~deployer.node.base.Env` wrapper around a node instance returns such a +:class:`~deployer.host_container.HostsContainer` object. :: @@ -17,12 +18,14 @@ object. caching_servers = Host3 def do_something(self): - # self.hosts here, is a HostsContainer instance. + # ``self.hosts`` here is a HostsContainer instance. self.hosts.filter('caching_servers').run('echo hello') Reference --------- -.. automodule:: deployer.host_container +.. autoclass:: deployer.host_container.HostsContainer :members: +.. autoclass:: deployer.host_container.HostContainer + :members: diff --git a/docs/pages/interactive_shell.rst b/docs/pages/interactive_shell.rst index 4d8ff64..5ab5de4 100644 --- a/docs/pages/interactive_shell.rst +++ b/docs/pages/interactive_shell.rst @@ -5,8 +5,9 @@ The interactive shell ===================== It's very easy to create an interactive command line shell from a node tree. -Suppose that you have a `deployer.node.Node` called ``MyRootNode``, then you -can create a shell by making an executable file like this: +Suppose that you have a :class:`~deployer.node.base.Node` called +``MyRootNode``, then you can create a shell by making an executable file like +this: :: @@ -51,8 +52,10 @@ If you save this as ``client.py`` and call it by typing ``python ./client.py --version : Show version information. There are several options to start such a shell. It can be multi or single -threaded, or you can run it as a telnet-server. Normally, you just type the -following to get the interactive prompt: +threaded, or you can run it as a telnet-server. Assuming you made the file also +executable using ``chmod +x client.py``, you just type the following to get the +interactive prompt: + :: diff --git a/docs/pages/node.rst b/docs/pages/node.rst index 39d301d..62dbdbf 100644 --- a/docs/pages/node.rst +++ b/docs/pages/node.rst @@ -18,23 +18,37 @@ A simple example of a node: def hello(self): self.host.run('echo hello world') -.. note:: It is interesting to know that ``self`` is actually not a ``Node`` instance, - but an ``Env`` object which will proxy this actual Node class. This is - because there is some metaclass magic going on, which takes care of sandboxing, - logging and some other nice stuff, that you get for free. +.. note:: It is interesting to know that ``self`` is actually not a + :class:`~deployer.node.base.Node` instance like you would expect, but an + :class:`~deployer.node.base.Env` object which will proxy this actual Node + class. This is because there is some metaclass magic going on, which + takes care of sandboxing, logging and some other nice stuff, that you get + for free. - Except that a few other variables like ``self.console`` are available, - you normally won't notice anything. + Except that a few other variables like :func:`self.console + ` are available, you normally won't notice + anything. Running the code ---------------- +In order to run methods of a node, it has to be wrapped in an +:class:`~deployer.node.base.Env` object. This will manage execution, optional +sandboxing, logging and much more. It will also make sure that +:attr:`self.hosts ` actually becomes a +:class:`~deployer.host_container.HostsContainer`, a proxy through which you can +run methods on a series of hosts. + +The easiest way to wrap a node inside an :class:`~deployer.node.base.Env` is by +using the :func:`~deployer.node.base.Env.default_from_node` helper. This will +make sure that you can see the output and you can interact. + :: from deployer.node import Env - env = Env(MyNode()) + env = Env.default_from_node(MyNode()) env.hello() @@ -99,9 +113,6 @@ classes: The importance of ``ParallelNode`` ---------------------------------- -.. note:: :class:`ParallelNode ` was called - ``SimpleNode`` before. - There are several kind of setups. You can have many hosts which are all doing exactly the same, or many hosts that do something different. Simply said, :class:`ParallelNode ` should be used when you @@ -145,7 +156,7 @@ of the four hosts. Look how our ``WebSystem`` acts like an array: :: - websystem = WebSystem() + websystem = Env.default_from_node(WebSystem()) websystem[Host1].deploy('abcde6565eee...') websystem[Host2].restart() @@ -156,14 +167,15 @@ sequentially. :: - websystem = WebSystem() + websystem = Env.default_from_node(WebSystem()) websystem.deploy('abcde6565eee...') # Parallel execution. -.. note:: One thing worth noting is that there is a variable ``host`` in the - class. This is because the isolation always happens by convention on - the role named ``host``. Both sides of the following equation will - represent a host container containing exactly one host: the host of - the current isolation. +.. note:: One thing worth noting is that there is a variable + :attr:`~deployer.node.base.ParallelNode.host` in the class. This is + because the isolation always happens by convention on the role named + ``host``. Both sides of the following equation will represent a + :class:`~deployer.host_container.HostContainer` containing exactly + one host: the host of the current isolation. ::