Skip to content
This repository has been archived by the owner on Dec 21, 2022. It is now read-only.

Commit

Permalink
Improved documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanslenders committed Feb 27, 2014
1 parent 4e66841 commit 98c7625
Show file tree
Hide file tree
Showing 9 changed files with 153 additions and 85 deletions.
4 changes: 2 additions & 2 deletions deployer/host/base.py
Expand Up @@ -71,8 +71,8 @@ def __exit__(context, *args):


def cd(self, path, expand=False): def cd(self, path, expand=False):
""" """
Execute commands in this directory. Execute commands in this directory. Nesting of cd-statements is
Nesting of cd-statements is allowed. allowed.
:: ::
Expand Down
120 changes: 78 additions & 42 deletions deployer/host_container.py
@@ -1,5 +1,5 @@
from contextlib import nested from contextlib import nested
from deployer.host import Host, HostContext from deployer.host import Host
from deployer.exceptions import ExecCommandFailed from deployer.exceptions import ExecCommandFailed
from deployer.utils import isclass, esc1 from deployer.utils import isclass, esc1
from functools import wraps from functools import wraps
Expand All @@ -9,18 +9,21 @@


class HostsContainer(object): class HostsContainer(object):
""" """
Facade to the host instances. Proxy to a group of :class:`~deployer.host.base.Host` instances.
if you have a role, name 'www' inside the service webserver, you can do:
For instance, if you have a role, name 'www' inside the container, you
- webserver.hosts.run(...) could do:
- webserver.hosts.www.run(...)
- webserver.hosts[0].run(...) ::
- webserver.hosts.www[0].run(...)
- webserver.hosts.filter('www')[0].run(...) host_container.run(...)
host_container[0].run(...)
The host container also keeps track of HostStatus. So, if we fork a new host_container.filter('www')[0].run(...)
thread, and the HostStatus object gets modified in either thread. Clone
this HostsContainer first. 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): def __init__(self, hosts, pty=None, logger=None, is_sandbox=False):
# the hosts parameter is a dictionary, mapping roles to <Host> instances, or lists # the hosts parameter is a dictionary, mapping roles to <Host> instances, or lists
Expand Down Expand Up @@ -99,7 +102,7 @@ def get_hosts_as_dict(self):
Return a dictionary which maps all the roles to the set of Return a dictionary which maps all the roles to the set of
:class:`deployer.host.Host` classes for each role. :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): def __repr__(self):
return ('<%s\n' % self.__class__.__name__ + return ('<%s\n' % self.__class__.__name__ +
Expand Down Expand Up @@ -194,10 +197,12 @@ def __iter__(self):
def expand_path(self, path): def expand_path(self, path):
return [ h.expand_path(path) for h in self._all ] return [ h.expand_path(path) for h in self._all ]


@wraps(Host.run)
def run(self, *a, **kw): def run(self, *a, **kw):
""" """run(command, sandbox=False, interactive=True, user=None, ignore_exit_status=False, initial_input=None)
Call ``run`` with this parameters on every host.
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. :returns: An array of all the results.
""" """
Expand Down Expand Up @@ -235,40 +240,64 @@ def call(pty):
else: else:
return [ c(self._pty) for c in callables ] return [ c(self._pty) for c in callables ]


@wraps(Host.sudo)
def sudo(self, *args, **kwargs): def sudo(self, *args, **kwargs):
""" """sudo(command, sandbox=False, interactive=True, user=None, ignore_exit_status=False, initial_input=None)
Call ``sudo`` with this parameters on every host.
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 kwargs['use_sudo'] = True
return HostsContainer.run(self, *args, **kwargs) return HostsContainer.run(self, *args, **kwargs)
# NOTE: here we use HostsContainer instead of self, to be # NOTE: here we use HostsContainer instead of self, to be
# sure that we don't call te overriden method in # sure that we don't call te overriden method in
# HostContainer. # HostContainer.


@wraps(HostContext.prefix) def prefix(self, command):
def prefix(self, *a, **kw):
""" """
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, path, expand=False):
def cd(self, *a, **kw):
""" """
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, variable, value, escape=True):
def env(self, *a, **kw):
""" """
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): 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 ] return [ h._host.getcwd() for h in self ]


