Add resource for Rackspace Cloud Servers.
Blueprint rackspace-cloud-servers-provider Change-Id: Ie6659e0e8b519180ce5973cc798c914b56a95426
This commit is contained in:
parent
1bf9014cff
commit
2684f2bb4c
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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 {}
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue