Source code for kingpin.actors.rightscale.mci

# 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.
#
# Copyright 2014 Nextdoor.com, Inc

"""
:mod:`kingpin.actors.rightscale.mci`
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. _MultiCloudImages_:
    http://reference.rightscale.com/api1.5/resources/ResourceMultiCloudImages.html
    http://reference.rightscale.com/api1.5/resources/ResourceMultiCloudImageSettings.html
"""

import logging

from tornado import gen

from kingpin.actors import exceptions
from kingpin.actors.rightscale import base
from kingpin.constants import REQUIRED

log = logging.getLogger(__name__)

__author__ = 'Matt Wise <matt@nextdoor.com>'


class MCIBaseActor(base.RightScaleBaseActor):

    """Abstract MCI Actor that provides some utility methods."""


[docs]class Create(MCIBaseActor): """Creates a RightScale Multi Cloud Image. Options match the documentation in RightScale: http://reference.rightscale.com/api1.5/resources/ResourceMultiCloudImages.html **Options** :name: The name of the MCI to be created. :description: The description of the MCI to be created. (*optional*) :images: A list of dicts that each describe a single cloud and the image in that cloud to launch. See below for details. **Image Definitions** Each cloud image definition is a dictionary that takes a few keys. :cloud: The name of the cloud as found in RightScale. We use the cloud 'Name' which can be found in your `Settings -> Account Settings -> Clouds -> insert_cloud_here` page. For example `AWS us-west-2`. :image: The cloud-specific Image UID. For example `ami-a1234abc`. :instance_type: The default instance type to launch when this AMI is launched. For example, `m1.small`. (*optional*) :user_data: The custom user data to pass to the instance on-bootup. (*optional*) **Examples** .. code-block:: json { "actor": "rightscale.mci.Create", "desc": "Create an MCI", "options": { "name": "Ubuntu i386 14.04", "description": "this is our test mci", "images": [ { "cloud": "EC2 us-west-2", "image": "ami-e29774d1", "instance_type": "m1.small", "user_data": "cd /bin/bash" }, { "cloud": "EC2 us-west-1", "image": "ami-b58142f1", "instance_type": "m1.small", "user_data": "cd /bin/bash" } ] } } """ all_options = { 'name': (str, REQUIRED, 'The name of the MCI to be created.'), 'description': ( str, '', 'The description of the MCI to be created.'), 'images': ( list, [], 'A list of objects that describe our per cloud image settings.'), } def __init__(self, *args, **kwargs): """Validate the user-supplied parameters at instantiation time.""" super(Create, self).__init__(*args, **kwargs) allowed_image_options = ( 'cloud', 'image', 'instance_type', 'user_data') required_image_options = ('cloud', 'image') for image in self.option('images'): # Sanity check that no extra options were passed in for key in image.keys(): if key not in allowed_image_options: raise exceptions.InvalidOptions( 'Invalid option (%s) found in Image %s' % (key, image)) # Make sure that the required options were passed in for required in required_image_options: if required not in image.keys(): raise exceptions.InvalidOptions( 'Missing option "%s" in Image %s' % (required, image)) @gen.coroutine def _get_image_def(self, description): """Returns a fully populated set of Multi Cloud Image settings. This method takes in a dictionary with as set of parameters (cloud, image, instance_type, user_data) and returns a fully ready-to-use set of RightScale-formatted parameters to create that image. The method handles discovering the RightScale HREFs for the cloud, image and instance_type options. Args: description: A dictionary with the keys: cloud, image, instance_type, user_data Returns: A RightScale-formatted array of tuples. """ # Get our cloud object first -- its required so that we can search for # the image/ramdisk/etc hrefs. cloud = yield self._client.find_by_name_and_keys( collection=self._client._client.clouds, name=description['cloud']) if not cloud: raise exceptions.InvalidOptions( 'Invalid Cloud name supplied: %s' % description['cloud']) # Find our image by searching for the resource_uid that matches. image = yield self._client.find_by_name_and_keys( collection=cloud.images, resource_uid=description['image']) if not image: raise exceptions.InvalidOptions( 'Invalid cloud image name supplied: %s' % description['image']) # Find our instance type now too instance = yield self._client.find_by_name_and_keys( collection=cloud.instance_types, name=description['instance_type']) if not instance: raise exceptions.InvalidOptions( 'Invalid cloud instance_type supplied: %s' % description['instance_type']) # Generate our mci parameters, and each of the image settings # parameters. This validates that our inputs are all correct one last # time. definition = self._generate_rightscale_params( prefix='multi_cloud_image_setting', params={ 'cloud_href': cloud.href, 'image_href': image.href, 'instance_type_href': instance.href, 'user_data': description['user_data'], }) self.log.debug('Prepared MCI Image Definition: %s' % definition) raise gen.Return(definition) @gen.coroutine def _execute(self): # Make sure the MCI doesn't already exist. If it does, we bail. mci = yield self._client.find_by_name_and_keys( collection=self._client._client.multi_cloud_images, name=self.option('name')) if mci: raise exceptions.InvalidOptions( 'MCI "%s" already exists.' % self.option('name')) # Generate the parameters for creating the top level MCI object mci_params = self._generate_rightscale_params( prefix='multi_cloud_image', params={ 'description': self.option('description'), 'name': self.option('name') }) # Now, we need to validate that all of the inputs are correct by # discovering the hrefs for each of the images supplied. image_futures = [] for image in self.option('images'): image_futures.append(self._get_image_def(image)) mci_settings_params = yield image_futures # Finally, if we're dry, bail out.. if self._dry: self.log.info('Would have created MCI: %s' % self.option('name')) for setting in mci_settings_params: self.log.info('Image Def: %s' % setting) raise gen.Return() # Ok, lets create this thing! self.log.info('Creating MCI %s' % self.option('name')) mci = yield self._client.create_resource( self._client._client.multi_cloud_images, mci_params) # Now add each of the image descriptions to the mci for setting in mci_settings_params: self.log.info('Creating MCI Setting: %s' % setting) yield self._client.create_resource( mci.settings, setting)
[docs]class Destroy(MCIBaseActor): """Deletes a RightScale MCI. Options match the documentation in RightScale: http://reference.rightscale.com/api1.5/resources/ResourceMultiCloudImages.html **Options** :name: The name of the multi cloud image to be deleted. **Examples** .. code-block:: json { "actor": "rightscale.mci.Destroy", "desc": "Create an MCI", "options": { "name": "Ubuntu i386 14.04", } } """ all_options = { 'name': (str, REQUIRED, 'The name of the multi cloud image to be deleted.'), } @gen.coroutine def _execute(self): mci = yield self._client.find_by_name_and_keys( collection=self._client._client.multi_cloud_images, name=self.option('name')) if not mci: raise exceptions.InvalidOptions( 'MCI "%s" does not exist.' % self.option('name')) info = (yield self._client.show(mci.self)).soul if self._dry: self.log.info('Would delete MCI %s' % info['name']) raise gen.Return() self.log.info('Deleting MCI %s' % info['name']) yield self._client.destroy_resource(mci)