# #
Expand All @@ -277,7 +306,8 @@ def getcwd(self):
# #
def exists(self, filename, use_sudo=True): 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): def on_host(container):
return container._host.exists(filename, use_sudo=use_sudo) return container._host.exists(filename, use_sudo=use_sudo)
Expand Down Expand Up @@ -311,7 +341,8 @@ def is_64_bit(self): # TODO: deprecate!!!


class HostContainer(HostsContainer): 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 @property
def _host(self): def _host(self):
Expand All @@ -326,7 +357,7 @@ def slug(self):
return self._host.slug return self._host.slug


@wraps(Host.get_file) @wraps(Host.get_file)
def get_file(self, *args,**kwargs): def get_file(self, *args, **kwargs):
kwargs['sandbox'] = self._sandbox kwargs['sandbox'] = self._sandbox
return self._host.get_file(*args, **kwargs) return self._host.get_file(*args, **kwargs)


Expand Down Expand Up @@ -369,10 +400,15 @@ def __getattr__(self, name):
def expand_path(self, *a, **kw): def expand_path(self, *a, **kw):
return HostsContainer.expand_path(self, *a, **kw)[0] return HostsContainer.expand_path(self, *a, **kw)[0]


@wraps(HostsContainer.exists) def exists(self, filename, use_sudo=True):
def exists(self, *a, **kw): """
return HostsContainer.exists(self, *a, **kw)[0] 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, command, use_sudo=False):
def has_command(self, *a, **kw): """
return HostsContainer.has_command(self, *a, **kw)[0] 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]
15 changes: 14 additions & 1 deletion deployer/node/base.py
Expand Up @@ -205,7 +205,9 @@ def default_from_node(cls, node):
Create a default environment for this node to run. Create a default environment for this node to run.
It will be attached to stdin/stdout and commands will be logged to 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.pseudo_terminal import Pty
from deployer.loggers import LoggerInterface from deployer.loggers import LoggerInterface
Expand Down Expand Up @@ -647,7 +649,10 @@ def __new__(cls, parent=None):


@_internal @_internal
def __init__(self, parent=None): 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 self.parent = parent

if self._node_type in (NodeTypes.SIMPLE_ARRAY, NodeTypes.SIMPLE_ONE) and not 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) raise Exception('Cannot initialize a node of type %s without a parent' % self._node_type)


Expand Down Expand Up @@ -801,12 +806,20 @@ class ParallelNode(Node):
Multiple hosts can be given for this role, but all of them will be isolated, 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'. 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. :note: This was called `SimpleNode` before.
""" """
__metaclass__ = ParallelNodeBase __metaclass__ = ParallelNodeBase
_node_type = NodeTypes.SIMPLE _node_type = NodeTypes.SIMPLE


def host(self): def host(self):
"""
This is the proxy to the active host.
:returns: :class:`~deployer.host_container.HostContainer` instance.
"""
if self._node_is_isolated: if self._node_is_isolated:
host = self.hosts.filter('host') host = self.hosts.filter('host')
if len(host) != 1: if len(host) != 1:
Expand Down
12 changes: 6 additions & 6 deletions docs/index.rst
Expand Up @@ -27,12 +27,12 @@ meant to replace anything, it's another tool for your toolbox.
.. _Fabric: http://docs.fabfile.org/ .. _Fabric: http://docs.fabfile.org/
.. _Saltstack: http://saltstack.com .. _Saltstack: http://saltstack.com


Questions? Just `create a ticket <create-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 <getting-started>` and :ref:`Django - Read the tutorials: :ref:`Hello world <hello-world>` and :ref:`Deploying an
tutorial <django-tutorial>` application <django-tutorial>`
- Find the source code at `github`_. - Find the source code at `github`_.


.. _github: https://github.com/jonathanslenders/python-deployer .. _github: https://github.com/jonathanslenders/python-deployer
Expand All @@ -45,8 +45,8 @@ Table of contents
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3


