Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Commit

Permalink
Add shell provisioner (#56)
Browse files Browse the repository at this point in the history
* Added support for shell provisioning

* Added support for scripts in the shell provisioner

* Removed unnecessary check in homedir_expanded_path method

* Refactor the shell provisioner schema

* Used shlex to split raw commands comming from the shell provisioner

* Added a note related to the use of the shell provisioner
  • Loading branch information
ellmetha authored and Virgil Dupras committed Apr 2, 2017
1 parent 82ae9e0 commit fa46266
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 80 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ __pycache__
.coverage.*
.env/
env/
*.retry
9 changes: 9 additions & 0 deletions docs/provisioners/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,18 @@ defined in your LXDock file using the ``provisioning`` option:
- type: ansible
playbook: deploy/site.yml
.. warning::

When using provisioners you should keep in mind that some of them can execute local actions on the
host. For example Ansible playbooks can trigger local actions that will be run on your system.
Other provisioners (like the shell provisioner) can define commands to be runned on the host side
or in provileged containers. **You have to** trust the projects that use these provisioning tools
before running LXDock!

Documentation sections for the supported provisioning tools or methods are listed here.

.. toctree::
:maxdepth: 1

ansible
shell
77 changes: 77 additions & 0 deletions docs/provisioners/shell.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#####
Shell
#####

The shell provisioner allows you to execute commands on the guest side or the host side in order to
provision your containers.

Usage
-----

Just append a ``shell`` provisioning operation to your LXDock file as follows:

.. code-block:: yaml
name: myproject
image: ubuntu/xenial
provisioning:
- type: shell
inline: echo "Hello, World!"
.. note::

Keep in mind that the shell provisioner will use the LXD's ``exec`` method in order to run your
commands on containers (the same method used by the ``lxc exec`` command). This means that common
shell patterns (like file redirects, ``|``, ``>``, ``<``, ...) won't work because the ``exec``
method doesn't use a shell (so the kernel will not be able to understand these shell patterns).
The only way to overcome this is to put things like ``sh -c 'ls -l > /tmp/test'`` in your
``inline`` options.

Required options
----------------

inline
======

The ``inline`` option allows you to specify a shell command that should be executed on the guest
side or on the host. Note that the ``inline`` option and the ``script`` option are mutually
exclusive.

.. code-block:: yaml
[...]
provisioning:
- type: shell
inline: echo "Hello, World!"
script
======

The ``script`` option lets you define the path to an existing script that should be executed on the
guest side or on the host. Note that the ``script`` option and the ``inline`` option are mutually
exclusive.

.. code-block:: yaml
[...]
provisioning:
- type: shell
script: path/to/my/script.sh
Optional options
----------------

side
====

Use the ``side`` option if you want to define that the shell commands/scripts should be executed on
the host side. The default value for this option is ``guest``. Here is an example:

.. code-block:: yaml
[...]
provisioning:
- type: shell
side: host
inline: echo "Hello, World!"
1 change: 1 addition & 0 deletions docs/release_notes/v0.2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ New features
* Add the possibility to use the ``lxdock`` command from subfolders
* Add support for ansible options related to ansible-vault
(``ask_vault_pass``, ``vault_password_file``)
* Add support for shell provisioning
3 changes: 2 additions & 1 deletion lxdock/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ def provision(self, barebone=None):
provisioning_type = provisioning_item['type'].lower()
provisioner_class = Provisioner.provisioners.get(provisioning_type)
if provisioner_class is not None:
provisioner = provisioner_class(self.homedir, self._container, provisioning_item)
provisioner = provisioner_class(
self.homedir, self._host, self._guest, provisioning_item)
logger.info('Provisioning with {0}'.format(provisioning_item['type']))
provisioner.provision()

Expand Down
6 changes: 3 additions & 3 deletions lxdock/guests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ def install_ansible_packages(self): # pragma: no cover
# HELPER METHODS #
##################

def run(self, cmd):
def run(self, cmd_args):
""" Runs the specified command inside the current container. """
logger.debug('Running {0}'.format(' '.join(cmd)))
exit_code, stdout, stderr = self.lxd_container.execute(cmd)
logger.debug('Running {0}'.format(' '.join(cmd_args)))
exit_code, stdout, stderr = self.lxd_container.execute(cmd_args)
logger.debug(stdout)
logger.debug(stderr)
return exit_code
Expand Down
24 changes: 19 additions & 5 deletions lxdock/hosts/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
supported by LXDock.
"""

import logging
import os
import platform
import subprocess
Expand All @@ -15,6 +16,8 @@

__all__ = ['Host', ]

logger = logging.getLogger(__name__)


class InvalidHost(Exception):
""" The `Host` subclass is not valid. """
Expand Down Expand Up @@ -89,7 +92,7 @@ def get_ssh_pubkey(self):

def give_current_user_access_to_share(self, source):
""" Give read/write access to `source` for the current user. """
subprocess.Popen('setfacl -Rdm u:{}:rwX {}'.format(os.getuid(), source), shell=True).wait()
self.run(['setfacl', '-Rdm', 'u:{}:rwX'.format(os.getuid()), source])

def give_mapped_user_access_to_share(self, source, userpath=None):
""" Give read/write access to `source` for the mapped user owning `userpath`.
Expand All @@ -109,7 +112,18 @@ def give_mapped_user_access_to_share(self, source, userpath=None):
container_path = os.path.join(*container_path_parts)
container_path_stats = os.stat(container_path)
host_userpath_uid = container_path_stats.st_uid
subprocess.Popen(
'setfacl -Rm user:lxd:rwx,default:user:lxd:rwx,'
'user:{0}:rwx,default:user:{0}:rwx {1}'.format(host_userpath_uid, source),
shell=True).wait()
self.run([
'setfacl', '-Rm',
'user:lxd:rwx,default:user:lxd:rwx,'
'user:{0}:rwx,default:user:{0}:rwx'.format(host_userpath_uid), source,
])

##################
# HELPER METHODS #
##################

def run(self, cmd_args):
""" Runs the specified command on the host. """
cmd = ' '.join(cmd_args)
logger.debug('Running {0} on the host'.format(cmd))
subprocess.Popen(cmd, shell=True).wait()
1 change: 1 addition & 0 deletions lxdock/provisioners/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@

from .ansible import * # noqa
from .base import * # noqa
from .shell import * # noqa
14 changes: 5 additions & 9 deletions lxdock/provisioners/ansible.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import logging
import subprocess
import tempfile

from voluptuous import IsFile, Required
Expand All @@ -22,17 +21,14 @@ class AnsibleProvisioner(Provisioner):
}

