From ee0324accd68dcf880ef5e22811fd2a8c440cd86 Mon Sep 17 00:00:00 2001 From: Mayfly277 Date: Wed, 28 Aug 2024 12:15:59 +0200 Subject: [PATCH] goad python wrapper update --- ansible/private_data_dir/.git_keep | 0 ansible/wait5m.yml | 14 ++ globalsettings.ini | 1 + goad.ini | 29 +--- goad.py | 119 +++++++++------ goad/command/cmd.py | 13 ++ goad/command/linux.py | 77 +++++++++- goad/config.py | 19 +-- goad/exceptions.py | 7 + goad/gui.py | 26 ---- goad/infos.py | 39 +++++ goad/jumpbox.py | 64 ++++++++ goad/lab_controller.py | 56 ------- goad/lab_manager.py | 116 +++++++++++++++ goad/labs.py | 3 + goad/log.py | 2 +- goad/menu.py | 45 ++++-- goad/provider/provider.py | 5 +- goad/provider/terraform/aws.py | 6 + goad/provider/terraform/azure.py | 211 +++++++++++++++++++++++++++ goad/provider/terraform/proxmox.py | 2 + goad/provider/terraform/terraform.py | 12 +- goad/provider/vagrant/vagrant.py | 3 - goad/provider/vagrant/virtualbox.py | 2 + goad/provider/vagrant/vmware.py | 2 + goad/provisioner/ansible/ansible.py | 97 ++++++++++++ goad/provisioner/ansible/docker.py | 8 + goad/provisioner/ansible/local.py | 27 ++++ goad/provisioner/ansible/remote.py | 50 +++++++ goad/provisioner/ansible/runner.py | 38 +++++ goad/provisioner/provisioner.py | 29 ++++ goad/utils.py | 56 ++++++- playbooks.yml | 66 +++++++++ requirements.yml | 0 34 files changed, 1049 insertions(+), 195 deletions(-) create mode 100644 ansible/private_data_dir/.git_keep create mode 100644 ansible/wait5m.yml delete mode 100644 goad/gui.py create mode 100644 goad/infos.py create mode 100644 goad/jumpbox.py delete mode 100644 goad/lab_controller.py create mode 100644 goad/lab_manager.py create mode 100644 goad/provisioner/ansible/ansible.py create mode 100644 goad/provisioner/ansible/docker.py create mode 100644 goad/provisioner/ansible/local.py create mode 100644 goad/provisioner/ansible/remote.py create mode 100644 goad/provisioner/ansible/runner.py create mode 100644 goad/provisioner/provisioner.py create mode 100644 playbooks.yml create mode 100644 requirements.yml diff --git a/ansible/private_data_dir/.git_keep b/ansible/private_data_dir/.git_keep new file mode 100644 index 00000000..e69de29b diff --git a/ansible/wait5m.yml b/ansible/wait5m.yml new file mode 100644 index 00000000..439e1032 --- /dev/null +++ b/ansible/wait5m.yml @@ -0,0 +1,14 @@ +#Aleem Ladha @LadhaAleem +#Credits to SOCFortress and Mayfly277 +- name: Install and configure Wazuh Manager + hosts: wazuh_server + become: yes + roles: + - { role: 'wazuh_manager', tags: 'wazuh_manager' } + +- name: Install Wazuh Agent + hosts: wazuh_agents + roles: + - { role: 'wazuh_agent', tags: 'wazuh_agent' } + vars: + wazuh_manager_host: "{{ hostvars['wazuh']['ansible_host'] }}" diff --git a/globalsettings.ini b/globalsettings.ini index ae5c2ac4..c7646ada 100644 --- a/globalsettings.ini +++ b/globalsettings.ini @@ -1,4 +1,5 @@ [all:vars] +; This is the global inventory file, data here will override all lab or provider inventory datas ; modify this to add layouts to VMs keyboard_layouts=["en-US", "fr-FR"] ; define here the default layout to use (must be in the keyboard_layouts list) diff --git a/goad.ini b/goad.ini index fd3c8f41..803207c7 100644 --- a/goad.ini +++ b/goad.ini @@ -3,25 +3,10 @@ lab = GOAD ; provider : virtualbox / vmware / aws / azure provider = vmware -; providing method : local / docker -providing_method = local - - -[proxmox] - -[aws] -profile=default -aws_access_key_id= -aws_secret_access_key= - -[azure] -profile=default -subcription= - -[GOAD-extensions] -wazuh = false -elk = false -ws01 = false -attackbox = false -guacamole = false -linux = false +; provisioning method : +; local (default) : use subprocess to run ansible playbook +; runner : use ansible runner localy to run ansible playbook +; docker : use docker container to run ansible +; remote : launch ansible with ssh through the jumpbox (azure/aws only) +; if provisioner is not compatible it will be force to default +provisioner = local diff --git a/goad.py b/goad.py index c70409ef..23880a4b 100644 --- a/goad.py +++ b/goad.py @@ -3,32 +3,17 @@ from rich import print from goad.config import Config -from goad.lab_controller import LabController -from goad.menu import print_menu +from goad.jumpbox import JumpBox +from goad.lab_manager import LabManager +from goad.menu import print_menu, print_logo from goad.log import Log from goad.utils import * from goad.labs import * -from goad.gui import * +from goad.infos import * class Goad(cmd.Cmd): - @staticmethod - def print_logo(): - logo = """[white] - _____ _____ _____ - / ____| / ||| \ [blue] /\\\\[/blue] | __ \ - | | __|| ||| | [blue]/ \\\\[/blue] | | | | - | | |_ || ||| |[blue]/ /\ \\\\[/blue] | | | | - | |__| || ||| [blue]/ /__\ \\\\[/blue]| |__| | - \_____| \_|||_[blue]/________\\\\[/blue]_____/ - [bold]Game Of Active Directory[/bold] - [yellow][italic]Pwning is comming[/italic][/yellow] -[/white] -Goad management console type help or ? to list commands -""" - print(logo) - def __init__(self, args): super().__init__() # get the arguments @@ -40,19 +25,19 @@ def __init__(self, args): config = Config().merge_config(args) # prepare lab controller to manage labs - self.lab_controller = LabController().init(labs, config) + self.lab_manager = LabManager().init(labs, config) # set current lab and provider self.refresh_prompt() def refresh_prompt(self): - self.prompt = f"\n{self.lab_controller.get_current_lab_name()} @ {self.lab_controller.get_current_provider_name()} > " + self.prompt = f"\n{self.lab_manager.get_current_lab_name()} @ {self.lab_manager.get_current_provider_name()} > " def default(self, line): print() def do_help(self, arg): - print_menu() + print_menu(self.lab_manager) def do_exit(self, arg): print('bye') @@ -60,25 +45,58 @@ def do_exit(self, arg): # main commands def do_status(self, arg): - self.lab_controller.get_current_provider().status() + self.lab_manager.get_current_provider().status() def do_check(self, arg): - self.lab_controller.get_current_provider().check() + self.lab_manager.get_current_provider().check() def do_install(self, arg): - self.lab_controller.get_current_provider().install() + self.lab_manager.get_current_provider().install() def do_start(self, arg): - self.lab_controller.get_current_provider().start() + self.lab_manager.get_current_provider().start() def do_stop(self, arg): - self.lab_controller.get_current_provider().stop() + self.lab_manager.get_current_provider().stop() def do_destroy(self, arg): - self.lab_controller.get_current_provider().destroy() + self.lab_manager.get_current_provider().destroy() + + def do_provide(self, arg): + self.lab_manager.get_current_provider().install() + + def do_provision(self, arg): + if arg == '': + Log.error('missing playbook argument') + Log.info('provision ') + else: + # run playbook + self.lab_manager.get_current_provisioner().run(arg) - def do_lab_info(self, arg): - pass + def do_provision_lab(self, arg): + self.lab_manager.get_current_provisioner().run() + + def do_provision_lab_from(self, arg): + self.lab_manager.get_current_provisioner().run_from(arg) + + def do_prepare_jumpbox(self, arg): + if self.lab_manager.get_current_provisioner().provisioner_name == 'ansible_remote': + self.lab_manager.get_current_provisioner().prepare_jumpbox() + else: + Log.error('no remote provisioning') + + def do_show_config(self, arg): + show_current_config(self.lab_manager) + + def do_ssh_jumpbox(self, arg): + if self.lab_manager.get_current_provider_name() == AZURE or self.lab_manager.get_current_provider_name() == AWS: + try: + jump_box = JumpBox(self.lab_manager.get_current_lab_name(), self.lab_manager.get_current_provider()) + jump_box.ssh() + except JumpBoxInitFailed as e: + Log.error('Jumpbox retrieve connection info failed, abort') + else: + Log.error('No jump box for this provider') # configuration def do_set_lab(self, arg): @@ -92,15 +110,13 @@ def do_set_lab(self, arg): Log.info('set_lab ') else: try: - if self.lab_controller.set_lab(arg): + if self.lab_manager.set_lab(arg): Log.success(f'Lab {arg} loaded') - # lab has changed, so change the provider too - self.do_set_provider(self.lab_controller.get_current_provider_name()) self.refresh_prompt() except ValueError as err: Log.error(err.args[0]) Log.info('Available labs :') - for lab in self.lab_controller.labs: + for lab in self.lab_manager.labs: Log.info(f' - {lab}') def do_set_provider(self, arg): @@ -111,31 +127,40 @@ def do_set_provider(self, arg): """ if arg == '': Log.error('missing provider argument') - Log.info('set_provider ') + Log.info(f'set_provider (allowed values : {",".join(ALLOWED_PROVIDERS)})') else: try: - if self.lab_controller.set_provider(arg): + if self.lab_manager.set_provider(arg): Log.success(f'Provider {arg} loaded') self.refresh_prompt() else: - Log.error(f'provider {arg} does not exist on lab {self.lab_controller.get_current_lab_name()}') + Log.error(f'provider {arg} does not exist on lab {self.lab_manager.get_current_lab_name()}') Log.info('Available Providers :') - for provider_name in self.lab_controller.get_lab_providers(self.lab_controller.get_current_lab_name()): + for provider_name in self.lab_manager.get_lab_providers(self.lab_manager.get_current_lab_name()): Log.info(f' - {provider_name}') except ValueError as err: Log.error(err.args[0]) - # def do_show_config(self, arg): - # self.config.show_config() + def do_set_provisioning_method(self, arg): + if arg == '': + Log.error('missing provisioner argument') + Log.info(f'set_provisioner (allowed values : {",".join(ALLOWED_PROVISIONER)})') + else: + try: + if self.lab_manager.set_provisioner(arg): + Log.success(f'Provisioner {arg} loaded') + self.refresh_prompt() + else: + Log.error(f'provisioner {arg} does not exist on lab {self.lab_manager.get_current_lab_name()}') + Log.info(f'Available Provisioner : {",".join(ALLOWED_PROVISIONER)}') + except ValueError as err: + Log.error(err.args[0]) - def do_show_table_providers(self, arg): - show_labs_providers(self.lab_controller.get_labs()) + def do_show_providers_table(self, arg): + show_labs_providers_table(self.lab_manager.get_labs()) def do_show_list_providers(self, arg): - for lab in self.lab_controller.get_labs(): - Log.success(f'*** {lab.lab_name} ***') - for provider in lab.providers.keys(): - Log.info(f' {provider}') + show_labs_providers_list(self.lab_manager.get_labs()) def parse_args(): @@ -163,7 +188,7 @@ def show_help(): if __name__ == '__main__': - Goad.print_logo() + print_logo() args = parse_args() goad = Goad(args) diff --git a/goad/command/cmd.py b/goad/command/cmd.py index d4737009..5ac903d1 100644 --- a/goad/command/cmd.py +++ b/goad/command/cmd.py @@ -1,5 +1,8 @@ class Command: + def run(self, cmd, args, path): + pass + def check_vagrant(self): pass @@ -11,3 +14,13 @@ def check_terraform(self): def run_terraform(self, args, path): pass + + def run_terraform_output(self, args, path): + pass + + def run_ansible(self, args, path): + pass + + def get_azure_account_output(self): + pass + diff --git a/goad/command/linux.py b/goad/command/linux.py index 8326e5f6..502a660b 100644 --- a/goad/command/linux.py +++ b/goad/command/linux.py @@ -3,10 +3,29 @@ from goad.command.cmd import Command import subprocess from goad.log import Log +from goad.utils import * class LinuxCommand(Command): + def run_shell(self, command, path): + try: + Log.info('CWD: ' + get_relative_path(str(path))) + Log.cmd(command) + subprocess.run(command, cwd=path, shell=True) + except subprocess.CalledProcessError as e: + Log.error(f"An error occurred while running the command: {e}") + + def run_command(self, command, path): + result = None + try: + Log.info('CWD: ' + get_relative_path(str(path))) + Log.cmd(command) + result = subprocess.run(command, cwd=path, stderr=sys.stderr, stdout=sys.stdout, shell=True) + except subprocess.CalledProcessError as e: + Log.error(f"An error occurred while running the command: {e}") + return result + def check_vagrant(self): command = 'which vagrant >/dev/null' try: @@ -22,7 +41,7 @@ def run_vagrant(self, args, path): try: command = ['vagrant'] command += args - Log.info('CWD: ' + str(path)) + Log.info('CWD: ' + get_relative_path(str(path))) Log.cmd(' '.join(command)) result = subprocess.run(command, cwd=path, stderr=sys.stderr, stdout=sys.stdout) except subprocess.CalledProcessError as e: @@ -44,15 +63,63 @@ def run_terraform(self, args, path): try: command = ['terraform'] command += args - Log.info('CWD: ' + str(path)) + Log.info('CWD: ' + get_relative_path(str(path))) Log.cmd(' '.join(command)) result = subprocess.run(command, cwd=path, stderr=sys.stderr, stdout=sys.stdout) except subprocess.CalledProcessError as e: Log.error(f"An error occurred while running the command: {e}") return result + def run_terraform_output(self, args, path): + result = None + try: + command = ['terraform', 'output', '-raw'] + command += args + Log.info('CWD: ' + get_relative_path(str(path))) + Log.cmd(' '.join(command)) + result = subprocess.run(command, cwd=path, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + if result.returncode != 0: + print(f"Error: {result.stderr}") + return None + + return result.stdout + except subprocess.CalledProcessError as e: + Log.error(f"An error occurred while running the command: {e}") + return None + def run_ansible(self, args, path): - pass + result = None + try: + command = 'ansible-playbook ' + command += args + Log.info('CWD: ' + get_relative_path(str(path))) + Log.cmd(command) + result = subprocess.run(command, cwd=path, stderr=sys.stderr, stdout=sys.stdout, shell=True) + except subprocess.CalledProcessError as e: + Log.error(f"An error occurred while running the command: {e}") + return False + return result + + def get_azure_account_output(self): + result = subprocess.run( + ["az", "account", "list", "--output", "json"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + if result.returncode != 0: + print(f"Error: {result.stderr}") + return None + + return result.stdout - def run_bash_script(self, args, path): - pass \ No newline at end of file + def rsync(self, source, destination, ssh_key): + # rsync = f'rsync -a --exclude-from='.gitignore' -e "ssh -o 'StrictHostKeyChecking no' -i $CURRENT_DIR/ad/$lab/providers/$provider/ssh_keys/ubuntu-jumpbox.pem" "$CURRENT_DIR/" goad@$public_ip:~/GOAD/' + Log.info(f'Launch Rsync {source} -> {destination}') + ssh_command = f"ssh -o 'StrictHostKeyChecking no' -i {ssh_key}" + command = f'rsync -a --exclude-from=".gitignore" -e "{ssh_command}" {source} {destination}' + self.run_shell(command, source) diff --git a/goad/config.py b/goad/config.py index aa26cea7..addac417 100644 --- a/goad/config.py +++ b/goad/config.py @@ -25,7 +25,7 @@ def merge_config(self, args): if args.provider: self.set(PROVIDER, args.provider) if args.method: - self.set(PROVIDING_METHOD, args.provider) + self.set(PROVISIONER, args.provider) # if args.extensions: # for extension in args.extensions: # lab_extensions_key = self.current_lab() + '-extensions' @@ -39,20 +39,3 @@ def get(self, key, section='default'): def set(self, key, value, section='default'): return self.config.set(section, key, value) - def current_provider(self): - return self.get(PROVIDER) - - def current_method(self): - return self.get(PROVIDING_METHOD) - - def show_config(self): - for key in self.config.sections(): - Log.success(f'*** {key} ***') - for param in self.config[key]: - config_value = str(self.config[key][param]) - if config_value == 'true': - config_value = f'[green]{config_value}[/green]' - elif config_value == 'false': - config_value = f'[red]{config_value}[/red]' - Log.info(f'{param} [white]'.ljust(48, '.') + f'[/white] {config_value}') - print() diff --git a/goad/exceptions.py b/goad/exceptions.py index f8073200..5900e4a6 100644 --- a/goad/exceptions.py +++ b/goad/exceptions.py @@ -1,3 +1,10 @@ class ProviderPathNotFound(Exception): pass + +class AuthenticationFailed(Exception): + pass + + +class JumpBoxInitFailed(Exception): + pass diff --git a/goad/gui.py b/goad/gui.py deleted file mode 100644 index de40d032..00000000 --- a/goad/gui.py +++ /dev/null @@ -1,26 +0,0 @@ -from rich.table import Table -from rich import print - - -def show_labs_providers(labs): - table = Table(title="Labs providers") - table.add_column('Lab') - headers = [] - for lab in labs: - for provider in lab.providers.keys(): - if provider not in headers: - headers.append(provider) - - for header in headers: - table.add_column(header) - - for lab in labs: - row_value = [lab.lab_name] - for header in headers: - if header in lab.providers.keys(): - row_value.append('[green]X[/green]') - else: - row_value.append('[red]-[/red]') - table.add_row(*row_value) - - print(table) diff --git a/goad/infos.py b/goad/infos.py new file mode 100644 index 00000000..63dc3977 --- /dev/null +++ b/goad/infos.py @@ -0,0 +1,39 @@ +from goad.log import * +from rich.table import Table + + +def show_current_config(lab_manager): + Log.info(f'Current Lab : {lab_manager.get_current_lab_name()}') + Log.info(f'Current Provider : {lab_manager.get_current_provider_name()}') + Log.info(f'Current Provisioner : {lab_manager.get_current_provisioner_name()}') + + +def show_labs_providers_list(labs): + for lab in labs: + Log.success(f'*** {lab.lab_name} ***') + for provider in lab.providers.keys(): + Log.info(f' {provider}') + + +def show_labs_providers_table(labs): + table = Table(title="Labs providers") + table.add_column('Lab') + headers = [] + for lab in labs: + for provider in lab.providers.keys(): + if provider not in headers: + headers.append(provider) + + for header in headers: + table.add_column(header) + + for lab in labs: + row_value = [lab.lab_name] + for header in headers: + if header in lab.providers.keys(): + row_value.append('[green]✓[/green]') + else: + row_value.append('[red]X[/red]') + table.add_row(*row_value) + + print(table) diff --git a/goad/jumpbox.py b/goad/jumpbox.py new file mode 100644 index 00000000..b8894a86 --- /dev/null +++ b/goad/jumpbox.py @@ -0,0 +1,64 @@ +import platform +from goad.command.linux import LinuxCommand +from goad.command.windows import WindowsCommand +from goad.exceptions import JumpBoxInitFailed +from goad.log import Log +from goad.utils import * + + +class JumpBox: + + def __init__(self, lab_name, provider): + self.lab_name = lab_name + self.provider = provider + self.ssh_key = self._get_jumpbox_key() + self.ip = provider.get_jumpbox_ip() + self.username = 'goad' + + if platform.system() == 'Windows': + self.command = WindowsCommand() + else: + self.command = LinuxCommand() + + if self.ip is None or self.ssh_key is None: + raise JumpBoxInitFailed('Missing elements for JumpBox remote connection') + + def _get_jumpbox_key(self): + ssh_key = get_ubuntu_jumpbox_key(self.lab_name, self.provider.provider_name) + if not os.path.isfile(ssh_key): + Log.error('Key file not found') + return None + return ssh_key + + def prepare_jumpbox(self): + script_name = self.provider.jumpbox_setup_script + script_file = get_script_path(script_name) + if not os.path.isfile(script_file): + Log.error(f'script file: {script_file} not found !') + return None + self.run_script(script_file) + + def ssh(self): + ssh_cmd = f"ssh -o 'StrictHostKeyChecking no' -i {self.ssh_key} {self.username}@{self.ip}" + self.command.run_shell(ssh_cmd, project_path) + + def run_script(self, script): + ssh_cmd = f"ssh -o 'StrictHostKeyChecking no' -i {self.ssh_key} {self.username}@{self.ip} 'bash -s' < {script}" + self.command.run_shell(ssh_cmd, project_path) + + def sync_sources(self): + """ + rsync ansible folder to the jumpbox ip + :return: + """ + # # rsync -a --exclude-from='.gitignore' -e "ssh -o 'StrictHostKeyChecking no' -i $CURRENT_DIR/ad/$lab/providers/$provider/ssh_keys/ubuntu-jumpbox.pem" "$CURRENT_DIR/" goad@$public_ip:~/GOAD/ + source = get_project_path() + destination = f'{self.username}@{self.ip}:~/GOAD/' + self.command.rsync(source, destination, self.ssh_key) + + def run_command(self, command, path): + ssh_cmd = f"ssh -t -o 'StrictHostKeyChecking no' -i {self.ssh_key} {self.username}@{self.ip} 'cd {path} && {command}'" + result = self.command.run_command(ssh_cmd, project_path) + if result is None or result.returncode != 0: + return False + return True diff --git a/goad/lab_controller.py b/goad/lab_controller.py deleted file mode 100644 index 747c0d5c..00000000 --- a/goad/lab_controller.py +++ /dev/null @@ -1,56 +0,0 @@ -import os -from goad.config import Config - -from goad.utils import * - - -class LabController(metaclass=SingletonMeta): - - def __init__(self): - self.labs = None - self.current_lab = None - self.current_provider = None - self.current_extensions = [] - self.config = None - - def init(self, labs, config): - self.labs = labs - self.config = config - # init lab and provider with config values - self.set_lab(self.config.get(LAB)) - self.set_provider(self.config.get(PROVIDER)) - return self - - def set_lab(self, lab_name): - self.current_lab = self.labs.get_lab(lab_name) - if self.current_lab is not None: - return self.current_lab - raise ValueError(f'lab {lab_name} not found') - - def set_provider(self, provider_name): - if self.current_lab is not None: - self.current_provider = self.current_lab.get_provider(provider_name) - if self.current_provider is not None: - return self.current_provider - else: - raise ValueError(f'provider {provider_name} not found') - else: - raise ValueError(f'current_lab not set') - - def get_labs(self): - return self.labs.get_labs_list() - - def get_current_lab(self): - return self.current_lab - - def get_current_lab_name(self): - return self.current_lab.lab_name - - def get_current_provider(self): - return self.current_provider - - def get_current_provider_name(self): - return self.current_provider.provider_name - - def get_lab_providers(self, lab): - return self.labs.get_current_lab(lab).providers.keys() diff --git a/goad/lab_manager.py b/goad/lab_manager.py new file mode 100644 index 00000000..2dd6f624 --- /dev/null +++ b/goad/lab_manager.py @@ -0,0 +1,116 @@ +import os +from goad.config import Config +from goad.provisioner.ansible.docker import DockerAnsibleProvisioner +from goad.provisioner.ansible.local import LocalAnsibleProvisionerCmd +from goad.provisioner.ansible.runner import LocalAnsibleProvisionerEmbed +from goad.provisioner.ansible.remote import RemoteAnsibleProvisioner + +from goad.utils import * +from goad.provisioner.provisioner import * +from goad.provisioner.ansible.ansible import * + + +class LabManager(metaclass=SingletonMeta): + + def __init__(self): + self.labs = None + self.current_lab = None + self.current_provider = None + self.current_provisioner = None + self.current_extensions = [] + self.config = None + + def init(self, labs, config): + self.labs = labs + self.config = config + # init lab and provider with config values + self.set_lab(self.config.get(LAB)) + self.set_provider(self.config.get(PROVIDER)) + self.set_provisioner(self.config.get(PROVISIONER)) + return self + + def set_lab(self, lab_name): + self.current_lab = self.labs.get_lab(lab_name) + if self.current_lab is not None: + if self.current_provider is not None: + # lab changed, change provider if needed + old_provider_name = self.current_provider.provider_name + # check if previous provider is present in the lab + if self.current_lab.get_provider(old_provider_name) is None: + Log.info(f'Provider {old_provider_name} not found in lab {lab_name}') + new_provider_name = self.current_lab.get_first_provider_name() + Log.info(f'Change provider to {new_provider_name}') + self.set_provider(new_provider_name) + return self.current_lab + else: + Log.error(f'lab {lab_name} not found') + Log.info('fallback to GOAD lab') + self.set_lab('GOAD') + + def set_provider(self, provider_name): + if self.current_lab is not None: + self.current_provider = self.current_lab.get_provider(provider_name) + if self.current_provider is not None: + if self.current_provisioner is None: + default_provisioner = self.current_provider.default_provisioner + self.set_provisioner(default_provisioner) + else: + self.set_provisioner(self.current_provisioner.provisioner_name) + return self.current_provider + else: + Log.error(f'provider {provider_name} not found') + new_provider_name = self.current_lab.get_first_provider_name() + Log.info(f'fallback to first provider found: {new_provider_name}, change it with : "set_provider"') + self.set_provider(new_provider_name) + else: + raise ValueError(f'current_lab not set') + + def set_provisioner(self, provisioner_name): + if self.current_lab is None: + raise ValueError(f'current_lab not set') + + if self.current_provider is None: + raise ValueError(f'current provider is not set') + + if provisioner_name not in self.current_provider.allowed_provisioners: + Log.info(f'provisioner method {provisioner_name} is not allowed for provider {self.current_provider.provider_name}') + Log.info(f'automatic changing provisioner method {provisioner_name} to default for this provider : {self.current_provider.default_provisioner}') + provisioner_name = self.current_provider.default_provisioner + + if provisioner_name == PROVISIONING_DOCKER: + self.current_provisioner = DockerAnsibleProvisioner(self.current_lab.lab_name, self.current_provider) + elif provisioner_name == PROVISIONING_RUNNER: + self.current_provisioner = LocalAnsibleProvisionerEmbed(self.current_lab.lab_name, self.current_provider) + elif provisioner_name == PROVISIONING_LOCAL: + self.current_provisioner = LocalAnsibleProvisionerCmd(self.current_lab.lab_name, self.current_provider) + elif provisioner_name == PROVISIONING_REMOTE: + self.current_provisioner = RemoteAnsibleProvisioner(self.current_lab.lab_name, self.current_provider) + + if self.current_provisioner is None: + raise ValueError(f'error during provisioner set {provisioner_name} not found') + + return self.current_provisioner + + def get_labs(self): + return self.labs.get_labs_list() + + def get_current_lab(self): + return self.current_lab + + def get_current_lab_name(self): + return self.current_lab.lab_name + + def get_current_provider(self): + return self.current_provider + + def get_current_provider_name(self): + return self.current_provider.provider_name + + def get_lab_providers(self, lab): + return self.labs.get_current_lab(lab).providers.keys() + + def get_current_provisioner(self): + return self.current_provisioner + + def get_current_provisioner_name(self): + return self.current_provisioner.provisioner_name diff --git a/goad/labs.py b/goad/labs.py index 762c4fd2..e4ed8783 100644 --- a/goad/labs.py +++ b/goad/labs.py @@ -68,6 +68,9 @@ def get_provider(self, provider_name): return None return self.providers[provider_name] + def get_first_provider_name(self): + return next(iter(self.providers)) + def get_extension(self, extension_name): return self.providers[extension_name] diff --git a/goad/log.py b/goad/log.py index acddfac9..f64fd7a0 100644 --- a/goad/log.py +++ b/goad/log.py @@ -30,4 +30,4 @@ def info(message, level=INFO): @staticmethod def cmd(message, level=INFO): if level >= log_level: - print(f'[cyan][*] [/cyan] Running command : [yellow]{message}[/yellow]') + print(f'[cyan][*] [/cyan]Running command : [yellow]{message}[/yellow]') diff --git a/goad/menu.py b/goad/menu.py index 8b1e84c8..9a6773f2 100644 --- a/goad/menu.py +++ b/goad/menu.py @@ -1,4 +1,20 @@ from rich import print +from goad.utils import * + +def print_logo(): + logo = """[white] + _____ _____ _____ + / ____| / ||| \ [blue] /\\\\[/blue] | __ \ + | | __|| ||| | [blue]/ \\\\[/blue] | | | | + | | |_ || ||| |[blue]/ /\ \\\\[/blue] | | | | + | |__| || ||| [blue]/ /__\ \\\\[/blue]| |__| | + \_____| \_|||_[blue]/________\\\\[/blue]_____/ + [bold]Game Of Active Directory[/bold] + [yellow][italic]Pwning is comming[/italic][/yellow] +[/white] +Goad management console type help or ? to list commands +""" + print(logo) def print_menu_title(title): @@ -11,28 +27,37 @@ def print_menu_entry(cmd, description): print(f'{line}[/white] [sky_blue3]{description}[/sky_blue3]') -def print_menu(): - print_menu_title('Main commands') - print_menu_entry('status', 'show current status') +def print_menu(lab_manager): + provider = lab_manager.get_current_provider_name() + + print_menu_title('Installation commands') print_menu_entry('check', 'check dependencies for install') - print_menu_entry('install', 'launch install') + print_menu_entry('install', 'launch install (provide + provision_lab)') + + print_menu_title('Manage commands') + print_menu_entry('status', 'show current status') print_menu_entry('start', 'start lab') print_menu_entry('stop', 'stop lab') print_menu_entry('destroy', 'destroy lab') - print_menu_entry('lab_info', 'display lab infos') print_menu_title('Configuration') print_menu_entry('show_config', 'show current configuration') print_menu_entry('set_lab ', 'change the lab to use') print_menu_entry('set_provider ', 'change the provider to use') - print_menu_entry('set_method ', 'change the provisioning method') + print_menu_entry('set_provisioning_method ', 'change the provisioning method') print_menu_title('Providing (Vagrant/Terrafom)') - print_menu_entry('create', 'run only the providing (vagrant/terraform)') + print_menu_entry('provide', 'run only the providing (vagrant/terraform)') + + if provider == AZURE or provider == AWS: + print_menu_title('JumpBox') + print_menu_entry('prepare_jumpbox', 'install package on the jumpbox for provisioning') + print_menu_entry('ssh_jumpbox', 'connect to jump box with ssh') print_menu_title('Provisioning (Ansible)') - print_menu_entry('run ', 'run specific ansible playbook') - print_menu_entry('run_all', 'run all the current lab ansible playbooks') + print_menu_entry('provision ', 'run specific ansible playbook') + print_menu_entry('provision_lab', 'run all the current lab ansible playbooks') + print_menu_entry('provision_lab_from ', 'run all the current lab ansible playbooks from specific playbook to the end') print_menu_title('Global commands') - print_menu_entry('global_status', 'show all lab status') + print_menu_entry('show_providers_table', 'show all labs and availble providers') diff --git a/goad/provider/provider.py b/goad/provider/provider.py index e7e9dc1d..5bcde369 100644 --- a/goad/provider/provider.py +++ b/goad/provider/provider.py @@ -10,6 +10,8 @@ class Provider(ABC): lab_name = '' provider_name = None + default_provisioner = PROVISIONING_LOCAL + allowed_provisioners = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER, PROVISIONING_REMOTE] def __init__(self, lab_name): self.lab_name = lab_name @@ -22,9 +24,6 @@ def __init__(self, lab_name): def check(self): pass - def dependencies(self): - pass - def install(self): pass diff --git a/goad/provider/terraform/aws.py b/goad/provider/terraform/aws.py index 4432701f..20aeb208 100644 --- a/goad/provider/terraform/aws.py +++ b/goad/provider/terraform/aws.py @@ -4,3 +4,9 @@ class AwsProvider(TerraformProvider): provider_name = AWS + default_provisioner = PROVISIONING_REMOTE + allowed_provisioners = [PROVISIONING_REMOTE] + + def __init__(self, lab_name): + super().__init__(lab_name) + self.jumpbox_setup_script = 'setup_aws.sh' \ No newline at end of file diff --git a/goad/provider/terraform/azure.py b/goad/provider/terraform/azure.py index b7c76584..30066f44 100644 --- a/goad/provider/terraform/azure.py +++ b/goad/provider/terraform/azure.py @@ -1,6 +1,217 @@ +import json +from goad.log import Log from goad.provider.terraform.terraform import TerraformProvider from goad.utils import * +from rich.table import Table +from goad.exceptions import * +from rich import print +from azure.identity import DefaultAzureCredential +from azure.core.exceptions import ClientAuthenticationError +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.compute import ComputeManagementClient +from azure.mgmt.network import NetworkManagementClient class AzureProvider(TerraformProvider): provider_name = AZURE + default_provisioner = PROVISIONING_REMOTE + allowed_provisioners = [PROVISIONING_REMOTE] + + def __init__(self, lab_name): + super().__init__(lab_name) + self.resource_group = lab_name + self.jumpbox_setup_script = 'setup_azure.sh' + + def _get_subscription_id(self): + credential = DefaultAzureCredential() + if credential is not None: + # find default subscription with subprocess (python sdk doesn't show it) + az_accounts = self.command.get_azure_account_output() + if az_accounts is not None: + subscriptions = json.loads(az_accounts) + for subscription in subscriptions: + if subscription.get("isDefault"): + return subscription.get("id") + return None + + def check(self): + # check terraform bin + super().check() + # check azure login + try: + credential = DefaultAzureCredential() + if credential is not None: + Log.success(f'Azure authentication ok') + + # find default subscription with subprocess (python sdk doesn't show it) + az_accounts = self.command.get_azure_account_output() + if az_accounts is not None: + subscriptions = json.loads(az_accounts) + for subscription in subscriptions: + if subscription.get("isDefault"): + Log.info(f'Subscription name : {subscription.get("name")}') + Log.info(f'Subscription id : {subscription.get("id")}') + Log.info(f'Tenant ID : {subscription.get("tenantId")}') + Log.info(f'State : {subscription.get("state")}') + Log.info('If you want to change subscription use: az account set --subscription "" ') + + except ClientAuthenticationError as error: + Log.error(f'Azure authentication error : {error.message}') + Log.info('Please login before launching the app with "az login"') + except Exception as error: + Log.error(f'Exception during the azure check : {error.message}') + + def _auth(self): + # Initialize DefaultAzureCredential + credential = DefaultAzureCredential() + if credential is None: + raise AuthenticationFailed('Azure authentication ko') + subscription_id = self._get_subscription_id() + if subscription_id is None: + raise AuthenticationFailed('Subscription ID not found') + return credential, subscription_id + + def status(self): + try: + credential, subscription_id = self._auth() + except AuthenticationFailed as e: + Log.error(e) + return False + + try: + # get VMS status + # Initialize the ComputeManagementClient with the credential + compute_client = ComputeManagementClient(credential, subscription_id) + network_client = NetworkManagementClient(credential, subscription_id) + + # List all VMs in the specified resource group + vms = compute_client.virtual_machines.list(self.resource_group) + + table = Table(title=f"Azure VMs for resource {self.resource_group}") + table.add_column('VM Id') + table.add_column('Name') + table.add_column('Location') + table.add_column('PowerState') + table.add_column('PublicIP') + table.add_column('PrivateIP') + + # Fetch details of each VM + vm_details = [] + for vm in vms: + # IPs + vm_public_ips = [] + vm_private_ips = [] + for interface in vm.network_profile.network_interfaces: + nic_name = " ".join(interface.id.split('/')[-1:]) + nic_resource_group = "".join(interface.id.split('/')[4]) + for ip in network_client.network_interfaces.get(nic_resource_group, nic_name).ip_configurations: + if ip.private_ip_address is not None: + vm_private_ips.append(ip.private_ip_address) + if ip.public_ip_address is not None: + public_ip_name = ip.public_ip_address.id.split('/')[-1] + # Get public IP address details + public_ip = network_client.public_ip_addresses.get(nic_resource_group, public_ip_name) + vm_public_ips.append(public_ip.ip_address) + + # powerstate + instance_view = compute_client.virtual_machines.instance_view(self.resource_group, vm.name) + + power_state = next((status.code for status in instance_view.statuses if status.code.startswith('PowerState/')), 'Unknown') + if power_state.startswith('PowerState/'): + power_state = power_state.split("PowerState/", 1)[1] + + if 'running' in power_state: + power_state = f'[green]{power_state}[/green]' + elif 'Unknown' in power_state: + power_state = f'[yellow]{power_state}[/yellow]' + else: + power_state = f'[red]{power_state}[/red]' + # Append the VM details to the list + table.add_row(vm.vm_id, vm.name, vm.location, power_state, ','.join(vm_public_ips), ','.join(vm_private_ips)) + print(table) + except Exception as e: + Log.error('Error retreiving running vms') + return False + + def start(self): + try: + credential, subscription_id = self._auth() + except AuthenticationFailed as e: + Log.error(e) + return False + try: + # Initialize the ComputeManagementClient with the credential + compute_client = ComputeManagementClient(credential, subscription_id) + + Log.info('Start the lab vms') + # List all VMs in the specified resource group + for vm in compute_client.virtual_machines.list(self.resource_group): + async_vm_stop = compute_client.virtual_machines.begin_start(self.resource_group, vm.name) + async_vm_stop.wait() + Log.success(f'vm {vm.name} started') + self.status() + except Exception as e: + Log.error('Error starting the vms') + return False + + def stop(self): + try: + credential, subscription_id = self._auth() + except AuthenticationFailed as e: + Log.error(e) + return False + + try: + # Initialize the ComputeManagementClient with the credential + compute_client = ComputeManagementClient(credential, subscription_id) + Log.info('Stopping the lab vms') + # List all VMs in the specified resource group + for vm in compute_client.virtual_machines.list(self.resource_group): + async_vm_stop = compute_client.virtual_machines.begin_deallocate(self.resource_group, vm.name) + async_vm_stop.wait() + Log.success(f'vm {vm.name} deallocate (no more billed)') + self.status() + except Exception as e: + Log.error('Error stoping the lab vms') + return False + + def _get_az_jumpbox_ip(self): + try: + credential, subscription_id = self._auth() + except AuthenticationFailed as e: + Log.error(e) + return None + # get VMS status + # Initialize the ComputeManagementClient with the credential + try: + compute_client = ComputeManagementClient(credential, subscription_id) + network_client = NetworkManagementClient(credential, subscription_id) + + # List all VMs in the specified resource group + vms = compute_client.virtual_machines.list(self.resource_group) + # Fetch details of each VM + vm_details = [] + for vm in vms: + if 'ubuntu-jumpbox' in vm.name: + # IPs + vm_public_ips = [] + for interface in vm.network_profile.network_interfaces: + nic_name = " ".join(interface.id.split('/')[-1:]) + nic_resource_group = "".join(interface.id.split('/')[4]) + for ip in network_client.network_interfaces.get(nic_resource_group, nic_name).ip_configurations: + if ip.public_ip_address is not None: + public_ip_name = ip.public_ip_address.id.split('/')[-1] + # Get public IP address details + public_ip = network_client.public_ip_addresses.get(nic_resource_group, public_ip_name) + return public_ip.ip_address + except Exception as e: + Log.error('Error retreiving jumpbox ip') + return False + return None + + def get_jumpbox_ip(self): + jumpbox_ip = self.command.run_terraform_output(['ubuntu-jumpbox-ip'], self.path) + if jumpbox_ip is None: + Log.error('Jump box ip not found') + return None + return jumpbox_ip diff --git a/goad/provider/terraform/proxmox.py b/goad/provider/terraform/proxmox.py index 29fbf92d..50f2a47c 100644 --- a/goad/provider/terraform/proxmox.py +++ b/goad/provider/terraform/proxmox.py @@ -4,3 +4,5 @@ class ProxmoxProvider(TerraformProvider): provider_name = PROXMOX + default_provisioner = PROVISIONING_LOCAL + allowed_provisioners = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER, PROVISIONING_REMOTE] diff --git a/goad/provider/terraform/terraform.py b/goad/provider/terraform/terraform.py index d4c6ecc4..6bbb444c 100644 --- a/goad/provider/terraform/terraform.py +++ b/goad/provider/terraform/terraform.py @@ -1,15 +1,18 @@ from goad.provider.provider import Provider +import os class TerraformProvider(Provider): + def __init__(self, lab_name): + super().__init__(lab_name) + self.path = self.path + 'terraform' + os.path.sep + def check(self): self.command.check_terraform() - def dependencies(self): - pass - def install(self): + self.command.run_terraform(['init'], self.path) self.command.run_terraform(['plan'], self.path) self.command.run_terraform(['apply'], self.path) @@ -24,3 +27,6 @@ def stop(self): def status(self): pass + + def ssh_jumpbox(self): + pass diff --git a/goad/provider/vagrant/vagrant.py b/goad/provider/vagrant/vagrant.py index 70c51270..36ad1474 100644 --- a/goad/provider/vagrant/vagrant.py +++ b/goad/provider/vagrant/vagrant.py @@ -7,9 +7,6 @@ class VagrantProvider(Provider): def check(self): self.command.check_vagrant() - def dependencies(self): - pass - def install(self): self.command.run_vagrant(['up'], self.path) diff --git a/goad/provider/vagrant/virtualbox.py b/goad/provider/vagrant/virtualbox.py index eef7d16c..26de4eae 100644 --- a/goad/provider/vagrant/virtualbox.py +++ b/goad/provider/vagrant/virtualbox.py @@ -4,3 +4,5 @@ class VirtualboxProvider(VagrantProvider): provider_name = VIRTUALBOX + default_provisioner = PROVISIONING_LOCAL + allowed_provisioners = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER] \ No newline at end of file diff --git a/goad/provider/vagrant/vmware.py b/goad/provider/vagrant/vmware.py index 50a445f6..3e7b1a94 100644 --- a/goad/provider/vagrant/vmware.py +++ b/goad/provider/vagrant/vmware.py @@ -4,3 +4,5 @@ class VmwareProvider(VagrantProvider): provider_name = VMWARE + default_provisioner = PROVISIONING_LOCAL + allowed_provisioners = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER] diff --git a/goad/provisioner/ansible/ansible.py b/goad/provisioner/ansible/ansible.py new file mode 100644 index 00000000..02acde44 --- /dev/null +++ b/goad/provisioner/ansible/ansible.py @@ -0,0 +1,97 @@ +import ansible_runner +import time +import yaml +from goad.utils import * +from goad.log import Log +from goad.provisioner.provisioner import Provisioner + + +class Ansible(Provisioner): + + @staticmethod + def get_inventory(lab_name, provider_name): + inventory = [] + Log.info('Loading inventory') + # Lab inventory + lab_inventory = get_lab_inventory_path(lab_name) + if os.path.isfile(lab_inventory): + inventory.append(lab_inventory) + Log.success(f'Lab inventory : {lab_inventory} file found') + # Provider inventory + provider_inventory = get_provider_inventory_path(lab_name, provider_name) + if os.path.isfile(provider_inventory): + inventory.append(provider_inventory) + Log.success(f'Provider inventory : {provider_inventory} file found') + # Global inventory + global_inventory = get_global_inventory_path() + if os.path.isfile(global_inventory): + inventory.append(global_inventory) + Log.success(f'Global inventory : {global_inventory} file found') + return inventory + + @staticmethod + def get_playbook_list(lab_name): + Log.info('Loading playbook list') + playbook_organisation_file = get_playbooks_lab_config() + playbook_list = [] + with open(playbook_organisation_file, 'r') as playbooks: + data_loaded = yaml.safe_load(playbooks) + if lab_name in data_loaded: + playbook_datas = data_loaded[lab_name] + else: + playbook_datas = data_loaded['default'] + + # validate playbooks + for playbook in playbook_datas: + playbook_path = get_provisioner_path() + os.path.sep + playbook + if not os.path.isfile(playbook_path): + Log.error(f'{playbook} not valid, file {playbook_path} not found') + else: + playbook_list.append(playbook) + Log.success(f'{playbook} file found') + return playbook_list + + def run(self, playbook=None): + inventory = self.get_inventory(self.lab_name, self.provider_name) + provision_result = False + if playbook is None: + playbooks = self.get_playbook_list(self.lab_name) + for playbook in playbooks: + provision_result = self.run_playbook(playbook, inventory) + if not provision_result: + Log.error(f'Something wrong during the provisioning task : {playbook}') + return False + else: + provision_result = self.run_playbook(playbook, inventory) + return provision_result + + def run_from(self, task): + inventory = self.get_inventory(self.lab_name, self.provider_name) + playbooks = self.get_playbook_list(self.lab_name) + + if task == '' or task is None: + Log.error('Missing playbook to start from') + Log.info('Playbook list :') + for playbook in playbooks: + Log.info(f' - {playbook}') + return False + + skip = True + for playbook in playbooks: + if playbook == task: + skip = False + if skip: + Log.info(f'skip {playbook}') + else: + provision_result = self.run_playbook(playbook, inventory) + if not provision_result: + Log.error(f'Something wrong during the provisioning task : {playbook}') + return False + return True + + def run_playbook(self, playbook, inventories, tries=3, timeout=30): + # abstract + pass + + + diff --git a/goad/provisioner/ansible/docker.py b/goad/provisioner/ansible/docker.py new file mode 100644 index 00000000..ace3a5f3 --- /dev/null +++ b/goad/provisioner/ansible/docker.py @@ -0,0 +1,8 @@ +from goad.provisioner.ansible.ansible import Ansible +from goad.utils import * + +class DockerAnsibleProvisioner(Ansible): + provisioner_name = PROVISIONING_DOCKER + + def run_playbook(self, playbook, inventories, tries=3, timeout=30): + pass \ No newline at end of file diff --git a/goad/provisioner/ansible/local.py b/goad/provisioner/ansible/local.py new file mode 100644 index 00000000..b726ced6 --- /dev/null +++ b/goad/provisioner/ansible/local.py @@ -0,0 +1,27 @@ +import time + +import ansible_runner + +from goad.log import Log +from goad.utils import * +from goad.provisioner.ansible.ansible import Ansible + + +class LocalAnsibleProvisionerCmd(Ansible): + provisioner_name = PROVISIONING_LOCAL + + def run_playbook(self, playbook, inventories, tries=3, timeout=30): + + Log.info(f'Run playbook : {playbook} with inventory file(s) : {", ".join(inventories)}') + + args = f'-i {"-i ".join(inventories)} {playbook}' + + run_complete = False + nb_try = 0 + while not run_complete: + nb_try += 1 + run_complete = self.command.run_ansible(args, self.path) + if not run_complete and nb_try > tries: + Log.error('3 fails abort.') + break + return run_complete diff --git a/goad/provisioner/ansible/remote.py b/goad/provisioner/ansible/remote.py new file mode 100644 index 00000000..8c07576a --- /dev/null +++ b/goad/provisioner/ansible/remote.py @@ -0,0 +1,50 @@ +from goad.jumpbox import JumpBox +from goad.log import Log +from goad.provisioner.ansible.ansible import Ansible +from goad.exceptions import JumpBoxInitFailed +from goad.utils import * + + +class RemoteAnsibleProvisioner(Ansible): + provisioner_name = PROVISIONING_REMOTE + + def __init__(self, lab_name, provider): + super().__init__(lab_name, provider) + self.jumpbox = None + self.remote_project_path = '/home/goad/GOAD' + + def prepare_jumpbox(self): + try: + self.jumpbox = JumpBox(self.lab_name, self.provider) + self.jumpbox.sync_sources() + self.jumpbox.prepare_jumpbox() + except JumpBoxInitFailed as e: + Log.error('Jumpbox retrieve connection info failed, abort') + + def run(self, playbook=None): + try: + if self.jumpbox is None: + self.jumpbox = JumpBox(self.lab_name, self.provider) + super().run(playbook) + except JumpBoxInitFailed as e: + Log.error('Jumpbox retrieve connection info failed, abort') + + def run_playbook(self, playbook, inventories, tries=3, timeout=30): + remote_inventories = [] + for inventory in inventories: + remote_inventories.append(transform_path(inventory, self.remote_project_path)) + command = f'/home/goad/.local/bin/ansible-playbook -i {" -i ".join(remote_inventories)} {playbook}' + + Log.info(f'Run playbook : {playbook} with inventory file(s) : {", ".join(remote_inventories)}') + Log.cmd('command') + + run_complete = False + nb_try = 0 + while not run_complete: + nb_try += 1 + run_complete = self.jumpbox.run_command(command, self.remote_project_path + '/ansible/') + + if not run_complete and nb_try > tries: + Log.error('3 fails abort.') + break + return run_complete diff --git a/goad/provisioner/ansible/runner.py b/goad/provisioner/ansible/runner.py new file mode 100644 index 00000000..d208cb66 --- /dev/null +++ b/goad/provisioner/ansible/runner.py @@ -0,0 +1,38 @@ +import time + +import ansible_runner +from goad.utils import * +from goad.log import Log +from goad.provisioner.ansible.ansible import Ansible + + +class LocalAnsibleProvisionerEmbed(Ansible): + provisioner_name = PROVISIONING_RUNNER + + def run_playbook(self, playbook, inventories, tries=3, timeout=30): + Log.info(f'Run playbook : {playbook} with inventory file(s) : {", ".join(inventories)}') + Log.cmd(f'ansible-playbook -i {" -i ".join(inventories)} {playbook}') + + run_complete = False + runner_result = None + nb_try = 0 + while not run_complete: + nb_try += 1 + runner_result = ansible_runner.run(private_data_dir=self.path + 'private_data_dir', + playbook=self.path + playbook, + inventory=inventories) + if len(runner_result.stats['ok'].keys()) >= 1: + run_complete = True + if len(runner_result.stats['dark'].keys()) >= 1: + Log.error('Unreachable vm wait 30 sec and restart ansible') + time.sleep(30) + run_complete = False + if len(runner_result.stats['failures'].keys()) >= 1: + Log.error(f'Error during playbook iteration {str(nb_try)}, restart') + run_complete = False + if nb_try > tries: + Log.error('3 fails abort.') + break + # print(runner_result.stats) + return run_complete + diff --git a/goad/provisioner/provisioner.py b/goad/provisioner/provisioner.py new file mode 100644 index 00000000..6ddcdf9b --- /dev/null +++ b/goad/provisioner/provisioner.py @@ -0,0 +1,29 @@ +from abc import ABC +from goad.config import Config +import os +import platform +from goad.command.linux import LinuxCommand +from goad.command.windows import WindowsCommand +from goad.utils import * + + +class Provisioner(ABC): + lab_name = '' + provider_name = '' + provisioner_name = None + + def __init__(self, lab_name, provider): + self.lab_name = lab_name + self.provider_name = provider.provider_name + self.provider = provider + self.path = get_provisioner_path() + os.path.sep + if platform.system() == 'Windows': + self.command = WindowsCommand() + else: + self.command = LinuxCommand() + + def run(self, arg): + pass + + def run_from(self, arg): + pass diff --git a/goad/utils.py b/goad/utils.py index 732b6474..07b951ad 100644 --- a/goad/utils.py +++ b/goad/utils.py @@ -4,7 +4,7 @@ # constants LAB = 'lab' PROVIDER = 'provider' -PROVIDING_METHOD = 'providing_method' +PROVISIONER = 'provisioner' # log level INFO = 5 @@ -19,17 +19,71 @@ PROXMOX = 'proxmox' ALLOWED_PROVIDERS = [AWS, VIRTUALBOX, AZURE, VMWARE, PROXMOX] +# provisioning method +PROVISIONING_LOCAL = 'local' +PROVISIONING_RUNNER = 'runner' +PROVISIONING_DOCKER = 'docker' +PROVISIONING_REMOTE = 'remote' +ALLOWED_PROVISIONER = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER, PROVISIONING_REMOTE] + +# provisioner allowed +AWS_ALLOWED_PROVISIONER = [PROVISIONING_REMOTE] +AZURE_ALLOWED_PROVISIONER = [PROVISIONING_REMOTE] +PROXMOX_ALLOWED_PROVISIONER = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER] +VMWARE_ALLOWED_PROVISIONER = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER] +VIRTUALBOX_ALLOWED_PROVISIONER = [PROVISIONING_LOCAL, PROVISIONING_RUNNER, PROVISIONING_DOCKER] + project_path = os.path.normpath(os.path.dirname(os.path.abspath(__file__)) + os.path.sep + '..') +def get_project_path(): + return project_path + os.path.sep + + def get_labs_path(): return project_path + os.path.sep + 'ad' +def get_relative_path(path): + return path[len(project_path):] + + +def transform_path(origin, remote_project_path): + return remote_project_path + origin[len(project_path):] + + def get_providers_path(lab_name): return project_path + os.path.sep + 'ad' + os.path.sep + lab_name + os.path.sep + 'providers' +def get_provisioner_path(): + return project_path + os.path.sep + 'ansible' + + +def get_global_inventory_path(): + return project_path + os.path.sep + 'globalsettings.ini' + + +def get_lab_inventory_path(lab_name): + return project_path + os.path.sep + 'ad' + os.path.sep + lab_name + os.path.sep + 'data' + os.path.sep + 'inventory' + + +def get_provider_inventory_path(lab_name, provider): + return project_path + os.path.sep + 'ad' + os.path.sep + lab_name + os.path.sep + 'providers' + os.path.sep + provider + os.path.sep + 'inventory' + + +def get_ubuntu_jumpbox_key(lab_name, provider): + return project_path + os.path.sep + 'ad' + os.path.sep + lab_name + os.path.sep + 'providers' + os.path.sep + provider + os.path.sep + 'ssh_keys' + os.path.sep + 'ubuntu-jumpbox.pem' + + +def get_script_path(script): + return project_path + os.path.sep + 'scripts' + os.path.sep + script + + +def get_playbooks_lab_config(): + return project_path + os.path.sep + 'playbooks.yml' + + def get_extensions_path(lab_name): return project_path + os.path.sep + 'ad' + os.path.sep + lab_name + os.path.sep + 'extensions' diff --git a/playbooks.yml b/playbooks.yml new file mode 100644 index 00000000..5427bc69 --- /dev/null +++ b/playbooks.yml @@ -0,0 +1,66 @@ +NHA: + - build.yml + - ad-servers.yml + - ad-parent_domain.yml + # - ad-child_domain.yml + - ad-members.yml + - ad-trusts.yml + - ad-data.yml + - ad-gmsa.yml + # - laps.yml + - ad-relations.yml + - adcs.yml + - ad-acl.yml + - servers.yml + - security.yml + - vulnerabilities.yml + +SCCM: + - build.yml + - ad-servers.yml + - ad-parent_domain.yml + # - ad-child_domain.yml + - ad-members.yml + # - ad-trusts.yml + - ad-data.yml + # - ad-gmsa.yml + # - laps.yml + - ad-relations.yml + # - adcs.yml + - ad-acl.yml + - servers.yml + - security.yml + - vulnerabilities.yml + - sccm-install.yml + # Waiting 10 minutes for the install to complete + - wait5m.yml + - wait5m.yml + - reboot.yml + # reboot before launching the sccm config to finish the install + # Waiting 5 minutes before launching the configuration + - wait5m.yml + - dhcp.yml + - sccm-config.yml + - sccm-pxe.yml + # replay client install fix some issue in enrollment for CLIENT + - sccm-client.yml + +default: + - build.yml + - ad-servers.yml + - ad-parent_domain.yml + # Wait after the child domain creation before adding servers + - ad-child_domain.yml + # Waiting 5 minutes for the child domain to be ready + - wait5m.yml + - ad-members.yml + - ad-trusts.yml + - ad-data.yml + - ad-gmsa.yml + - laps.yml + - ad-relations.yml + - adcs.yml + - ad-acl.yml + - servers.yml + - security.yml + - vulnerabilities.yml \ No newline at end of file diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 00000000..e69de29b