pages/getting_started pages/hello_world
examples/django-deployment pages/django-deployment
pages/architecture_of_roles_and_nodes pages/architecture_of_roles_and_nodes
pages/interactive_shell pages/interactive_shell


Expand Down
Expand Up @@ -123,9 +123,9 @@ the repository. You can add the ``install_git``, ``git_clone`` and
with self.host.cd(self.project_directory, expand=True): with self.host.cd(self.project_directory, expand=True):
self.host.run("git checkout '%s'" % esc1(commit)) self.host.run("git checkout '%s'" % esc1(commit))


Probably obvious, we have a clone and checkout function that are meant to move 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. Some to a certain directory on the server and run a shell command in there through
points worth noting: :func:`~deployer.host.base.Host.run`. Some points worth noting:


- ``expand=True``: this means that we should do tilde-expension. You want the - ``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, tilde to be replaced with the home directory. If you have an absolute path,
Expand Down Expand Up @@ -254,6 +254,7 @@ Anyway, suppose that you have a configuration that you want to upload to
""" """


class DjangoDeployment(Node): class DjangoDeployment(Node):
...
def upload_django_settings(self): def upload_django_settings(self):
""" Upload the content of the variable 'local_settings' in the """ Upload the content of the variable 'local_settings' in the
local_settings.py file. """ local_settings.py file. """
Expand Down Expand Up @@ -858,4 +859,4 @@ would overwrite it with.
Also, learn about :ref:`query expressions <query-expressions>` and the Also, learn about :ref:`query expressions <query-expressions>` and the
``parent`` variable which are very powerful. :attr:`~deployer.node.base.Node.parent` variable which are very powerful.
@@ -1,4 +1,4 @@
.. _getting-started: .. _hello-world:


Tutorial: Hello world Tutorial: Hello world
===================== =====================
Expand Down
19 changes: 11 additions & 8 deletions docs/pages/host_container.rst
@@ -1,13 +1,14 @@
host_container host_container
============== ==============


Access to hosts from within a ``Node`` class happens through a Access to hosts from within a :class:`~deployer.node.base.Node` class happens
``HostsContainer`` proxy. This container object has also methods for reducing through a :class:`~deployer.host_container.HostsContainer` proxy. This
the amount of hosts on which commands are executed, by filtering according to container object has also methods for reducing the amount of hosts on which
conditions. commands are executed, by filtering according to conditions.


The ``hosts`` property of a node instance returns such a ``HostsContainer`` The :attr:`~deployer.node.base.Env.hosts` property of
object. :class:`~deployer.node.base.Env` wrapper around a node instance returns such a
:class:`~deployer.host_container.HostsContainer` object.


:: ::


Expand All @@ -17,12 +18,14 @@ object.
caching_servers = Host3 caching_servers = Host3


def do_something(self): 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') self.hosts.filter('caching_servers').run('echo hello')


Reference Reference
--------- ---------


.. automodule:: deployer.host_container .. autoclass:: deployer.host_container.HostsContainer
:members: :members:


.. autoclass:: deployer.host_container.HostContainer
:members:
11 changes: 7 additions & 4 deletions docs/pages/interactive_shell.rst
Expand Up @@ -5,8 +5,9 @@ The interactive shell
===================== =====================


It's very easy to create an interactive command line shell from a node tree. 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 Suppose that you have a :class:`~deployer.node.base.Node` called
can create a shell by making an executable file like this: ``MyRootNode``, then you can create a shell by making an executable file like
this:


:: ::


Expand Down Expand Up @@ -51,8 +52,10 @@ If you save this as ``client.py`` and call it by typing ``python ./client.py
--version : Show version information. --version : Show version information.


There are several options to start such a shell. It can be multi or single 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 threaded, or you can run it as a telnet-server. Assuming you made the file also
following to get the interactive prompt: executable using ``chmod +x client.py``, you just type the following to get the
interactive prompt:



:: ::


Expand Down

0 comments on commit 98c7625

Please sign in to comment.