Skip to content

Commit

Permalink
Merge pull request #196 from gvlproject/improve_cleanup
Browse files Browse the repository at this point in the history
Improve cleanup
  • Loading branch information
nuwang committed May 7, 2020
2 parents e699556 + f12a74d commit bc7f4b4
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 28 deletions.
65 changes: 38 additions & 27 deletions django-cloudlaunch/cloudlaunch/backend_plugins/base_vm_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,8 +315,14 @@ def deploy(self, name, task, app_config, provider_config):
'instance').get('id')

if app_config.get('config_appliance'):
c_result = self._configure_host(name, task, app_config,
provider_config)
try:
c_result = self._configure_host(name, task, app_config,
provider_config)
except Exception:
if host_config.get('instance_id'):
provider = provider_config.get('cloud_provider')
provider.compute.instances.delete(host_config['instance_id'])
raise
# Merge result dicts; right-most dict keys take precedence
return {'cloudLaunch': {**p_result.get('cloudLaunch', {}),
**c_result.get('cloudLaunch', {})}}
Expand Down Expand Up @@ -382,31 +388,36 @@ def _provision_host(self, name, task, app_config, provider_config):
task.update_state(state="PROGRESSING",
meta={"action": "Waiting for instance %s" % inst.id})
log.debug("Waiting for instance {0} to be ready...".format(inst.id))
inst.wait_till_ready()
static_ip = cloudlaunch_config.get('staticIP')
if static_ip:
task.update_state(state='PROGRESSING',
meta={'action': "Assigning requested floating "
"IP: %s" % static_ip})
inst.add_floating_ip(static_ip)
inst.refresh()
results = {}
results['keyPair'] = {'id': kp.id, 'name': kp.name,
'material': kp.material}
# FIXME: this does not account for multiple VM fw and expects one
results['securityGroup'] = {'id': vmfl[0].id, 'name': vmfl[0].name}
results['instance'] = {'id': inst.id}
# Support for legacy NeCTAR
results['publicIP'] = self._attach_public_ip(
provider, inst, subnet.network_id if subnet else None)
# Configure hostname (if set)
results['hostname'] = self._configure_hostname(
provider, results['publicIP'], cloudlaunch_config.get('hostnameConfig'))
task.update_state(
state='PROGRESSING',
meta={"action": "Instance created successfully. " +
"Public IP: %s" % results.get('publicIP') or ""})
return {"cloudLaunch": results}
try:
inst.wait_till_ready()
static_ip = cloudlaunch_config.get('staticIP')
if static_ip:
task.update_state(state='PROGRESSING',
meta={'action': "Assigning requested floating "
"IP: %s" % static_ip})
inst.add_floating_ip(static_ip)
inst.refresh()
results = {}
results['keyPair'] = {'id': kp.id, 'name': kp.name,
'material': kp.material}
# FIXME: this does not account for multiple VM fw and expects one
if vmfl:
results['securityGroup'] = {'id': vmfl[0].id, 'name': vmfl[0].name}
results['instance'] = {'id': inst.id}
# Support for legacy NeCTAR
results['publicIP'] = self._attach_public_ip(
provider, inst, subnet.network_id if subnet else None)
# Configure hostname (if set)
results['hostname'] = self._configure_hostname(
provider, results['publicIP'], cloudlaunch_config.get('hostnameConfig'))
task.update_state(
state='PROGRESSING',
meta={"action": "Instance created successfully. " +
"Public IP: %s" % results.get('publicIP') or ""})
return {"cloudLaunch": results}
except Exception:
inst.delete()
raise

def _configure_hostname(self, provider, public_ip, hostname_config):
if not hostname_config:
Expand Down
2 changes: 1 addition & 1 deletion django-cloudlaunch/cloudlaunch/configurers.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def _run_playbook(self, playbook, inventory, host, pk, user='ubuntu',
cmd += ["--extra-vars", "{0}=\"{1}\"".format(pev[0], pev[1])]
# TODO: Sanitize before printing
log.debug("Running Ansible with command %s", " ".join(cmd))
process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
universal_newlines=True, cwd=repo_path)
output_buffer = ""
while process.poll() is None:
Expand Down
205 changes: 205 additions & 0 deletions django-cloudlaunch/cloudlaunch/tests/test_launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from unittest.mock import patch
import yaml

from django.contrib.auth.models import User
from django.urls import reverse
from djcloudbridge import models as cb_models
from rest_framework.test import APILiveServerTestCase

from cloudlaunch.models import (
Application,
ApplicationDeployment,
ApplicationVersion,
ApplicationVersionCloudConfig,
ApplicationDeploymentTask,
CloudDeploymentTarget,
Image)


class CLLaunchTestBase(APILiveServerTestCase):

def setUp(self):
self.user = User.objects.create(username='test-user')
self.client.force_authenticate(user=self.user)

def create_mock_provider(self, name, config):
provider_class = self.get_provider_class("mock")
return provider_class(config)

patcher2 = patch('cloudbridge.factory.CloudProviderFactory.create_provider',
new=create_mock_provider)
patcher2.start()
self.addCleanup(patcher2.stop)

patcher3 = patch('cloudlaunch.configurers.SSHBasedConfigurer._check_ssh')
patcher3.start()
self.addCleanup(patcher3.stop)

patcher4 = patch('cloudlaunch.configurers.AnsibleAppConfigurer.configure')
patcher4.start()
self.addCleanup(patcher4.stop)

