Add resource for Rackspace Cloud Servers.

Blueprint rackspace-cloud-servers-provider

Change-Id: Ie6659e0e8b519180ce5973cc798c914b56a95426
This commit is contained in:
Jason Dunsmore 2013-07-22 11:00:51 -05:00
parent 1bf9014cff
commit 2684f2bb4c
8 changed files with 836 additions and 4 deletions

View File

@ -85,6 +85,9 @@ def remote_error(ex):
'StackValidationFailed': exc.HTTPBadRequest,
'InvalidTemplateReference': exc.HTTPBadRequest,
'UnknownUserParameter': exc.HTTPBadRequest,
'RevertFailed': exc.HTTPInternalServerError,
'ServerBuildFailed': exc.HTTPInternalServerError,
'NotSupported': exc.HTTPBadRequest,
'MissingCredentialError': exc.HTTPBadRequest,
'UserParameterMissing': exc.HTTPBadRequest,
}

View File

@ -270,3 +270,7 @@ class ResourceFailure(OpenstackException):
exc_type = type(exception).__name__
super(ResourceFailure, self).__init__(exc_type=exc_type,
message=str(exception))
class NotSupported(OpenstackException):
message = _("%(feature)s is not supported.")

View File

@ -380,10 +380,17 @@ class Instance(resource.Resource):
self._set_ipaddress(server.networks)
volume_attach.start()
return volume_attach.done()
elif server.status == 'ERROR':
delete = scheduler.TaskRunner(self._delete_server, server)
delete(wait_time=0.2)
exc = exception.Error("Build of server %s failed." %
server.name)
raise exception.ResourceFailure(exc)
else:
raise exception.Error('%s instance[%s] status[%s]' %
exc = exception.Error('%s instance[%s] status[%s]' %
('nova reported unexpected',
self.name, server.status))
raise exception.ResourceFailure(exc)
else:
return volume_attach.step()

View File

@ -0,0 +1,409 @@
# 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 tempfile
import json
import paramiko
from Crypto.PublicKey import RSA
import novaclient.exceptions as novaexception
from heat.common import exception
from heat.openstack.common import log as logging
from heat.engine import scheduler
from heat.engine.resources import instance
from heat.engine.resources.rackspace import rackspace_resource
from heat.db.sqlalchemy import api as db_api
logger = logging.getLogger(__name__)
class CloudServer(instance.Instance):
"""Resource for Rackspace Cloud Servers."""
properties_schema = {'ServerName': {'Type': 'String', 'Required': True},
'Flavor': {'Type': 'String', 'Required': True},
'ImageName': {'Type': 'String', 'Required': True},
'UserData': {'Type': 'String'},
'PublicKey': {'Type': 'String'},
'Volumes': {'Type': 'List'}}
attributes_schema = {'PrivateDnsName': ('Private DNS name of the specified'
' instance.'),
'PublicDnsName': ('Public DNS name of the specified '
'instance.'),
'PrivateIp': ('Private IP address of the specified '
'instance.'),
'PublicIp': ('Public IP address of the specified '
'instance.')}
base_script = """#!/bin/bash
# Install cloud-init and heat-cfntools
%s
# Create data source for cloud-init
mkdir -p /var/lib/cloud/seed/nocloud-net
mv /tmp/userdata /var/lib/cloud/seed/nocloud-net/user-data
touch /var/lib/cloud/seed/nocloud-net/meta-data
chmod 600 /var/lib/cloud/seed/nocloud-net/*
# Run cloud-init & cfn-init
cloud-init start
bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1
"""
ubuntu_script = base_script % """\
apt-get update
apt-get install -y cloud-init python-boto python-pip gcc python-dev
pip install heat-cfntools
"""
fedora_script = base_script % """\
yum install -y cloud-init python-boto python-pip gcc python-devel
pip-python install heat-cfntools
"""
# TODO(jason): Install cloud-init & other deps from third-party repos
centos_script = base_script % """\
yum install -y cloud-init python-boto python-pip gcc python-devel
pip-python install heat-cfntools
"""
# TODO(jason): Install cloud-init & other deps from third-party repos
arch_script = base_script % """\
pacman -S --noconfirm python-pip gcc
"""
# TODO(jason): Install cloud-init & other deps from third-party repos
gentoo_script = base_script % """\
emerge cloud-init python-boto python-pip gcc python-devel
"""
# TODO(jason): Install cloud-init & other deps from third-party repos
opensuse_script = base_script % """\
zypper --non-interactive rm patterns-openSUSE-minimal_base-conflicts
zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
"""
# List of supported Linux distros and their corresponding config scripts
image_scripts = {'arch': None,
'centos': None,
'debian': None,
'fedora': fedora_script,
'gentoo': None,
'opensuse': None,
'rhel': None,
'ubuntu': ubuntu_script}
# Cache data retrieved from APIs in class attributes
_image_id_map = {}
_distro_map = {}
_server_map = {}
# Template keys supported for handle_update. Properties not
# listed here trigger an UpdateReplace
update_allowed_keys = ('Metadata', 'Properties')
update_allowed_properties = ('Flavor', 'ServerName')
def __init__(self, name, json_snippet, stack):
super(CloudServer, self).__init__(name, json_snippet, stack)
self._private_key = None
self.rs = rackspace_resource.RackspaceResource(name,
json_snippet,
stack)
def nova(self):
return self.rs.nova() # Override the Instance method
def cinder(self):
return self.rs.cinder()
@property
def server(self):
"""Get the Cloud Server object."""
if self.resource_id in self.__class__._server_map:
return self.__class__._server_map[self.resource_id]
else:
server = self.nova().servers.get(self.resource_id)
self.__class__._server_map[self.resource_id] = server
return server
@property
def image_id(self):
"""Get the image ID corresponding to the ImageName property."""
image_name = self.properties['ImageName']
if image_name in self.__class__._image_id_map:
return self.__class__._image_id_map[image_name]
else:
image_id = self._get_image_id(image_name)
self.__class__._image_id_map[image_name] = image_id
return image_id
@property
def distro(self):
"""Get the Linux distribution for this server."""
if self.image_id in self.__class__._distro_map:
return self.__class__._distro_map[self.image_id]
else:
image = self.nova().images.get(self.image_id)
distro = image.metadata['os_distro']
self.__class__._distro_map[self.image_id] = distro
return distro
@property
def script(self):
"""Get the config script for the Cloud Server image."""
return self.image_scripts[self.distro]
@property
def flavors(self):
"""Get the flavors from the API or cache (updated every 6 hours)."""
return [flavor.id for flavor in self.nova().flavors.list()]
@property
def private_key(self):
"""Return the private SSH key for the resource."""
if self._private_key:
return self._private_key
if self.id is not None:
private_key = db_api.resource_data_get(self, 'private_key')
if not private_key:
return None
self._private_key = private_key
return private_key
@private_key.setter
def private_key(self, private_key):
"""Save the resource's private SSH key to the database."""
self._private_key = private_key
if self.id is not None:
db_api.resource_data_set(self, 'private_key', private_key, True)
def _get_ip(self, ip_type):
"""Return the IP of the Cloud Server."""
def ip_not_found():
exc = exception.Error("Could not determine the %s IP of %s." %
(ip_type, self.properties['ImageName']))
raise exception.ResourceFailure(exc)
if ip_type not in self.server.addresses:
ip_not_found()
for ip in self.server.addresses[ip_type]:
if ip['version'] == 4:
return ip['addr']
ip_not_found()
@property
def public_ip(self):
"""Return the public IP of the Cloud Server."""
return self._get_ip('public')
@property
def private_ip(self):
"""Return the private IP of the Cloud Server."""
try:
return self._get_ip('private')
except exception.ResourceFailure as ex:
logger.info(ex.message)
def validate(self):
"""Validate user parameters."""
if self.properties['Flavor'] not in self.flavors:
return {'Error': "Flavor not found."}
if not self.script:
return {'Error': "Image %s not supported." %
self.properties['ImageName']}
def _run_ssh_command(self, command):
"""Run a shell command on the Cloud Server via SSH."""
with tempfile.NamedTemporaryFile() as private_key_file:
private_key_file.write(self.private_key)
private_key_file.seek(0)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy())
ssh.connect(self.public_ip,
username="root",
key_filename=private_key_file.name)
stdin, stdout, stderr = ssh.exec_command(command)
logger.debug(stdout.read())
logger.debug(stderr.read())
def _sftp_files(self, files):
"""Transfer files to the Cloud Server via SFTP."""
with tempfile.NamedTemporaryFile() as private_key_file:
private_key_file.write(self.private_key)
private_key_file.seek(0)
pkey = paramiko.RSAKey.from_private_key_file(private_key_file.name)
transport = paramiko.Transport((self.public_ip, 22))
transport.connect(hostkey=None, username="root", pkey=pkey)
sftp = paramiko.SFTPClient.from_transport(transport)
for remote_file in files:
sftp_file = sftp.open(remote_file['path'], 'w')
sftp_file.write(remote_file['data'])
sftp_file.close()
def handle_create(self):
"""Create a Rackspace Cloud Servers container.
Rackspace Cloud Servers does not have the metadata service
running, so we have to transfer the user-data file to the
server and then trigger cloud-init.
"""
# Retrieve server creation parameters from properties
flavor = self.properties['Flavor']
user_public_key = self.properties['PublicKey'] or ''
# Generate SSH public/private keypair
rsa = RSA.generate(1024)
self.private_key = rsa.exportKey()
public_key = rsa.publickey().exportKey('OpenSSH')
public_keys = public_key + "\n" + user_public_key
personality_files = {"/root/.ssh/authorized_keys": public_keys}
# Create server
client = self.nova().servers
server = client.create(self.properties['ServerName'],
self.image_id,
flavor,
files=personality_files)
# Save resource ID to db
self.resource_id_set(server.id)
return server, scheduler.TaskRunner(self._attach_volumes_task())
def _attach_volumes_task(self):
tasks = (scheduler.TaskRunner(self._attach_volume, volume_id, device)
for volume_id, device in self.volumes())
return scheduler.PollingTaskGroup(tasks)
def _attach_volume(self, volume_id, device):
self.nova().volumes.create_server_volume(self.server.id,
volume_id,
device or None)
yield
volume = self.cinder().get(volume_id)
while volume.status in ('available', 'attaching'):
yield
volume.get()
if volume.status != 'in-use':
raise exception.Error(volume.status)
def _detach_volumes_task(self):
tasks = (scheduler.TaskRunner(self._detach_volume, volume_id)
for volume_id, device in self.volumes())
return scheduler.PollingTaskGroup(tasks)
def _detach_volume(self, volume_id):
volume = self.cinder().get(volume_id)
volume.detach()
yield
while volume.status in ('in-use', 'detaching'):
yield
volume.get()
if volume.status != 'available':
raise exception.Error(volume.status)
def check_create_complete(self, cookie):
"""Check if server creation is complete and handle server configs."""
if not self._check_active(cookie):
return False
# Create heat-script and userdata files on server
raw_userdata = self.properties['UserData'] or ''
userdata = self._build_userdata(raw_userdata)
files = [{'path': "/tmp/userdata", 'data': userdata},
{'path': "/root/heat-script.sh", 'data': self.script}]
self._sftp_files(files)
# Connect via SSH and run script
command = "bash -ex /root/heat-script.sh > /root/heat-script.log 2>&1"
self._run_ssh_command(command)
return True
# TODO(jason): Make this consistent with Instance and inherit
def _delete_server(self, server):
"""Return a coroutine that deletes the Cloud Server."""
server.delete()
while True:
yield
try:
server.get()
if server.status == "ERROR":
exc = exception.Error("Deletion of server %s failed." %
server.name)
raise exception.ResourceFailure(exc)
except novaexception.NotFound:
break
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
"""Try to update a Cloud Server's parameters.
If the Cloud Server's Metadata or Flavor changed, update the
Cloud Server. If any other parameters changed, re-create the
Cloud Server with the new parameters.
"""
if 'Metadata' in tmpl_diff:
self.metadata = json_snippet['Metadata']
metadata_string = json.dumps(self.metadata)
files = [{'path': "/var/cache/heat-cfntools/last_metadata",
'data': metadata_string}]
self._sftp_files(files)
command = "bash -x /var/lib/cloud/data/cfn-userdata > " + \
"/root/cfn-userdata.log 2>&1"
self._run_ssh_command(command)
if 'Flavor' in prop_diff:
self.flavor = json_snippet['Properties']['Flavor']
self.server.resize(self.flavor)
resize = scheduler.TaskRunner(self._check_resize,
self.server,
self.flavor)
resize(wait_time=1.0)
# If ServerName is the only update, fail update
if prop_diff.keys() == ['ServerName'] and \
tmpl_diff.keys() == ['Properties']:
raise exception.NotSupported(feature="Cloud Server rename")
# Other updates were successful, so don't cause update to fail
elif 'ServerName' in prop_diff:
logger.info("Cloud Server rename not supported.")
return True
def _resolve_attribute(self, key):
"""Return the method that provides a given template attribute."""
attribute_function = {'PublicIp': self.public_ip,
'PrivateIp': self.private_ip,
'PublicDnsName': self.public_ip,
'PrivateDnsName': self.public_ip}
if key not in attribute_function:
raise exception.InvalidTemplateAttribute(resource=self.name,
key=key)
function = attribute_function[key]
logger.info('%s._resolve_attribute(%s) == %s'
% (self.name, key, function))
return unicode(function)
# pyrax module is required to work with Rackspace cloud server provider.
# If it is not installed, don't register cloud server provider
def resource_mapping():
if rackspace_resource.PYRAX_INSTALLED:
return {'Rackspace::Cloud::Server': CloudServer}
else:
return {}

