import arrow
import json
import os
import sys
from paramiko import SSHClient
from sh import ssh, rsync, Command, \
ErrorReturnCode_1, ErrorReturnCode_255, ErrorReturnCode
from slugify import slugify
from subprocess32 import call, Popen
from .config import expose_username, expose_url, data_dir, verbosity
from .expose import expose
from .log import get_logger
from .utils import quote
from .vagrant import ansible_env
from .virtualbox import list_vms, vm_network, vm_ip, \
vm_info, vm_start, vm_suspend
logger = get_logger('box')
class BoxList(list):
def not_created(self):
return [box for box in self if box.status() == 'not created']
def running(self):
return [box for box in self if box.is_running()]
def suspended(self):
return [box for box in self if box.status() == 'saved']
def status(self, status):
return [box for box in self if box.status() == status]
[docs]class Box(object):
"""
Represents an AerisCloud VM, might or might not be running, take
a Project instance and an entry from the .aeriscloud.yml infra
configuration
:param project: project.Project
:param data: dict[str,str]
"""
NO_PROJECT_DIR = 254
def __init__(self, project, data):
self.project = project
self.data = data
self._vm_name = ''.join([self.project.name(), '-', self.data['name']])
self._logger = get_logger(self._vm_name)
self.basebox = self.data.get('basebox', 'chef/centos-7.0')
[docs] def name(self):
"""
Return the name of the box in the project's infra
:return: str
"""
return self.data['name']
def image(self):
return self.basebox
[docs] def vm_name(self):
"""
Return the VM name
:return: str
"""
return self._vm_name
[docs] def info(self):
"""
Return all the information about a box, kinda messy
:return: dict[str,str]
"""
return vm_info(self._vm_name)
[docs] def status(self):
"""
Return the current status of the VM
:return: str
"""
if self._vm_name not in list_vms():
return 'not created'
info = self.info()
if 'VMState' in info:
return info['VMState'].strip('"')
return 'shutdown'
def last_status_change(self):
info = self.info()
if 'VMStateChangeTime' in info:
change_time = info['VMStateChangeTime'].strip('"')
return arrow.get(change_time[:-10]).to('local')
return None
def forwards(self):
headers = ['protocol', 'host_ip', 'host_port',
'guest_ip', 'guest_port']
return dict([
(
info.strip('"').split(',')[0],
dict(zip(headers, info.strip('"').split(',')[1:]))
)
for key, info in self.info().iteritems()
if key.startswith('Forwarding')
])
[docs] def is_running(self):
"""
Return whether or not the VM is currently running
:return: bool
"""
return self._vm_name in list_vms(True)
[docs] def network(self):
"""
Return the information for every network interfaces on the VM
(kinda slow)
:return: list[map[str,str]]
"""
return vm_network(self._vm_name)
[docs] def ip(self, id=1):
"""
Return the IP of a box for the given interface, by default
aeriscloud VMs have 2 interfaces:
* id 0: NAT interface, on the 10.0.0.0 network
* id 1: Host Only interface on the 172.16.0.0 network
:param id: int
:return: str
"""
return vm_ip(self._vm_name, id)
def ssh_key(self):
# Vagrant 1.7+ support
local_key = os.path.join(self.project.vagrant_dir(), 'machines',
self.name(), 'virtualbox', 'private_key')
insecure_key = os.path.join(os.environ['HOME'], '.vagrant.d',
'insecure_private_key')
if os.path.isfile(local_key):
self._logger.debug('using key "%s" for ssh connection', local_key)
return local_key
self._logger.debug('using key "%s" for ssh connection', insecure_key)
return insecure_key
[docs] def ssh(self, **kwargs):
"""
Return a pre-baked ssh client method to be used for calling
commands on the distant server. Each command called will be in
a different connection.
:return: sh.Command
"""
return ssh.bake(self.ip(), '-A', '-t', i=self.ssh_key(), l='vagrant',
o='StrictHostKeyChecking no', **kwargs)
[docs] def ssh_client(self):
"""
When needing a more precise SSH client, returns a paramiko SSH client
:return: paramiko.SSHClient
"""
client = SSHClient()
client.load_system_host_keys()
client.connect(self.ip(), username='vagrant', pkey=self.ssh_key())
return client
[docs] def ssh_shell(self, cmd=None, cd=True, popen=False, **kwargs):
"""
Create an interactive ssh shell on the remote VM
:return: subprocess32.Popen
"""
call_args = [
'ssh', self.ip(), '-t', '-A',
'-l', 'vagrant',
'-i', self.ssh_key()]
if cmd:
if isinstance(cmd, tuple) or isinstance(cmd, list):
cmd = ' '.join(map(quote, cmd))
if cd:
cmd = '[ ! -d "{0}" ] && exit {1}; cd "{0}"; {2}'.format(
self.project.name(),
self.NO_PROJECT_DIR,
cmd
)
call_args.append(cmd)
self._logger.debug('calling %s', ' '.join(call_args))
if popen:
return Popen(call_args, start_new_session=True, **kwargs)
return call(call_args, **kwargs)
def up(self, *args, **kwargs):
res = self.vagrant('up', *args, **kwargs)
if res == 0:
expose.add(self)
return res
def halt(self, *args, **kwargs):
res = self.vagrant('halt', *args, **kwargs)
if res == 0:
expose.remove(self)
return res
[docs] def resume(self):
"""
Resume a suspended box
:return: bool
"""
if self.status() != 'saved':
return False
vm_start(self._vm_name)
expose.add(self)
return True
[docs] def suspend(self):
"""
Save the state of a running box
:return: bool
"""
if not self.is_running():
return False
vm_suspend(self._vm_name)
expose.remove(self)
return True
def destroy(self):
res = self.vagrant('destroy')
if res == 0:
expose.remove(self)
return res
def expose(self):
expose.add(self)
[docs] def vagrant(self, *args, **kwargs):
"""
Runs a vagrant command
"""
args = tuple(list(args) + [self.name()])
return self.project.vagrant(*args, **kwargs)
[docs] def browse(self, endpoint='', ip=False):
"""
Given an endpoint, returns the URI to that endpoint
:param endpoint: str
:param ip: bool
:return: str
"""
if not ip and expose.enabled():
host = '%s.%s.%s.%s' % (self.data['name'],
self.project.name(),
expose_username(),
expose_url())
else:
host = self.ip()
project_config = self.project.config()
if 'browse' in project_config \
and endpoint in project_config['browse']:
path = project_config['browse'][endpoint]
else:
services = dict()
for service in self.services():
if 'path' not in service:
continue
services[slugify(service['name'])] = service
if endpoint in services:
service = services[endpoint]
return 'http://' + self.ip() + ':' \
+ service['port'] + service['path']
path = '/'
return 'http://' + host + path
[docs] def services(self):
"""
List the services running on the box
:return: dict[str,str,str,str]
"""
try:
return [
dict(zip(
['name', 'port', 'path', 'protocol'],
service.strip().split(',')
))
for service in self.ssh().cat('/etc/aeriscloud.d/*')
]
except ErrorReturnCode_1 as e:
self._logger.warn(e.stderr)
return []
except ErrorReturnCode_255 as e:
self._logger.error(e.stderr)
return []
def history(self):
try:
return [json.loads(line.strip()) for line in
self.ssh().cat('/home/vagrant/.provision')]
except ErrorReturnCode_1:
return []
except ErrorReturnCode_255 as e:
self._logger.error(e.stderr)
return []
def ansible(self, cmd='ansible-playbook'):
tmp_inventory_dir = os.path.join(data_dir(), 'vagrant-inventory')
if not os.path.isdir(tmp_inventory_dir):
os.makedirs(tmp_inventory_dir)
# create a temporary inventory file for ansible
tmp_inventory_file = os.path.join(tmp_inventory_dir, self.vm_name())
with open(tmp_inventory_file, 'w') as f:
f.write('%s ansible_ssh_host=%s ansible_ssh_port=22 '
'ansible_ssh_private_key_file=%s' % (
self.vm_name(),
self.ip(),
self.ssh_key()
))
ansible = Command(cmd)
new_env = ansible_env(os.environ.copy())
return ansible.bake('-i', tmp_inventory_file,
'--extra-vars', '@%s' %
self.project.config_file(),
_env=new_env,
_out_bufsize=0,
_err_bufsize=0)
def rsync(self, src, dest):
# enable arcfour and no compression for faster speed
ssh_options = 'ssh -T -c arcfour -o Compression=no -x ' \
'-i "%s" -l vagrant' % self.ssh_key()
# basic args for rsync
args = ['--delete', '--archive', '--hard-links',
'--one-file-system', '--compress-level=0',
'--omit-dir-times', '-e', ssh_options]
if verbosity():
args.append('-v')
if verbosity() > 1:
args.append('--progress')
if verbosity() > 2:
args.append('--stats')
# check for ignore
conf = self.project.config()
if 'rsync_ignores' in conf:
# TODO: check format
args += map(lambda x: '--exclude="%s"' % x,
conf['rsync_ignores'])
# then add sec and dest
args += [src, dest]
self._logger.debug('running: rsync %s' % ' '.join(args))
try:
rsync(*args,
_out=sys.stdout, _err=sys.stderr,
_out_bufsize=0, _err_bufsize=0)
except ErrorReturnCode:
return False
return True
def rsync_up(self):
if not self.project.rsync_enabled():
return
return self.rsync(
'%s/' % self.project.folder(),
'%s:/data/%s/' % (self.ip(), self.project.name())
)
def rsync_down(self):
if not self.project.rsync_enabled():
return
return self.rsync(
'%s:/data/%s/' % (self.ip(), self.project.name()),
'%s/' % self.project.folder()
)
def __repr__(self):
return '<Box %s from project %s>' % (self.name(),
self.project.name())