diff --git a/awx/main/constants.py b/awx/main/constants.py index 4ff22662b3..9c0b01a3d4 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -5,7 +5,7 @@ import re from django.utils.translation import ugettext_lazy as _ -CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'satellite6', 'cloudforms') +CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'ovirt4', 'satellite6', 'cloudforms') SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom', 'scm',) PRIVILEGE_ESCALATION_METHODS = [ ('sudo', _('Sudo')), ('su', _('Su')), ('pbrun', _('Pbrun')), ('pfexec', _('Pfexec')), ('dzdo', _('DZDO')), ('pmrun', _('Pmrun')), ('runas', _('Runas'))] ANSI_SGR_PATTERN = re.compile(r'\x1b\[[0-9;]*m') diff --git a/awx/main/migrations/0009_v322_add_support_for_ovirt4_inventory.py b/awx/main/migrations/0009_v322_add_support_for_ovirt4_inventory.py new file mode 100644 index 0000000000..e29bdfb4d3 --- /dev/null +++ b/awx/main/migrations/0009_v322_add_support_for_ovirt4_inventory.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +# AWX +from awx.main.migrations import _credentialtypes as credentialtypes + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0008_v320_drop_v1_credential_fields'), + ] + + operations = [ + migrations.RunPython(credentialtypes.create_ovirt4_credtype), + migrations.AlterField( + model_name='inventorysource', + name='source', + field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a Project'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'ovirt4', 'oVirt4'), (b'custom', 'Custom Script')]), + ), + migrations.AlterField( + model_name='inventoryupdate', + name='source', + field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a Project'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'ovirt4', 'oVirt4'), (b'custom', 'Custom Script')]), + ), + ] diff --git a/awx/main/migrations/_credentialtypes.py b/awx/main/migrations/_credentialtypes.py index f34d08903a..1c90822bab 100644 --- a/awx/main/migrations/_credentialtypes.py +++ b/awx/main/migrations/_credentialtypes.py @@ -173,3 +173,6 @@ def migrate_job_credentials(apps, schema_editor): finally: utils.get_current_apps = orig_current_apps + +def create_ovirt4_credtype(apps, schema_editor): + CredentialType.defaults['ovirt4']().save() diff --git a/awx/main/models/base.py b/awx/main/models/base.py index ebfe0bf1c5..93bb484a46 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -52,7 +52,7 @@ PROJECT_UPDATE_JOB_TYPE_CHOICES = [ (PERM_INVENTORY_CHECK, _('Check')), ] -CLOUD_INVENTORY_SOURCES = ['ec2', 'vmware', 'gce', 'azure_rm', 'openstack', 'custom', 'satellite6', 'cloudforms', 'scm',] +CLOUD_INVENTORY_SOURCES = ['ec2', 'vmware', 'gce', 'azure_rm', 'openstack', 'ovirt4', 'custom', 'satellite6', 'cloudforms', 'scm',] VERBOSITY_CHOICES = [ (0, '0 (Normal)'), diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 5a3f0b9662..5a1832f323 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -59,6 +59,7 @@ class V1Credential(object): ('gce', 'Google Compute Engine'), ('azure_rm', 'Microsoft Azure Resource Manager'), ('openstack', 'OpenStack'), + ('ovirt4', 'oVirt4'), ('insights', 'Insights'), ] FIELDS = { @@ -1000,3 +1001,48 @@ def insights(cls): }, }, ) + + +@CredentialType.default +def ovirt4(cls): + return cls( + kind='cloud', + name='oVirt4', + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': 'Host (Authentication URL)', + 'type': 'string', + 'help_text': ('The host to authenticate with.') + }, { + 'id': 'username', + 'label': 'Username', + 'type': 'string' + }, { + 'id': 'password', + 'label': 'Password', + 'type': 'string', + 'secret': True, + }, { + 'id': 'ca_file', + 'label': 'CA File', + 'type': 'string', + 'help_text': ('Absolute file path to the CA file to use (optional)') + }], + 'required': ['host', 'username', 'password'], + }, + injectors={ + 'file': { + 'template': '\n'.join([ + '[ovirt]', + 'ovirt_url={{host}}', + 'ovirt_username={{username}}', + 'ovirt_password={{password}}', + '{% if ca_file %}ovirt_ca_file={{ca_file}}{% endif %}']) + }, + 'env': { + 'OVIRT_INI_PATH': '{{tower.filename}}' + } + }, + ) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 7b5e3d3dfa..f5cd0d8e58 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -872,6 +872,7 @@ class InventorySourceOptions(BaseModel): ('satellite6', _('Red Hat Satellite 6')), ('cloudforms', _('Red Hat CloudForms')), ('openstack', _('OpenStack')), + ('ovirt4', _('oVirt4')), ('custom', _('Custom Script')), ] @@ -1120,6 +1121,11 @@ class InventorySourceOptions(BaseModel): """Red Hat CloudForms region choices (not implemented)""" return [('all', 'All')] + @classmethod + def get_ovirt4_region_choices(self): + """No region supprt""" + return [('all', 'All')] + def clean_credential(self): if not self.source: return None diff --git a/awx/main/tests/functional/test_credential.py b/awx/main/tests/functional/test_credential.py index b4dd0cb0e4..9bcf23e198 100644 --- a/awx/main/tests/functional/test_credential.py +++ b/awx/main/tests/functional/test_credential.py @@ -25,6 +25,7 @@ def test_default_cred_types(): 'insights', 'net', 'openstack', + 'ovirt4', 'satellite6', 'scm', 'ssh', diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 3a7a218133..a9fb3dd448 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -689,6 +689,41 @@ class TestJobCredentials(TestJobExecution): self.run_pexpect.side_effect = run_pexpect_side_effect self.task.run(self.pk) + @pytest.mark.parametrize("ca_file", [None, '/path/to/some/file']) + def test_ovirt4_credentials(self, ca_file): + ovirt4 = CredentialType.defaults['ovirt4']() + inputs = { + 'host': 'some-ovirt-host.example.org', + 'username': 'bob', + 'password': 'some-pass', + } + if ca_file: + inputs['ca_file'] = ca_file + credential = Credential( + pk=1, + credential_type=ovirt4, + inputs=inputs + ) + credential.inputs['password'] = encrypt_field(credential, 'password') + self.instance.extra_credentials.add(credential) + + def run_pexpect_side_effect(*args, **kwargs): + args, cwd, env, stdout = args + config = ConfigParser.ConfigParser() + config.read(env['OVIRT_INI_PATH']) + assert config.get('ovirt', 'ovirt_url') == 'some-ovirt-host.example.org' + assert config.get('ovirt', 'ovirt_username') == 'bob' + assert config.get('ovirt', 'ovirt_password') == 'some-pass' + if ca_file: + assert config.get('ovirt', 'ovirt_ca_file') == ca_file + else: + with pytest.raises(ConfigParser.NoOptionError): + config.get('ovirt', 'ovirt_ca_file') + return ['successful', 0] + + self.run_pexpect.side_effect = run_pexpect_side_effect + self.task.run(self.pk) + def test_net_credentials(self): net = CredentialType.defaults['net']() credential = Credential( diff --git a/awx/plugins/inventory/ovirt4.py b/awx/plugins/inventory/ovirt4.py new file mode 100755 index 0000000000..6221325f34 --- /dev/null +++ b/awx/plugins/inventory/ovirt4.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, 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 . +# + +""" +oVirt dynamic inventory script +================================= + +Generates dynamic inventory file for oVirt. + +Script will return following attributes for each virtual machine: + - id + - name + - host + - cluster + - status + - description + - fqdn + - os_type + - template + - tags + - statistics + - devices + +When run in --list mode, virtual machines are grouped by the following categories: + - cluster + - tag + - status + + Note: If there is some virtual machine which has has more tags it will be in both tag + records. + +Examples: + # Execute update of system on webserver virtual machine: + + $ ansible -i contrib/inventory/ovirt4.py webserver -m yum -a "name=* state=latest" + + # Get webserver virtual machine information: + + $ contrib/inventory/ovirt4.py --host webserver + +Author: Ondra Machacek (@machacekondra) +""" + +import argparse +import os +import sys + +from collections import defaultdict + +try: + import ConfigParser as configparser +except ImportError: + import configparser + +try: + import json +except ImportError: + import simplejson as json + +try: + import ovirtsdk4 as sdk + import ovirtsdk4.types as otypes +except ImportError: + print('oVirt inventory script requires ovirt-engine-sdk-python >= 4.0.0') + sys.exit(1) + + +def parse_args(): + """ + Create command line parser for oVirt dynamic inventory script. + """ + parser = argparse.ArgumentParser( + description='Ansible dynamic inventory script for oVirt.', + ) + parser.add_argument( + '--list', + action='store_true', + default=True, + help='Get data of all virtual machines (default: True).', + ) + parser.add_argument( + '--host', + help='Get data of virtual machines running on specified host.', + ) + parser.add_argument( + '--pretty', + action='store_true', + default=False, + help='Pretty format (default: False).', + ) + return parser.parse_args() + + +def create_connection(): + """ + Create a connection to oVirt engine API. + """ + # Get the path of the configuration file, by default use + # 'ovirt.ini' file in script directory: + default_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + 'ovirt.ini', + ) + config_path = os.environ.get('OVIRT_INI_PATH', default_path) + + # Create parser and add ovirt section if it doesn't exist: + config = configparser.SafeConfigParser( + defaults={ + 'ovirt_url': None, + 'ovirt_username': None, + 'ovirt_password': None, + 'ovirt_ca_file': None, + } + ) + if not config.has_section('ovirt'): + config.add_section('ovirt') + config.read(config_path) + + # Create a connection with options defined in ini file: + return sdk.Connection( + url=config.get('ovirt', 'ovirt_url'), + username=config.get('ovirt', 'ovirt_username'), + password=config.get('ovirt', 'ovirt_password'), + ca_file=config.get('ovirt', 'ovirt_ca_file'), + insecure=config.get('ovirt', 'ovirt_ca_file') is None, + ) + + +def get_dict_of_struct(connection, vm): + """ + Transform SDK Vm Struct type to Python dictionary. + """ + if vm is None: + return dict() + + vms_service = connection.system_service().vms_service() + clusters_service = connection.system_service().clusters_service() + vm_service = vms_service.vm_service(vm.id) + devices = vm_service.reported_devices_service().list() + tags = vm_service.tags_service().list() + stats = vm_service.statistics_service().list() + labels = vm_service.affinity_labels_service().list() + groups = clusters_service.cluster_service( + vm.cluster.id + ).affinity_groups_service().list() + + return { + 'id': vm.id, + 'name': vm.name, + 'host': connection.follow_link(vm.host).name if vm.host else None, + 'cluster': connection.follow_link(vm.cluster).name, + 'status': str(vm.status), + 'description': vm.description, + 'fqdn': vm.fqdn, + 'os_type': vm.os.type, + 'template': connection.follow_link(vm.template).name, + 'tags': [tag.name for tag in tags], + 'affinity_labels': [label.name for label in labels], + 'affinity_groups': [ + group.name for group in groups + if vm.name in [vm.name for vm in connection.follow_link(group.vms)] + ], + 'statistics': dict( + (stat.name, stat.values[0].datum) for stat in stats + ), + 'devices': dict( + (device.name, [ip.address for ip in device.ips]) for device in devices if device.ips + ), + 'ansible_host': next((device.ips[0].address for device in devices if device.ips), None) + } + + +def get_data(connection, vm_name=None): + """ + Obtain data of `vm_name` if specified, otherwise obtain data of all vms. + """ + vms_service = connection.system_service().vms_service() + clusters_service = connection.system_service().clusters_service() + + if vm_name: + vm = vms_service.list(search='name=%s' % vm_name) or [None] + data = get_dict_of_struct( + connection=connection, + vm=vm[0], + ) + else: + vms = dict() + data = defaultdict(list) + for vm in vms_service.list(): + name = vm.name + vm_service = vms_service.vm_service(vm.id) + cluster_service = clusters_service.cluster_service(vm.cluster.id) + + # Add vm to vms dict: + vms[name] = get_dict_of_struct(connection, vm) + + # Add vm to cluster group: + cluster_name = connection.follow_link(vm.cluster).name + data['cluster_%s' % cluster_name].append(name) + + # Add vm to tag group: + tags_service = vm_service.tags_service() + for tag in tags_service.list(): + data['tag_%s' % tag.name].append(name) + + # Add vm to status group: + data['status_%s' % vm.status].append(name) + + # Add vm to affinity group: + for group in cluster_service.affinity_groups_service().list(): + if vm.name in [ + v.name for v in connection.follow_link(group.vms) + ]: + data['affinity_group_%s' % group.name].append(vm.name) + + # Add vm to affinity label group: + affinity_labels_service = vm_service.affinity_labels_service() + for label in affinity_labels_service.list(): + data['affinity_label_%s' % label.name].append(name) + + data["_meta"] = { + 'hostvars': vms, + } + + return data + + +def main(): + args = parse_args() + connection = create_connection() + + print( + json.dumps( + obj=get_data( + connection=connection, + vm_name=args.host, + ), + sort_keys=args.pretty, + indent=args.pretty * 2, + ) + ) + +if __name__ == '__main__': + main() diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index d4747a8078..b35b0ad376 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -821,6 +821,16 @@ OPENSTACK_HOST_FILTER = r'^.+$' OPENSTACK_EXCLUDE_EMPTY_GROUPS = True OPENSTACK_INSTANCE_ID_VAR = 'openstack.id' +# --------------------- +# ----- oVirt4 ----- +# --------------------- +OVIRT4_ENABLED_VAR = 'status' +OVIRT4_ENABLED_VALUE = 'up' +OVIRT4_GROUP_FILTER = r'^.+$' +OVIRT4_HOST_FILTER = r'^.+$' +OVIRT4_EXCLUDE_EMPTY_GROUPS = True +OVIRT4_INSTANCE_ID_VAR = 'id' + # --------------------- # ----- Foreman ----- # --------------------- diff --git a/tools/docker-compose/Dockerfile b/tools/docker-compose/Dockerfile index e6fb9c0d43..712c7cd607 100644 --- a/tools/docker-compose/Dockerfile +++ b/tools/docker-compose/Dockerfile @@ -14,7 +14,7 @@ requirements/requirements_dev_uninstall.txt \ RUN yum -y update && yum -y install curl epel-release RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - RUN yum -y localinstall http://download.postgresql.org/pub/repos/yum/9.4/redhat/rhel-6-x86_64/pgdg-centos94-9.4-3.noarch.rpm -RUN yum -y update && yum -y install openssh-server ansible mg vim tmux git mercurial subversion python-devel python-psycopg2 make postgresql postgresql-devel nginx nodejs python-psutil libxml2-devel libxslt-devel libstdc++.so.6 gcc cyrus-sasl-devel cyrus-sasl openldap-devel libffi-devel zeromq-devel python-pip xmlsec1-devel swig krb5-devel xmlsec1-openssl xmlsec1 xmlsec1-openssl-devel libtool-ltdl-devel rabbitmq-server bubblewrap zanata-python-client gettext gcc-c++ bzip2 +RUN yum -y update && yum -y install openssh-server ansible mg vim tmux git mercurial subversion python-devel python-psycopg2 make postgresql postgresql-devel nginx nodejs python-psutil libxml2-devel libxslt-devel libstdc++.so.6 gcc cyrus-sasl-devel cyrus-sasl openldap-devel libffi-devel zeromq-devel python-pip xmlsec1-devel swig krb5-devel xmlsec1-openssl xmlsec1 xmlsec1-openssl-devel libtool-ltdl-devel rabbitmq-server bubblewrap zanata-python-client gettext gcc-c++ libcurl-devel python-pycurl bzip2 RUN pip install virtualenv RUN /usr/bin/ssh-keygen -q -t rsa -N "" -f /root/.ssh/id_rsa RUN mkdir -p /data/db