From 285f2cbc28dff908b8fa17e517125ea9269c3a6c Mon Sep 17 00:00:00 2001 From: Jason Dunsmore Date: Wed, 26 Mar 2014 14:01:24 -0500 Subject: [PATCH] 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 --- .../rackspace/resources/cloud_server.py | 271 +------ .../tests/test_rackspace_cloud_server.py | 695 ++---------------- contrib/rackspace/requirements.txt | 1 - heat/engine/resources/server.py | 19 +- requirements.txt | 1 - 5 files changed, 60 insertions(+), 927 deletions(-) diff --git a/contrib/rackspace/rackspace/resources/cloud_server.py b/contrib/rackspace/rackspace/resources/cloud_server.py index 7f568b06f8..ae7095ed10 100644 --- a/contrib/rackspace/rackspace/resources/cloud_server.py +++ b/contrib/rackspace/rackspace/resources/cloud_server.py @@ -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): diff --git a/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py b/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py index e7286286b5..de3517ea7e 100644 --- a/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py +++ b/contrib/rackspace/rackspace/tests/test_rackspace_cloud_server.py @@ -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) diff --git a/contrib/rackspace/requirements.txt b/contrib/rackspace/requirements.txt index 1352e015cd..3a522c7c74 100644 --- a/contrib/rackspace/requirements.txt +++ b/contrib/rackspace/requirements.txt @@ -1,2 +1 @@ -e git://github.com/rackspace/pyrax.git@4441a5bf900f19fdb2304a0f9ed15b43151541d8#egg=pyrax -paramiko>=1.9.0 diff --git a/heat/engine/resources/server.py b/heat/engine/resources/server.py index e9758c89d3..7f7232d857 100644 --- a/heat/engine/resources/server.py +++ b/heat/engine/resources/server.py @@ -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()) diff --git a/requirements.txt b/requirements.txt index 5cd50606b5..2616ba7412 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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