From b2e075e6d3999c5793b3527bb6b3c2a481fecec5 Mon Sep 17 00:00:00 2001 From: chris93111 <christopheferreira@ymail.com> Date: Mon, 19 Oct 2020 13:40:07 +0200 Subject: [PATCH] new module nomad_job & nomad_job_info (#867) * nomad_job module * Delete nomad_job.py * new module nomad_job * fix symlink * disable test with centos6 , not supported * fix centos unsupported * fix * requested changes doc * disable freebsd ci * requested change docs + check_mode * lint * fix syntax * update docs * doc fix Co-authored-by: Felix Fontein <felix@fontein.de> * Update nomad_job.py fix docs + ssl true default * Update nomad_job.yml disable ssl ci * nomad_job_info * Update nomad_job_info.py fix token nomad job info * Update nomad_job.py idempotence + check_mode plan result * Update nomad_job.py fail if no id with json content * Update nomad_job.yml ci idempotence + check_mode , nomad_job and nomad_job_info * Update nomad_job.yml fix ci * Update main.yml add kill nomad ci * Update main.yml always kill * fix check mode delete job * ci with delete and check_mode * lint * force start in first deploy * 12.4 nomad * fix version nomad * fix ci assert * fix ci * fix ci * lint * fix version job id None, import os unused * lint job_info * Update aliases * docs frag + info refacto * lint lint * ci * jmespath * fix ci Co-authored-by: FERREIRA Christophe <christophe.ferreira@cnaf.fr> Co-authored-by: Felix Fontein <felix@fontein.de> --- plugins/doc_fragments/nomad.py | 51 +++ plugins/modules/clustering/nomad/nomad_job.py | 255 +++++++++++ .../clustering/nomad/nomad_job_info.py | 345 +++++++++++++++ plugins/modules/nomad_job.py | 1 + plugins/modules/nomad_job_info.py | 1 + tests/integration/targets/nomad/aliases | 6 + tests/integration/targets/nomad/files/job.hcl | 396 ++++++++++++++++++ tests/integration/targets/nomad/meta/main.yml | 4 + .../integration/targets/nomad/tasks/main.yml | 106 +++++ .../targets/nomad/tasks/nomad_job.yml | 90 ++++ 10 files changed, 1255 insertions(+) create mode 100644 plugins/doc_fragments/nomad.py create mode 100644 plugins/modules/clustering/nomad/nomad_job.py create mode 100644 plugins/modules/clustering/nomad/nomad_job_info.py create mode 120000 plugins/modules/nomad_job.py create mode 120000 plugins/modules/nomad_job_info.py create mode 100644 tests/integration/targets/nomad/aliases create mode 100644 tests/integration/targets/nomad/files/job.hcl create mode 100644 tests/integration/targets/nomad/meta/main.yml create mode 100644 tests/integration/targets/nomad/tasks/main.yml create mode 100644 tests/integration/targets/nomad/tasks/nomad_job.yml diff --git a/plugins/doc_fragments/nomad.py b/plugins/doc_fragments/nomad.py new file mode 100644 index 0000000000..3845c54120 --- /dev/null +++ b/plugins/doc_fragments/nomad.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2020 FERREIRA Christophe <christophe.ferreira@cnaf.fr> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +options: + host: + description: + - FQDN of Nomad server. + required: true + type: str + use_ssl: + description: + - Use TLS/SSL connection. + type: bool + default: true + timeout: + description: + - Timeout (in seconds) for the request to Nomad. + type: int + default: 5 + validate_certs: + description: + - Enable TLS/SSL certificate validation. + type: bool + default: true + client_cert: + description: + - Path of certificate for TLS/SSL. + type: path + client_key: + description: + - Path of certificate's private key for TLS/SSL. + type: path + namespace: + description: + - Namespace for Nomad. + type: str + token: + description: + - ACL token for authentification. + type: str +''' diff --git a/plugins/modules/clustering/nomad/nomad_job.py b/plugins/modules/clustering/nomad/nomad_job.py new file mode 100644 index 0000000000..6c28579773 --- /dev/null +++ b/plugins/modules/clustering/nomad/nomad_job.py @@ -0,0 +1,255 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020, FERREIRA Christophe <christophe.ferreira@cnaf.fr> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: nomad_job +author: FERREIRA Christophe (@chris93111) +version_added: "1.3.0" +short_description: Launch a Nomad Job +description: + - Launch a Nomad job. + - Stop a Nomad job. + - Force start a Nomad job +requirements: + - python-nomad +extends_documentation_fragment: + - community.general.nomad +options: + name: + description: + - Name of job for delete, stop and start job without source. + - Name of job for delete, stop and start job without source. + - Either this or I(content) must be specified. + type: str + state: + description: + - Deploy or remove job. + choices: ["present", "absent"] + required: true + type: str + force_start: + description: + - Force job to started. + type: bool + default: false + content: + description: + - Content of Nomad job. + - Either this or I(name) must be specified. + type: str + content_format: + description: + - Type of content of Nomad job. + choices: ["hcl", "json"] + default: hcl + type: str +notes: + - C(check_mode) is supported. +seealso: + - name: Nomad jobs documentation + description: Complete documentation for Nomad API jobs. + link: https://www.nomadproject.io/api-docs/jobs/ +''' + +EXAMPLES = ''' +- name: Create job + community.general.nomad_job: + host: localhost + state: present + content: "{{ lookup('ansible.builtin.file', 'job.hcl') }}" + timeout: 120 + +- name: Stop job + community.general.nomad_job: + host: localhost + state: absent + name: api + +- name: Force job to start + community.general.nomad_job: + host: localhost + state: present + name: api + timeout: 120 + force_start: true +''' + +import json + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native + +import_nomad = None +try: + import nomad + import_nomad = True +except ImportError: + import_nomad = False + + +def run(): + module = AnsibleModule( + argument_spec=dict( + host=dict(required=True, type='str'), + state=dict(required=True, choices=['present', 'absent']), + use_ssl=dict(type='bool', default=True), + timeout=dict(type='int', default=5), + validate_certs=dict(type='bool', default=True), + client_cert=dict(type='path', default=None), + client_key=dict(type='path', default=None), + namespace=dict(type='str', default=None), + name=dict(type='str', default=None), + content_format=dict(choices=['hcl', 'json'], default='hcl'), + content=dict(type='str', default=None), + force_start=dict(type='bool', default=False), + token=dict(type='str', default=None, no_log=True) + ), + supports_check_mode=True, + mutually_exclusive=[ + ["name", "content"] + ], + required_one_of=[ + ['name', 'content'] + ] + ) + + if not import_nomad: + module.fail_json(msg=missing_required_lib("python-nomad")) + + certificate_ssl = (module.params.get('client_cert'), module.params.get('client_key')) + + nomad_client = nomad.Nomad( + host=module.params.get('host'), + secure=module.params.get('use_ssl'), + timeout=module.params.get('timeout'), + verify=module.params.get('validate_certs'), + cert=certificate_ssl, + namespace=module.params.get('namespace'), + token=module.params.get('token') + ) + + if module.params.get('state') == "present": + + if module.params.get('name') and not module.params.get('force_start'): + module.fail_json(msg='For start job with name, force_start is needed') + + changed = False + if module.params.get('content'): + + if module.params.get('content_format') == 'json': + + job_json = module.params.get('content') + try: + job_json = json.loads(job_json) + except ValueError as e: + module.fail_json(msg=to_native(e)) + job = dict() + job['job'] = job_json + try: + job_id = job_json.get('ID') + if job_id is None: + module.fail_json(msg="Cannot retrieve job with ID None") + plan = nomad_client.job.plan_job(job_id, job, diff=True) + if not plan['Diff'].get('Type') == "None": + changed = True + if not module.check_mode: + result = nomad_client.jobs.register_job(job) + else: + result = plan + else: + result = plan + except Exception as e: + module.fail_json(msg=to_native(e)) + + if module.params.get('content_format') == 'hcl': + + try: + job_hcl = module.params.get('content') + job_json = nomad_client.jobs.parse(job_hcl) + job = dict() + job['job'] = job_json + except nomad.api.exceptions.BadRequestNomadException as err: + msg = str(err.nomad_resp.reason) + " " + str(err.nomad_resp.text) + module.fail_json(msg=to_native(msg)) + try: + job_id = job_json.get('ID') + plan = nomad_client.job.plan_job(job_id, job, diff=True) + if not plan['Diff'].get('Type') == "None": + changed = True + if not module.check_mode: + result = nomad_client.jobs.register_job(job) + else: + result = plan + else: + result = plan + except Exception as e: + module.fail_json(msg=to_native(e)) + + if module.params.get('force_start'): + + try: + job = dict() + if module.params.get('name'): + job_name = module.params.get('name') + else: + job_name = job_json['Name'] + job_json = nomad_client.job.get_job(job_name) + if job_json['Status'] == 'running': + result = job_json + else: + job_json['Status'] = 'running' + job_json['Stop'] = False + job['job'] = job_json + if not module.check_mode: + result = nomad_client.jobs.register_job(job) + else: + result = nomad_client.validate.validate_job(job) + if not result.status_code == 200: + module.fail_json(msg=to_native(result.text)) + result = json.loads(result.text) + changed = True + except Exception as e: + module.fail_json(msg=to_native(e)) + + if module.params.get('state') == "absent": + + try: + if not module.params.get('name') is None: + job_name = module.params.get('name') + else: + if module.params.get('content_format') == 'hcl': + job_json = nomad_client.jobs.parse(module.params.get('content')) + job_name = job_json['Name'] + if module.params.get('content_format') == 'json': + job_json = module.params.get('content') + job_name = job_json['Name'] + job = nomad_client.job.get_job(job_name) + if job['Status'] == 'dead': + changed = False + result = job + else: + if not module.check_mode: + result = nomad_client.job.deregister_job(job_name) + else: + result = job + changed = True + except Exception as e: + module.fail_json(msg=to_native(e)) + + module.exit_json(changed=changed, result=result) + + +def main(): + + run() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/clustering/nomad/nomad_job_info.py b/plugins/modules/clustering/nomad/nomad_job_info.py new file mode 100644 index 0000000000..9e93532843 --- /dev/null +++ b/plugins/modules/clustering/nomad/nomad_job_info.py @@ -0,0 +1,345 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# (c) 2020, FERREIRA Christophe <christophe.ferreira@cnaf.fr> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: nomad_job_info +author: FERREIRA Christophe (@chris93111) +version_added: "1.3.0" +short_description: Get Nomad Jobs info +description: + - Get info for one Nomad job. + - List Nomad jobs. +requirements: + - python-nomad +extends_documentation_fragment: + - community.general.nomad +options: + name: + description: + - Name of job for Get info. + - If not specified, lists all jobs. + type: str +notes: + - C(check_mode) is supported. +seealso: + - name: Nomad jobs documentation + description: Complete documentation for Nomad API jobs. + link: https://www.nomadproject.io/api-docs/jobs/ +''' + +EXAMPLES = ''' +- name: Get info for job awx + community.general.nomad_job: + host: localhost + name: awx + register: result + +- name: List Nomad jobs + community.general.nomad_job: + host: localhost + register: result + +''' + +RETURN = ''' +result: + description: List with dictionary contains jobs info + returned: success + type: list + sample: [ + { + "Affinities": null, + "AllAtOnce": false, + "Constraints": null, + "ConsulToken": "", + "CreateIndex": 13, + "Datacenters": [ + "dc1" + ], + "Dispatched": false, + "ID": "example", + "JobModifyIndex": 13, + "Meta": null, + "ModifyIndex": 13, + "Multiregion": null, + "Name": "example", + "Namespace": "default", + "NomadTokenID": "", + "ParameterizedJob": null, + "ParentID": "", + "Payload": null, + "Periodic": null, + "Priority": 50, + "Region": "global", + "Spreads": null, + "Stable": false, + "Status": "pending", + "StatusDescription": "", + "Stop": false, + "SubmitTime": 1602244370615307000, + "TaskGroups": [ + { + "Affinities": null, + "Constraints": null, + "Count": 1, + "EphemeralDisk": { + "Migrate": false, + "SizeMB": 300, + "Sticky": false + }, + "Meta": null, + "Migrate": { + "HealthCheck": "checks", + "HealthyDeadline": 300000000000, + "MaxParallel": 1, + "MinHealthyTime": 10000000000 + }, + "Name": "cache", + "Networks": null, + "ReschedulePolicy": { + "Attempts": 0, + "Delay": 30000000000, + "DelayFunction": "exponential", + "Interval": 0, + "MaxDelay": 3600000000000, + "Unlimited": true + }, + "RestartPolicy": { + "Attempts": 3, + "Delay": 15000000000, + "Interval": 1800000000000, + "Mode": "fail" + }, + "Scaling": null, + "Services": null, + "ShutdownDelay": null, + "Spreads": null, + "StopAfterClientDisconnect": null, + "Tasks": [ + { + "Affinities": null, + "Artifacts": null, + "CSIPluginConfig": null, + "Config": { + "image": "redis:3.2", + "port_map": [ + { + "db": 6379.0 + } + ] + }, + "Constraints": null, + "DispatchPayload": null, + "Driver": "docker", + "Env": null, + "KillSignal": "", + "KillTimeout": 5000000000, + "Kind": "", + "Leader": false, + "Lifecycle": null, + "LogConfig": { + "MaxFileSizeMB": 10, + "MaxFiles": 10 + }, + "Meta": null, + "Name": "redis", + "Resources": { + "CPU": 500, + "Devices": null, + "DiskMB": 0, + "IOPS": 0, + "MemoryMB": 256, + "Networks": [ + { + "CIDR": "", + "DNS": null, + "Device": "", + "DynamicPorts": [ + { + "HostNetwork": "default", + "Label": "db", + "To": 0, + "Value": 0 + } + ], + "IP": "", + "MBits": 10, + "Mode": "", + "ReservedPorts": null + } + ] + }, + "RestartPolicy": { + "Attempts": 3, + "Delay": 15000000000, + "Interval": 1800000000000, + "Mode": "fail" + }, + "Services": [ + { + "AddressMode": "auto", + "CanaryMeta": null, + "CanaryTags": null, + "Checks": [ + { + "AddressMode": "", + "Args": null, + "CheckRestart": null, + "Command": "", + "Expose": false, + "FailuresBeforeCritical": 0, + "GRPCService": "", + "GRPCUseTLS": false, + "Header": null, + "InitialStatus": "", + "Interval": 10000000000, + "Method": "", + "Name": "alive", + "Path": "", + "PortLabel": "", + "Protocol": "", + "SuccessBeforePassing": 0, + "TLSSkipVerify": false, + "TaskName": "", + "Timeout": 2000000000, + "Type": "tcp" + } + ], + "Connect": null, + "EnableTagOverride": false, + "Meta": null, + "Name": "redis-cache", + "PortLabel": "db", + "Tags": [ + "global", + "cache" + ], + "TaskName": "" + } + ], + "ShutdownDelay": 0, + "Templates": null, + "User": "", + "Vault": null, + "VolumeMounts": null + } + ], + "Update": { + "AutoPromote": false, + "AutoRevert": false, + "Canary": 0, + "HealthCheck": "checks", + "HealthyDeadline": 180000000000, + "MaxParallel": 1, + "MinHealthyTime": 10000000000, + "ProgressDeadline": 600000000000, + "Stagger": 30000000000 + }, + "Volumes": null + } + ], + "Type": "service", + "Update": { + "AutoPromote": false, + "AutoRevert": false, + "Canary": 0, + "HealthCheck": "", + "HealthyDeadline": 0, + "MaxParallel": 1, + "MinHealthyTime": 0, + "ProgressDeadline": 0, + "Stagger": 30000000000 + }, + "VaultNamespace": "", + "VaultToken": "", + "Version": 0 + } + ] + +''' + + +import os +import json + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native + +import_nomad = None +try: + import nomad + import_nomad = True +except ImportError: + import_nomad = False + + +def run(): + module = AnsibleModule( + argument_spec=dict( + host=dict(required=True, type='str'), + use_ssl=dict(type='bool', default=True), + timeout=dict(type='int', default=5), + validate_certs=dict(type='bool', default=True), + client_cert=dict(type='path', default=None), + client_key=dict(type='path', default=None), + namespace=dict(type='str', default=None), + name=dict(type='str', default=None), + token=dict(type='str', default=None, no_log=True) + ), + supports_check_mode=True + ) + + if not import_nomad: + module.fail_json(msg=missing_required_lib("python-nomad")) + + certificate_ssl = (module.params.get('client_cert'), module.params.get('client_key')) + + nomad_client = nomad.Nomad( + host=module.params.get('host'), + secure=module.params.get('use_ssl'), + timeout=module.params.get('timeout'), + verify=module.params.get('validate_certs'), + cert=certificate_ssl, + namespace=module.params.get('namespace'), + token=module.params.get('token') + ) + + changed = False + nomad_jobs = list() + try: + job_list = nomad_client.jobs.get_jobs() + for job in job_list: + nomad_jobs.append(nomad_client.job.get_job(job.get('ID'))) + result = nomad_jobs + except Exception as e: + module.fail_json(msg=to_native(e)) + + if module.params.get('name'): + filter = list() + try: + for job in result: + if job.get('ID') == module.params.get('name'): + filter.append(job) + result = filter + if not filter: + module.fail_json(msg="Couldn't find Job with id " + str(module.params.get('name'))) + except Exception as e: + module.fail_json(msg=to_native(e)) + + module.exit_json(changed=changed, result=result) + + +def main(): + + run() + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/nomad_job.py b/plugins/modules/nomad_job.py new file mode 120000 index 0000000000..763b37d1b4 --- /dev/null +++ b/plugins/modules/nomad_job.py @@ -0,0 +1 @@ +clustering/nomad/nomad_job.py \ No newline at end of file diff --git a/plugins/modules/nomad_job_info.py b/plugins/modules/nomad_job_info.py new file mode 120000 index 0000000000..9749646dc4 --- /dev/null +++ b/plugins/modules/nomad_job_info.py @@ -0,0 +1 @@ +clustering/nomad/nomad_job_info.py \ No newline at end of file diff --git a/tests/integration/targets/nomad/aliases b/tests/integration/targets/nomad/aliases new file mode 100644 index 0000000000..3141aee60b --- /dev/null +++ b/tests/integration/targets/nomad/aliases @@ -0,0 +1,6 @@ +shippable/posix/group2 +nomad_job_info +destructive +skip/aix +skip/centos6 +skip/freebsd diff --git a/tests/integration/targets/nomad/files/job.hcl b/tests/integration/targets/nomad/files/job.hcl new file mode 100644 index 0000000000..abcc6854f8 --- /dev/null +++ b/tests/integration/targets/nomad/files/job.hcl @@ -0,0 +1,396 @@ +# There can only be a single job definition per file. This job is named +# "example" so it will create a job with the ID and Name "example". + +# The "job" stanza is the top-most configuration option in the job +# specification. A job is a declarative specification of tasks that Nomad +# should run. Jobs have a globally unique name, one or many task groups, which +# are themselves collections of one or many tasks. +# +# For more information and examples on the "job" stanza, please see +# the online documentation at: +# +# +# https://www.nomadproject.io/docs/job-specification/job.html +# +job "example" { + # The "region" parameter specifies the region in which to execute the job. + # If omitted, this inherits the default region name of "global". + # region = "global" + # + # The "datacenters" parameter specifies the list of datacenters which should + # be considered when placing this task. This must be provided. + datacenters = ["dc1"] + + # The "type" parameter controls the type of job, which impacts the scheduler's + # decision on placement. This configuration is optional and defaults to + # "service". For a full list of job types and their differences, please see + # the online documentation. + # + # For more information, please see the online documentation at: + # + # https://www.nomadproject.io/docs/jobspec/schedulers.html + # + type = "service" + + + # The "constraint" stanza defines additional constraints for placing this job, + # in addition to any resource or driver constraints. This stanza may be placed + # at the "job", "group", or "task" level, and supports variable interpolation. + # + # For more information and examples on the "constraint" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/constraint.html + # + # constraint { + # attribute = "${attr.kernel.name}" + # value = "linux" + # } + + # The "update" stanza specifies the update strategy of task groups. The update + # strategy is used to control things like rolling upgrades, canaries, and + # blue/green deployments. If omitted, no update strategy is enforced. The + # "update" stanza may be placed at the job or task group. When placed at the + # job, it applies to all groups within the job. When placed at both the job and + # group level, the stanzas are merged with the group's taking precedence. + # + # For more information and examples on the "update" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/update.html + # + update { + # The "max_parallel" parameter specifies the maximum number of updates to + # perform in parallel. In this case, this specifies to update a single task + # at a time. + max_parallel = 1 + + # The "min_healthy_time" parameter specifies the minimum time the allocation + # must be in the healthy state before it is marked as healthy and unblocks + # further allocations from being updated. + min_healthy_time = "10s" + + # The "healthy_deadline" parameter specifies the deadline in which the + # allocation must be marked as healthy after which the allocation is + # automatically transitioned to unhealthy. Transitioning to unhealthy will + # fail the deployment and potentially roll back the job if "auto_revert" is + # set to true. + healthy_deadline = "3m" + + # The "progress_deadline" parameter specifies the deadline in which an + # allocation must be marked as healthy. The deadline begins when the first + # allocation for the deployment is created and is reset whenever an allocation + # as part of the deployment transitions to a healthy state. If no allocation + # transitions to the healthy state before the progress deadline, the + # deployment is marked as failed. + progress_deadline = "10m" + + # The "auto_revert" parameter specifies if the job should auto-revert to the + # last stable job on deployment failure. A job is marked as stable if all the + # allocations as part of its deployment were marked healthy. + auto_revert = false + + # The "canary" parameter specifies that changes to the job that would result + # in destructive updates should create the specified number of canaries + # without stopping any previous allocations. Once the operator determines the + # canaries are healthy, they can be promoted which unblocks a rolling update + # of the remaining allocations at a rate of "max_parallel". + # + # Further, setting "canary" equal to the count of the task group allows + # blue/green deployments. When the job is updated, a full set of the new + # version is deployed and upon promotion the old version is stopped. + canary = 0 + } + # The migrate stanza specifies the group's strategy for migrating off of + # draining nodes. If omitted, a default migration strategy is applied. + # + # For more information on the "migrate" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/migrate.html + # + migrate { + # Specifies the number of task groups that can be migrated at the same + # time. This number must be less than the total count for the group as + # (count - max_parallel) will be left running during migrations. + max_parallel = 1 + + # Specifies the mechanism in which allocations health is determined. The + # potential values are "checks" or "task_states". + health_check = "checks" + + # Specifies the minimum time the allocation must be in the healthy state + # before it is marked as healthy and unblocks further allocations from being + # migrated. This is specified using a label suffix like "30s" or "15m". + min_healthy_time = "10s" + + # Specifies the deadline in which the allocation must be marked as healthy + # after which the allocation is automatically transitioned to unhealthy. This + # is specified using a label suffix like "2m" or "1h". + healthy_deadline = "5m" + } + # The "group" stanza defines a series of tasks that should be co-located on + # the same Nomad client. Any task within a group will be placed on the same + # client. + # + # For more information and examples on the "group" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/group.html + # + group "cache" { + # The "count" parameter specifies the number of the task groups that should + # be running under this group. This value must be non-negative and defaults + # to 1. + count = 1 + + # The "restart" stanza configures a group's behavior on task failure. If + # left unspecified, a default restart policy is used based on the job type. + # + # For more information and examples on the "restart" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/restart.html + # + restart { + # The number of attempts to run the job within the specified interval. + attempts = 2 + interval = "30m" + + # The "delay" parameter specifies the duration to wait before restarting + # a task after it has failed. + delay = "15s" + + # The "mode" parameter controls what happens when a task has restarted + # "attempts" times within the interval. "delay" mode delays the next + # restart until the next interval. "fail" mode does not restart the task + # if "attempts" has been hit within the interval. + mode = "fail" + } + + # The "ephemeral_disk" stanza instructs Nomad to utilize an ephemeral disk + # instead of a hard disk requirement. Clients using this stanza should + # not specify disk requirements in the resources stanza of the task. All + # tasks in this group will share the same ephemeral disk. + # + # For more information and examples on the "ephemeral_disk" stanza, please + # see the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/ephemeral_disk.html + # + ephemeral_disk { + # When sticky is true and the task group is updated, the scheduler + # will prefer to place the updated allocation on the same node and + # will migrate the data. This is useful for tasks that store data + # that should persist across allocation updates. + # sticky = true + # + # Setting migrate to true results in the allocation directory of a + # sticky allocation directory to be migrated. + # migrate = true + # + # The "size" parameter specifies the size in MB of shared ephemeral disk + # between tasks in the group. + size = 300 + } + + # The "affinity" stanza enables operators to express placement preferences + # based on node attributes or metadata. + # + # For more information and examples on the "affinity" stanza, please + # see the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/affinity.html + # + # affinity { + # attribute specifies the name of a node attribute or metadata + # attribute = "${node.datacenter}" + + + # value specifies the desired attribute value. In this example Nomad + # will prefer placement in the "us-west1" datacenter. + # value = "us-west1" + + + # weight can be used to indicate relative preference + # when the job has more than one affinity. It defaults to 50 if not set. + # weight = 100 + # } + + + # The "spread" stanza allows operators to increase the failure tolerance of + # their applications by specifying a node attribute that allocations + # should be spread over. + # + # For more information and examples on the "spread" stanza, please + # see the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/spread.html + # + # spread { + # attribute specifies the name of a node attribute or metadata + # attribute = "${node.datacenter}" + + + # targets can be used to define desired percentages of allocations + # for each targeted attribute value. + # + # target "us-east1" { + # percent = 60 + # } + # target "us-west1" { + # percent = 40 + # } + # } + + # The "task" stanza creates an individual unit of work, such as a Docker + # container, web application, or batch processing. + # + # For more information and examples on the "task" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/task.html + # + task "redis" { + # The "driver" parameter specifies the task driver that should be used to + # run the task. + driver = "docker" + + # The "config" stanza specifies the driver configuration, which is passed + # directly to the driver to start the task. The details of configurations + # are specific to each driver, so please see specific driver + # documentation for more information. + config { + image = "redis:3.2" + + port_map { + db = 6379 + } + } + + # The "artifact" stanza instructs Nomad to download an artifact from a + # remote source prior to starting the task. This provides a convenient + # mechanism for downloading configuration files or data needed to run the + # task. It is possible to specify the "artifact" stanza multiple times to + # download multiple artifacts. + # + # For more information and examples on the "artifact" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/artifact.html + # + # artifact { + # source = "http://foo.com/artifact.tar.gz" + # options { + # checksum = "md5:c4aa853ad2215426eb7d70a21922e794" + # } + # } + + + # The "logs" stanza instructs the Nomad client on how many log files and + # the maximum size of those logs files to retain. Logging is enabled by + # default, but the "logs" stanza allows for finer-grained control over + # the log rotation and storage configuration. + # + # For more information and examples on the "logs" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/logs.html + # + # logs { + # max_files = 10 + # max_file_size = 15 + # } + + # The "resources" stanza describes the requirements a task needs to + # execute. Resource requirements include memory, network, cpu, and more. + # This ensures the task will execute on a machine that contains enough + # resource capacity. + # + # For more information and examples on the "resources" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/resources.html + # + resources { + cpu = 500 # 500 MHz + memory = 256 # 256MB + + network { + mbits = 10 + port "db" {} + } + } + # The "service" stanza instructs Nomad to register this task as a service + # in the service discovery engine, which is currently Consul. This will + # make the service addressable after Nomad has placed it on a host and + # port. + # + # For more information and examples on the "service" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/service.html + # + service { + name = "redis-cache" + tags = ["global", "cache"] + port = "db" + + check { + name = "alive" + type = "tcp" + interval = "10s" + timeout = "2s" + } + } + + # The "template" stanza instructs Nomad to manage a template, such as + # a configuration file or script. This template can optionally pull data + # from Consul or Vault to populate runtime configuration data. + # + # For more information and examples on the "template" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/template.html + # + # template { + # data = "---\nkey: {{ key \"service/my-key\" }}" + # destination = "local/file.yml" + # change_mode = "signal" + # change_signal = "SIGHUP" + # } + + # The "template" stanza can also be used to create environment variables + # for tasks that prefer those to config files. The task will be restarted + # when data pulled from Consul or Vault changes. + # + # template { + # data = "KEY={{ key \"service/my-key\" }}" + # destination = "local/file.env" + # env = true + # } + + # The "vault" stanza instructs the Nomad client to acquire a token from + # a HashiCorp Vault server. The Nomad servers must be configured and + # authorized to communicate with Vault. By default, Nomad will inject + # The token into the job via an environment variable and make the token + # available to the "template" stanza. The Nomad client handles the renewal + # and revocation of the Vault token. + # + # For more information and examples on the "vault" stanza, please see + # the online documentation at: + # + # https://www.nomadproject.io/docs/job-specification/vault.html + # + # vault { + # policies = ["cdn", "frontend"] + # change_mode = "signal" + # change_signal = "SIGHUP" + # } + + # Controls the timeout between signalling a task it will be killed + # and killing the task. If not set a default is used. + # kill_timeout = "20s" + } + } +} diff --git a/tests/integration/targets/nomad/meta/main.yml b/tests/integration/targets/nomad/meta/main.yml new file mode 100644 index 0000000000..f4c99a2ad7 --- /dev/null +++ b/tests/integration/targets/nomad/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_pkg_mgr + - setup_openssl diff --git a/tests/integration/targets/nomad/tasks/main.yml b/tests/integration/targets/nomad/tasks/main.yml new file mode 100644 index 0000000000..1e42e7b2f6 --- /dev/null +++ b/tests/integration/targets/nomad/tasks/main.yml @@ -0,0 +1,106 @@ +- name: Skip unsupported platforms + meta: end_play + when: ansible_distribution == 'CentOS' and ansible_distribution_major_version is not version('7', '>=') + +- name: Install Nomad and test + vars: + nomad_version: 0.12.4 + nomad_uri: https://releases.hashicorp.com/nomad/{{ nomad_version }}/nomad_{{ nomad_version }}_{{ ansible_system | lower }}_{{ nomad_arch }}.zip + nomad_cmd: '{{ output_dir }}/nomad' + block: + + - name: register pyOpenSSL version + command: '{{ ansible_python_interpreter }} -c ''import OpenSSL; print(OpenSSL.__version__)''' + register: pyopenssl_version + + - name: Install requests<2.20 (CentOS/RHEL 6) + pip: + name: requests<2.20 + register: result + until: result is success + when: ansible_distribution_file_variety|default() == 'RedHat' and ansible_distribution_major_version is version('6', '<=') + + - name: Install python-nomad + pip: + name: python-nomad + register: result + until: result is success + + - name: Install jmespath + pip: + name: jmespath + register: result + until: result is success + + - when: pyopenssl_version.stdout is version('0.15', '>=') + block: + - name: Generate privatekey + community.crypto.openssl_privatekey: + path: '{{ output_dir }}/privatekey.pem' + + - name: Generate CSR + community.crypto.openssl_csr: + path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + subject: + commonName: localhost + + - name: Generate selfsigned certificate + register: selfsigned_certificate + community.crypto.openssl_certificate: + path: '{{ output_dir }}/cert.pem' + csr_path: '{{ output_dir }}/csr.csr' + privatekey_path: '{{ output_dir }}/privatekey.pem' + provider: selfsigned + selfsigned_digest: sha256 + + - name: Install unzip + package: + name: unzip + register: result + until: result is success + when: ansible_distribution != "MacOSX" + + - assert: + that: ansible_architecture in ['i386', 'x86_64', 'amd64'] + + - set_fact: + nomad_arch: '386' + when: ansible_architecture == 'i386' + + - set_fact: + nomad_arch: amd64 + when: ansible_architecture in ['x86_64', 'amd64'] + + - name: Download nomad binary + unarchive: + src: '{{ nomad_uri }}' + dest: '{{ output_dir }}' + remote_src: true + register: result + until: result is success + + - vars: + remote_dir: '{{ echo_output_dir.stdout }}' + block: + + - command: echo {{ output_dir }} + register: echo_output_dir + + - name: Run tests integration + block: + - name: Start nomad (dev mode enabled) + shell: nohup {{ nomad_cmd }} agent -dev </dev/null >/dev/null 2>&1 & + + - name: wait nomad up + wait_for: + host: localhost + port: 4646 + delay: 10 + timeout: 60 + + - import_tasks: nomad_job.yml + always: + + - name: kill nomad + shell: pkill nomad diff --git a/tests/integration/targets/nomad/tasks/nomad_job.yml b/tests/integration/targets/nomad/tasks/nomad_job.yml new file mode 100644 index 0000000000..ed8ef2590d --- /dev/null +++ b/tests/integration/targets/nomad/tasks/nomad_job.yml @@ -0,0 +1,90 @@ +--- + +- name: run check deploy nomad job + nomad_job: + host: localhost + state: present + use_ssl: false + content: "{{ lookup('file', 'job.hcl') }}" + register: job_check_deployed + check_mode: true + +- name: run create nomad job + nomad_job: + host: localhost + state: present + use_ssl: false + content: "{{ lookup('file', 'job.hcl') }}" + force_start: true + register: job_deployed + +- name: get nomad job deployed + nomad_job_info: + host: localhost + use_ssl: false + name: example + register: get_nomad_job + +- name: get list of nomad jobs + nomad_job_info: + host: localhost + use_ssl: false + register: list_nomad_jobs + +- name: assert job is deployed and tasks is changed + assert: + that: + - job_check_deployed is changed + - job_deployed is changed + - get_nomad_job.result[0].ID == "example" + - list_nomad_jobs.result | length == 1 + +- name: run check deploy job idempotence + nomad_job: + host: localhost + state: present + use_ssl: false + content: "{{ lookup('file', 'job.hcl') }}" + register: job_check_deployed_idempotence + check_mode: true + +- name: run create nomad job idempotence + nomad_job: + host: localhost + state: present + use_ssl: false + content: "{{ lookup('file', 'job.hcl') }}" + register: job_deployed_idempotence + +- name: run check delete nomad job + nomad_job: + host: localhost + state: absent + use_ssl: false + content: "{{ lookup('file', 'job.hcl') }}" + register: job_deleted_check + check_mode: true + +- name: run delete nomad job + nomad_job: + host: localhost + state: absent + use_ssl: false + content: "{{ lookup('file', 'job.hcl') }}" + register: job_deleted + +- name: get job deleted + nomad_job_info: + host: localhost + use_ssl: false + name: example + register: get_job_delete + +- name: assert idempotence + assert: + that: + - job_check_deployed_idempotence is not changed + - job_deployed_idempotence is not changed + - job_deleted_check is changed + - job_deleted is changed + - get_job_delete.result[0].Stop