Don't use SSH in Rackspace::Cloud::Server

Rackspace Cloud Servers now supports config drive and the Rackspace
images have cloud-init installed by default, so we no longer have to SSH
to the Cloud Server to install dependencies and run the user-data
script.

Closes-Bug: #1298050
Change-Id: I5cb6a93c5b34b0a8cfe7480211da70d65638de54
This commit is contained in:
Jason Dunsmore 2014-03-26 14:01:24 -05:00
parent d477d3566b
commit 285f2cbc28
5 changed files with 60 additions and 927 deletions

View File

@ -12,11 +12,6 @@
# under the License.
import copy
import socket
import tempfile
from Crypto.PublicKey import RSA
import paramiko
from heat.common import exception
from heat.engine import properties
@ -37,72 +32,6 @@ logger = logging.getLogger(__name__)
class CloudServer(server.Server):
"""Resource for Rackspace Cloud Servers."""
SCRIPT_INSTALL_REQUIREMENTS = {
'ubuntu': """
apt-get update
export DEBIAN_FRONTEND=noninteractive
apt-get install -y -o Dpkg::Options::="--force-confdef" -o \
Dpkg::Options::="--force-confold" python-boto python-pip gcc python-dev
pip install heat-cfntools
cfn-create-aws-symlinks --source /usr/local/bin
""",
'fedora': """
yum install -y python-boto python-pip gcc python-devel
pip-python install heat-cfntools
cfn-create-aws-symlinks
""",
'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
fi
yum install -y python-boto python-pip gcc python-devel python-argparse
pip-python install heat-cfntools
""",
'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
fi
# The RPM DB stays locked for a few secs
while fuser /var/lib/rpm/*; do sleep 1; done
yum install -y python-boto python-pip gcc python-devel python-argparse
pip-python install heat-cfntools
cfn-create-aws-symlinks
""",
'debian': """
echo "deb http://mirror.rackspace.com/debian wheezy-backports main" >> \
/etc/apt/sources.list
apt-get update
apt-get -t wheezy-backports install -y cloud-init
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 = """
sed -i 's/ConfigDrive/NoCloud/' /etc/cloud/cloud.cfg.d/*
rm -rf /var/lib/cloud
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/*
"""
SCRIPT_RUN_CLOUD_INIT = """
cloud-init start || cloud-init init
"""
SCRIPT_RUN_CFN_USERDATA = """
bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
exit 42
"""
SCRIPT_ERROR_MSG = _("The %(path)s script exited with a non-zero exit "
"status. To see the error message, log into the "
"server at %(ip)s and view %(log)s")
# Managed Cloud automation statuses
MC_STATUS_IN_PROGRESS = 'In Progress'
MC_STATUS_COMPLETE = 'Complete'
@ -139,11 +68,9 @@ bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
def __init__(self, name, json_snippet, stack):
super(CloudServer, self).__init__(name, json_snippet, stack)
self.stack = stack
self._server = None
self._distro = None
self._image = None
self._retry_iterations = 0
self._managed_cloud_started_event_sent = False
self._rack_connect_started_event_sent = False
@ -163,26 +90,6 @@ bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
self._distro = image_data.metadata['os_distro']
return self._distro
@property
def script(self):
"""
Return the config script for the Cloud Server image.
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):
"""Return the server's image ID."""
@ -191,152 +98,14 @@ bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
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."""
return self.data().get('private_key')
@private_key.setter
def private_key(self, private_key):
"""Save the resource's private SSH key to the database."""
if self.id is not None:
self.data_set('private_key', private_key, True)
@property
def has_userdata(self):
"""Return True if the server has user_data, False otherwise."""
def _config_drive(self):
user_data = self.properties.get(self.USER_DATA)
if user_data or self.metadata != {}:
config_drive = self.properties.get(self.CONFIG_DRIVE)
if user_data or config_drive:
return True
else:
return False
def validate(self):
"""Validate user parameters."""
image = self.properties.get(self.IMAGE)
# It's okay if there's no script, as long as user_data and
# 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."""
with tempfile.NamedTemporaryFile() as private_key_file:
private_key_file.write(self.private_key)
private_key_file.seek(0)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy())
ssh.connect(self.server.accessIPv4,
username="root",
key_filename=private_key_file.name)
chan = ssh.get_transport().open_session()
chan.settimeout(self.stack.timeout_secs())
chan.exec_command(command)
try:
# The channel timeout only works for read/write operations
chan.recv(1024)
except socket.timeout:
raise exception.Error(
_("SSH command timed out after %s seconds") %
self.stack.timeout_secs())
else:
return chan.recv_exit_status()
finally:
ssh.close()
chan.close()
def _sftp_files(self, files):
"""Transfer files to the Cloud Server via SFTP."""
if self._retry_iterations > 30:
raise exception.Error(_("Failed to establish SSH connection after "
"30 tries"))
self._retry_iterations += 1
try:
transport = paramiko.Transport((self.server.accessIPv4, 22))
except paramiko.SSHException:
logger.debug("Failed to get SSH transport, will retry")
return False
with tempfile.NamedTemporaryFile() as private_key_file:
private_key_file.write(self.private_key)
private_key_file.seek(0)
pkey = paramiko.RSAKey.from_private_key_file(private_key_file.name)
try:
transport.connect(hostkey=None, username="root", pkey=pkey)
except EOFError:
logger.debug("Failed to connect to SSH transport, will retry")
return False
sftp = paramiko.SFTPClient.from_transport(transport)
try:
for remote_file in files:
sftp_file = sftp.open(remote_file['path'], 'w')
sftp_file.write(remote_file['data'])
sftp_file.close()
except:
raise
finally:
sftp.close()
transport.close()
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')]
# 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)}
# Add any user-provided personality files
user_personality = self.properties.get(self.PERSONALITY)
if user_personality:
personality.update(user_personality)
return personality
def _key_name(self):
return None
def _check_managed_cloud_complete(self, server):
if not self._managed_cloud_started_event_sent:
msg = _("Waiting for Managed Cloud automation to complete")
@ -409,36 +178,6 @@ bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
msg = _("Unknown RackConnect automation status: %s") % rc_status
raise exception.Error(msg)
def _run_userdata(self):
msg = _("Running user_data")
self._add_event(self.action, self.status, msg)
# 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}]
if self._sftp_files(files) is False:
return False
# 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",
'ip': self.server.accessIPv4,
'log': "/root/cfn-userdata.log"})
elif exit_code != 0:
raise exception.Error(self.SCRIPT_ERROR_MSG %
{'path': "heat-script.sh",
'ip': self.server.accessIPv4,
'log': "/root/heat-script.log"})
msg = _("Successfully ran user_data")
self._add_event(self.action, self.status, msg)
def check_create_complete(self, server):
"""Check if server creation is complete and handle server configs."""
if not self._check_active(server):
@ -454,10 +193,6 @@ bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1 ||
self._check_managed_cloud_complete(server):
return False
if self.has_userdata:
if self._run_userdata() is False:
return False
return True
def _resolve_attribute(self, name):

View File

@ -13,16 +13,13 @@
import mock
import mox
import paramiko
from heat.common import exception
from heat.common import template_format
from heat.db import api as db_api
from heat.engine import clients
from heat.engine import environment
from heat.engine import parser
from heat.engine import resource
from heat.engine.resources import image
from heat.engine import scheduler
from heat.openstack.common import uuidutils
from heat.tests.common import HeatTestCase
@ -57,23 +54,6 @@ wp_template = '''
}
'''
rsa_key = """-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDibWGom/83F2xYfVylBZhUbREiVlw42X7afUuHzNJuh/5EyhXQ
BmBHjVGL1mxZY4GoISrxIkW1jVmTXbm8FknIlS3jxEOC+xF3IkLBtmZEkFVLOUCv
Fpru1xThFS0L/pRttiTWLm+dsjboCV4qtg/+y30O0RJ5AAFgGkoVs8idrQIDAQAB
AoGAQU/7037r5yBCiGPgzVkHz5KGVrlCcMOL68ood0uFh4yCs6T3FcJBE2KYGxYG
uuIRDEZE9LlGElBrfi6S3MYxEbewITK9Li1cr8K0fJlIbg5PI1MxwiTXzG7i0f8Y
trtZjo/fs8XNSS4xlGWCUgtiNXvLS6wxyDGGbqeh1BmETgECQQDmoPJ3h5kuZguA
o7B+iTaKXqyWPf0ImsZ0UQYBgnEWTaZEh8W0015jP55mndALWA9pmhHJm+BC/Hfe
Kp6jtVyxAkEA+1YctDe62u5pXU/GK8UfDJwi4m1VxUfASrlxh+ALag9knwe6Dlev
EKKIe8R6HZs2zavaJs6dddxHRcIi8rXfvQJAW6octOVwPMDSUY69140x4E1Ay3ZX
29OojRKnEHKIABVcwGA2dGiOW2Qt0RtoVRnrBk32Q+twdy9hdSv7YZX0AQJAVDaj
QYNW2Zp+tWRQa0QORkRer+2gioyjEqaWMsfQK0ZjGaIWJk4c+37qKkZIAHmMYFeP
recW/XHEc8w7t4VXJQJAevSyciBfFcWMZTwlqq8wXNMCRLJt5CxvO4gSO+hPNrDe
gDZkz7KcZC7TkO0NYVRssA6/84mCqx6QHpKaYNG9kg==
-----END RSA PRIVATE KEY-----
"""
class CloudServersTest(HeatTestCase):
def setUp(self):
@ -85,44 +65,6 @@ class CloudServersTest(HeatTestCase):
resource._register_class("Rackspace::Cloud::Server",
cloud_server.CloudServer)
def _mock_ssh_sftp(self, exit_code=0):
# SSH
self.m.StubOutWithMock(paramiko, "SSHClient")
self.m.StubOutWithMock(paramiko, "MissingHostKeyPolicy")
ssh = self.m.CreateMockAnything()
paramiko.SSHClient().AndReturn(ssh)
paramiko.MissingHostKeyPolicy()
ssh.set_missing_host_key_policy(None)
ssh.connect(mox.IgnoreArg(),
key_filename=mox.IgnoreArg(),
username='root')
fake_chan = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko.SSHClient, "get_transport")
chan = ssh.get_transport().AndReturn(fake_chan)
fake_chan_session = self.m.CreateMockAnything()
chan_session = chan.open_session().AndReturn(fake_chan_session)
fake_chan_session.settimeout(3600.0)
chan_session.exec_command(mox.IgnoreArg())
fake_chan_session.recv(1024)
chan_session.recv_exit_status().AndReturn(exit_code)
fake_chan_session.close()
ssh.close()
# SFTP
self.m.StubOutWithMock(paramiko, "Transport")
transport = self.m.CreateMockAnything()
paramiko.Transport((mox.IgnoreArg(), 22)).AndReturn(transport)
transport.connect(hostkey=None, username="root", pkey=mox.IgnoreArg())
sftp = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko, "SFTPClient")
paramiko.SFTPClient.from_transport(transport).AndReturn(sftp)
sftp_file = self.m.CreateMockAnything()
sftp.open(mox.IgnoreArg(), 'w').MultipleTimes().AndReturn(sftp_file)
sftp_file.write(mox.IgnoreArg()).MultipleTimes()
sftp_file.close().MultipleTimes()
sftp.close()
transport.close()
def _setup_test_stack(self, stack_name):
t = template_format.parse(wp_template)
template = parser.Template(t)
@ -154,14 +96,14 @@ class CloudServersTest(HeatTestCase):
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
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,
key_name='test',
name=override_name and server.name or utils.PhysName(
stack_name, server.name),
security_groups=[],
@ -171,16 +113,12 @@ class CloudServersTest(HeatTestCase):
nics=None,
availability_zone=None,
block_device_mapping=None,
config_drive=None,
config_drive=True,
disk_config=None,
reservation_id=None,
files=mox.IgnoreArg(),
admin_pass=None).AndReturn(return_server)
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
self._mock_ssh_sftp(exit_code)
return server
def _create_test_server(self, return_server, name, override_name=False,
@ -193,7 +131,6 @@ class CloudServersTest(HeatTestCase):
return server
def _update_test_server(self, return_server, name, exit_code=0):
self._mock_ssh_sftp(exit_code)
self.m.StubOutWithMock(cloud_server.CloudServer, "nova")
cloud_server.CloudServer.nova().MultipleTimes().AndReturn(self.fc)
@ -201,229 +138,6 @@ class CloudServersTest(HeatTestCase):
image_data = mock.Mock(metadata={'os_distro': 'centos'})
self.fc.images.get = mock.Mock(return_value=image_data)
def test_script_raw_userdata(self):
stack_name = 'raw_userdata_s'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['user_data_format'] = \
'RAW'
server = cloud_server.CloudServer('WebServer',
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._mock_metadata_os_distro()
self.m.ReplayAll()
self.assertNotIn("/var/lib/cloud/data/cfn-userdata", server.script)
self.m.VerifyAll()
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.m.StubOutWithMock(clients.OpenStackClients, "nova")
clients.OpenStackClients.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.StubOutWithMock(clients.OpenStackClients, "nova")
clients.OpenStackClients.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,
admin_pass=None).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(image.ImageConstraint, "validate")
image.ImageConstraint.validate(
mox.IgnoreArg(), mox.IgnoreArg()).MultipleTimes().AndReturn(True)
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)
self.m.StubOutWithMock(clients.OpenStackClients, 'nova')
clients.OpenStackClients.nova().MultipleTimes().AndReturn(self.fc)
self.m.ReplayAll()
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_private_key(self):
stack_name = 'test_private_key'
(t, stack) = self._setup_test_stack(stack_name)
server = cloud_server.CloudServer('server_private_key',
t['Resources']['WebServer'],
stack)
# This gives the fake cloud server an id and created_time attribute
server._store_or_update(server.CREATE, server.IN_PROGRESS,
'test_store')
server.private_key = 'fake private key'
self.ctx = utils.dummy_context()
rs = db_api.resource_get_by_name_and_stack(self.ctx,
'server_private_key',
stack.id)
encrypted_key = rs.data[0]['value']
self.assertNotEqual(encrypted_key, "fake 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'}
@ -594,355 +308,6 @@ class CloudServersTest(HeatTestCase):
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.assertEqual("Error: The heat-script.sh script exited with a "
"non-zero exit status. To see the error message, "
"log into the server at 192.0.2.0 and view "
"/root/heat-script.log", 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.assertEqual("Error: The cfn-userdata script exited with a "
"non-zero exit status. To see the error message, "
"log into the server at 192.0.2.0 and view "
"/root/cfn-userdata.log", 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.StubOutWithMock(clients.OpenStackClients, "nova")
clients.OpenStackClients.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()
def test_ssh_exception_recovered(self):
return_server = self.fc.servers.list()[1]
stack_name = 'test_create_ssh_exception_recovered'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['image'] = 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['flavor'] = '256 MB Server'
server_name = 'test_create_ssh_exception_server'
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)
server._private_key = rsa_key
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1,
flavor=1,
key_name=None,
name=mox.IgnoreArg(),
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(),
admin_pass=None).AndReturn(return_server)
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
# Make paramiko raise an SSHException the first time
self.m.StubOutWithMock(paramiko, "Transport")
paramiko.Transport((mox.IgnoreArg(), 22)).AndRaise(
paramiko.SSHException())
transport = self.m.CreateMockAnything()
# The second time it works
paramiko.Transport((mox.IgnoreArg(), 22)).AndReturn(transport)
transport.connect(hostkey=None, username="root", pkey=mox.IgnoreArg())
sftp = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko, "SFTPClient")
paramiko.SFTPClient.from_transport(transport).AndReturn(sftp)
sftp_file = self.m.CreateMockAnything()
sftp.open(mox.IgnoreArg(), 'w').MultipleTimes().AndReturn(sftp_file)
sftp_file.write(mox.IgnoreArg()).MultipleTimes()
sftp_file.close().MultipleTimes()
sftp.close()
transport.close()
self.m.StubOutWithMock(paramiko, "SSHClient")
self.m.StubOutWithMock(paramiko, "MissingHostKeyPolicy")
ssh = self.m.CreateMockAnything()
paramiko.SSHClient().AndReturn(ssh)
paramiko.MissingHostKeyPolicy()
ssh.set_missing_host_key_policy(None)
ssh.connect(mox.IgnoreArg(),
key_filename=mox.IgnoreArg(),
username='root')
fake_chan = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko.SSHClient, "get_transport")
chan = ssh.get_transport().AndReturn(fake_chan)
fake_chan_session = self.m.CreateMockAnything()
chan_session = chan.open_session().AndReturn(fake_chan_session)
fake_chan_session.settimeout(3600.0)
chan_session.exec_command(mox.IgnoreArg())
fake_chan_session.recv(1024)
chan_session.recv_exit_status().AndReturn(0)
fake_chan_session.close()
ssh.close()
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.assertEqual((server.CREATE, server.COMPLETE), server.state)
self.m.VerifyAll()
def test_ssh_exception_failed(self):
return_server = self.fc.servers.list()[1]
stack_name = 'test_create_ssh_exception_failed'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['image'] = 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['flavor'] = '256 MB Server'
server_name = 'test_create_ssh_exception_server'
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)
server._private_key = rsa_key
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1,
flavor=1,
key_name=None,
name=mox.IgnoreArg(),
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(),
admin_pass=None).AndReturn(return_server)
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
# Make paramiko raise an SSHException every time
self.m.StubOutWithMock(paramiko, "Transport")
paramiko.Transport((mox.IgnoreArg(), 22)).MultipleTimes().AndRaise(
paramiko.SSHException())
self.m.ReplayAll()
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertEqual("Error: Failed to establish SSH connection after 30 "
"tries", str(exc))
self.m.VerifyAll()
def test_eof_error_recovered(self):
return_server = self.fc.servers.list()[1]
stack_name = 'test_create_ssh_exception_recovered'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['image'] = 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['flavor'] = '256 MB Server'
server_name = 'test_create_ssh_exception_server'
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)
server._private_key = rsa_key
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1,
flavor=1,
key_name=None,
name=mox.IgnoreArg(),
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(),
admin_pass=None).AndReturn(return_server)
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
transport = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko, "Transport")
paramiko.Transport((mox.IgnoreArg(), 22)).MultipleTimes().\
AndReturn(transport)
# Raise an EOFError the first time
transport.connect(hostkey=None, username="root",
pkey=mox.IgnoreArg()).AndRaise(EOFError)
transport.connect(hostkey=None, username="root",
pkey=mox.IgnoreArg())
sftp = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko, "SFTPClient")
paramiko.SFTPClient.from_transport(transport).AndReturn(sftp)
sftp_file = self.m.CreateMockAnything()
sftp.open(mox.IgnoreArg(), 'w').MultipleTimes().AndReturn(sftp_file)
sftp_file.write(mox.IgnoreArg()).MultipleTimes()
sftp_file.close().MultipleTimes()
sftp.close()
transport.close()
self.m.StubOutWithMock(paramiko, "SSHClient")
self.m.StubOutWithMock(paramiko, "MissingHostKeyPolicy")
ssh = self.m.CreateMockAnything()
paramiko.SSHClient().AndReturn(ssh)
paramiko.MissingHostKeyPolicy()
ssh.set_missing_host_key_policy(None)
ssh.connect(mox.IgnoreArg(),
key_filename=mox.IgnoreArg(),
username='root')
fake_chan = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko.SSHClient, "get_transport")
chan = ssh.get_transport().AndReturn(fake_chan)
fake_chan_session = self.m.CreateMockAnything()
chan_session = chan.open_session().AndReturn(fake_chan_session)
fake_chan_session.settimeout(3600.0)
chan_session.exec_command(mox.IgnoreArg())
fake_chan_session.recv(1024)
chan_session.recv_exit_status().AndReturn(0)
fake_chan_session.close()
ssh.close()
self.m.ReplayAll()
scheduler.TaskRunner(server.create)()
self.assertEqual((server.CREATE, server.COMPLETE), server.state)
self.m.VerifyAll()
def test_eof_error_failed(self):
return_server = self.fc.servers.list()[1]
stack_name = 'test_create_ssh_exception_failed'
(t, stack) = self._setup_test_stack(stack_name)
t['Resources']['WebServer']['Properties']['image'] = 'CentOS 5.2'
t['Resources']['WebServer']['Properties']['flavor'] = '256 MB Server'
server_name = 'test_create_ssh_exception_server'
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)
server._private_key = rsa_key
self.m.StubOutWithMock(self.fc.servers, 'create')
self.fc.servers.create(
image=1,
flavor=1,
key_name=None,
name=mox.IgnoreArg(),
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(),
admin_pass=None).AndReturn(return_server)
self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
cloud_server.CloudServer.script = "foobar"
transport = self.m.CreateMockAnything()
self.m.StubOutWithMock(paramiko, "Transport")
paramiko.Transport((mox.IgnoreArg(), 22)).MultipleTimes().\
AndReturn(transport)
# Raise an EOFError every time
transport.connect(hostkey=None, username="root",
pkey=mox.IgnoreArg()).MultipleTimes().\
AndRaise(EOFError)
self.m.ReplayAll()
create = scheduler.TaskRunner(server.create)
exc = self.assertRaises(exception.ResourceFailure, create)
self.assertEqual("Error: Failed to establish SSH connection after 30 "
"tries", str(exc))
self.m.VerifyAll()
@mock.patch.object(clients.OpenStackClients, 'nova')
@mock.patch.object(resource.Resource, 'data_set')
def test_create_store_admin_pass_resource_data(self,
@ -957,8 +322,6 @@ class CloudServersTest(HeatTestCase):
t['Resources']['WebServer']['Properties']['save_admin_pass'] = True
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
server._sftp_files = mock.Mock()
server._run_ssh_command = mock.Mock(return_value=0)
mock_nova.return_value = self.fc
self.fc.servers.create = mock.Mock(return_value=return_server)
@ -982,8 +345,6 @@ class CloudServersTest(HeatTestCase):
t['Resources']['WebServer']['Properties']['save_admin_pass'] = False
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
server._sftp_files = mock.Mock()
server._run_ssh_command = mock.Mock(return_value=0)
mock_nova.return_value = self.fc
self.fc.servers.create = mock.Mock(return_value=return_server)
@ -1007,8 +368,6 @@ class CloudServersTest(HeatTestCase):
t['Resources']['WebServer']['Properties']['save_admin_pass'] = None
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
server._sftp_files = mock.Mock()
server._run_ssh_command = mock.Mock(return_value=0)
mock_nova.return_value = self.fc
self.fc.servers.create = mock.Mock(return_value=return_server)
@ -1030,8 +389,6 @@ class CloudServersTest(HeatTestCase):
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
server._sftp_files = mock.Mock()
server._run_ssh_command = mock.Mock(return_value=0)
mock_nova.return_value = self.fc
self.fc.servers.create = mock.Mock(return_value=return_server)
@ -1060,3 +417,49 @@ class CloudServersTest(HeatTestCase):
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
self.assertEqual('foo', server.FnGetAtt('admin_pass'))
@mock.patch.object(clients.OpenStackClients, 'nova')
def _test_server_config_drive(self, user_data, config_drive, result,
mock_nova):
return_server = self.fc.servers.list()[1]
stack_name = 'no_user_data'
(t, stack) = self._setup_test_stack(stack_name)
properties = t['Resources']['WebServer']['Properties']
properties['user_data'] = user_data
properties['config_drive'] = config_drive
server = cloud_server.CloudServer('WebServer',
t['Resources']['WebServer'], stack)
mock_nova.return_value = self.fc
mock_servers_create = mock.Mock(return_value=return_server)
self.fc.servers.create = mock_servers_create
scheduler.TaskRunner(server.create)()
mock_servers_create.assert_called_with(
image=mock.ANY,
flavor=mock.ANY,
key_name=mock.ANY,
name=mock.ANY,
security_groups=mock.ANY,
userdata=mock.ANY,
scheduler_hints=mock.ANY,
meta=mock.ANY,
nics=mock.ANY,
availability_zone=mock.ANY,
block_device_mapping=mock.ANY,
config_drive=result,
disk_config=mock.ANY,
reservation_id=mock.ANY,
files=mock.ANY,
admin_pass=mock.ANY)
def test_server_user_data_no_config_drive(self):
self._test_server_config_drive("my script", False, True)
def test_server_user_data_config_drive(self):
self._test_server_config_drive("my script", True, True)
def test_server_no_user_data_config_drive(self):
self._test_server_config_drive(None, True, True)
def test_server_no_user_data_no_config_drive(self):
self._test_server_config_drive(None, False, False)

View File

@ -1,2 +1 @@
-e git://github.com/rackspace/pyrax.git@4441a5bf900f19fdb2304a0f9ed15b43151541d8#egg=pyrax
paramiko>=1.9.0

View File

@ -341,13 +341,9 @@ class Server(stack_user.StackUser):
return super(Server, self).physical_resource_name()
def _personality(self):
def _config_drive(self):
# 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)
return self.properties.get(self.CONFIG_DRIVE)
@staticmethod
def _get_deployments_metadata(heatclient, server_id):
@ -503,9 +499,10 @@ class Server(stack_user.StackUser):
block_device_mapping = self._build_block_device_mapping(
self.properties.get(self.BLOCK_DEVICE_MAPPING))
reservation_id = self.properties.get(self.RESERVATION_ID)
config_drive = self.properties.get(self.CONFIG_DRIVE)
disk_config = self.properties.get(self.DISK_CONFIG)
admin_pass = self.properties.get(self.ADMIN_PASS) or None
personality_files = self.properties.get(self.PERSONALITY)
key_name = self.properties.get(self.KEY_NAME)
server = None
try:
@ -513,7 +510,7 @@ class Server(stack_user.StackUser):
name=self.physical_resource_name(),
image=image,
flavor=flavor_id,
key_name=self._key_name(),
key_name=key_name,
security_groups=security_groups,
userdata=userdata,
meta=instance_meta,
@ -522,9 +519,9 @@ class Server(stack_user.StackUser):
availability_zone=availability_zone,
block_device_mapping=block_device_mapping,
reservation_id=reservation_id,
config_drive=config_drive,
config_drive=self._config_drive(),
disk_config=disk_config,
files=self._personality(),
files=personality_files,
admin_pass=admin_pass)
finally:
# Avoid a race condition where the thread could be cancelled
@ -911,7 +908,7 @@ class Server(stack_user.StackUser):
# retrieve provider's absolute limits if it will be needed
metadata = self.properties.get(self.METADATA)
personality = self._personality()
personality = self.properties.get(self.PERSONALITY)
if metadata is not None or personality is not None:
limits = nova_utils.absolute_limits(self.nova())

View File

@ -8,7 +8,6 @@ kombu>=2.4.8
lxml>=2.3
netaddr>=0.7.6
oslo.config>=1.2.0
paramiko>=1.9.0
PasteDeploy>=1.5.0
pbr>=0.6,!=0.7,<1.0
posix_ipc