View File

@ -139,7 +139,7 @@ class InstancesTest(HeatTestCase):
expected_ip = return_server.networks['public'][0]
self.assertEqual(instance.FnGetAtt('PublicIp'), expected_ip)
self.assertEqual(instance.FnGetAtt('PrivateIp'), expected_ip)
self.assertEqual(instance.FnGetAtt('PrivateDnsName'), expected_ip)
self.assertEqual(instance.FnGetAtt('PublicDnsName'), expected_ip)
self.assertEqual(instance.FnGetAtt('PrivateDnsName'), expected_ip)
self.m.VerifyAll()

View File

@ -0,0 +1,409 @@
# 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 copy
import mox
import paramiko
import novaclient
from heat.db import api as db_api
from heat.tests.v1_1 import fakes
from heat.common import template_format
from heat.common import exception
from heat.engine import parser
from heat.engine import resource
from heat.engine import scheduler
from heat.engine import environment
from heat.engine.resources.rackspace import cloud_server
from heat.engine.resources.rackspace import rackspace_resource
from heat.openstack.common import uuidutils
from heat.tests.common import HeatTestCase
from heat.tests.utils import setup_dummy_db
wp_template = '''
{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "WordPress",
"Parameters" : {
"Flavor" : {
"Description" : "Rackspace Cloud Server flavor",
"Type" : "String",
"Default" : "2",
"AllowedValues" : [ "2", "3", "4", "5", "6", "7", "8" ],
"ConstraintDescription" : "must be a valid Rackspace Cloud Server flavor"
},
},
"Resources" : {
"WebServer": {
"Type": "Rackspace::Cloud::Server",
"Properties": {
"ImageName" : "Fedora 17 (Beefy Miracle)",
"ServerName" : "Heat test",
"Flavor" : "2",
"UserData" : "wordpress"
}
}
}
}
'''
class RackspaceCloudServerTest(HeatTestCase):
def setUp(self):
super(RackspaceCloudServerTest, self).setUp()
self.fc = fakes.FakeClient()
setup_dummy_db()
# Test environment may not have pyrax client library installed and if
# pyrax is not installed resource class would not be registered.
# So register resource provider class explicitly for unit testing.
resource._register_class("Rackspace::Cloud::Server",
cloud_server.CloudServer)
f2 = self.m.CreateMockAnything()
f2.id = '2'
f3 = self.m.CreateMockAnything()
f3.id = '3'
f4 = self.m.CreateMockAnything()
f4.id = '4'
f5 = self.m.CreateMockAnything()
f5.id = '5'
f6 = self.m.CreateMockAnything()
f6.id = '6'
f7 = self.m.CreateMockAnything()
f7.id = '7'
f8 = self.m.CreateMockAnything()
f8.id = '8'
self.flavors = [f2, f3, f4, f5, f6, f7, f8]
def _setup_test_stack(self, stack_name):
t = template_format.parse(wp_template)
template = parser.Template(t)
stack = parser.Stack(None, stack_name, template,
environment.Environment({'Flavor': '2'}),
stack_id=uuidutils.generate_uuid())
return (t, stack)
def _mock_ssh_sftp(self):
# SSH
self.m.StubOutWithMock(paramiko, "SSHClient")
self.m.StubOutWithMock(paramiko, "MissingHostKeyPolicy")
ssh = self.m.CreateMockAnything()
paramiko.SSHClient().AndReturn(ssh)
paramiko.MissingHostKeyPolicy()
ssh.set_missing_host_key_policy(None)
ssh.connect(mox.IgnoreArg(),
key_filename=mox.IgnoreArg(),
username='root')
stdin = self.m.CreateMockAnything()
stdout = self.m.CreateMockAnything()
stderr = self.m.CreateMockAnything()
stdout.read().AndReturn("stdout")
stderr.read().AndReturn("stderr")
ssh.exec_command(mox.IgnoreArg()).AndReturn((stdin, stdout, stderr))
# SFTP
self.m.StubOutWithMock(paramiko, "Transport")
transport = self.m.CreateMockAnything()
paramiko.Transport((mox.IgnoreArg(), 22)).AndReturn(transport)
transport.connect(hostkey=None, username="root", pkey=mox.IgnoreArg())
sftp = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko, "SFTPClient")
paramiko.SFTPClient.from_transport(transport).AndReturn(sftp)
sftp_file = self.m.CreateMockAnything()
sftp.open(mox.IgnoreArg(), 'w').AndReturn(sftp_file)
sftp_file.write(mox.IgnoreArg())
sftp_file.close()
sftp_file = self.m.CreateMockAnything()
sftp.open(mox.IgnoreArg(), 'w').AndReturn(sftp_file)
sftp_file.write(mox.IgnoreArg())
sftp_file.close()
def _setup_test_cs(self, return_server, name):
stack_name = '%s_stack' % name
(t, stack) = self._setup_test_stack(stack_name)
server_name = "Heat test"
t['Resources']['WebServer']['Properties']['ServerName'] = server_name
cs_name = 'Fedora 17 (Beefy Miracle)'
t['Resources']['WebServer']['Properties']['ImageName'] = cs_name
t['Resources']['WebServer']['Properties']['Flavor'] = '2'
cs = cloud_server.CloudServer('%s_name' % name,
t['Resources']['WebServer'], stack)
cs.t = cs.stack.resolve_runtime_data(cs.t)
flavor = t['Resources']['WebServer']['Properties']['Flavor']
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(server_name, "1", flavor,
files=mox.IgnoreArg()).AndReturn(return_server)
return_server.adminPass = "foobar"
self.m.StubOutWithMock(cloud_server.CloudServer, 'image_id')
cloud_server.CloudServer.image_id = "1"
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
self.m.StubOutWithMock(rackspace_resource.RackspaceResource, "nova")
rackspace_resource.RackspaceResource.nova().MultipleTimes()\
.AndReturn(self.fc)
self._mock_ssh_sftp()
return cs
def _create_test_cs(self, return_server, name):
cs = self._setup_test_cs(return_server, name)
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
return cs
def _update_test_cs(self, return_server, name):
self._mock_ssh_sftp()
self.m.StubOutWithMock(rackspace_resource.RackspaceResource, "nova")
rackspace_resource.RackspaceResource.nova().MultipleTimes()\
.AndReturn(self.fc)
def test_cs_create(self):
return_server = self.fc.servers.list()[1]
cs = self._create_test_cs(return_server, 'test_cs_create')
# this makes sure the auto increment worked on cloud server creation
self.assertTrue(cs.id > 0)
expected_public = return_server.networks['public'][0]
expected_private = return_server.networks['private'][0]
self.assertEqual(cs.FnGetAtt('PublicIp'), expected_public)
self.assertEqual(cs.FnGetAtt('PrivateIp'), expected_private)
self.assertEqual(cs.FnGetAtt('PublicDnsName'), expected_public)
self.assertEqual(cs.FnGetAtt('PrivateDnsName'), expected_public)
self.m.VerifyAll()
def test_cs_create_with_image_name(self):
return_server = self.fc.servers.list()[1]
cs = self._setup_test_cs(return_server, 'test_cs_create_image_id')
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
# this makes sure the auto increment worked on cloud server creation
self.assertTrue(cs.id > 0)
expected_public = return_server.networks['public'][0]
expected_private = return_server.networks['private'][0]
self.assertEqual(cs.FnGetAtt('PublicIp'), expected_public)
self.assertEqual(cs.FnGetAtt('PrivateIp'), expected_private)
self.assertEqual(cs.FnGetAtt('PublicDnsName'), expected_public)
self.assertEqual(cs.FnGetAtt('PrivateDnsName'), expected_public)
self.assertRaises(exception.InvalidTemplateAttribute,
cs.FnGetAtt, 'foo')
self.m.VerifyAll()
def test_cs_create_image_name_err(self):
stack_name = 'test_cs_create_image_name_err_stack'
(t, stack) = self._setup_test_stack(stack_name)
# create a cloud server with non exist image name
t['Resources']['WebServer']['Properties']['ImageName'] = 'Slackware'
# Mock flavors
self.m.StubOutWithMock(cloud_server.CloudServer, "flavors")
cloud_server.CloudServer.flavors.__contains__('2').AndReturn(True)
cloud_server.CloudServer.script = None
self.m.ReplayAll()
cs = cloud_server.CloudServer('cs_create_image_err',
t['Resources']['WebServer'], stack)
self.assertEqual({'Error': "Image %s not supported." % 'Slackware'},
cs.validate())
self.m.VerifyAll()
def test_cs_create_flavor_err(self):
"""validate() should throw an if the Flavor is invalid."""
stack_name = 'test_cs_create_flavor_err_stack'
(t, stack) = self._setup_test_stack(stack_name)
# create a cloud server with non exist image name
t['Resources']['WebServer']['Properties']['Flavor'] = '1'
# Mock flavors
self.m.StubOutWithMock(cloud_server.CloudServer, "flavors")
flavors = ['2', '3', '4', '5', '6', '7', '8']
cloud_server.CloudServer.flavors = flavors
self.m.ReplayAll()
cs = cloud_server.CloudServer('cs_create_flavor_err',
t['Resources']['WebServer'], stack)
self.assertEqual({'Error': "Flavor not found."}, cs.validate())
self.m.VerifyAll()
def test_cs_create_delete(self):
return_server = self.fc.servers.list()[1]
cs = self._create_test_cs(return_server,
'test_cs_create_delete')
cs.resource_id = 1234
# this makes sure the auto-increment worked on cloud server creation
self.assertTrue(cs.id > 0)
self.m.StubOutWithMock(self.fc.client, 'get_servers_1234')
get = self.fc.client.get_servers_1234
get().AndRaise(novaclient.exceptions.NotFound(404))
mox.Replay(get)
cs.delete()
self.assertTrue(cs.resource_id is None)
self.assertEqual(cs.state, (cs.DELETE, cs.COMPLETE))
self.m.VerifyAll()
def test_cs_update_metadata(self):
return_server = self.fc.servers.list()[1]
cs = self._create_test_cs(return_server, 'test_cs_metadata_update')
self.m.UnsetStubs()
self._update_test_cs(return_server, 'test_cs_metadata_update')
self.m.ReplayAll()
update_template = copy.deepcopy(cs.t)
update_template['Metadata'] = {'test': 123}
self.assertEqual(None, cs.update(update_template))
self.assertEqual(cs.metadata, {'test': 123})
def test_cs_update_replace(self):
return_server = self.fc.servers.list()[1]
cs = self._create_test_cs(return_server, 'test_cs_update')
update_template = copy.deepcopy(cs.t)
update_template['Notallowed'] = {'test': 123}
self.assertRaises(resource.UpdateReplace, cs.update, update_template)
def test_cs_update_properties(self):
return_server = self.fc.servers.list()[1]
cs = self._create_test_cs(return_server, 'test_cs_update')
update_template = copy.deepcopy(cs.t)
update_template['Properties']['UserData'] = 'mustreplace'
self.assertRaises(resource.UpdateReplace,
cs.update, update_template)
def test_cs_status_build(self):
return_server = self.fc.servers.list()[0]
cs = self._setup_test_cs(return_server, 'test_cs_status_build')
cs.resource_id = 1234
# Bind fake get method which cs.check_create_complete will call
def activate_status(server):
server.status = 'ACTIVE'
return_server.get = activate_status.__get__(return_server)
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
self.assertEqual(cs.state, (cs.CREATE, cs.COMPLETE))
def test_cs_status_hard_reboot(self):
self._test_cs_status_not_build_active('HARD_REBOOT')
def test_cs_status_password(self):
self._test_cs_status_not_build_active('PASSWORD')
def test_cs_status_reboot(self):
self._test_cs_status_not_build_active('REBOOT')
def test_cs_status_rescue(self):
self._test_cs_status_not_build_active('RESCUE')
def test_cs_status_resize(self):
self._test_cs_status_not_build_active('RESIZE')
def test_cs_status_revert_resize(self):
self._test_cs_status_not_build_active('REVERT_RESIZE')
def test_cs_status_shutoff(self):
self._test_cs_status_not_build_active('SHUTOFF')
def test_cs_status_suspended(self):
self._test_cs_status_not_build_active('SUSPENDED')
def test_cs_status_verify_resize(self):
self._test_cs_status_not_build_active('VERIFY_RESIZE')
def _test_cs_status_not_build_active(self, uncommon_status):
return_server = self.fc.servers.list()[0]
cs = self._setup_test_cs(return_server, 'test_cs_status_build')
cs.resource_id = 1234
# Bind fake get method which cs.check_create_complete will call
def activate_status(server):
if hasattr(server, '_test_check_iterations'):
server._test_check_iterations += 1
else:
server._test_check_iterations = 1
if server._test_check_iterations == 1:
server.status = uncommon_status
if server._test_check_iterations > 2:
server.status = 'ACTIVE'
return_server.get = activate_status.__get__(return_server)
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
self.assertEqual(cs.state, (cs.CREATE, cs.COMPLETE))
self.m.VerifyAll()
def mock_get_ip(self, cs):
self.m.UnsetStubs()
self.m.StubOutWithMock(cloud_server.CloudServer, "server")
cloud_server.CloudServer.server = cs
self.m.ReplayAll()
def test_cs_get_ip(self):
stack_name = 'test_cs_get_ip_err'
(t, stack) = self._setup_test_stack(stack_name)
cs = cloud_server.CloudServer('cs_create_image_err',
t['Resources']['WebServer'],
stack)
cs.addresses = {'public': [{'version': 4, 'addr': '4.5.6.7'},
{'version': 6, 'addr': 'fake:ip::6'}],
'private': [{'version': 4, 'addr': '10.13.12.13'}]}
self.mock_get_ip(cs)
self.assertEqual(cs.public_ip, '4.5.6.7')
self.mock_get_ip(cs)
self.assertEqual(cs.private_ip, '10.13.12.13')
cs.addresses = {'public': [],
'private': []}
self.mock_get_ip(cs)
self.assertRaises(exception.ResourceFailure, cs._get_ip, 'public')
def test_private_key(self):
stack_name = 'test_private_key'
(t, stack) = self._setup_test_stack(stack_name)
cs = cloud_server.CloudServer('cs_private_key',
t['Resources']['WebServer'],
stack)
# This gives the fake cloud server an id and created_time attribute
cs._store_or_update(cs.CREATE, cs.IN_PROGRESS, 'test_store')
cs.private_key = 'fake private key'
rs = db_api.resource_get_by_name_and_stack(None,
'cs_private_key',
stack.id)
encrypted_key = rs.data[0]['value']
self.assertNotEqual(encrypted_key, "fake private key")
decrypted_key = cs.private_key
self.assertEqual(decrypted_key, "fake private key")

View File

@ -1,6 +1,6 @@
d2to1>=0.2.10,<0.3
pbr>=0.5.10,<0.6
PyCrypto>=2.1.0
PyCrypto>=2.6
boto>=2.4.0
eventlet>=0.12.0
greenlet>=0.3.2
@ -22,3 +22,4 @@ python-quantumclient>=2.2.0
python-cinderclient>=1.0.4
PyYAML>=3.1.0
oslo.config>=1.1.0
paramiko>=1.8.0

View File

@ -9,7 +9,6 @@ discover
mox==0.5.3
testtools>=0.9.29
testrepository>=0.0.13
paramiko
python-glanceclient
sphinx>=1.1.2
Babel