From d6e3e05796b5b6e72ef7af2cc23f5bb45999be8f Mon Sep 17 00:00:00 2001 From: emdee Date: Tue, 26 Dec 2023 00:06:43 +0000 Subject: [PATCH] add --- __init__.py | 0 gentooimgr/command.py | 2 + gentooimgr/common.py | 14 ++- gentooimgr/configs/base.json | 2 +- gentooimgr/install.py | 2 +- gentooimgr/qemu.py | 25 +++- gentooimgr/run.py | 5 + gentooimgr/status.py | 5 +- library/ansible-keepassxc.py | 189 ++++++++++++++++++++++++++++ library/ansible_gentooimgr.py | 224 ++++++++++++++++++++++++++++++++++ 10 files changed, 455 insertions(+), 13 deletions(-) create mode 100644 __init__.py create mode 100755 library/ansible-keepassxc.py create mode 100755 library/ansible_gentooimgr.py diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gentooimgr/command.py b/gentooimgr/command.py index 136f854..3628ae9 100644 --- a/gentooimgr/command.py +++ b/gentooimgr/command.py @@ -1,4 +1,6 @@ import sys +from subprocess import Popen, PIPE + import gentooimgr.chroot def command(config, *args): diff --git a/gentooimgr/common.py b/gentooimgr/common.py index 1102db9..085461a 100644 --- a/gentooimgr/common.py +++ b/gentooimgr/common.py @@ -15,9 +15,8 @@ def older_than_a_day(fullpath): return time.time() - filetime > (gentooimgr.config.DAY_IN_SECONDS * gentooimgr.config.DAYS) - - def find_iso(download_dir): + LOG.info(f"Looking for iso in {download_dir}") name = None ext = None found = [] @@ -34,9 +33,10 @@ def make_iso_from_dir(mydir): path to iso that was created or NoneType if mydir is not found """ if not os.path.exists(mydir): + LOG.warn(f"\t:: dir not found {mydir}") return - print(f"\t:: Making ISO with dir of {mydir}") + LOG.info(f"\t:: Making ISO with dir of {mydir}") path = os.path.join(mydir, "..", "cloudgen.iso") proc = Popen(["mkisofs", "--input-charset", "utf-8", @@ -52,15 +52,17 @@ def make_iso_from_dir(mydir): return path -def portage_from_dir(d, filename=None): +def portage_from_dir(download_dir, filename=None): """Find portage file from directory. Will do a check in os.listdir() for portage*.tar.bz2. If a filename is provided, this function either returns that filename assuming it exists in d, or return None. If filename is None, this looks through all entries for portage files and if only one exists, returns it, otherwise None. """ + assert download_dir, f"empty {download_dir} for for portage" + LOG.info(f"Looking for portage in {download_dir}") found = [] - for f in os.listdir(d): + for f in os.listdir(download_dir): if filename is not None: if filename == f: found.append(f) @@ -70,7 +72,7 @@ def portage_from_dir(d, filename=None): if len(found) > 1: LOG.error("\tEE: More than one portage file exists, please specify the exact portage file with --portage [file] or remove all others\n") LOG.error(''.join([f"\t{f}\n" for f in found])) - LOG.error(f"in {d}\n") + LOG.error(f"in {download_dir}\n") sys.exit(1) return found[0] if found else None diff --git a/gentooimgr/configs/base.json b/gentooimgr/configs/base.json index be534e5..f3b3cc3 100644 --- a/gentooimgr/configs/base.json +++ b/gentooimgr/configs/base.json @@ -72,7 +72,7 @@ "syslog-ng": "default", "cronie": "default", "acpid": "default", - "ntp": "default" + "ntp": "default", "qemu-guest-agent": "default" }, "iso": null, diff --git a/gentooimgr/install.py b/gentooimgr/install.py index a98b881..d58eec0 100644 --- a/gentooimgr/install.py +++ b/gentooimgr/install.py @@ -151,7 +151,7 @@ def step10_emerge_pkgs(args, cfg): proc = Popen(["emerge", "-j1", single]) proc.communicate() - LOG.info("KERNEL PACKAGES", packages.get("kernel")) + LOG.info(f"KERNEL PACKAGES {packages.get('kernel')}") if packages.get("kernel", []): cmd = ["emerge", "-j", str(args.threads)] + packages.get("kernel", []) proc = Popen(cmd) diff --git a/gentooimgr/qemu.py b/gentooimgr/qemu.py index 9d5f049..0e710da 100644 --- a/gentooimgr/qemu.py +++ b/gentooimgr/qemu.py @@ -3,6 +3,8 @@ import os import sys import argparse from subprocess import Popen, PIPE + +from gentooimgr import LOG import gentooimgr.config import gentooimgr.common def create_image(args, config: dict, overwrite: bool = False) -> str: @@ -36,6 +38,8 @@ def run_image( - mount_isos: list of iso paths to mount in qemu as disks. """ iso = config.get("iso") + prefix = args.temporary_dir + LOG.info(f"iso from config {iso}") if iso is None: iso = gentooimgr.common.find_iso( os.path.join( @@ -43,6 +47,13 @@ def run_image( ".." ) ) + LOG.info(f"iso from cwd {iso}") + if not iso: + prefix = config.get('temporary_dir') + iso = gentooimgr.common.find_iso(prefix) + LOG.info(f"iso from {prefix} {iso}") + + assert iso, f"iso not found {iso}" if isinstance(iso, list): assert len(iso), f"iso list is empty {iso}" @@ -55,6 +66,11 @@ def run_image( qmounts.append("-drive") qmounts.append(f"file={i},media=cdrom") + assert image, f"image is empty {image}" + if not os.path.exists(image): + if os.path.exists(os.path.join(prefix, image)): + image = os.path.join(prefix, image) + assert os.path.exists(image), f"image not found {image}" threads = args.threads cmd = [ "qemu-system-x86_64", @@ -65,7 +81,6 @@ def run_image( "-drive", f"file={image},if=virtio,index=0", "-cdrom", iso, "-net", "nic,model=virtio", - "-net", "user", "-vga", "virtio", "-cpu", "kvm64", "-chardev", "file,id=charserial0,path=gentoo.log", @@ -73,12 +88,14 @@ def run_image( "-chardev", "pty,id=charserial1", "-device", "isa-serial,chardev=charserial1,id=serial1" ] + # "-net", "user", + # -net user: network backend 'user' is not compiled into this binary" + cmd += qmounts - print(cmd) + LOG.info(cmd) proc = Popen(cmd, stderr=PIPE, stdout=PIPE) stdout, stderr = proc.communicate() if stderr: - sys.stderr.write(str(stderr)) - sys.stderr.write("\n") + LOG.error(str(stderr)) diff --git a/gentooimgr/run.py b/gentooimgr/run.py index 270c9d0..ce63533 100644 --- a/gentooimgr/run.py +++ b/gentooimgr/run.py @@ -23,6 +23,11 @@ def run(args, config: dict) -> None: assert os.path.isfile(main_iso), f"iso not found {main_iso}" LOG.info(args) LOG.info(f'iso={args.iso}') + if args.iso != config['iso']: + LOG.warn(f'iso={args.iso}') + config['iso'] = args.iso + else: + LOG.info(f'iso={args.iso}') gentooimgr.qemu.run_image( args, config, diff --git a/gentooimgr/status.py b/gentooimgr/status.py index 42f4672..b045a62 100644 --- a/gentooimgr/status.py +++ b/gentooimgr/status.py @@ -17,14 +17,17 @@ Step 16: Sysconfig Step 17: fstab """ import os +import sys import json # from gentooimgr import LOG import gentooimgr.config import gentooimgr.configs +from gentooimgr import install -def print_template(args, configjson): +def print_template(args, configjson, prefix='/tmp'): print(__doc__) + sys.stderr.write(f"the last step to succeed is {install.getlaststep(prefix)}\n") print(f"the last step to succeed is {install.getlaststep(prefix)}\n") print(f"""------------------------ STATUS ------------------------ diff --git a/library/ansible-keepassxc.py b/library/ansible-keepassxc.py new file mode 100755 index 0000000..ef391be --- /dev/null +++ b/library/ansible-keepassxc.py @@ -0,0 +1,189 @@ +#!/usr/bin/python + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import traceback +from ansible.module_utils.basic import AnsibleModule, missing_required_lib + +IMPORT_ERR = None +try: +# import _argon2_xffi_bindings + import pykeepass as keepass +except ImportError: + IMPORT_ERR = traceback.format_exc() + +DOCUMENTATION = r''' +--- +module: ansible-keepassxc + +short_description: Module to read credentials from KeePassXC + +version_added: "0.0.1" + +description: Module to read credentials from KeePassXC + +options: + database: + description: Path to database file + required: true + type: str + password: + description: Database Password + required: true + type: str + keyfile: + description: Path to key file + required: false + type: str + entry: + description: Entry name for the attribute to fetch + required: true + type: str + group: + decription: Group name that the Entry belongs to + required: false + type: str + +author: + - Jeremy Lumley (@jlumley) +''' + +EXAMPLES = r''' +# Fetch the credentials for the server_1 entry in any group +- name: Fetch server_1 credentials + jlumley.jlumley.ansible-keepassxc: + database: "/secrets/db.kdbx" + password: "s3cure_p4550rd" + entry: "server_1" + +# Fetch the reddit entry in the social group +- name: Fetching reddit credentials + jlumley.jlumley.ansible-keepassxc: + database: "/secrets/db.kdbx" + password: "sup3r_s3cure_p4550rd" + entry: "reddit" + group: "social" + +# Fetch a custom strig attribute from the github entry +- name: Fetch Github API Token + jlumley.jlumley.ansible-keepassxc: + database: "/secrets/db.kdbx" + password: "d0pe_s3cure_p4550rd" + keyfile: "/secrets/top_secret_key" + entry: "github" + group: "development" + +''' + +RETURN = r''' +# Return values +username: + description: Username of entry if present + type: str + returned: always + sample: 's3cr3t_us3r' +password: + description: Password of entry if present + type: str + returned: always + sample: 's3cr3t_p455word' +url: + description: Url of entry if present + type: str + returned: always + sample: 'http://reddit.com' +custom_fields: + description: dictionary containing all custom fields + type: dict + returned: always + sample: False +no_log: + description: suppress logging of password + type: bool + returned: never + sample: False +''' + +def run_module(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + database = dict(type='str', required=True), + password = dict(type='str', required=False, + default=os.environ.get('ANSIBLE_KEEPASSXC_PASSWORD')), + keyfile = dict(type='str', required=False, default=None), + entry = dict(type='str', required=True), + group = dict(type='str', required=False), + no_log = dict(type='bool', required=False, default=False), + ) + + # seed the result dict in the object + result = dict( + changed=False, + username='', + password='', + url='', + custom_fields={} + ) + + # Currently no support for a check_mode this maybe added later if + # functionality to modify the database is added later + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=False, + ) + + if IMPORT_ERR: + module.fail_json( + msg=missing_required_lib("pykeepass"), + exception=IMPORT_ERR + ) + + # unlock local keepass database + try: + kp = keepass.PyKeePass( + module.params['database'], + password=module.params['password'], + keyfile=module.params['keyfile']) + except keepass.exceptions.CredentialsError: + module.fail_json(msg='Invalid Credentials') + + # find entry + entry = kp.find_entries( + title=module.params['entry'], + group=module.params['group'] + ) + + # fail is entry is not present + if not entry: + module.fail_json(msg=f"Unable to find entry: {module.params['entry']}") + + else: + entry = entry[0] + custom_field_keys = entry._get_string_field_keys(exclude_reserved=True) + custom_fields = dict() + for key in custom_field_keys: + custom_fields[key] = entry.get_custom_property(key) + result = dict ( + changed=False, + username=entry.username, + password=entry.password, + url=entry.url, + custom_fields=custom_fields + ) + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() + + diff --git a/library/ansible_gentooimgr.py b/library/ansible_gentooimgr.py new file mode 100755 index 0000000..af66548 --- /dev/null +++ b/library/ansible_gentooimgr.py @@ -0,0 +1,224 @@ +#!/usr/bin/python3 + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import sys +import logging +from argparse import Namespace +import pathlib +import traceback + +sys.path.append('/mnt/o/var/local/src/play_tox/src/ansible_gentooimgr') +# in the library +mod_path = os.path.dirname(os.path.realpath('__file__')) +mod_path = os.path.join(mod_path, 'src', 'ansible_gentooimgr') +assert os.path.isdir(mod_path), f"parent {mod_path}" +assert os.path.isfile(os.path.join(mod_path, '__init__.py')),f"index {mod_path}" +assert os.path.isdir(os.path.join(mod_path, 'gentooimgr')), f"sub {mod_path}" +sys.path.append(mod_path) +try: + import gentooimgr +except Exception as e: + sys.stderr.write(f"{mod_path} {sys.path} {traceback.print_exc()}") + raise +import ansible + +DOCUMENTATION = rf''' +--- +module: gentooimgr + +short_description: Gentoo Image Builder for Cloud and Turnkey ISO installers + + +version_added: "1.0.0" + +description: +* This project enables easy access to building ``systemd`` or ``openrc`` -based images. +* Performs automatic download AND verification of the linux iso, stage3 tarball and portage. +* Caches the iso and stage3 .txt files for at most a day before redownloading and rechecking for new files +* Sane and readable cli commands to build, run and test. +* Step system to enable user to continue off at the same place if a step fails +* No heavy packages like rust included ** TODO + +options: + action: + description: The action to be run by the image builder + choices: + - build + - run + - status + - install + - chroot + - unchroot + - command + - shrink + - kernel + required: true + # clean test + config: + default: cloud.json + description: init configuration file or or base.json or cloud.json + required: false + loglevel: + default: {logging.INFO} + description: python logging level <= 50, INFO=20 + required: false + threads: + default: 1 + description: Number of threads to use + required: false + profile: + default: openrc + description: The init system + choices: + - openrc + - systemd + required: false + kernel_dir: + default: /usr/src/linux + description: Where kernel is specified. By default uses the active linux kernel + required: false + portage: + description: Extract the specified portage tarball onto the filesystem + required: false + stage3: + description: Extract the specified stage3 package onto the filesystema + required: false + action_args: + default: [] + description: Arguments for some of the actions - UNUSED! + required: false + temporary_dir: + description: Path to temporary directory for downloading files (20G) + required: false + qcow: + description: Path to file to serve as the base image + required: false + +# Specify this value according to your collection +# in format of namespace.collection.doc_fragment_name +# extends_documentation_fragment: +# - my_namespace.my_collection.my_doc_fragment_name + +author: + - Your Name (@yourGitHubHandle) +''' + +#[-y DAYS] +# [-d DOWNLOAD_DIR] +# [-f] +# [--format FORMAT] + +EXAMPLES = r''' +# Pass in a message +- name: Test with a message + my_namespace.my_collection.my_test: + name: hello world + +# pass in a message and have changed true +- name: Test with a message and changed output + my_namespace.my_collection.my_test: + name: hello world + new: true + +# fail the module +- name: Test failure of the module + my_namespace.my_collection.my_test: + name: fail me +''' + +RETURN = r''' +# These are examples of possible return values, and in general should use other names for return values. +message: + description: The output message that the test module generates. + type: str + returned: always + sample: 'goodbye' +''' + +from ansible.module_utils.basic import AnsibleModule + + +def run_module(): + # define available arguments/parameters a user can pass to the module + module_args = dict( + action=dict(type='str', required=True), + loglevel=dict(type='int', required=False, default=logging.INFO), + threads=dict(type='int', required=False, default=1), + config=dict(type='str', default='cloud.json', required=False), + profile=dict(type='str', required=False), + kernel_dir=dict(type='path', required=False), + portage=dict(type='path', required=False), + stage3=dict(type='path', required=False), + temporary_dir=dict(type='path', required=False, default=pathlib.Path(os.getcwd())), + download_dir=dict(type='path', required=False, default=pathlib.Path(os.getcwd())), + qcow=dict(type='path', required=False), + ) + + # seed the result dict in the object + # we primarily care about changed and state + # changed is if this module effectively modified the target + # state will include any data that you want your module to pass back + # for consumption, for example, in a subsequent task + result = dict( + changed=False, + original_message='', + message='' + ) + + # the AnsibleModule object will be our abstraction working with Ansible + # this includes instantiation, a couple of common attr would be the + # args/params passed to the execution, as well as if the module + # supports check mode + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True + ) + + # if the user is working with this module in only check mode we do not + # want to make any changes to the environment, just return the current + # state with no modifications + if module.check_mode: + module.exit_json(**result) + + # manipulate or modify the state as needed (this is going to be the + # part where your module will do what it needs to do) + # if module.params.get('thirsty'): + + oargs = Namespace(**module.params) + # during the execution of the module, if there is an exception or a + # conditional state that effectively causes a failure, run + # AnsibleModule.fail_json() to pass in the message and the result + result['original_message'] = "" + try: + from gentooimgr.__main__ import main + retval = main(oargs) + except Exception as e: + result['message'] = str(e) + e = traceback.print_exc() + if e: result['original_message'] += f"{e}" + module.fail_json(msg='Exception', **result) + else: + result['message'] = str(retval) + + # use whatever logic you need to determine whether or not this module + # made any modifications to your target + if dArgs['action'] in ['status']: + result['changed'] = False + else: + result['changed'] = True + + # in the event of a successful module execution, you will want to + # simple AnsibleModule.exit_json(), passing the key/value results + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() +