def provision(self):
""" Performs the provisioning operations using the considered provisioner. """
ip = get_ipv4_ip(self.lxd_container)
""" Performs the provisioning operations using ansible-playbook. """
ip = get_ipv4_ip(self.guest.lxd_container)
with tempfile.NamedTemporaryFile() as tmpinv:
tmpinv.write('{} ansible_user=root'.format(ip).encode('ascii'))
tmpinv.flush()
cmd = self._build_ansible_playbook_command(tmpinv.name)
logger.debug(cmd)
p = subprocess.Popen(cmd, shell=True)
p.wait()
self.host.run(self._build_ansible_playbook_command_args(tmpinv.name))

def _build_ansible_playbook_command(self, inventory_filename):
def _build_ansible_playbook_command_args(self, inventory_filename):
cmd_args = ['ANSIBLE_HOST_KEY_CHECKING=False', 'ansible-playbook', ]
cmd_args.extend(['--inventory-file', inventory_filename, ])

Expand All @@ -48,4 +44,4 @@ def _build_ansible_playbook_command(self, inventory_filename):

# Append the playbook filepath and return the final command.
cmd_args.append(self.homedir_expanded_path(self.options['playbook']))
return ' '.join(cmd_args)
return cmd_args
13 changes: 7 additions & 6 deletions lxdock/provisioners/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class Provisioner(with_metaclass(_ProvisionerBase)):
""" Represents a single provisioner.
`Provisioner` subclasses will be used by `Container` instances to run provisioning operations
associated with the considered containers. For example they can be used to run Ansible playbook
associated with the considered containers. For example they can be used to run Ansible playbooks
to provision a web application on the container.
"""

Expand All @@ -78,9 +78,10 @@ class Provisioner(with_metaclass(_ProvisionerBase)):
# considered provisioner.
schema = None

def __init__(self, homedir, lxd_container, options):
def __init__(self, homedir, host, guest, options):
self.homedir = homedir
self.lxd_container = lxd_container
self.host = host
self.guest = guest
self.options = options.copy()

def provision(self):
Expand All @@ -91,6 +92,6 @@ def provision(self):
# HELPER METHODS #
##################

def homedir_expanded_path(self, relative_path):
""" Expands the considered path with the absolute path of the home homedir. """
return os.path.join(self.homedir, relative_path)
def homedir_expanded_path(self, path):
""" Expands the considered path with the path of the home homedir if applicable. """
return os.path.join(self.homedir, path)
54 changes: 54 additions & 0 deletions lxdock/provisioners/shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os
import shlex

from voluptuous import Any, Exclusive, IsFile

from .base import Provisioner


class ShellProvisioner(Provisioner):
""" Allows to perform provisioning shell operations on the host/guest sides. """

name = 'shell'
schema = {
Exclusive('inline', 'shelltype'): str,
Exclusive('script', 'shelltype'): IsFile(),
'side': Any('guest', 'host'),
}

def provision(self):
""" Executes the shell commands in the guest container or in the host. """
if 'script' in self.options and self._is_for_guest:
# First case: we have to run the script inside the container. So the first step is
# to copy the content of the script to a temporary file in the container, ensure
# that the script is executable and then run the script.
guest_scriptpath = os.path.join('/tmp/', os.path.basename(self.options['script']))
with open(self.homedir_expanded_path(self.options['script'])) as fd:
self.guest.lxd_container.files.put(guest_scriptpath, fd.read())
self.guest.run(['chmod', '+x', guest_scriptpath])
self.guest.run([guest_scriptpath, ])
elif 'script' in self.options and self._is_for_host:
# Second case: the script is executed on the host side.
self.host.run([self.homedir_expanded_path(self.options['script']), ])
elif 'inline' in self.options:
# Final case: we run a command directly inside the container or outside.
host_or_guest = getattr(self, self._side)
host_or_guest.run(shlex.split(self.options['inline']))

##################################
# PRIVATE METHODS AND PROPERTIES #
##################################

@property
def _is_for_guest(self):
""" Returns True if this provisioner should run on the guest side. """
return self._side == 'guest'

@property
def _is_for_host(self):
""" Returns True if this provisioner should run on the host side. """
return self._side == 'host'

@property
def _side(self):
return self.options.get('side', 'guest')

0 comments on commit fa46266

Please sign in to comment.