--- /dev/null
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import tempfile
+
+import json
+import paramiko
+from Crypto.PublicKey import RSA
+import novaclient.exceptions as novaexception
+
+from heat.common import exception
+from heat.openstack.common import log as logging
+from heat.engine import scheduler
+from heat.engine.resources import instance
+from heat.engine.resources.rackspace import rackspace_resource
+from heat.db.sqlalchemy import api as db_api
+
+logger = logging.getLogger(__name__)
+
+
+class CloudServer(instance.Instance):
+ """Resource for Rackspace Cloud Servers."""
+
+ properties_schema = {'ServerName': {'Type': 'String', 'Required': True},
+ 'Flavor': {'Type': 'String', 'Required': True},
+ 'ImageName': {'Type': 'String', 'Required': True},
+ 'UserData': {'Type': 'String'},
+ 'PublicKey': {'Type': 'String'},
+ 'Volumes': {'Type': 'List'}}
+
+ attributes_schema = {'PrivateDnsName': ('Private DNS name of the specified'
+ ' instance.'),
+ 'PublicDnsName': ('Public DNS name of the specified '
+ 'instance.'),
+ 'PrivateIp': ('Private IP address of the specified '
+ 'instance.'),
+ 'PublicIp': ('Public IP address of the specified '
+ 'instance.')}
+
+ base_script = """#!/bin/bash
+
+# Install cloud-init and heat-cfntools
+%s
+# Create data source for cloud-init
+mkdir -p /var/lib/cloud/seed/nocloud-net
+mv /tmp/userdata /var/lib/cloud/seed/nocloud-net/user-data
+touch /var/lib/cloud/seed/nocloud-net/meta-data
+chmod 600 /var/lib/cloud/seed/nocloud-net/*
+
+# Run cloud-init & cfn-init
+cloud-init start
+bash -x /var/lib/cloud/data/cfn-userdata > /root/cfn-userdata.log 2>&1
+"""
+
+ ubuntu_script = base_script % """\
+apt-get update
+apt-get install -y cloud-init python-boto python-pip gcc python-dev
+pip install heat-cfntools
+"""
+
+ fedora_script = base_script % """\
+yum install -y cloud-init python-boto python-pip gcc python-devel
+pip-python install heat-cfntools
+"""
+
+ # TODO(jason): Install cloud-init & other deps from third-party repos
+ centos_script = base_script % """\
+yum install -y cloud-init python-boto python-pip gcc python-devel
+pip-python install heat-cfntools
+"""
+
+ # TODO(jason): Install cloud-init & other deps from third-party repos
+ arch_script = base_script % """\
+pacman -S --noconfirm python-pip gcc
+"""
+
+ # TODO(jason): Install cloud-init & other deps from third-party repos
+ gentoo_script = base_script % """\
+emerge cloud-init python-boto python-pip gcc python-devel
+"""
+
+ # TODO(jason): Install cloud-init & other deps from third-party repos
+ opensuse_script = base_script % """\
+zypper --non-interactive rm patterns-openSUSE-minimal_base-conflicts
+zypper --non-interactive in cloud-init python-boto python-pip gcc python-devel
+"""
+
+ # List of supported Linux distros and their corresponding config scripts
+ image_scripts = {'arch': None,
+ 'centos': None,
+ 'debian': None,
+ 'fedora': fedora_script,
+ 'gentoo': None,
+ 'opensuse': None,
+ 'rhel': None,
+ 'ubuntu': ubuntu_script}
+
+ # Cache data retrieved from APIs in class attributes
+ _image_id_map = {}
+ _distro_map = {}
+ _server_map = {}
+
+ # Template keys supported for handle_update. Properties not
+ # listed here trigger an UpdateReplace
+ update_allowed_keys = ('Metadata', 'Properties')
+ update_allowed_properties = ('Flavor', 'ServerName')
+
+ def __init__(self, name, json_snippet, stack):
+ super(CloudServer, self).__init__(name, json_snippet, stack)
+ self._private_key = None
+ self.rs = rackspace_resource.RackspaceResource(name,
+ json_snippet,
+ stack)
+
+ def nova(self):
+ return self.rs.nova() # Override the Instance method
+
+ def cinder(self):
+ return self.rs.cinder()
+
+ @property
+ def server(self):
+ """Get the Cloud Server object."""
+ if self.resource_id in self.__class__._server_map:
+ return self.__class__._server_map[self.resource_id]
+ else:
+ server = self.nova().servers.get(self.resource_id)
+ self.__class__._server_map[self.resource_id] = server
+ return server
+
+ @property
+ def image_id(self):
+ """Get the image ID corresponding to the ImageName property."""
+ image_name = self.properties['ImageName']
+ if image_name in self.__class__._image_id_map:
+ return self.__class__._image_id_map[image_name]
+ else:
+ image_id = self._get_image_id(image_name)
+ self.__class__._image_id_map[image_name] = image_id
+ return image_id
+
+ @property
+ def distro(self):
+ """Get the Linux distribution for this server."""
+ if self.image_id in self.__class__._distro_map:
+ return self.__class__._distro_map[self.image_id]
+ else:
+ image = self.nova().images.get(self.image_id)
+ distro = image.metadata['os_distro']
+ self.__class__._distro_map[self.image_id] = distro
+ return distro
+
+ @property
+ def script(self):
+ """Get the config script for the Cloud Server image."""
+ return self.image_scripts[self.distro]
+
+ @property
+ def flavors(self):
+ """Get the flavors from the API or cache (updated every 6 hours)."""
+ return [flavor.id for flavor in self.nova().flavors.list()]
+
+ @property
+ def private_key(self):
+ """Return the private SSH key for the resource."""
+ if self._private_key:
+ return self._private_key
+ if self.id is not None:
+ private_key = db_api.resource_data_get(self, 'private_key')
+ if not private_key:
+ return None
+ self._private_key = private_key
+ return private_key
+
+ @private_key.setter
+ def private_key(self, private_key):
+ """Save the resource's private SSH key to the database."""
+ self._private_key = private_key
+ if self.id is not None:
+ db_api.resource_data_set(self, 'private_key', private_key, True)
+
+ def _get_ip(self, ip_type):
+ """Return the IP of the Cloud Server."""
+ def ip_not_found():
+ exc = exception.Error("Could not determine the %s IP of %s." %
+ (ip_type, self.properties['ImageName']))
+ raise exception.ResourceFailure(exc)
+
+ if ip_type not in self.server.addresses:
+ ip_not_found()
+ for ip in self.server.addresses[ip_type]:
+ if ip['version'] == 4:
+ return ip['addr']
+ ip_not_found()
+
+ @property
+ def public_ip(self):
+ """Return the public IP of the Cloud Server."""
+ return self._get_ip('public')
+
+ @property
+ def private_ip(self):
+ """Return the private IP of the Cloud Server."""
+ try:
+ return self._get_ip('private')
+ except exception.ResourceFailure as ex:
+ logger.info(ex.message)
+
+ def validate(self):
+ """Validate user parameters."""
+ if self.properties['Flavor'] not in self.flavors:
+ return {'Error': "Flavor not found."}
+ if not self.script:
+ return {'Error': "Image %s not supported." %
+ self.properties['ImageName']}
+
+ def _run_ssh_command(self, command):
+ """Run a shell command on the Cloud Server via SSH."""
+ with tempfile.NamedTemporaryFile() as private_key_file:
+ private_key_file.write(self.private_key)
+ private_key_file.seek(0)
+ ssh = paramiko.SSHClient()
+ ssh.set_missing_host_key_policy(paramiko.MissingHostKeyPolicy())
+ ssh.connect(self.public_ip,
+ username="root",
+ key_filename=private_key_file.name)
+ stdin, stdout, stderr = ssh.exec_command(command)
+ logger.debug(stdout.read())
+ logger.debug(stderr.read())
+
+ def _sftp_files(self, files):
+ """Transfer files to the Cloud Server via SFTP."""
+ with tempfile.NamedTemporaryFile() as private_key_file:
+ private_key_file.write(self.private_key)
+ private_key_file.seek(0)
+ pkey = paramiko.RSAKey.from_private_key_file(private_key_file.name)
+ transport = paramiko.Transport((self.public_ip, 22))
+ transport.connect(hostkey=None, username="root", pkey=pkey)
+ sftp = paramiko.SFTPClient.from_transport(transport)
+ for remote_file in files:
+ sftp_file = sftp.open(remote_file['path'], 'w')
+ sftp_file.write(remote_file['data'])
+ sftp_file.close()
+
+ def handle_create(self):
+ """Create a Rackspace Cloud Servers container.
+
+ Rackspace Cloud Servers does not have the metadata service
+ running, so we have to transfer the user-data file to the
+ server and then trigger cloud-init.
+ """
+ # Retrieve server creation parameters from properties
+ flavor = self.properties['Flavor']
+ user_public_key = self.properties['PublicKey'] or ''
+
+ # Generate SSH public/private keypair
+ rsa = RSA.generate(1024)
+ self.private_key = rsa.exportKey()
+ public_key = rsa.publickey().exportKey('OpenSSH')
+ public_keys = public_key + "\n" + user_public_key
+ personality_files = {"/root/.ssh/authorized_keys": public_keys}
+
+ # Create server
+ client = self.nova().servers
+ server = client.create(self.properties['ServerName'],
+ self.image_id,
+ flavor,
+ files=personality_files)
+
+ # Save resource ID to db
+ self.resource_id_set(server.id)
+
+ return server, scheduler.TaskRunner(self._attach_volumes_task())
+
+ def _attach_volumes_task(self):
+ tasks = (scheduler.TaskRunner(self._attach_volume, volume_id, device)
+ for volume_id, device in self.volumes())
+ return scheduler.PollingTaskGroup(tasks)
+
+ def _attach_volume(self, volume_id, device):
+ self.nova().volumes.create_server_volume(self.server.id,
+ volume_id,
+ device or None)
+ yield
+ volume = self.cinder().get(volume_id)
+ while volume.status in ('available', 'attaching'):
+ yield
+ volume.get()
+
+ if volume.status != 'in-use':
+ raise exception.Error(volume.status)
+
+ def _detach_volumes_task(self):
+ tasks = (scheduler.TaskRunner(self._detach_volume, volume_id)
+ for volume_id, device in self.volumes())
+ return scheduler.PollingTaskGroup(tasks)
+
+ def _detach_volume(self, volume_id):
+ volume = self.cinder().get(volume_id)
+ volume.detach()
+ yield
+ while volume.status in ('in-use', 'detaching'):
+ yield
+ volume.get()
+
+ if volume.status != 'available':
+ raise exception.Error(volume.status)
+
+ def check_create_complete(self, cookie):
+ """Check if server creation is complete and handle server configs."""
+ if not self._check_active(cookie):
+ return False
+
+ # Create heat-script and userdata files on server
+ raw_userdata = self.properties['UserData'] or ''
+ userdata = self._build_userdata(raw_userdata)
+ files = [{'path': "/tmp/userdata", 'data': userdata},
+ {'path': "/root/heat-script.sh", 'data': self.script}]
+ self._sftp_files(files)
+
+ # Connect via SSH and run script
+ command = "bash -ex /root/heat-script.sh > /root/heat-script.log 2>&1"
+ self._run_ssh_command(command)
+
+ return True
+
+ # TODO(jason): Make this consistent with Instance and inherit
+ def _delete_server(self, server):
+ """Return a coroutine that deletes the Cloud Server."""
+ server.delete()
+ while True:
+ yield
+ try:
+ server.get()
+ if server.status == "ERROR":
+ exc = exception.Error("Deletion of server %s failed." %
+ server.name)
+ raise exception.ResourceFailure(exc)
+ except novaexception.NotFound:
+ break
+
+ def handle_update(self, json_snippet, tmpl_diff, prop_diff):
+ """Try to update a Cloud Server's parameters.
+
+ If the Cloud Server's Metadata or Flavor changed, update the
+ Cloud Server. If any other parameters changed, re-create the
+ Cloud Server with the new parameters.
+ """
+ if 'Metadata' in tmpl_diff:
+ self.metadata = json_snippet['Metadata']
+ metadata_string = json.dumps(self.metadata)
+
+ files = [{'path': "/var/cache/heat-cfntools/last_metadata",
+ 'data': metadata_string}]
+ self._sftp_files(files)
+
+ command = "bash -x /var/lib/cloud/data/cfn-userdata > " + \
+ "/root/cfn-userdata.log 2>&1"
+ self._run_ssh_command(command)
+
+ if 'Flavor' in prop_diff:
+ self.flavor = json_snippet['Properties']['Flavor']
+ self.server.resize(self.flavor)
+ resize = scheduler.TaskRunner(self._check_resize,
+ self.server,
+ self.flavor)
+ resize(wait_time=1.0)
+
+ # If ServerName is the only update, fail update
+ if prop_diff.keys() == ['ServerName'] and \
+ tmpl_diff.keys() == ['Properties']:
+ raise exception.NotSupported(feature="Cloud Server rename")
+ # Other updates were successful, so don't cause update to fail
+ elif 'ServerName' in prop_diff:
+ logger.info("Cloud Server rename not supported.")
+
+ return True
+
+ def _resolve_attribute(self, key):
+ """Return the method that provides a given template attribute."""
+ attribute_function = {'PublicIp': self.public_ip,
+ 'PrivateIp': self.private_ip,
+ 'PublicDnsName': self.public_ip,
+ 'PrivateDnsName': self.public_ip}
+ if key not in attribute_function:
+ raise exception.InvalidTemplateAttribute(resource=self.name,
+ key=key)
+ function = attribute_function[key]
+ logger.info('%s._resolve_attribute(%s) == %s'
+ % (self.name, key, function))
+ return unicode(function)
+
+
+# pyrax module is required to work with Rackspace cloud server provider.
+# If it is not installed, don't register cloud server provider
+def resource_mapping():
+ if rackspace_resource.PYRAX_INSTALLED:
+ return {'Rackspace::Cloud::Server': CloudServer}
+ else:
+ return {}
--- /dev/null
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import copy
+
+import mox
+import paramiko
+import novaclient
+
+from heat.db import api as db_api
+from heat.tests.v1_1 import fakes
+from heat.common import template_format
+from heat.common import exception
+from heat.engine import parser
+from heat.engine import resource
+from heat.engine import scheduler
+from heat.engine import environment
+from heat.engine.resources.rackspace import cloud_server
+from heat.engine.resources.rackspace import rackspace_resource
+from heat.openstack.common import uuidutils
+from heat.tests.common import HeatTestCase
+from heat.tests.utils import setup_dummy_db
+
+
+wp_template = '''
+{
+ "AWSTemplateFormatVersion" : "2010-09-09",
+ "Description" : "WordPress",
+ "Parameters" : {
+ "Flavor" : {
+ "Description" : "Rackspace Cloud Server flavor",
+ "Type" : "String",
+ "Default" : "2",
+ "AllowedValues" : [ "2", "3", "4", "5", "6", "7", "8" ],
+ "ConstraintDescription" : "must be a valid Rackspace Cloud Server flavor"
+ },
+ },
+ "Resources" : {
+ "WebServer": {
+ "Type": "Rackspace::Cloud::Server",
+ "Properties": {
+ "ImageName" : "Fedora 17 (Beefy Miracle)",
+ "ServerName" : "Heat test",
+ "Flavor" : "2",
+ "UserData" : "wordpress"
+ }
+ }
+ }
+}
+'''
+
+
+class RackspaceCloudServerTest(HeatTestCase):
+ def setUp(self):
+ super(RackspaceCloudServerTest, self).setUp()
+ self.fc = fakes.FakeClient()
+ setup_dummy_db()
+ # Test environment may not have pyrax client library installed and if
+ # pyrax is not installed resource class would not be registered.
+ # So register resource provider class explicitly for unit testing.
+ resource._register_class("Rackspace::Cloud::Server",
+ cloud_server.CloudServer)
+
+ f2 = self.m.CreateMockAnything()
+ f2.id = '2'
+ f3 = self.m.CreateMockAnything()
+ f3.id = '3'
+ f4 = self.m.CreateMockAnything()
+ f4.id = '4'
+ f5 = self.m.CreateMockAnything()
+ f5.id = '5'
+ f6 = self.m.CreateMockAnything()
+ f6.id = '6'
+ f7 = self.m.CreateMockAnything()
+ f7.id = '7'
+ f8 = self.m.CreateMockAnything()
+ f8.id = '8'
+ self.flavors = [f2, f3, f4, f5, f6, f7, f8]
+
+ def _setup_test_stack(self, stack_name):
+ t = template_format.parse(wp_template)
+ template = parser.Template(t)
+ stack = parser.Stack(None, stack_name, template,
+ environment.Environment({'Flavor': '2'}),
+ stack_id=uuidutils.generate_uuid())
+ return (t, stack)
+
+ def _mock_ssh_sftp(self):
+ # SSH
+ self.m.StubOutWithMock(paramiko, "SSHClient")
+ self.m.StubOutWithMock(paramiko, "MissingHostKeyPolicy")
+ ssh = self.m.CreateMockAnything()
+ paramiko.SSHClient().AndReturn(ssh)
+ paramiko.MissingHostKeyPolicy()
+ ssh.set_missing_host_key_policy(None)
+ ssh.connect(mox.IgnoreArg(),
+ key_filename=mox.IgnoreArg(),
+ username='root')
+ stdin = self.m.CreateMockAnything()
+ stdout = self.m.CreateMockAnything()
+ stderr = self.m.CreateMockAnything()
+ stdout.read().AndReturn("stdout")
+ stderr.read().AndReturn("stderr")
+ ssh.exec_command(mox.IgnoreArg()).AndReturn((stdin, stdout, stderr))
+
+ # SFTP
+ self.m.StubOutWithMock(paramiko, "Transport")
+ transport = self.m.CreateMockAnything()
+ paramiko.Transport((mox.IgnoreArg(), 22)).AndReturn(transport)
+ transport.connect(hostkey=None, username="root", pkey=mox.IgnoreArg())
+ sftp = self.m.CreateMockAnything()
+ self.m.StubOutWithMock(paramiko, "SFTPClient")
+ paramiko.SFTPClient.from_transport(transport).AndReturn(sftp)
+ sftp_file = self.m.CreateMockAnything()
+ sftp.open(mox.IgnoreArg(), 'w').AndReturn(sftp_file)
+ sftp_file.write(mox.IgnoreArg())
+ sftp_file.close()
+ sftp_file = self.m.CreateMockAnything()
+ sftp.open(mox.IgnoreArg(), 'w').AndReturn(sftp_file)
+ sftp_file.write(mox.IgnoreArg())
+ sftp_file.close()
+
+ def _setup_test_cs(self, return_server, name):
+ stack_name = '%s_stack' % name
+ (t, stack) = self._setup_test_stack(stack_name)
+
+ server_name = "Heat test"
+ t['Resources']['WebServer']['Properties']['ServerName'] = server_name
+ cs_name = 'Fedora 17 (Beefy Miracle)'
+ t['Resources']['WebServer']['Properties']['ImageName'] = cs_name
+ t['Resources']['WebServer']['Properties']['Flavor'] = '2'
+
+ cs = cloud_server.CloudServer('%s_name' % name,
+ t['Resources']['WebServer'], stack)
+ cs.t = cs.stack.resolve_runtime_data(cs.t)
+
+ flavor = t['Resources']['WebServer']['Properties']['Flavor']
+
+ self.m.StubOutWithMock(self.fc.servers, 'create')
+ self.fc.servers.create(server_name, "1", flavor,
+ files=mox.IgnoreArg()).AndReturn(return_server)
+ return_server.adminPass = "foobar"
+
+ self.m.StubOutWithMock(cloud_server.CloudServer, 'image_id')
+ cloud_server.CloudServer.image_id = "1"
+
+ self.m.StubOutWithMock(cloud_server.CloudServer, 'script')
+ cloud_server.CloudServer.script = "foobar"
+
+ self.m.StubOutWithMock(rackspace_resource.RackspaceResource, "nova")
+ rackspace_resource.RackspaceResource.nova().MultipleTimes()\
+ .AndReturn(self.fc)
+
+ self._mock_ssh_sftp()
+ return cs
+
+ def _create_test_cs(self, return_server, name):
+ cs = self._setup_test_cs(return_server, name)
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(cs.create)()
+ return cs
+
+ def _update_test_cs(self, return_server, name):
+ self._mock_ssh_sftp()
+ self.m.StubOutWithMock(rackspace_resource.RackspaceResource, "nova")
+ rackspace_resource.RackspaceResource.nova().MultipleTimes()\
+ .AndReturn(self.fc)
+
+ def test_cs_create(self):
+ return_server = self.fc.servers.list()[1]
+ cs = self._create_test_cs(return_server, 'test_cs_create')
+ # this makes sure the auto increment worked on cloud server creation
+ self.assertTrue(cs.id > 0)
+
+ expected_public = return_server.networks['public'][0]
+ expected_private = return_server.networks['private'][0]
+ self.assertEqual(cs.FnGetAtt('PublicIp'), expected_public)
+ self.assertEqual(cs.FnGetAtt('PrivateIp'), expected_private)
+ self.assertEqual(cs.FnGetAtt('PublicDnsName'), expected_public)
+ self.assertEqual(cs.FnGetAtt('PrivateDnsName'), expected_public)
+
+ self.m.VerifyAll()
+
+ def test_cs_create_with_image_name(self):
+ return_server = self.fc.servers.list()[1]
+ cs = self._setup_test_cs(return_server, 'test_cs_create_image_id')
+
+ self.m.ReplayAll()
+ scheduler.TaskRunner(cs.create)()
+
+ # this makes sure the auto increment worked on cloud server creation
+ self.assertTrue(cs.id > 0)
+
+ expected_public = return_server.networks['public'][0]
+ expected_private = return_server.networks['private'][0]
+ self.assertEqual(cs.FnGetAtt('PublicIp'), expected_public)
+ self.assertEqual(cs.FnGetAtt('PrivateIp'), expected_private)
+ self.assertEqual(cs.FnGetAtt('PublicDnsName'), expected_public)
+ self.assertEqual(cs.FnGetAtt('PrivateDnsName'), expected_public)
+ self.assertRaises(exception.InvalidTemplateAttribute,
+ cs.FnGetAtt, 'foo')
+ self.m.VerifyAll()
+
+ def test_cs_create_image_name_err(self):
+ stack_name = 'test_cs_create_image_name_err_stack'
+ (t, stack) = self._setup_test_stack(stack_name)
+
+ # create a cloud server with non exist image name
+ t['Resources']['WebServer']['Properties']['ImageName'] = 'Slackware'
+
+ # Mock flavors
+ self.m.StubOutWithMock(cloud_server.CloudServer, "flavors")
+ cloud_server.CloudServer.flavors.__contains__('2').AndReturn(True)
+ cloud_server.CloudServer.script = None
+ self.m.ReplayAll()
+
+ cs = cloud_server.CloudServer('cs_create_image_err',
+ t['Resources']['WebServer'], stack)
+
+ self.assertEqual({'Error': "Image %s not supported." % 'Slackware'},
+ cs.validate())
+
+ self.m.VerifyAll()
+
+ def test_cs_create_flavor_err(self):
+ """validate() should throw an if the Flavor is invalid."""
+ stack_name = 'test_cs_create_flavor_err_stack'
+ (t, stack) = self._setup_test_stack(stack_name)
+
+ # create a cloud server with non exist image name
+ t['Resources']['WebServer']['Properties']['Flavor'] = '1'
+
+ # Mock flavors
+ self.m.StubOutWithMock(cloud_server.CloudServer, "flavors")
+ flavors = ['2', '3', '4', '5', '6', '7', '8']
+ cloud_server.CloudServer.flavors = flavors
+ self.m.ReplayAll()
+
+ cs = cloud_server.CloudServer('cs_create_flavor_err',
+ t['Resources']['WebServer'], stack)
+
+ self.assertEqual({'Error': "Flavor not found."}, cs.validate())
+
+ self.m.VerifyAll()
+
+ def test_cs_create_delete(self):
+ return_server = self.fc.servers.list()[1]
+ cs = self._create_test_cs(return_server,
+ 'test_cs_create_delete')
+ cs.resource_id = 1234
+
+ # this makes sure the auto-increment worked on cloud server creation
+ self.assertTrue(cs.id > 0)
+
+ self.m.StubOutWithMock(self.fc.client, 'get_servers_1234')
+ get = self.fc.client.get_servers_1234
+ get().AndRaise(novaclient.exceptions.NotFound(404))
+ mox.Replay(get)
+
+ cs.delete()
+ self.assertTrue(cs.resource_id is None)
+ self.assertEqual(cs.state, (cs.DELETE, cs.COMPLETE))
+ self.m.VerifyAll()
+
+ def test_cs_update_metadata(self):
+ return_server = self.fc.servers.list()[1]
+ cs = self._create_test_cs(return_server, 'test_cs_metadata_update')
+ self.m.UnsetStubs()
+ self._update_test_cs(return_server, 'test_cs_metadata_update')
+ self.m.ReplayAll()
+ update_template = copy.deepcopy(cs.t)
+ update_template['Metadata'] = {'test': 123}
+ self.assertEqual(None, cs.update(update_template))
+ self.assertEqual(cs.metadata, {'test': 123})
+
+ def test_cs_update_replace(self):
+ return_server = self.fc.servers.list()[1]
+ cs = self._create_test_cs(return_server, 'test_cs_update')
+
+ update_template = copy.deepcopy(cs.t)
+ update_template['Notallowed'] = {'test': 123}
+ self.assertRaises(resource.UpdateReplace, cs.update, update_template)
+
+ def test_cs_update_properties(self):
+ return_server = self.fc.servers.list()[1]
+ cs = self._create_test_cs(return_server, 'test_cs_update')
+
+ update_template = copy.deepcopy(cs.t)
+ update_template['Properties']['UserData'] = 'mustreplace'
+ self.assertRaises(resource.UpdateReplace,
+ cs.update, update_template)
+
+ def test_cs_status_build(self):
+ return_server = self.fc.servers.list()[0]
+ cs = self._setup_test_cs(return_server, 'test_cs_status_build')
+ cs.resource_id = 1234
+
+ # Bind fake get method which cs.check_create_complete will call
+ def activate_status(server):
+ server.status = 'ACTIVE'
+ return_server.get = activate_status.__get__(return_server)
+ self.m.ReplayAll()
+
+ scheduler.TaskRunner(cs.create)()
+ self.assertEqual(cs.state, (cs.CREATE, cs.COMPLETE))
+
+ def test_cs_status_hard_reboot(self):
+ self._test_cs_status_not_build_active('HARD_REBOOT')
+
+ def test_cs_status_password(self):
+ self._test_cs_status_not_build_active('PASSWORD')
+
+ def test_cs_status_reboot(self):
+ self._test_cs_status_not_build_active('REBOOT')
+
+ def test_cs_status_rescue(self):
+ self._test_cs_status_not_build_active('RESCUE')
+
+ def test_cs_status_resize(self):
+ self._test_cs_status_not_build_active('RESIZE')
+
+ def test_cs_status_revert_resize(self):
+ self._test_cs_status_not_build_active('REVERT_RESIZE')
+
+ def test_cs_status_shutoff(self):
+ self._test_cs_status_not_build_active('SHUTOFF')
+
+ def test_cs_status_suspended(self):
+ self._test_cs_status_not_build_active('SUSPENDED')
+
+ def test_cs_status_verify_resize(self):
+ self._test_cs_status_not_build_active('VERIFY_RESIZE')
+
+ def _test_cs_status_not_build_active(self, uncommon_status):
+ return_server = self.fc.servers.list()[0]
+ cs = self._setup_test_cs(return_server, 'test_cs_status_build')
+ cs.resource_id = 1234
+
+ # Bind fake get method which cs.check_create_complete will call
+ def activate_status(server):
+ if hasattr(server, '_test_check_iterations'):
+ server._test_check_iterations += 1
+ else:
+ server._test_check_iterations = 1
+ if server._test_check_iterations == 1:
+ server.status = uncommon_status
+ if server._test_check_iterations > 2:
+ server.status = 'ACTIVE'
+ return_server.get = activate_status.__get__(return_server)
+ self.m.ReplayAll()
+
+ scheduler.TaskRunner(cs.create)()
+ self.assertEqual(cs.state, (cs.CREATE, cs.COMPLETE))
+
+ self.m.VerifyAll()
+
+ def mock_get_ip(self, cs):
+ self.m.UnsetStubs()
+ self.m.StubOutWithMock(cloud_server.CloudServer, "server")
+ cloud_server.CloudServer.server = cs
+ self.m.ReplayAll()
+
+ def test_cs_get_ip(self):
+ stack_name = 'test_cs_get_ip_err'
+ (t, stack) = self._setup_test_stack(stack_name)
+ cs = cloud_server.CloudServer('cs_create_image_err',
+ t['Resources']['WebServer'],
+ stack)
+ cs.addresses = {'public': [{'version': 4, 'addr': '4.5.6.7'},
+ {'version': 6, 'addr': 'fake:ip::6'}],
+ 'private': [{'version': 4, 'addr': '10.13.12.13'}]}
+ self.mock_get_ip(cs)
+ self.assertEqual(cs.public_ip, '4.5.6.7')
+ self.mock_get_ip(cs)
+ self.assertEqual(cs.private_ip, '10.13.12.13')
+
+ cs.addresses = {'public': [],
+ 'private': []}
+ self.mock_get_ip(cs)
+ self.assertRaises(exception.ResourceFailure, cs._get_ip, 'public')
+
+ def test_private_key(self):
+ stack_name = 'test_private_key'
+ (t, stack) = self._setup_test_stack(stack_name)
+ cs = cloud_server.CloudServer('cs_private_key',
+ t['Resources']['WebServer'],
+ stack)
+
+ # This gives the fake cloud server an id and created_time attribute
+ cs._store_or_update(cs.CREATE, cs.IN_PROGRESS, 'test_store')
+
+ cs.private_key = 'fake private key'
+ rs = db_api.resource_get_by_name_and_stack(None,
+ 'cs_private_key',
+ stack.id)
+ encrypted_key = rs.data[0]['value']
+ self.assertNotEqual(encrypted_key, "fake private key")
+ decrypted_key = cs.private_key
+ self.assertEqual(decrypted_key, "fake private key")