From 4d0abd4ccae28ce16e88e449f1577119b95d1f29 Mon Sep 17 00:00:00 2001 From: Luke Sneeringer Date: Mon, 21 Jul 2014 11:08:27 -0500 Subject: [PATCH] Removed PEM file things. --- awx/main/fields.py | 54 ------- awx/main/models/credential.py | 22 +-- awx/main/storage.py | 67 +++++++++ awx/plugins/inventory/gce.py | 257 ++++++++++++++++++++++++++++++++++ 4 files changed, 325 insertions(+), 75 deletions(-) create mode 100644 awx/main/storage.py create mode 100644 awx/plugins/inventory/gce.py diff --git a/awx/main/fields.py b/awx/main/fields.py index 9453eaa329..d8295503eb 100644 --- a/awx/main/fields.py +++ b/awx/main/fields.py @@ -40,57 +40,3 @@ class AutoOneToOneField(models.OneToOneField): add_introspection_rules([([AutoOneToOneField], [], {})], [r'^awx\.main\.fields\.AutoOneToOneField']) - - -# Copied, flat out, from Django 1.6. -# Vendored here because Tower is run against Django 1.5. -# -# Original: -# github.com/django/django/blob/master/django/db/models/fields/__init__.py -# -# Django is: -# Copyright (c) Django Software Foundation and individual contributors. -# All rights reserved. -# -# Used under license: -# github.com/django/django/blob/master/LICENSE -class BinaryField(models.Field): - description = _("Raw binary data") - empty_values = [None, b''] - - def __init__(self, *args, **kwargs): - kwargs['editable'] = False - super(BinaryField, self).__init__(*args, **kwargs) - if self.max_length is not None: - self.validators.append(validators.MaxLengthValidator(self.max_length)) - - def get_internal_type(self): - return "BinaryField" - - def get_default(self): - if self.has_default() and not callable(self.default): - return self.default - default = super(BinaryField, self).get_default() - if default == '': - return b'' - return default - - def get_db_prep_value(self, value, connection, prepared=False): - value = super(BinaryField, self).get_db_prep_value(value, connection, prepared) - if value is not None: - return connection.Database.Binary(value) - return value - - def value_to_string(self, obj): - """Binary data is serialized as base64""" - return b64encode(force_bytes(self._get_val_from_obj(obj))).decode('ascii') - - def to_python(self, value): - # If it's a string, it should be base64-encoded data - if isinstance(value, six.text_type): - return six.memoryview(b64decode(force_bytes(value))) - return value - - -add_introspection_rules([([BinaryField], [], {})], - [r'^awx\.main\.fields\.BinaryField']) diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index b3d2bbf8a3..0105166361 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -14,26 +14,12 @@ from django.core.urlresolvers import reverse # AWX from awx.main import storage -from awx.main.fields import BinaryField from awx.main.utils import decrypt_field from awx.main.models.base import * __all__ = ['Credential'] -class PEM(models.Model): - """Model representing a PEM p12 private key created with openssl. - - These are notably used as credentials for authenticating to Google - services, and Tower uses them for Google Compute Engine. - """ - filename = models.CharField(max_length=100, unique=True) - contents = BinaryField() - - class Meta: - app_label = 'main' - - class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ''' A credential contains information about how to talk to a remote resource @@ -51,7 +37,7 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): ] PASSWORD_FIELDS = ('password', 'ssh_key_data', 'ssh_key_unlock', - 'sudo_password', 'vault_password', 'pem_file') + 'sudo_password', 'vault_password') class Meta: app_label = 'main' @@ -137,12 +123,6 @@ class Credential(PasswordFieldsModel, CommonModelNameNotUnique): default='', help_text=_('Vault password (or "ASK" to prompt the user).'), ) - pem_file = models.FileField( - blank=True, - null=True, - upload_to='irrelevant', - storage=storage.DatabaseStorage(PEM), - ) @property def needs_password(self): diff --git a/awx/main/storage.py b/awx/main/storage.py new file mode 100644 index 0000000000..9c96be641f --- /dev/null +++ b/awx/main/storage.py @@ -0,0 +1,67 @@ +# Copyright (c) 2014 AnsibleWorks, Inc. +# All Rights Reserved. + +import base64 +import os +from cStringIO import StringIO + +from django.core import files +from django.core.files.storage import Storage +from django.core.urlresolvers import reverse + + +class DatabaseStorage(Storage): + """A class for storing uploaded files into the database, rather than + on the filesystem. + """ + def __init__(self, model): + self.model = model + + def _open(self, name, mode='rb'): + try: + f = self.model.objects.get(filename=name) + except self.model.DoesNotExist: + return None + fh = StringIO(base64.b64decode(f.contents)) + fh.name = name + fh.mode = mode + fh.size = f.size + return files.File(fh) + + def _save(self, name, content): + try: + file_ = self.model.objects.get(filename=name) + except self.model.DoesNotExist: + file_ = self.model(filename=name) + file_.contents = base64.b64encode(content.read()) + file_.save() + return name + + def exists(self, name): + """Return True if the given file already exists in the database, + or False otherwise. + """ + return bool(self.model.objects.filter(filename=name).count()) + + def delete(self, name): + """Delete the file in the database, failing silently if the file + does not exist. + """ + self.model.objects.filter(filename=name).delete() + + def listdir(self, path=None): + """Return a full list of files stored in the database, ignoring + whatever may be sent to the `path` argument. + """ + filenames = [i.filename for i in self.model.order_by('filename')] + return ([], filenames) + + def url(self, name): + raise NotImplementedError + + def size(self, name): + """Return the size of the given file, if it exists; raise DoesNotExist + if the file is not present. + """ + file_ = self.model.objects.get(filename=name) + return len(file_.contents) diff --git a/awx/plugins/inventory/gce.py b/awx/plugins/inventory/gce.py new file mode 100644 index 0000000000..6757e87b55 --- /dev/null +++ b/awx/plugins/inventory/gce.py @@ -0,0 +1,257 @@ +#!/usr/bin/python +# Copyright 2013 Google Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +''' +GCE external inventory script +================================= + +Generates inventory that Ansible can understand by making API requests +Google Compute Engine via the libcloud library. Full install/configuration +instructions for the gce* modules can be found in the comments of +ansible/test/gce_tests.py. + +When run against a specific host, this script returns the following variables +based on the data obtained from the libcloud Node object: + - gce_uuid + - gce_id + - gce_image + - gce_machine_type + - gce_private_ip + - gce_public_ip + - gce_name + - gce_description + - gce_status + - gce_zone + - gce_tags + - gce_metadata + - gce_network + +When run in --list mode, instances are grouped by the following categories: + - zone: + zone group name examples are us-central1-b, europe-west1-a, etc. + - instance tags: + An entry is created for each tag. For example, if you have two instances + with a common tag called 'foo', they will both be grouped together under + the 'tag_foo' name. + - network name: + the name of the network is appended to 'network_' (e.g. the 'default' + network will result in a group named 'network_default') + - machine type + types follow a pattern like n1-standard-4, g1-small, etc. + - running status: + group name prefixed with 'status_' (e.g. status_running, status_stopped,..) + - image: + when using an ephemeral/scratch disk, this will be set to the image name + used when creating the instance (e.g. debian-7-wheezy-v20130816). when + your instance was created with a root persistent disk it will be set to + 'persistent_disk' since there is no current way to determine the image. + +Examples: + Execute uname on all instances in the us-central1-a zone + $ ansible -i gce.py us-central1-a -m shell -a "/bin/uname -a" + + Use the GCE inventory script to print out instance specific information + $ plugins/inventory/gce.py --host my_instance + +Author: Eric Johnson +Version: 0.0.1 +''' + +USER_AGENT_PRODUCT="Ansible-gce_inventory_plugin" +USER_AGENT_VERSION="v1" + +import sys +import os +import argparse +import ConfigParser + +try: + import json +except ImportError: + import simplejson as json + +try: + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + _ = Provider.GCE +except: + print("GCE inventory script requires libcloud >= 0.13") + sys.exit(1) + + +class GceInventory(object): + def __init__(self): + # Read settings and parse CLI arguments + self.parse_cli_args() + self.driver = self.get_gce_driver() + + # Just display data for specific host + if self.args.host: + print self.json_format_dict(self.node_to_dict( + self.get_instance(self.args.host))) + sys.exit(0) + + # Otherwise, assume user wants all instances grouped + print self.json_format_dict(self.group_instances()) + sys.exit(0) + + + def get_gce_driver(self): + '''Determine GCE authorization settings and return libcloud driver.''' + + gce_ini_default_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "gce.ini") + gce_ini_path = os.environ.get('GCE_INI_PATH', gce_ini_default_path) + + config = ConfigParser.SafeConfigParser() + config.read(gce_ini_path) + + # the GCE params in 'secrets.py' will override these + secrets_path = config.get('gce', 'libcloud_secrets') + + secrets_found = False + try: + import secrets + args = getattr(secrets, 'GCE_PARAMS', ()) + kwargs = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + secrets_found = True + except: + pass + + if not secrets_found and secrets_path: + if not secrets_path.endswith('secrets.py'): + err = "Must specify libcloud secrets file as " + err += "/absolute/path/to/secrets.py" + print(err) + sys.exit(1) + sys.path.append(os.path.dirname(secrets_path)) + try: + import secrets + args = getattr(secrets, 'GCE_PARAMS', ()) + kwargs = getattr(secrets, 'GCE_KEYWORD_PARAMS', {}) + secrets_found = True + except: + pass + if not secrets_found: + args = ( + config.get('gce','gce_service_account_email_address'), + config.get('gce','gce_service_account_pem_file_path') + ) + kwargs = {'project': config.get('gce','gce_project_id')} + + gce = get_driver(Provider.GCE)(*args, **kwargs) + gce.connection.user_agent_append("%s/%s" % ( + USER_AGENT_PRODUCT, USER_AGENT_VERSION)) + return gce + + + def parse_cli_args(self): + ''' Command line argument processing ''' + + parser = argparse.ArgumentParser( + description='Produce an Ansible Inventory file based on GCE') + parser.add_argument('--list', action='store_true', default=True, + help='List instances (default: True)') + parser.add_argument('--host', action='store', + help='Get all information about an instance') + self.args = parser.parse_args() + + + def node_to_dict(self, inst): + md = {} + + if inst is None: + return {} + + if inst.extra['metadata'].has_key('items'): + for entry in inst.extra['metadata']['items']: + md[entry['key']] = entry['value'] + + net = inst.extra['networkInterfaces'][0]['network'].split('/')[-1] + return { + 'gce_uuid': inst.uuid, + 'gce_id': inst.id, + 'gce_image': inst.image, + 'gce_machine_type': inst.size, + 'gce_private_ip': inst.private_ips[0], + 'gce_public_ip': inst.public_ips[0], + 'gce_name': inst.name, + 'gce_description': inst.extra['description'], + 'gce_status': inst.extra['status'], + 'gce_zone': inst.extra['zone'].name, + 'gce_tags': inst.extra['tags'], + 'gce_metadata': md, + 'gce_network': net, + # Hosts don't have a public name, so we add an IP + 'ansible_ssh_host': inst.public_ips[0] + } + + def get_instance(self, instance_name): + '''Gets details about a specific instance ''' + try: + return self.driver.ex_get_node(instance_name) + except Exception, e: + return None + + def group_instances(self): + '''Group all instances''' + groups = {} + for node in self.driver.list_nodes(): + name = node.name + + zone = node.extra['zone'].name + if groups.has_key(zone): groups[zone].append(name) + else: groups[zone] = [name] + + tags = node.extra['tags'] + for t in tags: + tag = 'tag_%s' % t + if groups.has_key(tag): groups[tag].append(name) + else: groups[tag] = [name] + + net = node.extra['networkInterfaces'][0]['network'].split('/')[-1] + net = 'network_%s' % net + if groups.has_key(net): groups[net].append(name) + else: groups[net] = [name] + + machine_type = node.size + if groups.has_key(machine_type): groups[machine_type].append(name) + else: groups[machine_type] = [name] + + image = node.image and node.image or 'persistent_disk' + if groups.has_key(image): groups[image].append(name) + else: groups[image] = [name] + + status = node.extra['status'] + stat = 'status_%s' % status.lower() + if groups.has_key(stat): groups[stat].append(name) + else: groups[stat] = [name] + return groups + + def json_format_dict(self, data, pretty=False): + ''' Converts a dict to a JSON object and dumps it as a formatted + string ''' + + if pretty: + return json.dumps(data, sort_keys=True, indent=2) + else: + return json.dumps(data) + + +# Run the script +GceInventory()