Skip to content

Commit

Permalink
Merge pull request #2 from greg-hellings/libvirt
Browse files Browse the repository at this point in the history
  • Loading branch information
greg-hellings authored May 26, 2020
2 parents ed7195b + 905c338 commit a5e9cae
Show file tree
Hide file tree
Showing 74 changed files with 1,404 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ jobs:
- chrony
- disk_wipe
- firewalld
- libvirtd
- localectl
- molecule_docker_ci
- network_scripts
- nfs_server
- nmcli_add_addrs
- package_updater
Expand Down Expand Up @@ -57,7 +59,10 @@ jobs:
- name: Install dependencies
run: |
set -ex
sudo apt-get update
sudo apt-get install -y libapt-pkg-dev build-essential python3-setuptools
python -m pip install --upgrade pip
pip install -U setuptools wheel
pip install tox tox-ansible
- name: Test with tox
run: |
Expand Down
193 changes: 193 additions & 0 deletions plugins/modules/yaml_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# TODO: The following items are nice-to-haves that have not yet been found to
# be necessary, but could be beneficial in the future:
# * append to list vs replace. A use case might call for modifying the contents
# of a list rather than needing to replace it every time. An option could be
# added to append to a list
# * truncate a list. Reduce the length of a list to only equal to a given size
# * allow special characters in an identifier, such as ".", with an escape

from __future__ import (absolute_import, print_function)
__metaclass__ = type

ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview']}

DOCUMENTATION = '''
---
module: yaml_file
short_description: add or modify fields in a YAML file
description:
- Modify YAML files programmatically
options:
create:
description:
- Create the file, if not found. Only has meaning if `state` is set to
`present`.
type: bool
default: true
key:
description:
- The key in the file to modify. Child objects should be referenced
with a '.', elements of a list with a '[...]'. Use '[2]' to indicate
the particular index within the list or '[]' to indicate all elements
within the list should be modified.
required: true
path:
description:
- Path of the file to modify
required: true
type: path
aliases: ['file', 'dest']
state:
description:
- `present` to add/modify values, `absent` to delete them
default: 'present'
choices: ['absent', 'present']
value:
description:
- The value of the key(s) to set. Required if `state` is `present`
'''

from ansible.module_utils.basic import AnsibleModule # noqa: E402
from yaml import load, dump # noqa: E402
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
import os # noqa: E402
import re # noqa: E402
# Used to deep-compare two dictionaries
import json # noqa: E402


class Yaml(object):
def __init__(self, args):
self.create = args['create']
self.path = os.path.expanduser(args['path'])
self.state = args['state']
self.key = args['key']
self.value = args['value']

def check_module_arguments(self):
"""Execute a basic set of sanity checks.
Checks some basic pre-conditions before the module attempts to run so
that these things don't need to be checked for later on.
:returns: (boolean, string) where the first element says whether the
check passed and the second includes an appropriate error message if it
did not."""
# Check that file exists if state is 'present'
if self.state == 'present' and not self.create:
if not os.path.isfile(self.path):
return (False, "File not found when state is 'present'. Create"
" disabled")
# Be sure proper arguments are specified
if self.state == 'present' and self.value is None:
return False, 'When state is "present", a value must be specified'
# Parse the key value
try:
self.key_list = re.findall(r'([-\w]+|\[\d*\])', self.key)
if len(self.key_list) == 0:
return False, "No key value parsed. Please check the syntax."
except Exception as ex:
return False, ex.msg
# Massage "value" into expected type
if self.value is not None:
self.value = json.loads(self.value)
self.read_file()
return True, ''

def read_file(self):
"""Read the current state of the file.
Reads the current YAML file into memory, returns an empty dict if
the file does not exist or the parsed object if it does."""
if not os.path.isfile(self.path):
self.obj = dict()
else:
with open(self.path, 'r') as stream:
self.obj = load(stream, Loader=Loader)
if self.obj is None:
self.obj = dict()

def write_file(self):
"""Write the object to the target file.
Writes the value of data to the YAML file.
:param data: A dict of values to write out
:returns: nothing"""
with open(self.path, 'w') as stream:
dump(self.obj, stream, default_flow_style=False, Dumper=Dumper)

def present(self):
return self._present(self.obj, self.key_list)