# Patch some background celery tasks to reduce noise in the logs.
# They don't really affect the tests
patcher_update_task = patch('cloudlaunch.tasks.update_status_task')
patcher_update_task.start()
self.addCleanup(patcher_update_task.stop)
patcher_migrate_task = patch('cloudlaunch.tasks.migrate_launch_task')
patcher_migrate_task.start()
self.addCleanup(patcher_migrate_task.stop)
patcher_migrate_result = patch('cloudlaunch.tasks.migrate_task_result')
patcher_migrate_result.start()
self.addCleanup(patcher_migrate_result.stop)

super().setUp()

def assertResponse(self, response, status=None, data_contains=None):
if status:
self.assertEqual(response.status_code, status)
if data_contains:
self.assertDictContains(response.data, data_contains)

def assertDictContains(self, dict1, dict2):
for key in dict2:
self.assertTrue(key in dict1)
if isinstance(dict2[key], dict):
self.assertDictContains(dict1[key], dict2[key])
else:
self.assertEqual(dict1[key], dict2[key])


class ApplicationLaunchTests(CLLaunchTestBase):

DEFAULT_LAUNCH_CONFIG = {
'foo': 1,
'bar': 2,
}
DEFAULT_APP_CONFIG = {
'bar': 3,
'baz': 4,
'config_cloudlaunch': {
'instance_user_data': "userdata"
}
}

def _create_application_version(self):
application = Application.objects.create(
name="Ubuntu",
status=Application.LIVE,
)
application_version = ApplicationVersion.objects.create(
application=application,
version="16.04",
frontend_component_path="app/marketplace/plugins/plugins.module"
"#PluginsModule",
frontend_component_name="ubuntu-config",
backend_component_name="cloudlaunch.backend_plugins.base_vm_app"
".BaseVMAppPlugin",
)
return application_version

def _create_cloud_region_zone(self):
cloud = cb_models.AWSCloud.objects.create(
name='AWS'
)
region = cb_models.AWSRegion.objects.create(
cloud=cloud,
name='us-east-1'
)
zone = cb_models.Zone.objects.create(
region=region,
name='us-east-1a'
)
return zone

def _create_credentials(self, target_cloud):
user_profile = cb_models.UserProfile.objects.get(user=self.user)
return cb_models.AWSCredentials.objects.create(
cloud=target_cloud,
aws_access_key='access_key',
aws_secret_key='secret_key',
user_profile=user_profile,
default=True,
)

def _create_image(self, target_region):
return Image.objects.create(
image_id='abc123',
region=target_region,
)

def _create_app_version_cloud_config(self,
application_version,
target,
image,
launch_config=DEFAULT_LAUNCH_CONFIG):
return ApplicationVersionCloudConfig.objects.create(
application_version=application_version,
target=target,
image=image,
default_launch_config=yaml.safe_dump(launch_config),
)

def setUp(self):
super().setUp()
# Create test data
self.application_version = self._create_application_version()
self.target_zone = self._create_cloud_region_zone()
self.target_region = self.target_zone.region
self.target_cloud = self.target_region.cloud
self.ubuntu_image = self._create_image(self.target_region)
self.credentials = self._create_credentials(self.target_cloud)
self.credentials = self._create_credentials(self.target_cloud)
self.deployment_target = CloudDeploymentTarget.objects.get(
target_zone=self.target_zone)
self.app_version_cloud_config = self._create_app_version_cloud_config(
self.application_version, self.deployment_target, self.ubuntu_image)

def _create_deployment(self):
"""Create deployment from 'application' and 'application_version'."""
return self.client.post(reverse('deployments-list'), {
'name': 'test-deployment',
'application': self.application_version.application.slug,
'application_version': self.application_version.version,
'deployment_target_id': self.deployment_target.id,
})

def test_create_deployment(self):
with patch('cloudbridge.providers.aws.services.AWSInstanceService.delete') as mock_del:
mock_del.get.assert_not_called()
response = self._create_deployment()
self.assertResponse(response, status=201, data_contains={
'name': 'test-deployment',
'application_version': self.application_version.id,
'deployment_target': {
'id': self.deployment_target.id,
'target_zone': {
'zone_id': self.target_zone.name
}
},
'application_config': self.DEFAULT_LAUNCH_CONFIG,
'app_version_details': {
'version': self.application_version.version,
'application': {
'slug': self.application_version.application.slug,
}
},
})
# Check that deployment and its LAUNCH task were created
app_deployment = ApplicationDeployment.objects.get()
launch_task = ApplicationDeploymentTask.objects.get(
action=ApplicationDeploymentTask.LAUNCH,
deployment=app_deployment)
self.assertIsNotNone(launch_task)

def test_launch_error_triggers_cleanup(self):
"""
Checks whether an error during launch triggers a cleanup of the instance.
"""
with patch('cloudbridge.base.resources.BaseInstance.wait_till_ready') as mock_wait:
mock_wait.side_effect = Exception("Some exception occurred while waiting")
with patch('cloudbridge.providers.aws.services.AWSInstanceService.delete') as mock_del:
with self.assertRaises(Exception) as ex:
self.test_create_deployment()
mock_del.get.assert_called_once()
self.assertContains(ex, "Some exception occurred while waiting")
1 change: 1 addition & 0 deletions django-cloudlaunch/cloudlaunchserver/celeryconfig_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
result_serializer = 'json'
task_serializer = 'json'
accept_content = ['json']
task_always_eager = True
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ deps =
[testenv]
commands = {envpython} -m coverage run --source django-cloudlaunch --branch django-cloudlaunch/manage.py test django-cloudlaunch
setenv =
CELERY_CONFIG_MODULE=cloudlaunchserver.celeryconfig_test
DJANGO_SETTINGS_MODULE=cloudlaunchserver.settings_test
# Fix for import issue: https://github.com/travis-ci/travis-ci/issues/7940
BOTO_CONFIG=/dev/null
passenv =
Expand Down

0 comments on commit bc7f4b4

Please sign in to comment.