From ff6641f6df773c801ed72e97c6d031480d535b1f Mon Sep 17 00:00:00 2001 From: gerard Date: Tue, 3 Nov 2015 17:56:24 +1300 Subject: [PATCH 1/4] virtualbox virt lib --- lib/strider/virt/virtualbox.py | 118 +++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 lib/strider/virt/virtualbox.py diff --git a/lib/strider/virt/virtualbox.py b/lib/strider/virt/virtualbox.py new file mode 100644 index 0000000..07c6680 --- /dev/null +++ b/lib/strider/virt/virtualbox.py @@ -0,0 +1,118 @@ +# Copyright 2015 Gerard Cristofol +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Import the vboxapi package +from vboxapi import VirtualBoxManager + +import socket +import time +from strider.common.instance_data import InstanceData, SshData +import strider.common.logger + +class VBOX(object): + + def __init__(self, name=None, instance_type=None, virtualbox_manager=None, tags=None): + + self.name = name + self.instance_type = instance_type + self.tags = {} + self.virtualbox_manager = virtualbox_manager + + # utility instances + self.log = strider.utils.logger.get_logger('VBOX') + + # check for required args + if not self.name: + raise Exception("'name' is required") + if not self.instance_type: + raise Exception("'instance_type' is required") + + # coerce inputs + self.tags['Name'] = self.name + + + # -------------------------------------------------------------------------- + # PUBLIC VIRT API INTERFACE + # -------------------------------------------------------------------------- + + def up(self): + """ Instantiate instances if needed, otherwise just start them """ + + self.log("determining if we need to create an instance") + me = self.describe().provider_specific + if me is None: + self.log("creating an instance") + + #TODO Create the virtualbox instance + # 2. Get the IVirtualBox object + virtualbox = self.virtualbox_manager.getVirtualBox() + virtualbox.createInstance() #parameter home??? + + self.log("instance created") + else: + self.log("instance already exists, starting if needed") + self.connection.start_instances([me.id]) + + me = self.describe() + if not me.present: + raise Exception("unexpectedly can't find the instance.") + + # -------------------------------------------------------------------------- + + def destroy(self): + """ Destroy the described instance """ + + self.log("looking for instances to destroy") + me = self.describe() + if me.present: + self.log("destroying instance") + self.connection.terminate_instances(instance_ids=[me.provider_specific.id]) + self.log("instance destroyed") + else: + self.log("no instance found to destroy") + + # -------------------------------------------------------------------------- + + def describe(self): + """ Return details about the instance. Standardized between cloud providers """ + + details = self._details() + if details is None: + return InstanceData(present=False) + + # -------------------------------------------------------------------------- + # PRIVATE FUNCTIONS + # -------------------------------------------------------------------------- + + + def _details(self): + """ Return the cloud provider's info about the described instance""" + + # 2. Get the IVirtualBox object + virtualbox = self.virtualbox_manager.getVirtualBox() + + # For IVirtualBox array attributes you need to use + # the VirtualBoxManager.getArray() method: + + # Iterate the IVirtualBox::machines array. + # Each element is an IMachine instance. + machines = self.virtualbox_manager.getArray(virtualbox, 'machines') + for machine in machines: + print 'Virtual machine, name: ', machine.name + if machine.name is self.name: + return machine + + return None + + From f4834266de5c31b05fe4b5d834e52fb09e2c7124 Mon Sep 17 00:00:00 2001 From: Jimmy Tang Date: Sat, 3 Oct 2015 12:03:49 +0100 Subject: [PATCH 2/4] First pass at adding a vagrant virt backend --- examples/Vagrant_ansible/deploy/test.yml | 5 ++ examples/Vagrant_ansible/striderfile.py | 71 ++++++++++++++++++++ examples/Vagrant_ansible/userdata.sh | 6 ++ lib/strider/virt/vagrantbox.py | 85 ++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 examples/Vagrant_ansible/deploy/test.yml create mode 100755 examples/Vagrant_ansible/striderfile.py create mode 100644 examples/Vagrant_ansible/userdata.sh create mode 100644 lib/strider/virt/vagrantbox.py diff --git a/examples/Vagrant_ansible/deploy/test.yml b/examples/Vagrant_ansible/deploy/test.yml new file mode 100644 index 0000000..e37476f --- /dev/null +++ b/examples/Vagrant_ansible/deploy/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: "{{target|default('localhost')}}" + become: yes + tasks: + - apt: name=tree state=installed diff --git a/examples/Vagrant_ansible/striderfile.py b/examples/Vagrant_ansible/striderfile.py new file mode 100755 index 0000000..ccc881b --- /dev/null +++ b/examples/Vagrant_ansible/striderfile.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# example strider file + +from strider import Strider +from strider.virt.vagrantbox import Vagrantbox +from strider.provisioners.shell import Shell +import time +import os + +# ============================================================================== +# vagrant instance and builder configuration + +instance = Vagrantbox( + name = "%s-strider-test" % os.environ["USER"], # currently not used + basebox = "wheezy", # the basebox to use + ssh = dict( + username = "vagrant",# default username to login with + private_key_path = "/Users/jtang/.vagrant.d/insecure_private_key", # key to use + ), + bake_name = "strider-produced-basebox-%d" % int(time.time()), # output filename + bake_description = "Vagrant basebox description goes here version 1.00", + + # other optional parameters: + user_data = open("userdata.sh").read(), +) + +# ============================================================================== +# The provisioner decides how the instance will be configured + +provisioner = Shell( + commands = [ + + # you can deselect 'rsync' by changing to 'copy' below for the type parameter. + # rsync on AWS free tier has been observed to be unreliable - protocol errors + # so if you see this, know why + + "sudo DEBIAN_FRONTEND=noninteractive apt-get update -y", + "sudo DEBIAN_FRONTEND=noninteractive apt-get -y install rsync", + dict(type='rsync', copy_from="./deploy", copy_to="/home/vagrant/deploy_root"), + ] +) + +# Alternative: run ansible remotely from the build machine / workstation +# (more SSH activity, but shares less information with the guest) + +# provisioner = Shell( +# commands = [ +# dict(type='command', +# command='PYTHONUNBUFFERED=1 ansible-playbook -v -i {{ssh_host}}, -u {{ssh_user}} -e "target={{ssh_host}}" deploy/test.yml'), + +# ] +# ) + +# optional steps to run prior to --bake commands that will only run with --bake +pre_bake = Shell( + commands = [ + "sync" + ] +) + +# optional commands to run after successful bake jobs to use remaining compute time +post_bake = Shell( + commands = [ + "echo 'post bake steps!'" + ] +) + +# ============================================================================= +# go! + +strider = Strider(provisioner=provisioner, pre_bake=pre_bake, post_bake=post_bake).cli(instance) diff --git a/examples/Vagrant_ansible/userdata.sh b/examples/Vagrant_ansible/userdata.sh new file mode 100644 index 0000000..68658df --- /dev/null +++ b/examples/Vagrant_ansible/userdata.sh @@ -0,0 +1,6 @@ +#!/bin/bash +SUDOERS_FILE=/etc/sudoers.d/strider-init-requiretty +echo "Defaults:ec2-user !requiretty" > $SUDOERS_FILE +echo "Defaults:root !requiretty" >> $SUDOERS_FILE +chmod 440 $SUDOERS_FILE + diff --git a/lib/strider/virt/vagrantbox.py b/lib/strider/virt/vagrantbox.py new file mode 100644 index 0000000..0599504 --- /dev/null +++ b/lib/strider/virt/vagrantbox.py @@ -0,0 +1,85 @@ +import vagrant +import os +from subprocess import CalledProcessError +from strider.common.instance_data import InstanceData, SshData +import strider.common.logger + + +class Vagrantbox(object): + def __init__(self, + name=None, + ssh=None, + basebox=None, + bake_name=None, + bake_description=None, + user_data=None): + self.name = name + self.bake_name = bake_name + self.basebox = basebox + self.ssh = ssh + self.log = strider.utils.logger.get_logger('Vagrant') + if type(self.ssh) != dict: + raise Exception("expecting 'ssh' to be a dictionary") + + self.vagrant_instance = vagrant.Vagrant() + + def describe(self): + details = self._details() + if details is None: + return InstanceData(present=False) + else: + if self.ssh['username'] is not None: + username = self.ssh['username'] + else: + username = "vagrant" + + if self.ssh['private_key_path'] is not None: + private_key_path = self.ssh['private_key_path'] + else: + private_key_path = details['IdentityFile'] + + port = details['Port'] + host = details['HostName'] + ssh_data = SshData(keyfile=private_key_path, + user=username, + host=host, + port=port) + return InstanceData(present=True, + provider_specific=details, + ssh=ssh_data) + + def destroy(self): + self.log("destroying instance") + try: + self.vagrant_instance.destroy() + except CalledProcessError: + self.log("already destroyed instance") + try: + os.remove("./Vagrantfile") + except OSError: + self.log("already removed Vagrantfile") + + def up(self): + self.log("determining if we need to create an instance") + try: + self.vagrant_instance.init(box_name=self.basebox) + except CalledProcessError: + self.log("already initialised instance") + try: + self.log("bring up instance") + self.vagrant_instance.up() + except CalledProcessError: + self.log("already up") + + def _details(self): + try: + conf = self.vagrant_instance.conf() + return conf + except CalledProcessError: + self.log("No instance running") + return None + + def bake(self): + self.log("baking vagrant box") + os.system("vagrant package --output {}.box".format(self.bake_name)) + self.up() diff --git a/setup.py b/setup.py index 33a94a5..ede73fd 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ author_email='michael.dehaan@gmail.com', url='http://github.com/mpdehaan/strider/', license='Apache2', - install_requires=['boto'], + install_requires=['boto', 'python-vagrant'], package_dir={ '': 'lib' }, packages=find_packages('lib'), classifiers=[ From eeefe6a7723e4be4e1f0e1e9724a25d523af7cdc Mon Sep 17 00:00:00 2001 From: Jimmy Tang Date: Tue, 9 Feb 2016 07:40:23 +0000 Subject: [PATCH 3/4] Version bump, this include the vagrant component --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ede73fd..d25f003 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ from setuptools import setup, find_packages setup(name='strider', - version="0.0.11", + version="0.0.12", description='Strider builds dev VMs and bakes cloud images.', author='Michael DeHaan', author_email='michael.dehaan@gmail.com', From 96ebd7dc05428714f7b5e887e84651aa41865bd3 Mon Sep 17 00:00:00 2001 From: Jimmy Tang Date: Tue, 9 Feb 2016 19:35:33 +0000 Subject: [PATCH 4/4] Add initial helpers, tooling for testing --- .gitignore | 6 ++++++ requirements_test.txt | 2 ++ tests/strider_test.py | 4 ++++ tox.ini | 13 +++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 requirements_test.txt create mode 100644 tests/strider_test.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 0d20b64..a04470f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ *.pyc +.cache/ +.coverage +.tox/ +dist/ +*.egg-info +*.swp diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100644 index 0000000..49ec960 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,2 @@ +pytest +coverage diff --git a/tests/strider_test.py b/tests/strider_test.py new file mode 100644 index 0000000..dab3a10 --- /dev/null +++ b/tests/strider_test.py @@ -0,0 +1,4 @@ +import pytest + +def test_strider(): + pass diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..766c9c5 --- /dev/null +++ b/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = py27 + +[testenv] +setenv = + PYTHONDONTWRITEBYTECODE = 1 +commands = coverage run --source=lib -m py.test + coverage report +deps=-rrequirements_test.txt + +[pytest] +addopts = tests +pep8maxlinelength = 120