def _present(self, obj, keys):
"""Recursively walks to a specified key in the file.
:param obj: The current level of the object that is being walked
:param keys: A list of key fragments to walk to
:param value: The value to assign to the given key
:returns: True if changes were made, False otherwise"""
if len(keys) == 1:
if json.dumps(obj.get(keys[0], None), sort_keys=True) == \
json.dumps(self.value):
return False
else:
obj[keys[0]] = self.value
return True
else:
if keys[0] not in obj:
obj[keys[0]] = dict()
return self._present(obj[keys[0]], keys[1:])

def absent(self):
return self._absent(self.obj, self.key_list)

def _absent(self, obj, keys):
if len(keys) == 1:
if isinstance(obj, dict) and keys[0] in obj:
del obj[keys[0]]
return True
elif not isinstance(obj, dict):
raise Exception("Cannot subscript type found: {}".format(obj))
else:
return False
else:
if keys[0] in obj:
return self._absent(obj[keys[0]], keys[1:])
else:
return False


def main():
module = AnsibleModule(
argument_spec=dict(
create=dict(type='bool', default=False),
key=dict(type='str', required=True),
path=dict(type='str', aliases=['file', 'dest'], required=True),
state=dict(type='str', choices=['absent', 'present'],
default='present'),
value=dict(type='json')
)
)
yaml = Yaml(module.params)
check = yaml.check_module_arguments()
if not check[0]:
module.fail_json(msg=check[1])
else:
if module.params['state'] == 'present':
changes = yaml.present()
else:
changes = yaml.absent()
if changes:
yaml.write_file()
module.exit_json(changed=changes)


