Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Libvirt #2

Merged
merged 7 commits into from
May 26, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 6 additions & 31 deletions .github/workflows/tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,48 +17,20 @@ env:
COLLECTION: system

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout submodules
shell: bash
run: |
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
git submodule sync --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --remote --recursive
- name: Setup pip cache
uses: actions/cache@v1
with:
path: ~/.cache/pip
key: pip-modules
- name: Set up Python 3.8
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
shell: bash
run: |
set -e
python -m pip install --upgrade pip
pip install tox tox-ansible
- name: Run lint
shell: bash
run: tox -e lint_all

build:
needs: lint
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
role:
- cockpit
- chrony
- disk_wipe
- firewalld
- libvirtd
- localectl
- molecule_docker_ci
- network_scripts
- nfs_server
- nmcli_add_addrs
- package_updater
Expand Down Expand Up @@ -91,7 +63,10 @@ jobs:
python-version: 3.8
- name: Install dependencies
run: |
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]>
Loading