From f2b2f904333dcf7b4d7a92caf0ce4422c9d9bc48 Mon Sep 17 00:00:00 2001 From: Jan Musial Date: Wed, 24 Apr 2019 11:25:40 +0200 Subject: [PATCH 1/2] Initial code commit --- .gitignore | 4 + README.md | 44 ++ action_plugins/opencas.py | 142 ++++++ group_vars/opencas-nodes.yml.sample | 24 + library/opencas.py | 449 ++++++++++++++++++ opencas-deploy.yml | 8 + opencas-teardown.yml | 47 ++ roles/opencas-defaults/defaults/main.yml | 6 + roles/opencas-defaults/tasks/main.yml | 6 + roles/opencas-deploy/files/default.csv | 15 + .../tasks/configure-devices.yml | 11 + .../tasks/copy-ioclass-config.yml | 12 + roles/opencas-deploy/tasks/main.yml | 17 + .../opencas-deploy/tasks/validate-config.yml | 11 + roles/opencas-install/tasks/main.yml | 47 ++ roles/opencas-validate/tasks/main.yml | 16 + 16 files changed, 859 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 action_plugins/opencas.py create mode 100644 group_vars/opencas-nodes.yml.sample create mode 100755 library/opencas.py create mode 100644 opencas-deploy.yml create mode 100644 opencas-teardown.yml create mode 100644 roles/opencas-defaults/defaults/main.yml create mode 100644 roles/opencas-defaults/tasks/main.yml create mode 100644 roles/opencas-deploy/files/default.csv create mode 100644 roles/opencas-deploy/tasks/configure-devices.yml create mode 100644 roles/opencas-deploy/tasks/copy-ioclass-config.yml create mode 100644 roles/opencas-deploy/tasks/main.yml create mode 100644 roles/opencas-deploy/tasks/validate-config.yml create mode 100644 roles/opencas-install/tasks/main.yml create mode 100644 roles/opencas-validate/tasks/main.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a48d1b2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.retry + +*.py[cod] +__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..309e151 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# opencas-ansible +Collection of Ansible playbooks for setting up Open CAS accelerated devices. + +## Configuration and usage +Default playbook configuration tries to configure Open CAS on `opencas-nodes` +host group from inventory. + +### Configuring devices +Example configuration is shown in `group_vars/opencas-nodes.yml.sample`. +For default, out-of-the-box configuration you can only change the name to opencas-nodes.yml, +configure appropriate host groups and adjust the device names. + +### Configuring IO-classes +Default configuration is already present at `roles/opencas-deploy/files/default.csv`. +Any additional ioclass config files present in this directory will be copied over to +configured hosts and may be used in cache devices configuration in group variables. + +## Playbooks +### opencas-deploy +Installs Open CAS software on `opencas-node` group and configures caching devices +and cached volumes defined. + +### opencas-teardown +Stops all cache instances and removes Open CAS software. Make sure that +`/dev/casx-y` devices aren't used at time of teardown. + +## Roles +### opencas-validate +Validates the Open CAS configuration set (e.g. in `group_vars`). + +### opencas-common +Makes sure that the installer is present on target host. + +### opencas-defaults +Gathers custom facts needed for further processing, also in `defaults/main.yml` +there are some settings used by other roles. + +### opencas-install +Installs Open CAS software. + +### opencas-deploy +Copies over the IO-class configuration files, validates configuration and deploys +it on hosts. + diff --git a/action_plugins/opencas.py b/action_plugins/opencas.py new file mode 100644 index 0000000..9fe1736 --- /dev/null +++ b/action_plugins/opencas.py @@ -0,0 +1,142 @@ +# +# Copyright(c) 2012-2019 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import os +import csv + +from ansible.plugins.action import ActionBase +from ansible.errors import AnsibleError +from ansible.utils.vars import merge_hash + + +def validate_ioclass_file(ioclass_file): + if ioclass_file is None: + return + + file_path = "roles/opencas-deploy/files/{0}".format(ioclass_file) + if not os.path.exists(file_path): + raise AnsibleError( + "{0} io class file wasn't found in opencas-deploy files".format( + ioclass_file + ) + ) + + with open(file_path, "r") as f: + reader = csv.DictReader(f, restkey="unnamed fields") + + required = set( + ["IO class id", "IO class name", "Eviction priority", "Allocation"] + ) + if set(reader.fieldnames) != required: + raise AnsibleError( + "Invalid IO-class file format ({0})".format(ioclass_file) + ) + + ioclass_ids = [] + for ioclass in reader: + if "unnamed fields" in ioclass: + raise AnsibleError( + "Invalid IO-class file format ({0})".format(ioclass_file) + ) + + try: + id = int(ioclass["IO class id"]) + except: + raise AnsibleError( + "Invalid IO-class id({0}) found in {1}".format( + ioclass["IO class id"], ioclass_file + ) + ) + + if not (0 <= id < 33): + raise AnsibleError( + "Invalid IO-class id({0}) found in {1}".format( + id, ioclass_file + ) + ) + + if id in ioclass_ids: + raise AnsibleError( + "Duplicate IO-class id({0}) found in {1}".format( + id, ioclass_file + ) + ) + ioclass_ids += [id] + + try: + name = str(ioclass["IO class name"]) + except: + raise AnsibleError( + "Invalid IO-class name({0}) found in {1}".format( + ioclass["IO class name"], ioclass_file + ) + ) + + if len(name) >= 1024: + raise AnsibleError( + "Too long IO-class name({0}) found in {1}".format( + name, ioclass_file + ) + ) + + for c in name: + if c == "," or c == '"' or ord(c) < 32 or ord(c) > 126: + raise AnsibleError( + "Invalid character({0}) in IO-class name({1}) found in {2}".format( + c, name, ioclass_file + ) + ) + + try: + priority = int(ioclass["Eviction priority"]) + except: + raise AnsibleError( + "Invalid IO-class priority({0}) found in {1}".format( + ioclass["IO class id"], ioclass_file + ) + ) + + if not (0 <= priority <= 255): + raise AnsibleError( + "Out of range IO-class priority({0}) found in {1}".format( + priority, ioclass_file + ) + ) + + try: + allocation = int(ioclass["Allocation"]) + except: + raise AnsibleError( + "Invalid IO-class allocation({0}) found in {1}".format( + ioclass["Allocation"], ioclass_file + ) + ) + + if not (0 <= allocation <= 1): + raise AnsibleError( + "Invalid IO-class allocation({0}) found in {1}".format( + allocation, ioclass_file + ) + ) + + +class ActionModule(ActionBase): + def run(self, tmp=None, task_vars=None): + if ( + "check_cache_config" in self._task.args + and "io_class" in self._task.args["check_cache_config"] + and self._task.args["check_cache_config"]["io_class"] + ): + validate_ioclass_file( + self._task.args["check_cache_config"]["io_class"] + ) + del self._task.args["check_cache_config"]["io_class"] + + results = super(ActionModule, self).run(tmp, task_vars) + results = merge_hash( + results, self._execute_module(tmp=tmp, task_vars=task_vars) + ) + + return results diff --git a/group_vars/opencas-nodes.yml.sample b/group_vars/opencas-nodes.yml.sample new file mode 100644 index 0000000..c7d099d --- /dev/null +++ b/group_vars/opencas-nodes.yml.sample @@ -0,0 +1,24 @@ +--- +# This is a sample configuration of Intel CAS deployment configuration + +# List of all cache devices configuration +opencas_cache_devices: + - cache_device: /dev/nvme0n1 # path to device or partition + id: 1 # id used to corelate cores with cache instances + cache_mode: wt # caching mode + force: False # [OPTIONAL] Ignore and overwrite existing partition table + cleaning_policy: alru # [OPTIONAL] cleaning policy + line_size: 4 # [OPTIONAL] cache line size <4, 8, 16, 32, 64> [kb] + io_class: default.csv # [OPTIONAL] io classification file name + # all files used here should be put in + # roles/opencas-deploy/files/ + +# List of all cached volumes +opencas_cached_volumes: + - id: 1 # id of core device + cache_id: 1 # id of cache defined in open_cas_cache_devices list + cached_volume: /dev/sdc # path to cached device or partition + - id: 2 + cache_id: 1 + cached_volume: /dev/sdd +... diff --git a/library/opencas.py b/library/opencas.py new file mode 100755 index 0000000..a698a55 --- /dev/null +++ b/library/opencas.py @@ -0,0 +1,449 @@ +# +# Copyright(c) 2012-2019 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import sys +from copy import deepcopy + +try: + sys.path.append("/usr/lib/opencas/") + import opencas as cas_util +except ImportError: + cas_util = None + +ANSIBLE_METADATA = { + "metadata_version": "0.1", + "status": ["preview"], + "supported_by": "community", +} + +DOCUMENTATION = """ +--- +module: opencas + +short_description: Manage Open CAS software + +description: + - Deploy Open CAS configuration + +options: + gather_facts: + description: + - gathers facts about Open CAS configuration on host + required: False + + zap: + description: + - empties Open CAS device configuration file + required: False + + stop: + description: + - stops all Open CAS devices + suboptions: + flush: + description: + - Should data from cache devices be flushed to primary storage + required: False + + check_cache_config: + description: + - check if cache device configuration is valid + required: False + suboptions: + cache_device: + description: + - Path to device to be used as a cache + id: + description: + - id of cache to be created + cache_mode: + description: + - Caching mode for cache + choices: ['wt', 'wb', 'wa', 'pt'] + default: wt + cleaning_policy: + description: + - cleaning policy to be used by cache + choices: ['alru', 'acp', 'nop'] + default: alru + line_size: + description: + - cache line size in kb + choices: [4, 8, 16, 32, 64] + default: 4 + io_class: + description: + - name of io classification file (located in /etc/opencas/) + + configure_cache_device: + description: + - configure and start cache device + required: False + suboptions: + cache_device: + description: + - Path to device to be used as a cache + id: + description: + - id of cache to be created + cache_mode: + description: + - Caching mode for cache + choices: ['wt', 'wb', 'wa', 'pt'] + default: wt + cleaning_policy: + description: + - cleaning policy to be used by cache + choices: ['alru', 'acp', 'nop'] + default: alru + line_size: + description: + - cache line size in kb + choices: [4, 8, 16, 32, 64] + default: 4 + io_class: + description: + - name of io classification file (located in /etc/opencas/) + + check_core_config: + description: + - check if core configuration is valid + required: False + suboptions: + id: + description: + - id of core device to be added + cache_id: + description: + - id of cache device which will be servicing this core + cached_volume: + description: + - path to device to be cached + + configure_core_device: + description: + - configure and add device to cache + required: False + suboptions: + id: + description: + - id of core device to be added + cache_id: + description: + - id of cache device which will be servicing this core + cached_volume: + description: + - path to device to be cached +... +""" + +EXAMPLES = """ +- name: Gather facts about opencas installation + opencas: + gather_facts: True + +- name: Validate CAS cache configuration + opencas: + check_cache_config: + path: /dev/nvme0n1 + cache_id: 2 + mode: wb + line_size: 8 + +- name: Configure and start CAS cache + opencas: + configure_cache_device: + path: /dev/nvme0n1 + cache_id: 2 + mode: wb + line_size: 8 + +- name: Configure and add core device to CAS cache + opencas: + configure_core_device: + path: /dev/sda + cache_id: 2 + core_id: 3 + +- name: Remove Open CAS devices configuration + opencas: + zap: True + +- name: Stop all Open CAS devices + opencas: + stop: + flush: True +""" + +RETURN = """ # """ + + +def gather_facts(): + ret = {} + if cas_util is None: + ret["opencas_installed"] = False + return ret + + try: + ret["opencas_installed_version"] = cas_util.get_cas_version() + ret["opencas_installed"] = True + except: + ret["opencas_installed"] = False + + try: + config = cas_util.cas_config.from_file( + cas_util.cas_config.default_location + ) + except: + ret["opencas_config_nonempty"] = False + else: + ret["opencas_config_nonempty"] = not config.is_empty() + + ret["opencas_devices_started"] = len(cas_util.get_caches_list()) != 0 + + return ret + + +def zap(): + try: + original_config = cas_util.cas_config.from_file( + cas_util.cas_config.default_location + ) + except: + return False + + if original_config.is_empty(): + return False + + empty_config = cas_util.cas_config(version_tag=original_config.version_tag) + + empty_config.write(cas_util.cas_config.default_location) + + return True + + +def stop(flush): + caches_list = cas_util.get_caches_list() + + devices_count = len(caches_list) + if devices_count == 0: + return False + + cas_util.stop(flush) + + if len(cas_util.get_caches_list()) != 0: + raise Exception("Couldn't stop all cache devices") + + return True + + +def handle_core_config(config): + try: + path = config["cached_volume"] + cache_id = int(config["cache_id"]) + core_id = int(config["id"]) + except: + raise Exception("Missing core config parameters") + + return (path, core_id, cache_id) + + +def check_core_config(config): + path, core_id, cache_id = handle_core_config(config) + + core_config = cas_util.cas_config.core_config(cache_id, core_id, path) + + core_config.validate_config() + + +def configure_core_device(config): + path, core_id, cache_id = handle_core_config(config) + + try: + config = cas_util.cas_config.from_file( + cas_util.cas_config.default_location + ) + except: + raise + + config_copy = deepcopy(config) + + changed = True + core_config = cas_util.cas_config.core_config(cache_id, core_id, path) + + try: + config.insert_core(core_config) + except cas_util.cas_config.AlreadyConfiguredException: + changed = False + else: + config.write(cas_util.cas_config.default_location) + + if cas_util.is_core_added(core_config): + return changed + + try: + cas_util.add_core(core_config, False) + except cas_util.casadm.CasadmError as e: + config_copy.write(cas_util.cas_config.default_location) + raise Exception("Internal casadm error({0})".format(e.result.stderr)) + except: + config_copy.write(cas_util.cas_config.default_location) + raise + + return True + + +def handle_cache_config(config): + try: + path = config["cache_device"] + cache_id = int(config["id"]) + cache_mode = config["cache_mode"] + except: + raise Exception("Missing cache config parameters") + + params = dict() + cache_line_size = config.get("line_size") + if cache_line_size: + params["cache_line_size"] = str(cache_line_size) + + io_class = config.get("io_class") + if io_class: + params["ioclass_file"] = "/etc/opencas/ansible/{0}".format(io_class) + + cleaning_policy = config.get("cleaning_policy") + if cleaning_policy: + params["cleaning_policy"] = cleaning_policy + + force = config.get("force") + + return (path, cache_id, cache_mode, params, force) + + +def check_cache_config(config): + path, cache_id, cache_mode, params, force = handle_cache_config(config) + + cache_config = cas_util.cas_config.cache_config( + cache_id, path, cache_mode, **params + ) + cache_config.validate_config(force) + + +def configure_cache_device(config): + path, cache_id, cache_mode, params, force = handle_cache_config(config) + + try: + config = cas_util.cas_config.from_file( + cas_util.cas_config.default_location + ) + except: + raise + + config_copy = deepcopy(config) + + new_cache_config = cas_util.cas_config.cache_config( + cache_id, path, cache_mode, **params + ) + + changed = True + try: + config.insert_cache(new_cache_config) + except cas_util.cas_config.AlreadyConfiguredException: + changed = False + else: + config.write(cas_util.cas_config.default_location) + + if cas_util.is_cache_started(new_cache_config): + return changed + + try: + cas_util.start_cache(new_cache_config, load=False, force=force) + cas_util.configure_cache(new_cache_config) + except cas_util.casadm.CasadmError as e: + config_copy.write(cas_util.cas_config.default_location) + raise Exception("Internal casadm error({0})".format(e.result.stderr)) + except: + config_copy.write(cas_util.cas_config.default_location) + raise + + return True + + +argument_spec = { + "gather_facts": {"type": "bool", "required": False}, + "zap": {"type": "bool", "required": False}, + "stop": {"type": "dict", "required": False}, + "check_cache_config": {"type": "dict", "required": False}, + "configure_cache_device": {"type": "dict", "required": False}, + "check_core_config": {"type": "dict", "required": False}, + "configure_core_device": {"type": "dict", "required": False}, +} + + +def setup_module_object(): + module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=False) + + return module + + +def run_task(module): + ret = {"changed": False, "failed": False, "ansible_facts": {}} + + arg_gather_facts = module.params["gather_facts"] + if arg_gather_facts: + ret["ansible_facts"] = gather_facts() + return ret + + arg_zap = module.params["zap"] + if arg_zap: + ret["changed"] = zap() + return ret + + arg_stop = module.params["stop"] + if arg_stop: + ret["changed"] = ret["changed"] or stop(arg_stop["flush"]) + return ret + + arg_check_cache_config = module.params["check_cache_config"] + if arg_check_cache_config: + check_cache_config(arg_check_cache_config) + return ret + + arg_check_core_config = module.params["check_core_config"] + if arg_check_core_config: + check_core_config(arg_check_core_config) + return ret + + arg_configure_cache_device = module.params["configure_cache_device"] + if arg_configure_cache_device: + ret["changed"] = ret["changed"] or configure_cache_device( + arg_configure_cache_device + ) + return ret + + arg_configure_core_device = module.params["configure_core_device"] + if arg_configure_core_device: + ret["changed"] = ret["changed"] or configure_core_device( + arg_configure_core_device + ) + return ret + + return ret + + +def main(): + module = setup_module_object() + + try: + ret = run_task(module) + except Exception as e: + module.fail_json(msg="{0}: {1}".format(type(e).__name__, str(e))) + + module.exit_json(**ret) + + +from ansible.module_utils.basic import AnsibleModule + +if __name__ == "__main__": + main() diff --git a/opencas-deploy.yml b/opencas-deploy.yml new file mode 100644 index 0000000..746146e --- /dev/null +++ b/opencas-deploy.yml @@ -0,0 +1,8 @@ +--- +- hosts: opencas-nodes + roles: + - role: opencas-defaults + - role: opencas-install + - role: opencas-validate + - role: opencas-deploy +... diff --git a/opencas-teardown.yml b/opencas-teardown.yml new file mode 100644 index 0000000..0b76cbd --- /dev/null +++ b/opencas-teardown.yml @@ -0,0 +1,47 @@ +--- +- hosts: opencas-nodes + roles: + - role: opencas-defaults + + tasks: + - name: Flush and stop all Open CAS devices (this may take some time) + opencas: + stop: + flush: True + when: opencas_installed + become: True + + - name: Remove Open CAS devices configuration + opencas: + zap: True + when: opencas_installed + become: True + + - name: Clone and checkout Open CAS repository + git: + repo: "{{ opencas_repo_url }}" + dest: "{{ opencas_path }}" + version: "{{ opencas_version }}" + become: True + + - name: Uninstall Open CAS + make: + chdir: "{{ opencas_path }}" + target: uninstall + ignore_errors: True + become: True + + - name: Check if uninstalled + opencas: + gather_facts: True + failed_when: opencas_installed + become: True + + - name: Remove Open CAS directory + file: + state: absent + force: True + path: '{{ opencas_path }}' + become: True + +... diff --git a/roles/opencas-defaults/defaults/main.yml b/roles/opencas-defaults/defaults/main.yml new file mode 100644 index 0000000..2179bdc --- /dev/null +++ b/roles/opencas-defaults/defaults/main.yml @@ -0,0 +1,6 @@ +--- + opencas_path: "/tmp/.ansible/opencas/" + opencas_repo_url: "https://github.com/Open-CAS/open-cas-linux.git" + opencas_version: "HEAD" +... + diff --git a/roles/opencas-defaults/tasks/main.yml b/roles/opencas-defaults/tasks/main.yml new file mode 100644 index 0000000..fcdda86 --- /dev/null +++ b/roles/opencas-defaults/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: Gather Open CAS facts + opencas: + gather_facts: True + become: True +... diff --git a/roles/opencas-deploy/files/default.csv b/roles/opencas-deploy/files/default.csv new file mode 100644 index 0000000..bdcd81a --- /dev/null +++ b/roles/opencas-deploy/files/default.csv @@ -0,0 +1,15 @@ +IO class id,IO class name,Eviction priority,Allocation +0,unclassified,22,1 +1,metadata&done,0,1 +11,file_size:le:4096&done,9,1 +12,file_size:le:16384&done,10,1 +13,file_size:le:65536&done,11,1 +14,file_size:le:262144&done,12,1 +15,file_size:le:1048576&done,13,1 +16,file_size:le:4194304&done,14,1 +17,file_size:le:16777216&done,15,1 +18,file_size:le:67108864&done,16,1 +19,file_size:le:268435456&done,17,1 +20,file_size:le:1073741824&done,18,1 +21,file_size:gt:1073741824&done,19,1 +22,direct&done,20,1 diff --git a/roles/opencas-deploy/tasks/configure-devices.yml b/roles/opencas-deploy/tasks/configure-devices.yml new file mode 100644 index 0000000..865bbbf --- /dev/null +++ b/roles/opencas-deploy/tasks/configure-devices.yml @@ -0,0 +1,11 @@ +--- +- name: Configure cache devices + opencas: + configure_cache_device: "{{ item }}" + loop: "{{ opencas_cache_devices }}" + +- name: Configure core devices + opencas: + configure_core_device: "{{ item }}" + loop: "{{ opencas_cached_volumes }}" +... diff --git a/roles/opencas-deploy/tasks/copy-ioclass-config.yml b/roles/opencas-deploy/tasks/copy-ioclass-config.yml new file mode 100644 index 0000000..98d7abc --- /dev/null +++ b/roles/opencas-deploy/tasks/copy-ioclass-config.yml @@ -0,0 +1,12 @@ +--- +- name: Create directory for opencas-ansible IO class configs + file: + state: directory + path: /etc/opencas/ansible + +- name: Copy io class configurations files + copy: + src: files/ + dest: /etc/opencas/ansible + mode: 0777 +... diff --git a/roles/opencas-deploy/tasks/main.yml b/roles/opencas-deploy/tasks/main.yml new file mode 100644 index 0000000..7f8608f --- /dev/null +++ b/roles/opencas-deploy/tasks/main.yml @@ -0,0 +1,17 @@ +--- +- name: Check if Open CAS configuration for host group is defined + fail: + msg: "Couldn't find configuration for Open CAS devices. Did you configure group/host_vars?" + when: (opencas_cache_devices is not defined) or (opencas_cached_volumes is not defined) + +- name: Copy io classes configuration files + include: copy-ioclass-config.yml + become: True + +- name: Validate CAS config + include: validate-config.yml + +- name: Configure devices + include: configure-devices.yml + become: True +... diff --git a/roles/opencas-deploy/tasks/validate-config.yml b/roles/opencas-deploy/tasks/validate-config.yml new file mode 100644 index 0000000..d895313 --- /dev/null +++ b/roles/opencas-deploy/tasks/validate-config.yml @@ -0,0 +1,11 @@ +--- +- name: Validate cache devices configs + opencas: + check_cache_config: "{{ item }}" + loop: "{{ opencas_cache_devices }}" + +- name: Validate core devices configs + opencas: + check_core_config: "{{ item }}" + loop: "{{ opencas_cached_volumes }}" +... diff --git a/roles/opencas-install/tasks/main.yml b/roles/opencas-install/tasks/main.yml new file mode 100644 index 0000000..fb182d4 --- /dev/null +++ b/roles/opencas-install/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: Get dependencies + block: + - package: + name: git + state: present + - package: + name: make + state: present + become: True + +- name: Check if installation may proceed + fail: + msg: | + "Open CAS is currently installed in version + {{ opencas_installed_version['CAS Cache Kernel Module'] }} and has devices + configured. Please change your opencas_version variable (currently: {{ opencas_version }}) + and re-run or use teardown and rebuild the configuration." + when: (opencas_installed and + ((opencas_installed_version != opencas_version) and opencas_devices_started)) + +- name: Clone and checkout Open CAS repository + git: + repo: "{{ opencas_repo_url }}" + dest: "{{ opencas_path }}" + version: "{{ opencas_version }}" + when: not opencas_installed or opencas_installed_version != opencas_version + become: True + +- name: Compile Open CAS + make: + chdir: "{{ opencas_path }}" + when: not opencas_installed or opencas_installed_version != opencas_version + become: True + +- name: Install Open CAS + make: + chdir: "{{ opencas_path }}" + target: install + when: not opencas_installed or opencas_installed_version != opencas_version + become: True + +- name: Update facts + opencas: + gather_facts: True + become: True +... diff --git a/roles/opencas-validate/tasks/main.yml b/roles/opencas-validate/tasks/main.yml new file mode 100644 index 0000000..478c91d --- /dev/null +++ b/roles/opencas-validate/tasks/main.yml @@ -0,0 +1,16 @@ +--- +- name: Check if Open CAS configuration for host group is defined + fail: + msg: "Couldn't find configuration for Open CAS devices. Did you configure group/host_vars?" + when: (opencas_cache_devices is not defined) or (opencas_cached_volumes is not defined) + +- name: Validate cache devices configs + opencas: + check_cache_config: "{{ item }}" + loop: "{{ opencas_cache_devices }}" + +- name: Validate core devices configs + opencas: + check_core_config: "{{ item }}" + loop: "{{ opencas_cached_volumes }}" +... From 65a477aaaf751448d6389db61d597e8edeb19bca Mon Sep 17 00:00:00 2001 From: Jan Musial Date: Fri, 14 Jun 2019 14:50:56 +0200 Subject: [PATCH 2/2] Add tests Signed-off-by: Jan Musial --- .gitmodules | 3 + README.md | 11 +- action_plugins/{opencas.py => cas.py} | 0 group_vars/opencas-nodes.yml.sample | 4 +- library/{opencas.py => cas.py} | 24 +- opencas-deploy.yml | 2 +- opencas-teardown.yml | 15 +- roles/opencas-defaults/tasks/main.yml | 2 +- .../tasks/configure-devices.yml | 4 +- .../tasks/copy-ioclass-config.yml | 2 +- .../opencas-deploy/tasks/validate-config.yml | 4 +- roles/opencas-install/tasks/main.yml | 8 +- roles/opencas-validate/tasks/main.yml | 4 +- tests/conftest.py | 15 + tests/helpers.py | 107 +++ tests/open-cas-linux | 1 + tests/test_ansible_module_01.py | 739 ++++++++++++++++++ 17 files changed, 906 insertions(+), 39 deletions(-) create mode 100644 .gitmodules rename action_plugins/{opencas.py => cas.py} (100%) rename library/{opencas.py => cas.py} (97%) mode change 100755 => 100644 create mode 100644 tests/conftest.py create mode 100644 tests/helpers.py create mode 160000 tests/open-cas-linux create mode 100644 tests/test_ansible_module_01.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..300f3ed --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/open-cas-linux"] + path = tests/open-cas-linux + url = https://github.com/Open-CAS/open-cas-linux.git diff --git a/README.md b/README.md index 309e151..ef59e4b 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,12 @@ Collection of Ansible playbooks for setting up Open CAS accelerated devices. ## Configuration and usage -Default playbook configuration tries to configure Open CAS on `opencas-nodes` +Default playbook configuration tries to configure Open CAS on `opencas_nodes` host group from inventory. ### Configuring devices -Example configuration is shown in `group_vars/opencas-nodes.yml.sample`. -For default, out-of-the-box configuration you can only change the name to opencas-nodes.yml, +Example configuration is shown in `group_vars/opencas_nodes.yml.sample`. +For default, out-of-the-box configuration you can only change the name to opencas_nodes.yml, configure appropriate host groups and adjust the device names. ### Configuring IO-classes @@ -28,12 +28,9 @@ Stops all cache instances and removes Open CAS software. Make sure that ### opencas-validate Validates the Open CAS configuration set (e.g. in `group_vars`). -### opencas-common -Makes sure that the installer is present on target host. - ### opencas-defaults Gathers custom facts needed for further processing, also in `defaults/main.yml` -there are some settings used by other roles. +there are some settings used by other roles e.g. version of Open CAS to be installed. ### opencas-install Installs Open CAS software. diff --git a/action_plugins/opencas.py b/action_plugins/cas.py similarity index 100% rename from action_plugins/opencas.py rename to action_plugins/cas.py diff --git a/group_vars/opencas-nodes.yml.sample b/group_vars/opencas-nodes.yml.sample index c7d099d..f3d4815 100644 --- a/group_vars/opencas-nodes.yml.sample +++ b/group_vars/opencas-nodes.yml.sample @@ -1,11 +1,11 @@ --- -# This is a sample configuration of Intel CAS deployment configuration +# This is a sample configuration of Open CAS deployment configuration # List of all cache devices configuration opencas_cache_devices: - cache_device: /dev/nvme0n1 # path to device or partition id: 1 # id used to corelate cores with cache instances - cache_mode: wt # caching mode + cache_mode: wt # caching mode force: False # [OPTIONAL] Ignore and overwrite existing partition table cleaning_policy: alru # [OPTIONAL] cleaning policy line_size: 4 # [OPTIONAL] cache line size <4, 8, 16, 32, 64> [kb] diff --git a/library/opencas.py b/library/cas.py old mode 100755 new mode 100644 similarity index 97% rename from library/opencas.py rename to library/cas.py index a698a55..1b567a7 --- a/library/opencas.py +++ b/library/cas.py @@ -20,7 +20,7 @@ DOCUMENTATION = """ --- -module: opencas +module: cas short_description: Manage Open CAS software @@ -61,7 +61,7 @@ cache_mode: description: - Caching mode for cache - choices: ['wt', 'wb', 'wa', 'pt'] + choices: ['wt', 'wb', 'wa', 'pt', 'wo'] default: wt cleaning_policy: description: @@ -91,7 +91,7 @@ cache_mode: description: - Caching mode for cache - choices: ['wt', 'wb', 'wa', 'pt'] + choices: ['wt', 'wb', 'wa', 'pt', 'wo'] default: wt cleaning_policy: description: @@ -141,11 +141,11 @@ EXAMPLES = """ - name: Gather facts about opencas installation - opencas: + cas: gather_facts: True - name: Validate CAS cache configuration - opencas: + cas: check_cache_config: path: /dev/nvme0n1 cache_id: 2 @@ -153,7 +153,7 @@ line_size: 8 - name: Configure and start CAS cache - opencas: + cas: configure_cache_device: path: /dev/nvme0n1 cache_id: 2 @@ -161,18 +161,18 @@ line_size: 8 - name: Configure and add core device to CAS cache - opencas: + cas: configure_core_device: path: /dev/sda cache_id: 2 core_id: 3 - name: Remove Open CAS devices configuration - opencas: + cas: zap: True - name: Stop all Open CAS devices - opencas: + cas: stop: flush: True """ @@ -191,6 +191,7 @@ def gather_facts(): ret["opencas_installed"] = True except: ret["opencas_installed"] = False + return ret try: config = cas_util.cas_config.from_file( @@ -225,10 +226,7 @@ def zap(): def stop(flush): - caches_list = cas_util.get_caches_list() - - devices_count = len(caches_list) - if devices_count == 0: + if len(cas_util.get_caches_list()) == 0: return False cas_util.stop(flush) diff --git a/opencas-deploy.yml b/opencas-deploy.yml index 746146e..51f0a77 100644 --- a/opencas-deploy.yml +++ b/opencas-deploy.yml @@ -1,5 +1,5 @@ --- -- hosts: opencas-nodes +- hosts: opencas_nodes roles: - role: opencas-defaults - role: opencas-install diff --git a/opencas-teardown.yml b/opencas-teardown.yml index 0b76cbd..55d53f6 100644 --- a/opencas-teardown.yml +++ b/opencas-teardown.yml @@ -1,20 +1,20 @@ --- -- hosts: opencas-nodes +- hosts: opencas_nodes roles: - role: opencas-defaults tasks: - name: Flush and stop all Open CAS devices (this may take some time) - opencas: + cas: stop: flush: True - when: opencas_installed + when: opencas_installed | bool become: True - name: Remove Open CAS devices configuration - opencas: + cas: zap: True - when: opencas_installed + when: opencas_installed | bool become: True - name: Clone and checkout Open CAS repository @@ -22,6 +22,7 @@ repo: "{{ opencas_repo_url }}" dest: "{{ opencas_path }}" version: "{{ opencas_version }}" + force: True become: True - name: Uninstall Open CAS @@ -32,9 +33,9 @@ become: True - name: Check if uninstalled - opencas: + cas: gather_facts: True - failed_when: opencas_installed + failed_when: opencas_installed | bool become: True - name: Remove Open CAS directory diff --git a/roles/opencas-defaults/tasks/main.yml b/roles/opencas-defaults/tasks/main.yml index fcdda86..16b2afc 100644 --- a/roles/opencas-defaults/tasks/main.yml +++ b/roles/opencas-defaults/tasks/main.yml @@ -1,6 +1,6 @@ --- - name: Gather Open CAS facts - opencas: + cas: gather_facts: True become: True ... diff --git a/roles/opencas-deploy/tasks/configure-devices.yml b/roles/opencas-deploy/tasks/configure-devices.yml index 865bbbf..40cb391 100644 --- a/roles/opencas-deploy/tasks/configure-devices.yml +++ b/roles/opencas-deploy/tasks/configure-devices.yml @@ -1,11 +1,11 @@ --- - name: Configure cache devices - opencas: + cas: configure_cache_device: "{{ item }}" loop: "{{ opencas_cache_devices }}" - name: Configure core devices - opencas: + cas: configure_core_device: "{{ item }}" loop: "{{ opencas_cached_volumes }}" ... diff --git a/roles/opencas-deploy/tasks/copy-ioclass-config.yml b/roles/opencas-deploy/tasks/copy-ioclass-config.yml index 98d7abc..a226a60 100644 --- a/roles/opencas-deploy/tasks/copy-ioclass-config.yml +++ b/roles/opencas-deploy/tasks/copy-ioclass-config.yml @@ -8,5 +8,5 @@ copy: src: files/ dest: /etc/opencas/ansible - mode: 0777 + mode: 0644 ... diff --git a/roles/opencas-deploy/tasks/validate-config.yml b/roles/opencas-deploy/tasks/validate-config.yml index d895313..637b4bb 100644 --- a/roles/opencas-deploy/tasks/validate-config.yml +++ b/roles/opencas-deploy/tasks/validate-config.yml @@ -1,11 +1,11 @@ --- - name: Validate cache devices configs - opencas: + cas: check_cache_config: "{{ item }}" loop: "{{ opencas_cache_devices }}" - name: Validate core devices configs - opencas: + cas: check_core_config: "{{ item }}" loop: "{{ opencas_cached_volumes }}" ... diff --git a/roles/opencas-install/tasks/main.yml b/roles/opencas-install/tasks/main.yml index fb182d4..06be73f 100644 --- a/roles/opencas-install/tasks/main.yml +++ b/roles/opencas-install/tasks/main.yml @@ -27,6 +27,12 @@ when: not opencas_installed or opencas_installed_version != opencas_version become: True +- name: Configure Open CAS + shell: ./configure + args: + chdir: "{{ opencas_path }}" + become: True + - name: Compile Open CAS make: chdir: "{{ opencas_path }}" @@ -41,7 +47,7 @@ become: True - name: Update facts - opencas: + cas: gather_facts: True become: True ... diff --git a/roles/opencas-validate/tasks/main.yml b/roles/opencas-validate/tasks/main.yml index 478c91d..0eec3d0 100644 --- a/roles/opencas-validate/tasks/main.yml +++ b/roles/opencas-validate/tasks/main.yml @@ -5,12 +5,12 @@ when: (opencas_cache_devices is not defined) or (opencas_cached_volumes is not defined) - name: Validate cache devices configs - opencas: + cas: check_cache_config: "{{ item }}" loop: "{{ opencas_cache_devices }}" - name: Validate core devices configs - opencas: + cas: check_core_config: "{{ item }}" loop: "{{ opencas_cached_volumes }}" ... diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f61a197 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,15 @@ +# +# Copyright(c) 2019 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + + +import sys +import os + + +def pytest_configure(config): + sys.path.append( + os.path.join(os.path.dirname(__file__), "open-cas-linux/utils/") + ) + sys.path.append(os.path.join(os.path.dirname(__file__), "../library")) diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..5e15934 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,107 @@ +# +# Copyright(c) 2012-2019 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import mock +import re +import os +import sys +import logging +from io import StringIO +from textwrap import dedent + + +def get_process_mock(return_value, stdout, stderr): + process_mock = mock.Mock() + attrs = { + "wait.return_value": return_value, + "communicate.return_value": (stdout, stderr), + } + process_mock.configure_mock(**attrs) + + return process_mock + + +def get_mock_os_exists(existing_files): + return lambda x: x in existing_files + + +def get_hashed_config_list(conf): + """ Convert list of config lines to list of config lines hashes, drop empty lines """ + hashed_conf = [get_conf_line_hash(x) for x in conf] + + return [x for x in hashed_conf if x] + + +def get_conf_line_hash(line): + """ + Removes whitespace, lowercases, comments and sorts cache params if present. + Returns empty line for comment-only lines + + We don't care about order of params and kinds of whitespace in config lines + so normalize it to compare. We do care about case in paths, but to simplify + testing we pretend we don't. + """ + + def sort_cache_params(params): + return ",".join(sorted(params.split(","))) + + line = line.split("#")[0] + + cache_params_pattern = re.compile(r"(.*?\s)(\S+=\S+)") + match = cache_params_pattern.search(line) + if match: + sorted_params = sort_cache_params(match.group(2)) + line = match.group(1) + sorted_params + + return "".join(line.lower().split()) + + +class MockConfigFile(object): + def __init__(self, buffer=""): + self.set_contents(buffer) + + def __enter__(self): + return self.buffer + + def __exit__(self, *args, **kwargs): + self.set_contents(self.buffer.getvalue()) + + def __call__(self, path, mode): + if mode == "w": + self.buffer = StringIO() + + return self + + def read(self): + return self.buffer.read() + + def write(self, str): + return self.buffer.write(str) + + def close(self): + self.set_contents(self.buffer.getvalue()) + + def readline(self): + return self.buffer.readline() + + def __next__(self): + return self.buffer.__next__() + + def __iter__(self): + return self + + def set_contents(self, buffer): + self.buffer = StringIO(dedent(buffer).strip()) + + +class CopyableMock(mock.Mock): + def __init__(self, *args, **kwargs): + super(CopyableMock, self).__init__(*args, **kwargs) + self.copies = [] + + def __deepcopy__(self, memo): + copy = mock.Mock(spec=self) + self.copies += [copy] + return copy diff --git a/tests/open-cas-linux b/tests/open-cas-linux new file mode 160000 index 0000000..edd1a51 --- /dev/null +++ b/tests/open-cas-linux @@ -0,0 +1 @@ +Subproject commit edd1a51395114f0ebcfd8709f388f2bdfcd65b27 diff --git a/tests/test_ansible_module_01.py b/tests/test_ansible_module_01.py new file mode 100644 index 0000000..669d12e --- /dev/null +++ b/tests/test_ansible_module_01.py @@ -0,0 +1,739 @@ +# +# Copyright(c) 2012-2019 Intel Corporation +# SPDX-License-Identifier: BSD-3-Clause-Clear +# + +import pytest +from mock import patch, Mock +import helpers as h + +import cas +import opencas + + +class AnsibleFailJson(Exception): + pass + + +class AnsibleExitJson(Exception): + pass + + +class MockAnsibleModule(object): + def __init__(self, arg_spec): + self.params = {} + for key, value in arg_spec.items(): + if value["type"] == "bool": + self.params[key] = False + else: + self.params[key] = {} + + def fail_json(*args, **kwargs): + kwargs["failed"] = True + raise AnsibleFailJson(kwargs) + + def exit_json(*args, **kwargs): + if "changed" not in kwargs: + kwargs["changed"] = False + + raise AnsibleExitJson(kwargs) + + +def setup_module_with_params(**params): + mock_module = MockAnsibleModule(cas.argument_spec) + for k, v in params.items(): + mock_module.params[k] = v + + return mock_module + + +@patch("opencas.get_cas_version") +@patch("cas.setup_module_object") +def test_module_get_facts_not_installed(mock_setup_module, mock_get_version): + mock_get_version.side_effect = opencas.casadm.CasadmError("casadm error") + mock_setup_module.return_value = setup_module_with_params(gather_facts=True) + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'opencas_installed': False") + + +@patch("opencas.get_cas_version") +@patch("opencas.get_caches_list") +@patch("cas.setup_module_object") +def test_module_get_facts_installed( + mock_setup_module, mock_get_caches_list, mock_get_version +): + mock_get_version.return_value = { + "Open CAS Kernel Module": "03.08.00.01131011", + "Open CAS Disk Kernel Module": "03.08.00.01131011", + "Open CAS CLI Utility": "03.08.00.01131011", + } + mock_setup_module.return_value = setup_module_with_params(gather_facts=True) + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'opencas_installed': True") + e.match("03.08.00.01131011") + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_module_zap_no_file(mock_setup_module, mock_config_from_file): + mock_setup_module.return_value = setup_module_with_params(zap=True) + mock_config_from_file.side_effect = IOError() + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + mock_setup_module.assert_called_once() + e.match("'changed': False") + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_module_zap_config_empty(mock_setup_module, mock_config_from_file): + mock_setup_module.return_value = setup_module_with_params(zap=True) + mock_config_from_file.return_value = opencas.cas_config() + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': False") + + +mock_config_file = opencas.cas_config( + version_tag="DEADBEEF", + caches={"1": opencas.cas_config.cache_config(1, "/dev/dummy", "WT")}, + cores=[opencas.cas_config.core_config(1, 1, "/dev/dummycore")], +) + + +@patch("opencas.cas_config.from_file") +@patch("opencas.cas_config") +@patch("cas.setup_module_object") +def test_module_zap_config( + mock_setup_module, mock_new_config, mock_config_from_file +): + mock_setup_module.return_value = setup_module_with_params(zap=True) + mock_config_from_file.return_value = mock_config_file + new_config = Mock() + mock_new_config.return_value = new_config + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + new_config.write.assert_called_once() + mock_new_config.assert_called_with(version_tag="DEADBEEF") + e.match("'changed': True") + + +@patch("opencas.get_caches_list") +@patch("opencas.stop") +@patch("cas.setup_module_object") +def test_module_stop_no_devices(mock_setup_module, mock_stop, mock_get_list): + mock_setup_module.return_value = setup_module_with_params( + stop={"flush": True} + ) + mock_get_list.return_value = [] + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + mock_stop.assert_not_called() + e.match("'changed': False") + + +@patch("opencas.get_caches_list") +@patch("opencas.stop") +@patch("cas.setup_module_object") +def test_module_stop_list_exception(mock_setup_module, mock_stop, mock_get_list): + mock_setup_module.return_value = setup_module_with_params( + stop={"flush": True} + ) + mock_get_list.side_effect = Exception() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_stop.assert_not_called() + e.match("'failed': True") + + +@patch("opencas.get_caches_list") +@patch("opencas.stop") +@patch("cas.setup_module_object") +def test_module_stop_exception(mock_setup_module, mock_stop, mock_get_list): + mock_setup_module.return_value = setup_module_with_params( + stop={"flush": True} + ) + mock_get_list.return_value = [{}] + mock_stop.side_effect = Exception() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_stop.assert_called_with(True) + e.match("'failed': True") + + +@patch("opencas.get_caches_list") +@patch("opencas.stop") +@patch("cas.setup_module_object") +def test_module_stop_no_devices_stopped( + mock_setup_module, mock_stop, mock_get_list +): + mock_setup_module.return_value = setup_module_with_params( + stop={"flush": False} + ) + + # We're using empty dictionaries just to indicate devices count + # Here no caches were stopped between mock calls + mock_get_list.side_effect = [[{}, {}, {}], [{}, {}, {}]] + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_stop.assert_called_with(False) + e.match("'failed': True") + + +@patch("opencas.get_caches_list") +@patch("opencas.stop") +@patch("cas.setup_module_object") +def test_module_stop_some_devices_stopped( + mock_setup_module, mock_stop, mock_get_list +): + mock_setup_module.return_value = setup_module_with_params( + stop={"flush": False} + ) + mock_get_list.side_effect = [[{}, {}, {}], [{}]] + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_stop.assert_called_with(False) + e.match("'failed': True") + + +@patch("opencas.get_caches_list") +@patch("opencas.stop") +@patch("cas.setup_module_object") +def test_module_stop_all_devices_stopped( + mock_setup_module, mock_stop, mock_get_list +): + mock_setup_module.return_value = setup_module_with_params( + stop={"flush": False} + ) + mock_get_list.side_effect = [[{}, {}, {}], []] + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + mock_stop.assert_called_with(False) + e.match("'changed': True") + + +@pytest.mark.parametrize( + "cache_params", + [ + {"id": "1"}, + {"cache_device": "/dev/dummy"}, + {"cache_mode": "WT"}, + {"id": "1", "cache_device": "/dev/dummy"}, + {"cache_device": "/dev/dummy", "cache_mode": "WT"}, + {"cache_device": "/dev/dummy", "cache_mode": "WT"}, + { + "cache_device": "/dev/dummy", + "cache_mode": "WT", + "io_class": "dinosaurs.vbs", + }, + { + "cache_device": "/dev/dummy", + "cache_mode": "WT", + "cleaning_policy": "acp", + }, + {"io_class": "best_config.rar"}, + ], +) +@patch("cas.setup_module_object") +def test_module_check_cache_device_missing_params( + mock_setup_module, cache_params +): + mock_setup_module.return_value = setup_module_with_params( + check_cache_config=cache_params + ) + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + e.match("Missing") + e.match("'failed': True") + + +@patch("opencas.cas_config.cache_config.validate_config") +@patch("cas.setup_module_object") +def test_module_check_cache_device_validate_failed( + mock_setup_module, mock_validate +): + mock_setup_module.return_value = setup_module_with_params( + check_cache_config={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_validate.side_effect = Exception() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_validate.assert_called() + e.match("'failed': True") + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_cache_no_config(mock_setup_module, mock_from_file): + mock_setup_module.return_value = setup_module_with_params( + configure_cache_device={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_from_file.side_effect = ValueError() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_from_file.assert_called() + e.match("'failed': True") + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_cache_insert_failed(mock_setup_module, mock_from_file): + mock_setup_module.return_value = setup_module_with_params( + configure_cache_device={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_core.side_effect = Exception() + mock_from_file.return_value = mock_config + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_config.insert_cache.assert_called_once() + e.match("'failed': True") + + +@patch("opencas.start_cache") +@patch("opencas.is_cache_started") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_cache_already_configured_and_started( + mock_setup_module, mock_from_file, mock_cache_started, mock_start_cache +): + mock_setup_module.return_value = setup_module_with_params( + configure_cache_device={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_cache.side_effect = ( + opencas.cas_config.AlreadyConfiguredException() + ) + mock_from_file.return_value = mock_config + mock_cache_started.return_value = True + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': False") + mock_start_cache.assert_not_called() + + +@patch("opencas.is_cache_started") +@patch("opencas.start_cache") +@patch("opencas.configure_cache") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_cache_not_configured_not_started( + mock_setup_module, + mock_from_file, + mock_configure_cache, + mock_start_cache, + mock_cache_started, +): + mock_setup_module.return_value = setup_module_with_params( + configure_cache_device={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_from_file.return_value = mock_config + mock_cache_started.return_value = False + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': True") + + mock_config.write.assert_called() + + mock_start_cache.assert_called_once() + (args, kwargs) = mock_start_cache.call_args + cache_arg = args[0] + assert kwargs["load"] == False + assert kwargs["force"] == None + assert type(cache_arg) == opencas.cas_config.cache_config + assert cache_arg.cache_id == 1 + assert cache_arg.device == "/dev/dummy" + assert cache_arg.cache_mode == "WT" + + mock_configure_cache.assert_called_once() + (args, kwargs) = mock_configure_cache.call_args + assert args[0] == cache_arg + + +@patch("opencas.is_cache_started") +@patch("opencas.start_cache") +@patch("opencas.configure_cache") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_cache_configured_not_started( + mock_setup_module, + mock_from_file, + mock_configure_cache, + mock_start_cache, + mock_cache_started, +): + mock_setup_module.return_value = setup_module_with_params( + configure_cache_device={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_cache.side_effect = ( + opencas.cas_config.AlreadyConfiguredException() + ) + mock_from_file.return_value = mock_config + mock_cache_started.return_value = False + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': True") + + mock_config.write.assert_not_called() + + mock_start_cache.assert_called_once() + (args, kwargs) = mock_start_cache.call_args + cache_arg = args[0] + assert kwargs["load"] == False + assert kwargs["force"] == None + assert type(cache_arg) == opencas.cas_config.cache_config + assert cache_arg.cache_id == 1 + assert cache_arg.device == "/dev/dummy" + assert cache_arg.cache_mode == "WT" + + mock_configure_cache.assert_called_once() + (args, kwargs) = mock_configure_cache.call_args + assert args[0] == cache_arg + + +@patch("opencas.is_cache_started") +@patch("opencas.start_cache") +@patch("opencas.configure_cache") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_cache_not_configured_not_started_start_failed( + mock_setup_module, + mock_from_file, + mock_configure_cache, + mock_start_cache, + mock_cache_started, +): + mock_setup_module.return_value = setup_module_with_params( + configure_cache_device={ + "id": "1", + "cache_device": "/dev/dummy", + "cache_mode": "WT", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_from_file.return_value = mock_config + mock_cache_started.return_value = False + mock_start_cache.side_effect = Exception() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_config.write.assert_called() + + mock_start_cache.assert_called_once() + + e.match("'failed': True") + mock_configure_cache.assert_not_called() + assert len(mock_config.copies) == 1 + mock_config.copies[0].write.assert_called_once() + + +@pytest.mark.parametrize( + "core_params", + [ + {"cache_id": "1"}, + {"id": "1"}, + {"cached_volume": "/dev/dummy"}, + {"id": "1", "cached_volume": "/dev/dummy"}, + {"cache_id": "1", "cached_volume": "/dev/dummy"}, + {"id": "one", "cache_id": "1", "cached_volume": "/dev/dummy"}, + ], +) +@patch("cas.setup_module_object") +def test_module_check_core_device_missing_params(mock_setup_module, core_params): + mock_setup_module.return_value = setup_module_with_params( + check_core_config=core_params + ) + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + e.match("Missing") + e.match("'failed': True") + + +@patch("opencas.cas_config.core_config.validate_config") +@patch("cas.setup_module_object") +def test_module_check_cache_device_validate_failed( + mock_setup_module, mock_validate +): + mock_setup_module.return_value = setup_module_with_params( + check_core_config={ + "id": "1", + "cache_id": "1", + "cached_volume": "/dev/dummy", + } + ) + mock_validate.side_effect = Exception() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_validate.assert_called() + e.match("'failed': True") + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_no_config(mock_setup_module, mock_from_file): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "1", + "cached_volume": "/dev/dummy", + } + ) + mock_from_file.side_effect = ValueError() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_from_file.assert_called() + e.match("'failed': True") + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_insert_failed(mock_setup_module, mock_from_file): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "1", + "cached_volume": "/dev/dummy", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_core.side_effect = Exception() + mock_from_file.return_value = mock_config + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_config.insert_core.assert_called_once() + e.match("'failed': True") + + +@patch("opencas.add_core") +@patch("opencas.is_core_added") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_already_added( + mock_setup_module, mock_from_file, mock_core_added, mock_add_core +): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "1", + "cached_volume": "/dev/dummy", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_core.side_effect = ( + opencas.cas_config.AlreadyConfiguredException() + ) + mock_from_file.return_value = mock_config + mock_core_added.return_value = True + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': False") + mock_add_core.assert_not_called() + + +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_insert_failed(mock_setup_module, mock_from_file): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "1", + "cached_volume": "/dev/dummy", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_core.side_effect = Exception() + mock_from_file.return_value = mock_config + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_config.insert_core.assert_called_once() + e.match("'failed': True") + + +@patch("opencas.is_core_added") +@patch("opencas.add_core") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_not_configured_not_added( + mock_setup_module, mock_from_file, mock_add_core, mock_core_added +): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "2", + "cached_volume": "/dev/dummy", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_from_file.return_value = mock_config + mock_core_added.return_value = False + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': True") + + mock_config.write.assert_called() + + mock_add_core.assert_called_once() + (args, kwargs) = mock_add_core.call_args + core_arg = args[0] + assert type(core_arg) == opencas.cas_config.core_config + assert core_arg.cache_id == 2 + assert core_arg.core_id == 1 + assert core_arg.device == "/dev/dummy" + + +@patch("opencas.is_core_added") +@patch("opencas.add_core") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_configured_not_added( + mock_setup_module, mock_from_file, mock_add_core, mock_core_added +): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "2", + "cached_volume": "/dev/dummy", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_config.insert_core.side_effect = ( + opencas.cas_config.AlreadyConfiguredException() + ) + mock_from_file.return_value = mock_config + mock_core_added.return_value = False + + with pytest.raises(AnsibleExitJson) as e: + cas.main() + + e.match("'changed': True") + + mock_config.write.assert_not_called() + + mock_add_core.assert_called_once() + (args, kwargs) = mock_add_core.call_args + core_arg = args[0] + assert type(core_arg) == opencas.cas_config.core_config + assert core_arg.cache_id == 2 + assert core_arg.core_id == 1 + assert core_arg.device == "/dev/dummy" + + +@patch("opencas.is_core_added") +@patch("opencas.add_core") +@patch("opencas.cas_config.from_file") +@patch("cas.setup_module_object") +def test_modlue_configure_core_not_configured_not_added_add_failed( + mock_setup_module, mock_from_file, mock_add_core, mock_core_added +): + mock_setup_module.return_value = setup_module_with_params( + configure_core_device={ + "id": "1", + "cache_id": "2", + "cached_volume": "/dev/dummy", + } + ) + mock_config = h.CopyableMock() + mock_config.mock_add_spec(opencas.cas_config) + mock_from_file.return_value = mock_config + mock_core_added.return_value = False + mock_add_core.side_effect = Exception() + + with pytest.raises(AnsibleFailJson) as e: + cas.main() + + mock_config.write.assert_called() + + mock_add_core.assert_called_once() + e.match("'failed': True") + + assert len(mock_config.copies) == 1 + mock_config.copies[0].write.assert_called_once()