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:
parent
8c4adee13f
commit
5a4b14e51b
|
@ -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
|
||||
"""
|
||||
|
||||
# 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}
|
||||
|
||||
script_error_msg = (_("The %(path)s script exited with a non-zero exit "
|
||||
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"))
|
||||
"server and view %(log)s")
|
||||
|
||||
# Template keys supported for handle_update. Properties not
|
||||
# listed here trigger an UpdateReplace
|
||||
update_allowed_keys = ('Metadata', 'Properties')
|
||||
# Managed Cloud automation statuses
|
||||
MC_STATUS_IN_PROGRESS = 'In Progress'
|
||||
MC_STATUS_COMPLETE = 'Complete'
|
||||
MC_STATUS_BUILD_ERROR = 'Build Error'
|
||||
|
||||
# RackConnect automation statuses
|
||||
RC_STATUS_DEPLOYING = 'DEPLOYING'
|
||||
RC_STATUS_DEPLOYED = 'DEPLOYED'
|
||||
RC_STATUS_FAILED = 'FAILED'
|
||||
RC_STATUS_UNPROCESSABLE = 'UNPROCESSABLE'
|
||||
|
||||
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,117 +287,33 @@ 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):
|
||||
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 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
|
||||
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"))
|
||||
|
@ -471,22 +322,57 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
|
|||
mc_status = server.metadata['rax_service_level_automation']
|
||||
logger.debug(_("Managed Cloud automation status: ") + mc_status)
|
||||
|
||||
if mc_status == 'In Progress':
|
||||
if mc_status == self.MC_STATUS_IN_PROGRESS:
|
||||
return False
|
||||
|
||||
elif mc_status == 'Complete':
|
||||
pass
|
||||
elif mc_status == self.MC_STATUS_COMPLETE:
|
||||
return True
|
||||
|
||||
elif mc_status == 'Build Error':
|
||||
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)
|
||||
|
||||
if self.has_userdata:
|
||||
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] or ''
|
||||
raw_userdata = self.properties[self.USER_DATA]
|
||||
userdata = nova_utils.build_userdata(self, raw_userdata)
|
||||
|
||||
files = [{'path': "/tmp/userdata", 'data': userdata},
|
||||
|
@ -497,76 +383,37 @@ zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
|
|||
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 %
|
||||
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 %
|
||||
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.get()
|
||||
|
||||
if 'rack_connect' in self.context.roles and not \
|
||||
self._check_rack_connect_complete(server):
|
||||
return False
|
||||
|
||||
if 'rax_managed' in self.context.roles and not \
|
||||
self._check_managed_cloud_complete(server):
|
||||
return False
|
||||
|
||||
if self.has_userdata:
|
||||
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)
|
||||
|
|
|
@ -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,13 +34,11 @@ 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": {
|
||||
|
@ -50,6 +46,7 @@ wp_template = '''
|
|||
"Properties": {
|
||||
"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
|
||||
self.m.ReplayAll()
|
||||
|
||||
cs = cloud_server.CloudServer('cs_create_image_err',
|
||||
server = cloud_server.CloudServer('WebServer',
|
||||
t['Resources']['WebServer'], stack)
|
||||
|
||||
self.assertRaises(exception.ImageNotFound, cs.validate)
|
||||
self.m.StubOutWithMock(server, 'nova')
|
||||
server.nova().MultipleTimes().AndReturn(self.fc)
|
||||
self._mock_metadata_os_distro()
|
||||
self.m.ReplayAll()
|
||||
|
||||
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',
|
||||
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,
|
||||
server = self._setup_test_server(return_server,
|
||||
'test_rackconnect_unprocessable')
|
||||
cs.context.roles = ['rack_connect']
|
||||
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,
|
||||
server = self._setup_test_server(return_server,
|
||||
'test_managed_cloud_build_error')
|
||||
cs.context.roles = ['rax_managed']
|
||||
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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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'}]})
|
||||
|
|
Loading…
Reference in New Issue