;;; ansible.el --- Ansible minor mode
;; -*- Mode: Emacs-Lisp -*-
;; Copyright (C) 2014 by 101000code/101000LAB
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program; if not, write to the Free Software
;; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
;; Version: 0.3.2
;; Author: k1LoW (Kenichirou Oyama), <k1lowxb [at] gmail [dot] com> <k1low [at] 101000lab [dot] org>
;; URL: https://github.com/k1LoW/emacs-ansible
;; Package-Requires: ((s "1.9.0") (f "0.16.2"))
;;; Install
;; Put this file into load-path'ed directory, and byte compile it if
;; desired. And put the following expression into your ~/.emacs.
;;
;; (require 'ansible)
;;; Commentary:
;; This is minor-mode for editing ansible files.
;;; Commands:
;;
;; Below are complete command list:
;;
;; `ansible'
;; Ansible minor mode.
;;
;;; Customizable Options:
;;
;; Below are customizable option list:
;;
;; `ansible-dir-search-limit'
;; Search limit
;; default = 5
;;; Code:
;;require
(require 's)
(require 'f)
;; the 'cl package has been deprecated in favour of 'cl-lib. Load 'cl
;; on emacs < 26, otherwise load 'cl-lib.
(eval-when-compile
(if (version< emacs-version "26")
(require 'cl)
(require 'cl-lib)))
(require 'easy-mmode)
(defgroup ansible nil
"Ansible minor mode"
:group 'languages
:prefix "ansible-")
(defcustom ansible-dir-search-limit 5
"Search limit."
:type 'integer
:group 'ansible)
(defcustom ansible-vault-password-file "~/.vault_pass.txt"
"Filename containing ansible-vault password."
:type 'file
:group 'ansible)
;;;###autoload
(defvar ansible-key-map
(make-sparse-keymap)
"Keymap for Ansible.")
(defvar ansible-root-path nil
"Ansible spec directory path.")
(defvar ansible-hook nil
"Hook.")
(defvar ansible-section-face 'ansible-section-face)
(defface ansible-section-face
'((((class color) (min-colors 88) (background dark)) :foreground "indian red" ))
"Face for ansible first level section names (i.e.: vars, tasks, handlers) in playbooks."
:group 'ansible)
(defvar ansible-task-label-face 'ansible-task-label-face)
(defface ansible-task-label-face
'((((class color) (min-colors 88) (background dark)) :foreground "green" ))
"Face for ansible task names in playbooks"
:group 'ansible)
(defconst ansible-section-keywords-regex
(concat
"^ *-? "
(regexp-opt
'("hosts" "vars" "vars_prompt" "vars_files" "role" "include" "include_tasks"
"roles" "tasks" "import_tasks" "handlers" "pre_tasks" "post_tasks" "environment" ) t)
":")
"Special keywords used to identify toplevel information in a playbook.")
(defconst ansible-task-keywords-regex
(concat
"^ *-? "
(regexp-opt
'("a10_server" "a10_service_group" "a10_virtual_server" "acl" "add_host"
"airbrake_deployment" "alternatives" "apache2_module" "apk" "apt" "apt_key"
"apt_repository" "apt_rpm" "assemble" "assert" "async_status" "at" "authorized_key"
"azure" "azure_rm_deployment" "azure_rm_networkinterface"
"azure_rm_networkinterface_facts" "azure_rm_publicipaddress"
"azure_rm_publicipaddress_facts" "azure_rm_resourcegroup"
"azure_rm_resourcegroup_facts" "azure_rm_securitygroup"
"azure_rm_securitygroup_facts" "azure_rm_storageaccount"
"azure_rm_storageaccount_facts" "azure_rm_storageblob" "azure_rm_subnet"
"azure_rm_virtualmachine" "azure_rm_virtualmachineimage_facts"
"azure_rm_virtualnetwork" "azure_rm_virtualnetwork_facts" "bigip_facts"
"bigip_gtm_wide_ip" "bigip_monitor_http" "bigip_monitor_tcp" "bigip_node"
"bigip_pool" "bigip_pool_member" "bigip_virtual_server" "bigpanda" "blockinfile"
"boundary_meter" "bower" "bundler" "bzr" "campfire" "capabilities"
"circonus_annotation" "cl_bond" "cl_bridge" "cl_img_install" "cl_interface"
"cl_interface_policy" "cl_license" "cl_ports" "clc_aa_policy" "clc_alert_policy"
"clc_blueprint_package" "clc_firewall_policy" "clc_group" "clc_loadbalancer"
"clc_modify_server" "clc_publicip" "clc_server" "clc_server_snapshot"
"cloudflare_dns" "cloudformation" "cloudtrail" "command" "composer" "consul"
"consul_acl" "consul_kv" "consul_session" "copy" "cpanm" "cron" "cronvar" "crypttab"
"cs_account" "cs_affinitygroup" "cs_cluster" "cs_configuration" "cs_domain"
"cs_facts" "cs_firewall" "cs_instance" "cs_instance_facts" "cs_instancegroup"
"cs_ip_address" "cs_iso" "cs_loadbalancer_rule" "cs_loadbalancer_rule_member"
"cs_network" "cs_pod" "cs_portforward" "cs_project" "cs_resourcelimit"
"cs_securitygroup" "cs_securitygroup_rule" "cs_sshkeypair" "cs_staticnat"
"cs_template" "cs_user" "cs_vmsnapshot" "cs_volume" "cs_zone" "cs_zone_facts"
"datadog_event" "datadog_monitor" "debconf" "debug" "deploy_helper" "digital_ocean"
"digital_ocean_domain" "digital_ocean_sshkey" "django_manage" "dnf" "dnsimple"
"dnsmadeeasy" "docker" "docker_container" "docker_image" "docker_image_facts"
"docker_login" "docker_service" "dpkg_selections" "dynamodb_table" "easy_install"
"ec2" "ec2_ami" "ec2_ami_copy" "ec2_ami_find" "ec2_asg" "ec2_eip" "ec2_elb"
"ec2_elb_facts" "ec2_elb_lb" "ec2_eni" "ec2_eni_facts" "ec2_facts" "ec2_group"
"ec2_instance" "ec2_instance_info"
"ec2_key" "ec2_lc" "ec2_metric_alarm" "ec2_remote_facts" "ec2_scaling_policy"
"ec2_snapshot" "ec2_snapshot_facts" "ec2_tag" "ec2_vol" "ec2_vol_facts" "ec2_vpc"
"ec2_vpc_dhcp_options" "ec2_vpc_igw" "ec2_vpc_net" "ec2_vpc_net_facts"
"ec2_vpc_route_table" "ec2_vpc_route_table_facts" "ec2_vpc_subnet"
"ec2_vpc_subnet_facts" "ec2_win_password" "ecs_cluster" "ecs_service"
"ecs_service_facts" "ecs_task" "ecs_taskdefinition" "ejabberd_user" "elasticache"
"elasticache_subnet_group" "elasticsearch_plugin" "eos_command" "eos_config"
"eos_eapi" "eos_template" "expect" "facter" "fail" "fetch" "file" "filesystem"
"find" "firewalld" "flowdock" "gc_storage" "gce" "gce_img" "gce_lb" "gce_net"
"gce_pd" "gce_tag" "gem" "get_url" "getent" "git" "git_config" "github_hooks"
"gitlab_group" "gitlab_project" "gitlab_user" "gluster_volume" "group" "group_by"
"grove" "hall" "haproxy" "hg" "hipchat" "homebrew" "homebrew_cask" "homebrew_tap"
"hostname" "htpasswd" "iam" "iam_cert" "iam_policy" "include_vars"
"influxdb_database" "influxdb_retention_policy" "ini_file" "ios_command"
"ios_config" "ios_template" "iosxr_command" "iosxr_config" "iosxr_template"
"ipify_facts" "iptables" "irc" "jabber" "jboss" "jira" "junos_command"
"junos_config" "junos_facts" "junos_netconf" "junos_package" "junos_template"
"kernel_blacklist" "known_hosts" "kubernetes" "layman" "librato_annotation"
"lineinfile" "linode" "lldp" "locale_gen" "logentries" "lvg" "lvol" "lxc_container"
"macports" "mail" "make" "maven_artifact" "modprobe" "mongodb_parameter"
"mongodb_user" "monit" "mount" "mqtt" "mysql_db" "mysql_replication" "mysql_user"
"mysql_variables" "nagios" "netscaler" "newrelic_deployment" "nexmo" "nmcli" "npm"
"nxos_command" "nxos_config" "nxos_facts" "nxos_feature" "nxos_interface"
"nxos_ip_interface" "nxos_nxapi" "nxos_ping" "nxos_switchport" "nxos_template"
"nxos_vlan" "nxos_vrf" "nxos_vrf_interface" "nxos_vrrp" "ohai" "open_iscsi"
"openbsd_pkg" "openvswitch_bridge" "openvswitch_db" "openvswitch_port" "opkg"
"ops_command" "ops_config" "ops_facts" "ops_template" "os_auth" "os_client_config"
"os_flavor_facts" "os_floating_ip" "os_group" "os_image" "os_image_facts"
"os_ironic" "os_ironic_inspect" "os_ironic_node" "os_keypair" "os_keystone_domain"
"os_keystone_domain_facts" "os_keystone_role" "os_network" "os_networks_facts"
"os_nova_flavor" "os_object" "os_port" "os_port_facts" "os_project"
"os_project_facts" "os_router" "os_security_group" "os_security_group_rule"
"os_server" "os_server_actions" "os_server_facts" "os_server_volume" "os_subnet"
"os_subnets_facts" "os_user" "os_user_facts" "os_user_group" "os_user_role"
"os_volume" "osx_defaults" "osx_say" "ovirt" "package" "pacman" "pagerduty"
"pagerduty_alert" "pam_limits" "patch" "pause" "pear" "ping" "pingdom" "pip" "pkg5"
"pkg5_publisher" "pkgin" "pkgng" "pkgutil" "portage" "portinstall" "postgresql_db"
"postgresql_ext" "postgresql_lang" "postgresql_privs" "postgresql_user"
"profitbricks" "profitbricks_datacenter" "profitbricks_nic" "profitbricks_volume"
"profitbricks_volume_attachments" "proxmox" "proxmox_template" "puppet" "pushbullet"
"pushover" "rabbitmq_binding" "rabbitmq_exchange" "rabbitmq_parameter"
"rabbitmq_plugin" "rabbitmq_policy" "rabbitmq_queue" "rabbitmq_user"
"rabbitmq_vhost" "raw" "rax" "rax_cbs" "rax_cbs_attachments" "rax_cdb"
"rax_cdb_database" "rax_cdb_user" "rax_clb" "rax_clb_nodes" "rax_clb_ssl" "rax_dns"
"rax_dns_record" "rax_facts" "rax_files" "rax_files_objects" "rax_identity"
"rax_keypair" "rax_meta" "rax_mon_alarm" "rax_mon_check" "rax_mon_entity"
"rax_mon_notification" "rax_mon_notification_plan" "rax_network" "rax_queue"
"rax_scaling_group" "rax_scaling_policy" "rds" "rds_param_group" "rds_subnet_group"
"redhat_subscription" "redis" "replace" "rhn_channel" "rhn_register" "riak"
"rollbar_deployment" "route53" "route53_facts" "route53_health_check" "route53_zone"
"rpm_key" "s3" "s3_bucket" "s3_lifecycle" "s3_logging" "script" "seboolean"
"selinux" "selinux_permissive" "sendgrid" "sensu_check" "seport" "service"
"set_fact" "setup" "shell" "sl_vm" "slack" "slackpkg" "slurp" "snmp_facts" "sns"
"sns_topic" "solaris_zone" "sqs_queue" "stackdriver" "stat" "sts_assume_role"
"subversion" "supervisorctl" "svc" "svr4pkg" "swdepot" "synchronize" "sysctl" "systemd"
"taiga_issue" "template" "twilio" "typetalk" "ufw" "unarchive" "uptimerobot" "uri"
"urpmi" "user" "vca_fw" "vca_nat" "vca_vapp" "vertica_configuration" "vertica_facts"
"vertica_role" "vertica_schema" "vertica_user" "virt" "virt_net" "virt_pool"
"vmware_cluster" "vmware_datacenter" "vmware_dns_config" "vmware_dvs_host"
"vmware_dvs_portgroup" "vmware_dvswitch" "vmware_host" "vmware_maintenancemode"
"vmware_migrate_vmk" "vmware_portgroup" "vmware_target_canonical_facts"
"vmware_vm_facts" "vmware_vm_shell" "vmware_vm_vss_dvs_migrate" "vmware_vmkernel"
"vmware_vmkernel_ip_config" "vmware_vsan_cluster" "vmware_vswitch" "vsphere_copy"
"vsphere_guest" "wait_for" "webfaction_app" "webfaction_db" "webfaction_domain"
"webfaction_mailbox" "webfaction_site" "win_acl" "win_acl_inheritance"
"win_chocolatey" "win_copy" "win_dotnet_ngen" "win_environment" "win_feature"
"win_file" "win_file_version" "win_firewall_rule" "win_get_url" "win_group"
"win_iis_virtualdirectory" "win_iis_webapplication" "win_iis_webapppool"
"win_iis_webbinding" "win_iis_website" "win_lineinfile" "win_msi" "win_nssm"
"win_owner" "win_package" "win_ping" "win_reboot" "win_regedit" "win_regmerge"
"win_scheduled_task" "win_service" "win_share" "win_stat" "win_template"
"win_timezone" "win_unzip" "win_updates" "win_uri" "win_user" "win_webpicmd" "xattr"
"xenserver_facts" "yum" "yum_repository" "zabbix_group" "zabbix_host"
"zabbix_hostmacro" "zabbix_maintenance" "zabbix_screen" "zfs" "znode" "zypper"
"zypper_repository") t)
":")
"List of ansible task names.")
(defconst ansible-keywords-regex
(concat
"^ +"
(regexp-opt
'("with_items" "with_dict" "with_nested" "with_first_found" "with_fileglob"
"with_together" "with_subelements" "with_sequence" "with_random_choice" "until"
"retries" "delay" "with_lines" "with_indexed_items" "with_ini" "with_flattened"
"with_inventory_hostnames" "when" "notify" "register" "tags" "gather_facts"
"connection" "tags" "become" "become_user" "args" "local_action" "delegate_to"
"strategy") t)
":")
"Ansible keywords used with tasks.")
(defvar ansible-playbook-font-lock
`(("\\({{\\)\\([^}]+\\)\\(}}\\)"
(1 font-lock-builtin-face t)
(2 font-lock-function-name-face t)
(3 font-lock-builtin-face t))
(,ansible-section-keywords-regex (1 ansible-section-face t))
(,ansible-task-keywords-regex (1 font-lock-keyword-face t))
("^ *- \\(name\\):\\(.*\\)"
(1 font-lock-builtin-face t)
(2 ansible-task-label-face t))
(,ansible-keywords-regex (1 font-lock-builtin-face t)))
"Font lock definitions for ansible playbooks.")
(defun ansible-add-font-lock()
"Extend YAML with syntax highlight for ansible playbooks."
(interactive)
(font-lock-add-keywords 'nil ansible-playbook-font-lock 'append)
(font-lock-flush))
(defun ansible-remove-font-lock()
"Add syntax highlight to ansible playbooks."
(interactive)
(font-lock-remove-keywords 'nil ansible-playbook-font-lock)
(font-lock-flush))
(defun ansible-maybe-unload-snippets(&optional buffer-count)
"Unload ansible snippets in case no other ansible buffers exists."
;; mitigates: https://github.com/k1LoW/emacs-ansible/issues/5
(when (and (featurep 'yasnippet)
(= (or buffer-count 1) ;when called via kill-hook, the buffer is still existent
(seq-count (lambda (b) (with-current-buffer b ansible)) (buffer-list))))
(setq yas-snippet-dirs (delete ansible-snip-dir yas-snippet-dirs))
(yas-reload-all)))
;;;###autoload
(define-minor-mode ansible
"Ansible minor mode."
:lighter " Ansible"
:group 'ansible
(if ansible
(progn
(setq minor-mode-map-alist
(cons (cons 'ansible ansible-key-map)
minor-mode-map-alist))
(ansible-dict-initialize)
(ansible-remove-font-lock)
(ansible-add-font-lock)
(when (featurep 'yasnippet)
(add-to-list 'yas-snippet-dirs ansible-snip-dir t)
(yas-load-directory ansible-snip-dir))
(add-hook 'kill-buffer-hook #'ansible-maybe-unload-snippets nil t)
(run-hooks 'ansible-hook))
(ansible-remove-font-lock)
(ansible-maybe-unload-snippets 0)))
(defun ansible-update-root-path ()
"Update ansible-root-path."
(let ((spec-path (ansible-find-root-path)))
(unless (not spec-path)
(setq ansible-root-path spec-path))
(when ansible-root-path t)))
(defun ansible-find-root-path ()
"Find ansible directory."
(let ((current-dir (f-expand default-directory)))
(loop with count = 0
until (f-exists? (f-join current-dir "roles"))
;; Return nil if outside the value of
if (= count ansible-dir-search-limit)
do (return nil)
;; Or search upper directories.
else
do (incf count)
(unless (f-root? current-dir)
(setq current-dir (f-dirname current-dir)))
finally return current-dir)))
(defun ansible-list-playbooks ()
"Find .yml files in ansible-root-path."
(if (ansible-update-root-path)
(mapcar
(lambda (file) (f-relative file ansible-root-path))
(f-files ansible-root-path (lambda (file) (s-matches? ".yml" (f-long file))) t))
nil))
(defun ansible-vault-buffer (mode)
"Execute ansible-vault (MODE STR should be 'decrypt' or 'encrypt') and update current buffer."
(let* ((input (buffer-substring-no-properties (point-min) (point-max)))
(output (ansible-vault mode input)))
(delete-region (point-min) (point-max))
(insert output)))
(defun ansible-get-string-from-file (file-path)
"Return FILE-PATH's file content."
(with-temp-buffer
(insert-file-contents file-path)
(buffer-string)))
(defun ansible-vault (mode str)
"Execute ansible-vault (MODE STR should be 'decrypt' or 'encrypt')."
(let ((temp-file (make-temp-file "ansible-vault-ansible")))
(write-region str nil temp-file 'append)
(let* ((vault-str (if ansible-vault-password-file
(format "--vault-password-file=%s" ansible-vault-password-file)
""))
(command (format "ansible-vault %s %s %s"
mode vault-str temp-file))
(status (shell-command command))
(output (ansible-get-string-from-file temp-file)))
(if (/= status 0)
(error "Error in ansible-vault running %s!" command)
(delete-file temp-file)
output))))
(defun ansible-decrypt-buffer ()
"Decrypt current buffer."
(interactive)
(ansible-vault-buffer "decrypt")
;; force buffer to be marked as unmodified
(set-buffer-modified-p nil))
(defun ansible-encrypt-buffer ()
"Encrypt current buffer."
(interactive)
(ansible-vault-buffer "encrypt"))
(defconst ansible-dir (file-name-directory (or load-file-name
buffer-file-name)))
(defconst ansible-snip-dir (expand-file-name "snippets" ansible-dir))
(defun ansible-auto-decrypt-encrypt ()
"Decrypt current buffer if it is a vault encrypted file.
Also, automatically encrypts the file before saving the buffer."
(let ((vault-file? (string-match-p "\$ANSIBLE_VAULT;[0-9]+\.[0-9]+"
(buffer-substring-no-properties (point-min)
(point-max)))))
(when vault-file?
(condition-case ex
(progn
(ansible-decrypt-buffer)
(add-hook 'before-save-hook 'ansible-encrypt-buffer nil t)
(add-hook 'after-save-hook 'ansible-decrypt-buffer nil t))
('error
(message "Could not decrypt file. Make sure `ansible-vault-password-file' or the environment variable ANSIBLE_VAULT_PASSWORD_FILE is correctly set"))))))
;;;###autoload
(defun ansible-dict-initialize ()
"Initialize Ansible auto-complete."
(let ((dict-dir (expand-file-name "dict" ansible-dir)))
(when (and (f-directory? dict-dir) (boundp 'ac-user-dictionary-files))
(add-to-list 'ac-user-dictionary-files (f-join dict-dir "ansible") t))))
(provide 'ansible)
;;; ansible.el ends here