if __name__ == '__main__':
main()
7 changes: 7 additions & 0 deletions roles/libvirt_rhel_vm/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.*.swp
.*.swo
*.pyc
*.pyo
__pycache__/*
molecule/*/junit.xml
molecule/*/pytestdebug.log
99 changes: 99 additions & 0 deletions roles/libvirt_rhel_vm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
libvirt
===========

Spins up a RHEL/CentOS VM on the local host

Requirements
------------

Ansible 2.8 or higher

Role Variables
--------------

Currently the following variables are supported:

### General

* `libvirt_rhel_vm_storage` - Default: `/var/lib/libvirt/images`. The path on
the remote system to upload VM images to. Currently there is no support for
storage pools other than the local directories.
* `libvirt_rhel_vm_domain` - Default:
```yaml
libvirt_rhel_vm_domain:
name: foo.example.com
img_path: ~/CentOS-7-x86_64-GenericCloud.qcow2
os-variant: rhel7.7

ram: 4096
vcpus: 1
disk: 20G

root_passwd: "$6$Fa84yQfpK0gpluDJ$sdfdsfdsfdsfdsfdfdsfdsfddsfdaQpO6MKgioTOV5\
lRy.2tdA9IexTnvYNK3mP8clpC/sdsfdsfdsfdsfdsfsd60"
root_ssh_pub_keys:
- "ssh-rsa AAAdsfdfdsfdfdEAAAADAQABAAABAQD0yXYYdsfdAOSdIjcRp\
8TVOPnFplYJEY8VST+bQeW1Fosdfddfsdfmpmd/RdV9W/0d7sRfymL1diDm6ml3kwddff5Xn7A\
edsztdRahvZsBD9ADBqnQBli0adop6+PDRsdfdsfBjpFnrwVoe9QZPJVqZle6HBeJYIffffEY6\
1vhC8JXyGGDIJi7pdSjPdsfdsfdsfdsfdsfdsfsxTbAp4ddkEuS/9NR8JZ3HJg+h6mKoNffffq\
RUiikG98dfdfdsfdsfdfdfdfdfdfdsfdsfdsfdsfdsfyPMXK7nD+R0Jx4mmRlFWKmYTjffffSq\
sdfadfdsfdsfdsf"

bridges:
- br0

nics: # NICs to provision on the VM, using the network_scripts role
- filename: ifcfg-eth0
NAME: eth0
DEVICE: eth0
TYPE: Ethernet
BOOTPROTO: static
IPADDR: 192.168.1.10
GATEWAY: 192.168.1.1
NETMASK: 255.255.255.0
DEFROUTE: !!str yes
IPV6INIT: !!str no
ONBOOT: !!str yes
DNS1: 8.8.8.8
```
The VM to spin up. The image pointed to by `libvirt_rhel_vm_domain.img_path`
must already exist on the local system. It will be uploaded to the remote host
in the `libvirt_rhel_vm_storage` directory, resized to the value of `disk`,
spun up with the specified hardware options, fed the `nics` list as config files
from the `network_scripts` role, and configured with the provided passwords
and pubkeys. The provisioning script is relatively tightly bound with RHEL or
CentOS 7, hence the naming of this role.
* `libvirt_become` - Default: true. If this role needs administrator
privileges, then use the Ansible become functionality (based off sudo).
* `libvirt_become_user` - Default: root. If the role uses the become
functionality for privilege escalation, then this is the name of the target
user to change to.
* `libvirt_rhel_vm_nic_config_path` - Default: `null`. Path on the
remote host to install the nic-config scripts into before uploading them to
the VM. If left as `null`, then a tempdir will be created on the remote host
for uploading. If you set this value, then you are responsible to ensure that
the path exists before calling this role.

Dependencies
------------

None

Example Playbook
----------------

```yaml
- hosts: libvirt-servers
roles:
- role: oasis_roles.system.libvirt
```

License
-------

GPLv3

Author Information
------------------

Greg Hellings <[email protected]>
39 changes: 39 additions & 0 deletions roles/libvirt_rhel_vm/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
libvirt_rhel_vm_become: true
libvirt_rhel_vm_become_user: root
libvirt_rhel_vm_storage: /var/lib/libvirt/images
libvirt_rhel_vm_nic_config_path: null
libvirt_rhel_vm_domain:
name: foo.example.com
img_path: ~/CentOS-7-x86_64-GenericCloud.qcow2
os-variant: rhel7.7

ram: 4096
vcpus: 1
disk: 20G

root_passwd: "$6$Fa84yQfpK0gpluDJ$sdfdsfdsfdsfdsfdfdsfdsfddsfdaQpO6MKgioTOV5\
lRy.2tdA9IexTnvYNK3mP8clpC/sdsfdsfdsfdsfdsfsd60"
root_ssh_pub_keys:
- "ssh-rsa AAAdsfdfdsfdfdEAAAADAQABAAABAQD0yXYYdsfdAOSdIjcRp\
8TVOPnFplYJEY8VST+bQeW1Fosdfddfsdfmpmd/RdV9W/0d7sRfymL1diDm6ml3kwddff5Xn7A\
edsztdRahvZsBD9ADBqnQBli0adop6+PDRsdfdsfBjpFnrwVoe9QZPJVqZle6HBeJYIffffEY6\
1vhC8JXyGGDIJi7pdSjPdsfdsfdsfdsfdsfdsfsxTbAp4ddkEuS/9NR8JZ3HJg+h6mKoNffffq\
RUiikG98dfdfdsfdsfdfdfdfdfdfdsfdsfdsfdsfdsfyPMXK7nD+R0Jx4mmRlFWKmYTjffffSq\
sdfadfdsfdsfdsf"

bridges:
- br0

nics:
- filename: ifcfg-eth0
NAME: eth0
DEVICE: eth0
TYPE: Ethernet
BOOTPROTO: static
IPADDR: 192.168.1.10
GATEWAY: 192.168.1.1
NETMASK: 255.255.255.0
DEFROUTE: !!str yes
IPV6INIT: !!str no
ONBOOT: !!str yes
DNS1: 8.8.8.8
1 change: 1 addition & 0 deletions roles/libvirt_rhel_vm/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Handlers for libvirt
16 changes: 16 additions & 0 deletions roles/libvirt_rhel_vm/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
galaxy_info:
author: Greg Hellings
description: |-
Spins up a local VM from the RHEL/CentOS family on the target machine
company: Red Hat, Inc.
license: GPLv3
min_ansible_version: 2.8
platforms:
- name: EL
versions:
- 7

galaxy_tags:
- oasis

dependencies: []
5 changes: 5 additions & 0 deletions roles/libvirt_rhel_vm/molecule/default/molecule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
driver:
name: openstack
platforms:
- name: test-libvirt
flavor: ci.m1.large
4 changes: 4 additions & 0 deletions roles/libvirt_rhel_vm/molecule/default/tests/test_running.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def test_running(host):
with host.sudo():
virsh = host.check_output("virsh list")
assert "foo.example.com" in virsh
Loading

0 comments on commit a5e9cae

Please sign in to comment.