Native Nova Server compatibility for Cloud Server

Make the Rackspace Cloud Server resource compatible with the native
Nova Server resource so that a template author can replace
OS::Nova::Server with Rackspace::Cloud::Server, without making any other
changes, to make the resource use Rackspace Cloud Servers instead of
Nova.

Blueprint native-nova-cloud-server
Change-Id: I312ddc25e02843b0116b8a2cb88defef05ca88ac
This commit is contained in:
Jason Dunsmore 2014-01-27 14:51:46 -06:00
parent 8c4adee13f
commit 5a4b14e51b
6 changed files with 685 additions and 721 deletions

View File

@ -11,97 +11,36 @@
# under the License.
import socket
import copy
import tempfile
import json
import paramiko
from Crypto.PublicKey import RSA
import novaclient.exceptions as novaexception
from heat.common import exception
from heat.engine.resources import nova_utils
from heat.engine.resources import server
from heat.db.sqlalchemy import api as db_api
from heat.openstack.common import log as logging
from heat.openstack.common.gettextutils import _
from heat.engine import properties
from heat.engine import scheduler
from heat.engine.resources import instance
from heat.engine.resources import nova_utils
from heat.db.sqlalchemy import api as db_api
try:
import pyrax # noqa
except ImportError:
def resource_mapping():
return {}
else:
def resource_mapping():
return {'Rackspace::Cloud::Server': CloudServer}
logger = logging.getLogger(__name__)
class CloudServer(instance.Instance):
class CloudServer(server.Server):
"""Resource for Rackspace Cloud Servers."""
PROPERTIES = (
FLAVOR, IMAGE, USER_DATA, KEY_NAME, VOLUMES, NAME,
) = (
'flavor', 'image', 'user_data', 'key_name', 'Volumes', 'name',
)
properties_schema = {
FLAVOR: properties.Schema(
properties.Schema.STRING,
required=True,
update_allowed=True
),
IMAGE: properties.Schema(
properties.Schema.STRING,
required=True
),
USER_DATA: properties.Schema(
properties.Schema.STRING
),
KEY_NAME: properties.Schema(
properties.Schema.STRING
),
VOLUMES: properties.Schema(
properties.Schema.LIST,
default=[]
),
NAME: properties.Schema(
properties.Schema.STRING
),
}
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 || cloud-init init
bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
exit 42
"""
# - Ubuntu 12.04: Verified working
ubuntu_script = base_script % """\
SCRIPT_INSTALL_REQUIREMENTS = {
'ubuntu': """
apt-get update
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -o Dpkg::Options::="--force-confdef" -o \
@ -109,22 +48,13 @@ apt-get install -y -o Dpkg::Options::="--force-confdef" -o \
python-dev
pip install heat-cfntools
cfn-create-aws-symlinks --source /usr/local/bin
"""
# - Fedora 17: Verified working
# - Fedora 18: Not working. selinux needs to be in "Permissive"
# mode for cloud-init to work. It's disabled by default in the
# Rackspace Cloud Servers image. To enable selinux, a reboot is
# required.
# - Fedora 19: Verified working
fedora_script = base_script % """\
""",
'fedora': """
yum install -y cloud-init python-boto python-pip gcc python-devel
pip-python install heat-cfntools
cfn-create-aws-symlinks
"""
# - Centos 6.4: Verified working
centos_script = base_script % """\
""",
'centos': """
if ! (yum repolist 2> /dev/null | egrep -q "^[\!\*]?epel ");
then
rpm -ivh http://mirror.rackspace.com/epel/6/i386/epel-release-6-8.noarch.rpm
@ -132,10 +62,8 @@ fi
yum install -y cloud-init python-boto python-pip gcc python-devel \
python-argparse
pip-python install heat-cfntools
"""
# - RHEL 6.4: Verified working
rhel_script = base_script % """\
""",
'rhel': """
if ! (yum repolist 2> /dev/null | egrep -q "^[\!\*]?epel ");
then
rpm -ivh http://mirror.rackspace.com/epel/6/i386/epel-release-6-8.noarch.rpm
@ -146,9 +74,8 @@ yum install -y cloud-init python-boto python-pip gcc python-devel \
python-argparse
pip-python install heat-cfntools
cfn-create-aws-symlinks
"""
debian_script = base_script % """\
""",
'debian': """
echo "deb http://mirror.rackspace.com/debian wheezy-backports main" >> \
/etc/apt/sources.list
apt-get update
@ -157,44 +84,46 @@ export DEBIAN_FRONTEND=noninteractive
apt-get install -y -o Dpkg::Options::="--force-confdef" -o \
Dpkg::Options::="--force-confold" python-pip gcc python-dev
pip install heat-cfntools
"""}
SCRIPT_CREATE_DATA_SOURCE = """
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/*
"""
# - Arch 2013.6: Not working (deps not in default package repos)
# TODO(jason): Install cloud-init & other deps from third-party repos
arch_script = base_script % """\
pacman -S --noconfirm python-pip gcc
SCRIPT_RUN_CLOUD_INIT = """
cloud-init start || cloud-init init
"""
# - Gentoo 13.2: Not working (deps not in default package repos)
# 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
SCRIPT_RUN_CFN_USERDATA = """
bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
exit 42
"""
# - OpenSUSE 12.3: Not working (deps not in default package repos)
# 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
"""
SCRIPT_ERROR_MSG = _("The %(path)s script exited with a non-zero exit "
"status. To see the error message, log into the "
"server and view %(log)s")
# List of supported Linux distros and their corresponding config scripts
image_scripts = {'arch': None,
'centos': centos_script,
'debian': None,
'fedora': fedora_script,
'gentoo': None,
'opensuse': None,
'rhel': rhel_script,
'ubuntu': ubuntu_script}
# Managed Cloud automation statuses
MC_STATUS_IN_PROGRESS = 'In Progress'
MC_STATUS_COMPLETE = 'Complete'
MC_STATUS_BUILD_ERROR = 'Build Error'
script_error_msg = (_("The %(path)s script exited with a non-zero exit "
"status. To see the error message, log into the "
"server and view %(log)s"))
# RackConnect automation statuses
RC_STATUS_DEPLOYING = 'DEPLOYING'
RC_STATUS_DEPLOYED = 'DEPLOYED'
RC_STATUS_FAILED = 'FAILED'
RC_STATUS_UNPROCESSABLE = 'UNPROCESSABLE'
# Template keys supported for handle_update. Properties not
# listed here trigger an UpdateReplace
update_allowed_keys = ('Metadata', 'Properties')
attributes_schema = copy.deepcopy(server.Server.attributes_schema)
attributes_schema.update(
{
'distro': _('The Linux distribution on the server.'),
'privateIPv4': _('The private IPv4 address of the server.'),
}
)
def __init__(self, name, json_snippet, stack):
super(CloudServer, self).__init__(name, json_snippet, stack)
@ -202,59 +131,60 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
self._private_key = None
self._server = None
self._distro = None
self._public_ip = None
self._private_ip = None
self._flavor = None
self._image = None
@property
def server(self):
"""Get the Cloud Server object."""
if not self._server:
logger.debug(_("Calling nova().servers.get()"))
"""Return the Cloud Server object."""
if self._server is None:
self._server = self.nova().servers.get(self.resource_id)
return self._server
@property
def distro(self):
"""Get the Linux distribution for this server."""
if not self._distro:
logger.debug(_("Calling nova().images.get()"))
"""Return the Linux distribution for this server."""
image = self.properties.get(self.IMAGE)
if self._distro is None and image:
image_data = self.nova().images.get(self.image)
self._distro = image_data.metadata['os_distro']
return self._distro
@property
def script(self):
"""Get the config script for the Cloud Server image."""
return self.image_scripts[self.distro]
"""
Return the config script for the Cloud Server image.
@property
def flavor(self):
"""Get the flavors from the API."""
if not self._flavor:
flavor = self.properties[self.FLAVOR]
self._flavor = nova_utils.get_flavor_id(self.nova(), flavor)
return self._flavor
The config script performs the following steps:
1) Install cloud-init
2) Create cloud-init data source
3) Run cloud-init
4) If user_data_format is 'HEAT_CFNTOOLS', run cfn-userdata script
"""
base_script = (self.SCRIPT_INSTALL_REQUIREMENTS[self.distro] +
self.SCRIPT_CREATE_DATA_SOURCE +
self.SCRIPT_RUN_CLOUD_INIT)
userdata_format = self.properties.get(self.USER_DATA_FORMAT)
if userdata_format == 'HEAT_CFNTOOLS':
return base_script + self.SCRIPT_RUN_CFN_USERDATA
elif userdata_format == 'RAW':
return base_script
@property
def image(self):
if not self._image:
self._image = nova_utils.get_image_id(self.nova(),
self.properties[self.IMAGE])
"""Return the server's image ID."""
image = self.properties.get(self.IMAGE)
if image and self._image is None:
self._image = nova_utils.get_image_id(self.nova(), image)
return self._image
@property
def private_key(self):
"""Return the private SSH key for the resource."""
if self._private_key:
if self._private_key is not None:
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
self._private_key = db_api.resource_data_get(self, 'private_key')
return self._private_key
@private_key.setter
def private_key(self, private_key):
@ -263,49 +193,54 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
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."""
if ip_type in self.server.addresses:
for ip in self.server.addresses[ip_type]:
if ip['version'] == 4:
return ip['addr']
raise exception.Error(_("Could not determine the %(ip)s IP of "
"%(image)s.") %
{'ip': ip_type,
'image': self.properties[self.IMAGE]})
@property
def public_ip(self):
"""Return the public IP of the Cloud Server."""
if not self._public_ip:
self._public_ip = self._get_ip('public')
return self._public_ip
@property
def private_ip(self):
"""Return the private IP of the Cloud Server."""
if not self._private_ip:
self._private_ip = self._get_ip('private')
return self._private_ip
@property
def has_userdata(self):
if self.properties[self.USER_DATA] or self.metadata != {}:
"""Return True if the server has user_data, False otherwise."""
user_data = self.properties.get(self.USER_DATA)
if user_data or self.metadata != {}:
return True
else:
return False
def validate(self):
"""Validate user parameters."""
self.flavor
self.image
image = self.properties.get(self.IMAGE)
# It's okay if there's no script, as long as user_data and
# metadata are empty
if not self.script and self.has_userdata:
return {'Error': "user_data/metadata are not supported for image"
" %s." % self.properties[self.IMAGE]}
# metadata are both empty
if image and self.script is None and self.has_userdata:
msg = _("user_data is not supported for image %s.") % image
raise exception.StackValidationFailed(message=msg)
# Validate that the personality does not contain a reserved
# key and that the number of personalities does not exceed the
# Rackspace limit.
personality = self.properties.get(self.PERSONALITY)
if personality:
limits = nova_utils.absolute_limits(self.nova())
# One personality will be used for an SSH key
personality_limit = limits['maxPersonality'] - 1
if "/root/.ssh/authorized_keys" in personality:
msg = _('The personality property may not contain a key '
'of "/root/.ssh/authorized_keys"')
raise exception.StackValidationFailed(message=msg)
elif len(personality) > personality_limit:
msg = _("The personality property may not contain greater "
"than %s entries.") % personality_limit
raise exception.StackValidationFailed(message=msg)
super(CloudServer, self).validate()
# Validate that user_data is passed for servers with bootable
# volumes AFTER validating that the server has either an image
# or a bootable volume in Server.validate()
if not image and self.has_userdata:
msg = _("user_data scripts are not supported with bootable "
"volumes.")
raise exception.StackValidationFailed(message=msg)
def _run_ssh_command(self, command):
"""Run a shell command on the Cloud Server via SSH."""
@ -314,7 +249,7 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
private_key_file.seek(0)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy())
ssh.connect(self.public_ip,
ssh.connect(self.server.accessIPv4,
username="root",
key_filename=private_key_file.name)
chan = ssh.get_transport().open_session()
@ -324,8 +259,8 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
# The channel timeout only works for read/write operations
chan.recv(1024)
except socket.timeout:
raise exception.Error("SSH command timed out after %s minutes"
% self.stack.timeout_mins)
raise exception.Error(_("SSH command timed out after %s "
"minutes") % self.stack.timeout_mins)
else:
return chan.recv_exit_status()
finally:
@ -338,7 +273,7 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
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 = paramiko.Transport((self.server.accessIPv4, 22))
transport.connect(hostkey=None, username="root", pkey=pkey)
sftp = paramiko.SFTPClient.from_transport(transport)
try:
@ -352,221 +287,133 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
sftp.close()
transport.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.
"""
# Generate SSH public/private keypair
def _personality(self):
# Generate SSH public/private keypair for the engine to use
if self._private_key is not None:
rsa = RSA.importKey(self._private_key)
else:
rsa = RSA.generate(1024)
self.private_key = rsa.exportKey()
public_keys = [rsa.publickey().exportKey('OpenSSH')]
if self.properties.get(self.KEY_NAME):
key_name = self.properties[self.KEY_NAME]
public_keys.append(nova_utils.get_keypair(self.nova(),
key_name).public_key)
personality_files = {
"/root/.ssh/authorized_keys": '\n'.join(public_keys)}
# Create server
client = self.nova().servers
logger.debug(_("Calling nova().servers.create()"))
server = client.create(self.physical_resource_name(),
self.image,
self.flavor,
files=personality_files)
# Add the user-provided key_name to the authorized_keys file
key_name = self.properties.get(self.KEY_NAME)
if key_name:
user_keypair = nova_utils.get_keypair(self.nova(), key_name)
public_keys.append(user_keypair.public_key)
personality = {"/root/.ssh/authorized_keys": '\n'.join(public_keys)}
# Save resource ID to db
self.resource_id_set(server.id)
# Add any user-provided personality files
user_personality = self.properties.get(self.PERSONALITY)
if user_personality:
personality.update(user_personality)
return server, scheduler.TaskRunner(self._attach_volumes_task())
return personality
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 _key_name(self):
return None
def _attach_volume(self, volume_id, device):
logger.debug(_("Calling nova().volumes.create_server_volume()"))
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 super(CloudServer, self).check_create_complete(cookie):
def _check_managed_cloud_complete(self, server):
if 'rax_service_level_automation' not in server.metadata:
logger.debug(_("Managed Cloud server does not have the "
"rax_service_level_automation metadata tag yet"))
return False
mc_status = server.metadata['rax_service_level_automation']
logger.debug(_("Managed Cloud automation status: ") + mc_status)
if mc_status == self.MC_STATUS_IN_PROGRESS:
return False
elif mc_status == self.MC_STATUS_COMPLETE:
return True
elif mc_status == self.MC_STATUS_BUILD_ERROR:
raise exception.Error(_("Managed Cloud automation failed"))
else:
raise exception.Error(_("Unknown Managed Cloud automation "
"status: ") + mc_status)
def _check_rack_connect_complete(self, server):
if 'rackconnect_automation_status' not in server.metadata:
logger.debug(_("RackConnect server does not have the "
"rackconnect_automation_status metadata tag yet"))
return False
rc_status = server.metadata['rackconnect_automation_status']
logger.debug(_("RackConnect automation status: ") + rc_status)
if rc_status == self.RC_STATUS_DEPLOYING:
return False
elif rc_status == self.RC_STATUS_DEPLOYED:
self._server = None # The public IP changed, forget old one
return True
elif rc_status == self.RC_STATUS_UNPROCESSABLE:
# UNPROCESSABLE means the RackConnect automation was not
# attempted (eg. Cloud Server in a different DC than
# dedicated gear, so RackConnect does not apply). It is
# okay if we do not raise an exception.
reason = server.metadata.get('rackconnect_unprocessable_reason',
None)
if reason is not None:
logger.warning(_("RackConnect unprocessable reason: ") +
reason)
return True
elif rc_status == self.RC_STATUS_FAILED:
raise exception.Error(_("RackConnect automation FAILED"))
else:
raise exception.Error(_("Unknown RackConnect automation status: ")
+ rc_status)
def _run_userdata(self):
# Create heat-script and userdata files on server
raw_userdata = self.properties[self.USER_DATA]
userdata = nova_utils.build_userdata(self, 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
cmd = "bash -ex /root/heat-script.sh > /root/heat-script.log 2>&1"
exit_code = self._run_ssh_command(cmd)
if exit_code == 42:
raise exception.Error(self.SCRIPT_ERROR_MSG %
{'path': "cfn-userdata",
'log': "/root/cfn-userdata.log"})
elif exit_code != 0:
raise exception.Error(self.SCRIPT_ERROR_MSG %
{'path': "heat-script.sh",
'log': "/root/heat-script.log"})
def check_create_complete(self, server):
"""Check if server creation is complete and handle server configs."""
if not self._check_active(server):
return False
server = cookie[0]
server.get()
if 'rack_connect' in self.context.roles: # Account has RackConnect
if 'rackconnect_automation_status' not in server.metadata:
logger.debug(_("RackConnect server does not have the "
"rackconnect_automation_status metadata tag "
"yet"))
return False
rc_status = server.metadata['rackconnect_automation_status']
logger.debug(_("RackConnect automation status: ") + rc_status)
if 'rack_connect' in self.context.roles and not \
self._check_rack_connect_complete(server):
return False
if rc_status == 'DEPLOYING':
return False
elif rc_status == 'DEPLOYED':
self._public_ip = None # The public IP changed, forget old one
elif rc_status == 'FAILED':
raise exception.Error(_("RackConnect automation FAILED"))
elif rc_status == 'UNPROCESSABLE':
reason = server.metadata.get(
"rackconnect_unprocessable_reason", None)
if reason is not None:
logger.warning(_("RackConnect unprocessable reason: ")
+ reason)
# UNPROCESSABLE means the RackConnect automation was
# not attempted (eg. Cloud Server in a different DC
# than dedicated gear, so RackConnect does not apply).
# It is okay if we do not raise an exception.
else:
raise exception.Error(_("Unknown RackConnect automation "
"status: ") + rc_status)
if 'rax_managed' in self.context.roles: # Managed Cloud account
if 'rax_service_level_automation' not in server.metadata:
logger.debug(_("Managed Cloud server does not have the "
"rax_service_level_automation metadata tag yet"))
return False
mc_status = server.metadata['rax_service_level_automation']
logger.debug(_("Managed Cloud automation status: ") + mc_status)
if mc_status == 'In Progress':
return False
elif mc_status == 'Complete':
pass
elif mc_status == 'Build Error':
raise exception.Error(_("Managed Cloud automation failed"))
else:
raise exception.Error(_("Unknown Managed Cloud automation "
"status: ") + mc_status)
if 'rax_managed' in self.context.roles and not \
self._check_managed_cloud_complete(server):
return False
if self.has_userdata:
# Create heat-script and userdata files on server
raw_userdata = self.properties[self.USER_DATA] or ''
userdata = nova_utils.build_userdata(self, 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
cmd = "bash -ex /root/heat-script.sh > /root/heat-script.log 2>&1"
exit_code = self._run_ssh_command(cmd)
if exit_code == 42:
raise exception.Error(self.script_error_msg %
{'path': "cfn-userdata",
'log': "/root/cfn-userdata.log"})
elif exit_code != 0:
raise exception.Error(self.script_error_msg %
{'path': "heat-script.sh",
'log': "/root/heat-script.log"})
self._run_userdata()
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 == "DELETED":
break
elif server.status == "ERROR":
raise exception.Error(_("Deletion of server %s failed.") %
server.name)
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"
exit_code = self._run_ssh_command(command)
if exit_code != 0:
raise exception.Error(self.script_error_msg %
{'path': "cfn-userdata",
'log': "/root/cfn-userdata.log"})
if self.FLAVOR in prop_diff:
flav = json_snippet['Properties'][self.FLAVOR]
new_flavor = nova_utils.get_flavor_id(self.nova(), flav)
self.server.resize(new_flavor)
resize = scheduler.TaskRunner(nova_utils.check_resize,
self.server,
flav)
resize.start()
return resize
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)
def _resolve_attribute(self, name):
if name == 'distro':
return self.distro
if name == 'privateIPv4':
return nova_utils.get_ip(self.server, 'private', 4)
return super(CloudServer, self)._resolve_attribute(name)

View File

@ -10,24 +10,22 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import uuid
import mox
import paramiko
import novaclient
from heat.db import api as db_api
from heat.engine import environment
from heat.tests.v1_1 import fakes
from heat.common import template_format
from heat.common import exception
from heat.common import template_format
from heat.engine import clients
from heat.engine import parser
from heat.engine import resource
from heat.engine import scheduler
from heat.openstack.common import uuidutils
from heat.tests.common import HeatTestCase
from heat.tests import utils
from heat.engine import clients
from ..resources import cloud_server # noqa
@ -36,20 +34,19 @@ wp_template = '''
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "WordPress",
"Parameters" : {
"flavor" : {
"Description" : "Rackspace Cloud Server flavor",
"key_name" : {
"Description" : "key_name",
"Type" : "String",
"Default" : "m1.small",
"AllowedValues" : ["256 MB Server", "m1.small", "m1.large", "invalid"],
"ConstraintDescription" : "must be a valid Rackspace Cloud Server flavor"
},
"Default" : "test"
}
},
"Resources" : {
"WebServer": {
"Type": "Rackspace::Cloud::Server",
"Properties": {
"image" : "CentOS 5.2",
"flavor" : "256 MB Server",
"image" : "CentOS 5.2",
"flavor" : "256 MB Server",
"key_name" : "test",
"user_data" : "wordpress"
}
}
@ -75,9 +72,9 @@ gDZkz7KcZC7TkO0NYVRssA6/84mCqx6QHpKaYNG9kg==
"""
class RackspaceCloudServerTest(HeatTestCase):
class CloudServersTest(HeatTestCase):
def setUp(self):
super(RackspaceCloudServerTest, self).setUp()
super(CloudServersTest, self).setUp()
self.fc = fakes.FakeClient()
utils.setup_dummy_db()
# Test environment may not have pyrax client library installed and if
@ -86,14 +83,6 @@ class RackspaceCloudServerTest(HeatTestCase):
resource._register_class("Rackspace::Cloud::Server",
cloud_server.CloudServer)
def _setup_test_stack(self, stack_name):
t = template_format.parse(wp_template)
template = parser.Template(t)
stack = parser.Stack(utils.dummy_context(), stack_name, template,
{},
stack_id=str(uuid.uuid4()))
return (t, stack)
def _mock_ssh_sftp(self, exit_code=0):
# SSH
self.m.StubOutWithMock(paramiko, "SSHClient")
@ -132,343 +121,312 @@ class RackspaceCloudServerTest(HeatTestCase):
sftp.close()
transport.close()
def _setup_test_cs(self, return_server, name, exit_code=0):
stack_name = '%s_stack' % name
def _setup_test_stack(self, stack_name):
t = template_format.parse(wp_template)
template = parser.Template(t)
stack = parser.Stack(utils.dummy_context(), stack_name, template,
environment.Environment({'key_name': 'test'}),
stack_id=uuidutils.generate_uuid())
return (t, stack)
def _setup_test_server(self, return_server, name, image_id=None,
override_name=False, stub_create=True, exit_code=0):
stack_name = '%s_s' % name
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['image'] = \
image_id or 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['flavor'] = \
'256 MB Server'
server_name = '%s' % name
if override_name:
t['Resources']['WebServer']['Properties']['name'] = \
server_name
server = cloud_server.CloudServer(server_name,
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(cloud_server.CloudServer, "nova")
cloud_server.CloudServer.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
t['Resources']['WebServer']['Properties']['image'] = 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['flavor'] = '256 MB Server'
cs = cloud_server.CloudServer('%s_name' % name,
t['Resources']['WebServer'], stack)
cs._private_key = rsa_key
cs.t = cs.stack.resolve_runtime_data(cs.t)
self.m.StubOutWithMock(self.fc.servers, 'create')
name_limit = cloud_server.CloudServer.physical_resource_name_limit
server_name = utils.PhysName(stack_name, cs.name, limit=name_limit)
self.fc.servers.create(server_name, 1, 1,
files=mox.IgnoreArg()).AndReturn(return_server)
return_server.adminPass = "foobar"
server._private_key = rsa_key
server.t = server.stack.resolve_runtime_data(server.t)
if stub_create:
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1,
flavor=1,
key_name=None,
name=override_name and server.name or utils.PhysName(
stack_name, server.name),
security_groups=[],
userdata=mox.IgnoreArg(),
scheduler_hints=None,
meta=None,
nics=None,
availability_zone=None,
block_device_mapping=None,
config_drive=None,
disk_config=None,
reservation_id=None,
files=mox.IgnoreArg()).AndReturn(return_server)
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
self._mock_ssh_sftp(exit_code)
return cs
def _create_test_cs(self, return_server, name, exit_code=0):
cs = self._setup_test_cs(return_server, name, exit_code)
return server
def _create_test_server(self, return_server, name, override_name=False,
stub_create=True, exit_code=0):
server = self._setup_test_server(return_server, name,
stub_create=stub_create,
exit_code=exit_code)
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
return cs
scheduler.TaskRunner(server.create)()
return server
def _update_test_cs(self, return_server, name, exit_code=0):
def _update_test_server(self, return_server, name, exit_code=0):
self._mock_ssh_sftp(exit_code)
self.m.StubOutWithMock(clients.OpenStackClients, "nova")
clients.OpenStackClients.nova(
mox.IgnoreArg()).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(expected_public, cs.FnGetAtt('PublicIp'))
self.assertEqual(expected_private, cs.FnGetAtt('PrivateIp'))
self.assertEqual(expected_public, cs.FnGetAtt('PublicDnsName'))
self.assertEqual(expected_public, cs.FnGetAtt('PrivateDnsName'))
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(expected_public, cs.FnGetAtt('PublicIp'))
self.assertEqual(expected_private, cs.FnGetAtt('PrivateIp'))
self.assertEqual(expected_public, cs.FnGetAtt('PublicDnsName'))
self.assertEqual(expected_public, cs.FnGetAtt('PrivateDnsName'))
self.assertRaises(exception.InvalidTemplateAttribute,
cs.FnGetAtt, 'foo')
self.m.VerifyAll()
def test_cs_create_image_name_err(self):
self.m.StubOutWithMock(cloud_server.CloudServer, "nova")
cloud_server.CloudServer.nova().MultipleTimes().AndReturn(self.fc)
stack_name = 'test_cs_create_image_name_err_stack'
def _mock_metadata_os_distro(self):
image_data = self.m.CreateMockAnything()
image_data.metadata = {'os_distro': 'centos'}
self.m.StubOutWithMock(self.fc.images, 'get')
self.fc.images.get(mox.IgnoreArg()).MultipleTimes().\
AndReturn(image_data)
def test_script_raw_userdata(self):
stack_name = 'raw_userdata_s'
(t, stack) = self._setup_test_stack(stack_name)
# create a cloud server with non exist image name
t['Resources']['WebServer']['Properties']['image'] = 'Slackware'
t['Resources']['WebServer']['Properties']['user_data_format'] = \
'RAW'
# Mock flavors
cloud_server.CloudServer.script = None
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self._mock_metadata_os_distro()
self.m.ReplayAll()
cs = cloud_server.CloudServer('cs_create_image_err',
t['Resources']['WebServer'], stack)
self.assertRaises(exception.ImageNotFound, cs.validate)
self.assertNotIn("/var/lib/cloud/data/cfn-userdata", server.script)
self.m.VerifyAll()
def test_cs_create_image_name_okay(self):
def test_script_cfntools_userdata(self):
stack_name = 'raw_userdata_s'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['user_data_format'] = \
'HEAT_CFNTOOLS'
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self._mock_metadata_os_distro()
self.m.ReplayAll()
self.assertIn("/var/lib/cloud/data/cfn-userdata", server.script)
self.m.VerifyAll()
def test_validate_no_script_okay(self):
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
# create an server with non exist image Id
t['Resources']['WebServer']['Properties']['image'] = '1'
server = cloud_server.CloudServer('server_create_image_err',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(server.__class__, 'script')
server.script = None
self.m.StubOutWithMock(server.__class__, 'has_userdata')
server.has_userdata = False
self.m.StubOutWithMock(uuidutils, "is_uuid_like")
uuidutils.is_uuid_like('1').MultipleTimes().AndReturn(True)
self.m.ReplayAll()
self.assertIsNone(server.validate())
self.m.VerifyAll()
def test_validate_disallowed_personality(self):
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
# create an server with non exist image Id
t['Resources']['WebServer']['Properties']['personality'] = \
{"/fake/path1": "fake contents1",
"/fake/path2": "fake_contents2",
"/fake/path3": "fake_contents3",
"/root/.ssh/authorized_keys": "fake_contents4"}
server = cloud_server.CloudServer('server_create_image_err',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server.__class__, 'script')
server.script = None
self.m.StubOutWithMock(server.__class__, 'has_userdata')
server.has_userdata = False
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
server.validate)
self.assertEqual("The personality property may not contain a "
"key of \"/root/.ssh/authorized_keys\"", str(exc))
self.m.VerifyAll()
def test_user_personality(self):
return_server = self.fc.servers.list()[1]
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
# create an server with non exist image Id
t['Resources']['WebServer']['Properties']['personality'] = \
{"/fake/path1": "fake contents1",
"/fake/path2": "fake_contents2",
"/fake/path3": "fake_contents3"}
server = cloud_server.CloudServer('server_create_image_err',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server.__class__, 'script')
server.script = None
self.m.StubOutWithMock(server.__class__, 'has_userdata')
server.has_userdata = False
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
self.assertIsNone(server.validate())
expected_personality = {'/fake/path1': 'fake contents1',
'/fake/path3': 'fake_contents3',
'/fake/path2': 'fake_contents2',
'/root/.ssh/authorized_keys': mox.IgnoreArg()}
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1, flavor=1, key_name=None,
name=utils.PhysName(stack_name, server.name),
security_groups=[],
userdata=mox.IgnoreArg(), scheduler_hints=None,
meta=None, nics=None, availability_zone=None,
block_device_mapping=None, config_drive=None,
disk_config=None, reservation_id=None,
files=expected_personality).AndReturn(return_server)
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.m.VerifyAll()
def test_validate_no_script_not_okay(self):
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
# create a server with non-existent image ID
t['Resources']['WebServer']['Properties']['image'] = '1'
server = cloud_server.CloudServer('server_create_image_err',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server.__class__, 'script')
server.script = None
self.m.StubOutWithMock(server.__class__, 'has_userdata')
server.has_userdata = True
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
server.validate)
self.assertIn("user_data is not supported", str(exc))
self.m.VerifyAll()
def test_validate_with_bootable_vol_and_userdata(self):
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
# create a server without an image
del t['Resources']['WebServer']['Properties']['image']
t['Resources']['WebServer']['Properties']['block_device_mapping'] = \
[{
"device_name": u'vda',
"volume_id": "5d7e27da-6703-4f7e-9f94-1f67abef734c",
"delete_on_termination": False
}]
server = cloud_server.CloudServer('server_create_image_err',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server.__class__, 'has_userdata')
server.has_userdata = True
self.m.StubOutWithMock(cloud_server.CloudServer, "nova")
cloud_server.CloudServer.nova().MultipleTimes().AndReturn(self.fc)
stack_name = 'test_cs_create_image_name_err_stack'
(t, stack) = self._setup_test_stack(stack_name)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
# create a cloud server with non exist image name
t['Resources']['WebServer']['Properties']['image'] = 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['user_data'] = ''
cloud_server.CloudServer.script = None
self.m.ReplayAll()
cs = cloud_server.CloudServer('cs_create_image_err',
t['Resources']['WebServer'], stack)
self.assertIsNone(cs.validate())
exc = self.assertRaises(exception.StackValidationFailed,
server.validate)
self.assertIn("user_data scripts are not supported with bootable "
"volumes", str(exc))
self.m.VerifyAll()
def test_cs_create_heatscript_nonzero_exit_status(self):
return_server = self.fc.servers.list()[1]
cs = self._setup_test_cs(return_server, 'test_cs_create_image_id',
exit_code=1)
self.m.ReplayAll()
create = scheduler.TaskRunner(cs.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertIn("The heat-script.sh script exited", str(exc))
self.m.VerifyAll()
def test_cs_create_cfnuserdata_nonzero_exit_status(self):
return_server = self.fc.servers.list()[1]
cs = self._setup_test_cs(return_server, 'test_cs_create_image_id',
exit_code=42)
self.m.ReplayAll()
create = scheduler.TaskRunner(cs.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertIn("The cfn-userdata script exited", str(exc))
self.m.VerifyAll()
def test_cs_update_cfnuserdata_nonzero_exit_status(self):
return_server = self.fc.servers.list()[1]
cs = self._create_test_cs(return_server,
'test_cs_update_cfnuserdata_nonzero_exit')
self.m.UnsetStubs()
self._update_test_cs(return_server,
'test_cs_update_cfnuserdata_nonzero_exit',
exit_code=1)
self.m.ReplayAll()
update_template = copy.deepcopy(cs.t)
update_template['Metadata'] = {'test': 123}
update = scheduler.TaskRunner(cs.update, update_template)
exc = self.assertRaises(exception.ResourceFailure, update)
self.assertIn("The cfn-userdata script exited", str(exc))
def test_cs_create_flavor_err(self):
"""validate() should throw an if the flavor is invalid."""
self.m.StubOutWithMock(cloud_server.CloudServer, "nova")
cloud_server.CloudServer.nova().MultipleTimes().AndReturn(self.fc)
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'] = 'invalid'
# Mock flavors
self.m.ReplayAll()
cs = cloud_server.CloudServer('cs_create_flavor_err',
t['Resources']['WebServer'], stack)
self.assertRaises(exception.FlavorMissing, 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)
scheduler.TaskRunner(cs.delete)()
self.assertIsNone(cs.resource_id)
self.assertEqual((cs.DELETE, cs.COMPLETE), cs.state)
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}
scheduler.TaskRunner(cs.update, update_template)()
self.assertEqual({'test': 123}, cs.metadata)
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}
updater = scheduler.TaskRunner(cs.update, update_template)
self.assertRaises(resource.UpdateReplace, updater)
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']['user_data'] = 'mustreplace'
updater = scheduler.TaskRunner(cs.update, update_template)
self.assertRaises(resource.UpdateReplace, updater)
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.CREATE, cs.COMPLETE), cs.state)
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.CREATE, cs.COMPLETE), cs.state)
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('4.5.6.7', cs.public_ip)
self.mock_get_ip(cs)
self.assertEqual('10.13.12.13', cs.private_ip)
cs.addresses = {'public': [],
'private': []}
self.mock_get_ip(cs)
self.assertRaises(exception.Error, 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)
server = cloud_server.CloudServer('server_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')
server._store_or_update(server.CREATE, server.IN_PROGRESS,
'test_store')
cs.private_key = 'fake private key'
server.private_key = 'fake private key'
self.ctx = utils.dummy_context()
rs = db_api.resource_get_by_name_and_stack(self.ctx,
'cs_private_key',
'server_private_key',
stack.id)
encrypted_key = rs.data[0]['value']
self.assertNotEqual(encrypted_key, "fake private key")
# Test private_key property returns decrypted value
self.assertEqual("fake private key", cs.private_key)
decrypted_key = server.private_key
self.assertEqual("fake private key", decrypted_key)
def test_rackconnect_deployed(self):
return_server = self.fc.servers.list()[1]
return_server.metadata = {'rackconnect_automation_status': 'DEPLOYED'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server, 'test_rackconnect_deployed')
cs.context.roles = ['rack_connect']
server = self._setup_test_server(return_server,
'test_rackconnect_deployed')
server.context.roles = ['rack_connect']
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
self.assertEqual('CREATE', cs.action)
self.assertEqual('COMPLETE', cs.status)
scheduler.TaskRunner(server.create)()
self.assertEqual('CREATE', server.action)
self.assertEqual('COMPLETE', server.status)
self.m.VerifyAll()
def test_rackconnect_failed(self):
@ -476,10 +434,11 @@ class RackspaceCloudServerTest(HeatTestCase):
return_server.metadata = {'rackconnect_automation_status': 'FAILED'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server, 'test_rackconnect_failed')
cs.context.roles = ['rack_connect']
server = self._setup_test_server(return_server,
'test_rackconnect_failed')
server.context.roles = ['rack_connect']
self.m.ReplayAll()
create = scheduler.TaskRunner(cs.create)
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertEqual('Error: RackConnect automation FAILED', str(exc))
@ -491,13 +450,13 @@ class RackspaceCloudServerTest(HeatTestCase):
'Fake reason'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server,
'test_rackconnect_unprocessable')
cs.context.roles = ['rack_connect']
server = self._setup_test_server(return_server,
'test_rackconnect_unprocessable')
server.context.roles = ['rack_connect']
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
self.assertEqual('CREATE', cs.action)
self.assertEqual('COMPLETE', cs.status)
scheduler.TaskRunner(server.create)()
self.assertEqual('CREATE', server.action)
self.assertEqual('COMPLETE', server.status)
self.m.VerifyAll()
def test_rackconnect_unknown(self):
@ -505,25 +464,97 @@ class RackspaceCloudServerTest(HeatTestCase):
return_server.metadata = {'rackconnect_automation_status': 'FOO'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server, 'test_rackconnect_unknown')
cs.context.roles = ['rack_connect']
server = self._setup_test_server(return_server,
'test_rackconnect_unknown')
server.context.roles = ['rack_connect']
self.m.ReplayAll()
create = scheduler.TaskRunner(cs.create)
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertEqual('Error: Unknown RackConnect automation status: FOO',
str(exc))
def test_managed_cloud_complete(self):
return_server = self.fc.servers.list()[1]
return_server.metadata = {'rax_service_level_automation': 'Complete'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server, 'test_managed_cloud_complete')
cs.context.roles = ['rax_managed']
def test_rackconnect_deploying(self):
return_server = self.fc.servers.list()[0]
server = self._setup_test_server(return_server,
'srv_sts_bld')
server.resource_id = 1234
server.context.roles = ['rack_connect']
check_iterations = [0]
# Bind fake get method which check_create_complete will call
def activate_status(server):
check_iterations[0] += 1
if check_iterations[0] == 1:
server.metadata['rackconnect_automation_status'] = 'DEPLOYING'
if check_iterations[0] == 2:
server.status = 'ACTIVE'
if check_iterations[0] > 3:
server.metadata['rackconnect_automation_status'] = 'DEPLOYED'
return_server.get = activate_status.__get__(return_server)
self.m.ReplayAll()
scheduler.TaskRunner(cs.create)()
self.assertEqual('CREATE', cs.action)
self.assertEqual('COMPLETE', cs.status)
scheduler.TaskRunner(server.create)()
self.assertEqual((server.CREATE, server.COMPLETE), server.state)
self.m.VerifyAll()
def test_rackconnect_no_status(self):
return_server = self.fc.servers.list()[0]
server = self._setup_test_server(return_server,
'srv_sts_bld')
server.resource_id = 1234
server.context.roles = ['rack_connect']
check_iterations = [0]
# Bind fake get method which check_create_complete will call
def activate_status(server):
check_iterations[0] += 1
if check_iterations[0] == 1:
server.status = 'ACTIVE'
if check_iterations[0] == 2:
server.metadata = {}
if check_iterations[0] > 2:
server.metadata['rackconnect_automation_status'] = 'DEPLOYED'
return_server.get = activate_status.__get__(return_server)
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.assertEqual((server.CREATE, server.COMPLETE), server.state)
self.m.VerifyAll()
def test_managed_cloud_lifecycle(self):
return_server = self.fc.servers.list()[0]
server = self._setup_test_server(return_server,
'srv_sts_bld')
server.resource_id = 1234
server.context.roles = ['rack_connect', 'rax_managed']
check_iterations = [0]
# Bind fake get method which check_create_complete will call
def activate_status(server):
check_iterations[0] += 1
if check_iterations[0] == 1:
server.status = 'ACTIVE'
if check_iterations[0] == 2:
server.metadata = {'rackconnect_automation_status': 'DEPLOYED'}
if check_iterations[0] == 3:
server.metadata = {
'rackconnect_automation_status': 'DEPLOYED',
'rax_service_level_automation': 'In Progress'}
if check_iterations[0] > 3:
server.metadata = {
'rackconnect_automation_status': 'DEPLOYED',
'rax_service_level_automation': 'Complete'}
return_server.get = activate_status.__get__(return_server)
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.assertEqual((server.CREATE, server.COMPLETE), server.state)
self.m.VerifyAll()
def test_managed_cloud_build_error(self):
@ -532,11 +563,11 @@ class RackspaceCloudServerTest(HeatTestCase):
'Build Error'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server,
'test_managed_cloud_build_error')
cs.context.roles = ['rax_managed']
server = self._setup_test_server(return_server,
'test_managed_cloud_build_error')
server.context.roles = ['rax_managed']
self.m.ReplayAll()
create = scheduler.TaskRunner(cs.create)
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertEqual('Error: Managed Cloud automation failed', str(exc))
@ -545,10 +576,61 @@ class RackspaceCloudServerTest(HeatTestCase):
return_server.metadata = {'rax_service_level_automation': 'FOO'}
self.m.StubOutWithMock(return_server, 'get')
return_server.get()
cs = self._setup_test_cs(return_server, 'test_managed_cloud_unknown')
cs.context.roles = ['rax_managed']
server = self._setup_test_server(return_server,
'test_managed_cloud_unknown')
server.context.roles = ['rax_managed']
self.m.ReplayAll()
create = scheduler.TaskRunner(cs.create)
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertEqual('Error: Unknown Managed Cloud automation status: FOO',
str(exc))
def test_create_heatscript_nonzero_exit_status(self):
return_server = self.fc.servers.list()[1]
server = self._setup_test_server(return_server, 'test_create_image_id',
exit_code=1)
self.m.ReplayAll()
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertIn("The heat-script.sh script exited", str(exc))
self.m.VerifyAll()
def test_create_cfnuserdata_nonzero_exit_status(self):
return_server = self.fc.servers.list()[1]
server = self._setup_test_server(return_server, 'test_create_image_id',
exit_code=42)
self.m.ReplayAll()
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertIn("The cfn-userdata script exited", str(exc))
self.m.VerifyAll()
def test_validate_too_many_personality_rackspace(self):
stack_name = 'srv_val'
(t, stack) = self._setup_test_stack(stack_name)
# create an server with non exist image Id
t['Resources']['WebServer']['Properties']['personality'] = \
{"/fake/path1": "fake contents1",
"/fake/path2": "fake_contents2",
"/fake/path3": "fake_contents3",
"/fake/path4": "fake_contents4",
"/fake/path5": "fake_contents5"}
server = cloud_server.CloudServer('server_create_image_err',
t['Resources']['WebServer'], stack)
self.m.StubOutWithMock(server.__class__, 'script')
server.script = None
self.m.StubOutWithMock(server.__class__, 'has_userdata')
server.has_userdata = False
self.m.StubOutWithMock(server, 'nova')
server.nova().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
exc = self.assertRaises(exception.StackValidationFailed,
server.validate)
self.assertEqual("The personality property may not contain "
"greater than 4 entries.", str(exc))
self.m.VerifyAll()

View File

@ -88,6 +88,14 @@ def get_image_id(nova_client, image_identifier):
return image_id
def get_ip(server, net_type, ip_version):
"""Return the server's IP of the given type and version."""
if net_type in server.addresses:
for ip in server.addresses[net_type]:
if ip['version'] == ip_version:
return ip['addr']
def get_flavor_id(nova_client, flavor):
'''
Get the id for the specified flavor name.

View File

@ -281,6 +281,10 @@ class Server(resource.Resource):
# This method is overridden by the derived CloudServer resource
return self.properties.get(self.PERSONALITY)
def _key_name(self):
# This method is overridden by the derived CloudServer resource
return self.properties.get(self.KEY_NAME)
def handle_create(self):
security_groups = self.properties.get(self.SECURITY_GROUPS)
@ -318,7 +322,7 @@ class Server(resource.Resource):
name=self.physical_resource_name(),
image=image,
flavor=flavor_id,
key_name=self.properties[self.KEY_NAME],
key_name=self._key_name(),
security_groups=security_groups,
userdata=userdata,
meta=instance_meta,

View File

@ -50,6 +50,28 @@ class NovaUtilsTests(HeatTestCase):
self.nova_client, 'noimage')
self.m.VerifyAll()
def test_get_ip(self):
my_image = self.m.CreateMockAnything()
my_image.addresses = {
'public': [{'version': 4,
'addr': '4.5.6.7'},
{'version': 6,
'addr': '2401:1801:7800:0101:c058:dd33:ff18:04e6'}],
'private': [{'version': 4,
'addr': '10.13.12.13'}]}
expected = '4.5.6.7'
observed = nova_utils.get_ip(my_image, 'public', 4)
self.assertEqual(expected, observed)
expected = '10.13.12.13'
observed = nova_utils.get_ip(my_image, 'private', 4)
self.assertEqual(expected, observed)
expected = '2401:1801:7800:0101:c058:dd33:ff18:04e6'
observed = nova_utils.get_ip(my_image, 'public', 6)
self.assertEqual(expected, observed)
def test_get_flavor_id(self):
"""Tests the get_flavor_id function."""
flav_id = str(uuid.uuid4())

View File

@ -355,7 +355,8 @@ class FakeHTTPClient(base_client.HTTPClient):
#
def get_os_keypairs(self, *kw):
return (200, {"keypairs": [{'fingerprint': 'FAKE_KEYPAIR',
'name': 'test'}]})
'name': 'test',
'public_key': 'foo'}]})
def get_os_availability_zone(self, *kw):
return (200, {"availabilityZoneInfo": [{'zoneName': 'nova1'}]})