diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..812fc3b139 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,4 @@ +[report] +omit = + */python?.?/* + */site-packages/nose/* diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..a19ade077d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGELOG.md merge=union diff --git a/.gitignore b/.gitignore index 5fe1d994e3..5d3970a168 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ deb-build credentials.yml # test output .coverage +.tox results.xml coverage.xml /test/units/cover-html diff --git a/.gitmodules b/.gitmodules index 3f14953ec8..793522a29c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,16 +1,12 @@ [submodule "lib/ansible/modules/core"] path = lib/ansible/modules/core - url = https://github.com/ansible/ansible-modules-core.git - branch = devel + url = https://github.com/ansible/ansible-modules-core [submodule "lib/ansible/modules/extras"] path = lib/ansible/modules/extras - url = https://github.com/ansible/ansible-modules-extras.git - branch = devel -[submodule "v2/ansible/modules/core"] - path = v2/ansible/modules/core - url = https://github.com/ansible/ansible-modules-core.git - branch = devel -[submodule "v2/ansible/modules/extras"] - path = v2/ansible/modules/extras - url = https://github.com/ansible/ansible-modules-extras.git - branch = devel + url = https://github.com/ansible/ansible-modules-extras +[submodule "v1/ansible/modules/core"] + path = v1/ansible/modules/core + url = https://github.com/ansible/ansible-modules-core +[submodule "v1/ansible/modules/extras"] + path = v1/ansible/modules/extras + url = https://github.com/ansible/ansible-modules-extras diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..975bc3e35d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: python +env: + - TOXENV=py26 + - TOXENV=py27 +addons: + apt: + sources: + - deadsnakes + packages: + - python2.4 +install: + - pip install tox PyYAML Jinja2 sphinx +script: + - tox + #- make -C docsite all +after_success: + - coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index 4faa8f2ed3..94033d9be5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,335 @@ Ansible Changes By Release ========================== -## 1.8 "You Really Got Me" - Active Development +## 2.0 "TBD" - ACTIVE DEVELOPMENT + +Major Changes: + * Introducing the new block/rescue/always directives, allow for making task blocks and introducing exception like semantics + * New stratergy plugins, allow to control the flow of execution of tasks per play, the default will be the same as before + * Improved error handling, now you get much more detailed parser messages. General exception handling and display has been revamped. + * Task includes now get evaluated during execution, end behaviour will be the same but it now allows for more dynamic includes and options. + * First feature of the more dynamic includes is that with_ loops are now usable with them. + * callback, connection and lookup plugin APIs have changed, some will require modification to work with new version + * callbacks are now shipped in the active directory and don't need to be copied, just whitelisted in ansible.cfg + * Many API changes, this will break those currently using it directly, but the new API is much easier to use and test + * Settings are now more inheritable, what you set at play, block or role will be automatically inhertited by the contained, + this allows for new feautures to automatically be settable at all levels, previouslly we had to manually code this + * Many more tests, new API makes things more testable and we took advantage of it + * big_ip modules now support turning off ssl certificate validation (use only for self signed) + * template code now retains types for bools and Numbers instead of turning them into strings + If you need the old behaviour, quote the value and it will get passed around as a string + +Deprecated Modules (new ones in parens): + * ec2_ami_search (ec2_ami_find) + * quantum_network (os_network) + * glance_image + * nova_compute (os_server) + * quantum_floating_ip (os_floating_ip) + +New Modules: + * amazon: ec2_ami_copy + * amazon: ec2_ami_find + * amazon: ec2_eni + * amazon: ec2_eni_facts + * amazon: ec2_vpc_net + * amazon: ec2_win_password + * amazon: elasticache_subnet_group + * amazon: iam + * amazon: iam_policy + * amazon: route53_zone + * amazon: sts_assume_role + * amazon: s3_logging + * bundler + * centurylink: clc_publicip + * circonus_annotation + * consul + * consul_acl + * consul_kv + * consul_session + * cloudtrail + * cloudstack: cs_account + * cloudstack: cs_affinitygroup + * cloudstack: cs_facts + * cloudstack: cs_firewall + * cloudstack: cs_iso + * cloudstack: cs_instance + * cloudstack: cs_instancegroup + * cloudstack: cs_network + * cloudstack: cs_portforward + * cloudstack: cs_project + * cloudstack: cs_sshkeypair + * cloudstack: cs_securitygroup + * cloudstack: cs_securitygroup_rule + * cloudstack: cs_staticnat + * cloudstack: cs_template + * cloudstack: cs_vmsnapshot + * datadog_monitor + * dpkg_selections + * elasticsearch_plugin + * expect + * find + * hall + * maven_artifact + * openstack: os_ironic + * openstack: os_ironic_node + * openstack: os_client_config + * openstack: os_floating_ip + * openstack: os_image + * openstack: os_network + * openstack: os_nova_flavor + * openstack: os_object + * openstack: os_security_group + * openstack: os_security_group_rule + * openstack: os_server + * openstack: os_server_actions + * openstack: os_server_facts + * openstack: os_server_volume + * openstack: os_subnet + * openstack: os_volume + * osx_defaults + * pear + * profitbricks: profitbricks + * proxmox + * proxmox_template + * puppet + * pushover + * pushbullet + * rax: rax_mon_alarm + * rax: rax_mon_check + * rax: rax_mon_entity + * rax: rax_mon_notification + * rax: rax_mon_notification_plan + * rabbitmq_binding + * rabbitmq_exchange + * rabbitmq_queue + * sensu_check + * sensu_subscription + * vertica_configuration + * vertica_facts + * vertica_role + * vertica_schema + * vertica_user + * vmware_datacenter + * vsphere_copy + * webfaction_app + * webfaction_db + * webfaction_domain + * webfaction_mailbox + * webfaction_site + * win_environment + * win_scheduled_task + * win_iis_virtualdirectory + * win_iis_webapplication + * win_iis_webapppool + * win_iis_webbinding + * win_iis_website + * win_regedit + * win_unzip + * zabbix_host + * zabbix_hostmacro + * zabbix_screen + +New Inventory scripts: + * cloudstack + * fleetctl + * serf + +Other Notable Changes: + +## 1.9.2 "Dancing In the Street" - Jun 26, 2015 + +* Security fixes to check that hostnames match certificates with https urls (CVE-2015-3908) + - get_url and uri modules + - url and etcd lookup plugins +* Security fixes to the zone (Solaris containers), jail (bsd containers), + and chroot connection plugins. These plugins can be used to connect to + their respective container types in leiu of the standard ssh connection. + Prior to this fix being applied these connection plugins didn't properly + handle symlinks within the containers which could lead to files intended to + be written to or read from the container being written to or read from the + host system instead. (CVE pending) +* Fixed a bug in the service module where init scripts were being incorrectly used instead of upstart/systemd. +* Fixed a bug where sudo/su settings were not inherited from ansible.cfg correctly. +* Fixed a bug in the rds module where a traceback may occur due to an unbound variable. +* Fixed a bug where certain remote file systems where the SELinux context was not being properly set. +* Re-enabled several windows modules which had been partially merged (via action plugins): + - win_copy.ps1 + - win_copy.py + - win_file.ps1 + - win_file.py + - win_template.py +* Fix bug using with_sequence and a count that is zero. Also allows counting backwards isntead of forwards +* Fix get_url module bug preventing use of custom ports with https urls +* Fix bug disabling repositories in the yum module. +* Fix giving yum module a url to install a package from on RHEL/CENTOS5 +* Fix bug in dnf module preventing it from working when yum-utils was not already installed + +## 1.9.1 "Dancing In the Street" - Apr 27, 2015 + +* Fixed a bug related to Kerberos auth when using winrm with a domain account. +* Fixing several bugs in the s3 module. +* Fixed a bug with upstart service detection in the service module. +* Fixed several bugs with the user module when used on OSX. +* Fixed unicode handling in some module situations (assert and shell/command execution). +* Fixed a bug in redhat_subscription when using the activationkey parameter. +* Fixed a traceback in the gce module on EL6 distros when multiple pycrypto installations are available. +* Added support for PostgreSQL 9.4 in rds_param_group +* Several other minor fixes. + +## 1.9 "Dancing In the Street" - Mar 25, 2015 + +Major changes: + +* Added kerberos support to winrm connection plugin. +* Tags rehaul: added 'all', 'always', 'untagged' and 'tagged' special tags and normalized + tag resolution. Added tag information to --list-tasks and new --list-tags option. +* Privilege Escalation generalization, new 'Become' system and variables now will + handle existing and new methods. Sudo and su have been kept for backwards compatibility. + New methods pbrun and pfexec in 'alpha' state, planned adding 'runas' for winrm connection plugin. +* Improved ssh connection error reporting, now you get back the specific message from ssh. +* Added facility to document task module return values for registered vars, both for + ansible-doc and the docsite. Documented copy, stats and acl modules, the rest must be + updated individually (we will start doing so incrementally). +* Optimize the plugin loader to cache available plugins much more efficiently. + For some use cases this can lead to dramatic improvements in startup time. +* Overhaul of the checksum system, now supports more systems and more cases more reliably and uniformly. +* Fix skipped tasks to not display their parameters if no_log is specified. +* Many fixes to unicode support, standarized functions to make it easier to add to input/output boundaries. +* Added travis integration to github for basic tests, this should speed up ticket triage and merging. +* environment: directive now can also be applied to play and is inhertited by tasks, which can still override it. +* expanded facts and OS/distribution support for existing facts and improved performance with pypy. +* new 'wantlist' option to lookups allows for selecting a list typed variable vs a comma delimited string as the return. +* the shared module code for file backups now uses a timestamp resolution of seconds (previouslly minutes). +* allow for empty inventories, this is now a warning and not an error (for those using localhost and cloud modules). +* sped up YAML parsing in ansible by up to 25% by switching to CParser loader. + +New Modules: + +* cryptab: manages linux encrypted block devices +* gce_img: for utilizing GCE image resources +* gluster_volume: manage glusterfs volumes +* haproxy: for the load balancer of same name +* known_hosts: manages the ssh known_hosts file +* lxc_container: manage lxc containers +* patch: allows for patching files on target systems +* pkg5: installing and uninstalling packages on Solaris +* pkg5_publisher: manages Solaris pkg5 repository configuration +* postgresql_ext: manage postgresql extensions +* snmp_facts: gather facts via snmp +* svc: manages daemontools based services +* uptimerobot: manage monitoring with this service + +New Filters: + +* ternary: allows for trueval/falseval assignment dependent on conditional +* cartesian: returns the Cartesian product of 2 lists +* to_uuid: given a string it will return an ansible domain specific UUID +* checksum: uses the ansible internal checksum to return a hash from a string +* hash: get a hash from a string (md5, sha1, etc) +* password_hash: get a hash form as string that can be used as a password in the user module (and others) +* A whole set of ip/network manipulation filters: ipaddr,ipwrap,ipv4,ipv6ipsubnet,nthhost,hwaddr,macaddr + +Other Notable Changes: + +* New lookup plugins: + * dig: does dns resolution and returns IPs. + * url: allows pulling data from a url. +* New callback plugins: + * syslog_json: allows logging play output to a syslog network server using json format +* Many new enhancements to the amazon web service modules: + * ec2 now applies all specified security groups when creating a new instance. Previously it was only applying one + * ec2_vol gained the ability to specify the EBS volume type + * ec2_vol can now detach volumes by specifying instance=None + * Fix ec2_group to purge specific grants rather than whole rules + * Added tenancy support for the ec2 module + * rds module has gained the ability to manage tags and set charset and public accessibility + * ec2_snapshot module gained the capability to remove snapshots + * Add alias support for route53 + * Add private_zones support to route53 + * ec2_asg: Add wait_for_instances parameter that waits until an instance is ready before ending the ansible task +* Many new docker improvements: + * restart_policy parameters to configure when the container automatically restarts + * If the docker client or server doesn't support an option, the task will now fail instead of silently ignoring the option + * Add insecure_registry parameter for connecting to registries via http + * New parameter to set a container's domain name + * Undeprecated docker_image module until there's replacement functionality + * Allow setting the container's pid namespace + * Add a pull parameter that chooses when ansible will look for more recent images in the registry + * docker module states have been greatly enhanced. The reworked and new states are: + * present now creates but does not start containers + * restarted always restarts a container + * reloaded restarts a container if ansible detects that the configuration is different than what is specified + * reloaded accounts for exposed ports, env vars, and volumes + * Can now connect to the docker server using TLS +* Several source control modules had force parameters that defaulted to true. + These have been changed to default to false so as not to accidentally lose + work. Playbooks that depended on the former behaviour simply need to add + force=True to the task that needs it. Affected modules: + * bzr: When local modifications exist in a checkout, the bzr module used to + default to removing the modifications on any operation. Now the module + will not remove the modifications unless force=yes is specified. + Operations that depend on a clean working tree may fail unless force=yes is + added. + * git: When local modifications exist in a checkout, the git module will now + fail unless force is explicitly specified. Specifying force=yes will allow + the module to revert and overwrite local modifications to make git actions + succeed. + * hg: When local modifications exist in a checkout, the hg module used to + default to removing the modifications on any operation. Now the module + will not remove the modifications unless force=yes is specified. + * subversion: When updating a checkout with local modifications, you now need + to add force=yes so the module will revert the modifications before updating. +* New inventory scripts: + * vbox: virtualbox + * consul: use consul as an inventory source +* gce gained the ip_forward parameter to forward ip packets +* disk_auto_delete parameter to gce that will remove the boot disk after an instance is destroyed +* gce can now spawn instances with no external ip +* gce_pd gained the ability to choose a disk type +* gce_net gained target_tags parameter for creating firewall rules +* rax module has new parameters for making use of a boot volume +* Add scheduler_hints to the nova_compute module for optional parameters +* vsphere_guest now supports deploying guests from a template +* Many fixes for hardlink and softlink handling in file-related modules +* Implement user, group, mode, and selinux parameters for the unarchive module +* authorized_keys can now use url as a key source +* authorized_keys has a new exclusive parameter that determines if keys that weren't specified in the task +* The selinux module now sets the current running state to permissive if state='disabled' +* Can now set accounts to expire via the user module +* Overhaul of the service module to make code simpler and behave better for systems running several popular init systems +* yum module now has a parameter to refresh its cache of package metadata +* apt module gained a build_dep parameter to install a package's build dependencies +* Add parameters to the postgres modules to specify a unix socket to connect to the db +* The mount module now supports bind mounts +* Add a clone parameter to git module that allows you to get information about a remote repo even if it doesn't exist locally. +* Add a refspec argument to the git module that allows pulling commits that aren't part of a branch +* Many documentation additions and fixes. + +## 1.8.4 "You Really Got Me" - Feb 19, 2015 + +* Fixed regressions in ec2 and mount modules, introduced in 1.8.3 + +## 1.8.3 "You Really Got Me" - Feb 17, 2015 + +* Fixing a security bug related to the default permissions set on a temporary file created when using "ansible-vault view ". +* Many bug fixes, for both core code and core modules. + +## 1.8.2 "You Really Got Me" - Dec 04, 2014 + +* Various bug fixes for packaging issues related to modules. +* Various bug fixes for lookup plugins. +* Various bug fixes for some modules (continued cleanup of postgresql issues, etc.). + +* Add a clone parameter to git module that allows you to get information about a remote repo even if it doesn't exist locally. + +## 1.8.1 "You Really Got Me" - Nov 26, 2014 + +* Various bug fixes in postgresql and mysql modules. +* Fixed a bug related to lookup plugins used within roles not finding files based on the relative paths to the roles files/ directory. +* Fixed a bug related to vars specified in plays being templated too early, resulting in incorrect variable interpolation. +* Fixed a bug related to git submodules in bare repos. + +## 1.8 "You Really Got Me" - Nov 25, 2014 Major changes: @@ -17,6 +345,9 @@ Major changes: * new omit value can be used to leave off a parameter when not set, like so module_name: a=1 b={{ c | default(omit) }}, would not pass value for b (not even an empty value) if c was not set. * developers: 'baby JSON' in module responses, originally intended for writing modules in bash, is removed as a feature to simplify logic, script module remains available for running bash scripts. * async jobs started in "fire & forget" mode can now be checked on at a later time. +* added ability to subcategorize modules for docs.ansible.com +* added ability for shipped modules to have aliases with symlinks +* added ability to deprecate older modules by starting with "_" and including "deprecated: message why" in module docs New Modules: @@ -33,6 +364,7 @@ New Modules: Some other notable changes: * added the ability to set "instance filters" in the ec2.ini to limit results from the inventory plugin. +* upgrades for various variable precedence items and parsing related items * added a new "follow" parameter to the file and copy modules, which allows actions to be taken on the target of a symlink rather than the symlink itself. * if a module should ever traceback, it will return a standard error, catchable by ignore_errors, versus an 'unreachable' * ec2_lc: added support for multiple new parameters like kernel_id, ramdisk_id and ebs_optimized. @@ -58,7 +390,18 @@ Some other notable changes: * ec2_ami_search: support for SSD and IOPS provisioned EBS images * can set ansible_sudo_exe as an inventory variable which allows specifying a different sudo (or equivalent) command -* git module: Submodule handling has changed. Previously if you used the ``recursive`` parameter to handle submodules, ansible would track the submodule upstream's head revision. This has been changed to checkout the version of the submodule specified in the superproject's git repository. This is inline with what git submodule update does. If you want the old behaviour use the new module parameter track_submodules=yes +* git module: Submodule handling has changed. Previously if you used the + ``recursive`` parameter to handle submodules, ansible would track the + submodule upstream's head revision. This has been changed to checkout the + version of the submodule specified in the superproject's git repository. + This is inline with what git submodule update does. If you want the old + behaviour use the new module parameter track_submodules=yes +* Checksumming of transferred files has been made more portable and now uses + the sha1 algorithm instead of md5 to be compatible with FIPS-140. + - As a small side effect, the fetch module no longer returns a useful value + in remote_md5. If you need a replacement, switch to using remote_checksum + which returns the sha1sum of the remote file. +* ansible-doc CLI tool contains various improvements for working with different terminals And various other bug fixes and improvements ... @@ -69,7 +412,7 @@ And various other bug fixes and improvements ... - Fixes a bug in vault where the password file option was not being used correctly internally. - Improved multi-line parsing when using YAML literal blocks (using > or |). - Fixed a bug with the file module and the creation of relative symlinks. -- Fixed a bug where checkmode was not being honored during the templating of files. +- Fixed a bug where checkmode was not being honoured during the templating of files. - Other various bug fixes. ## 1.7.1 "Summer Nights" - Aug 14, 2014 @@ -112,7 +455,7 @@ New Modules: Other notable changes: * Security fixes - - Prevent the use of lookups when using legaxy "{{ }}" syntax around variables and with_* loops. + - Prevent the use of lookups when using legacy "{{ }}" syntax around variables and with_* loops. - Remove relative paths in TAR-archived file names used by ansible-galaxy. * Inventory speed improvements for very large inventories. * Vault password files can now be executable, to support scripts that fetch the vault password. @@ -256,7 +599,7 @@ Other notable changes: ## 1.5.4 "Love Walks In" - April 1, 2014 - Security fix for safe_eval, which further hardens the checking of the evaluation function. -- Changing order of variable precendence for system facts, to ensure that inventory variables take precedence over any facts that may be set on a host. +- Changing order of variable precedence for system facts, to ensure that inventory variables take precedence over any facts that may be set on a host. ## 1.5.3 "Love Walks In" - March 13, 2014 @@ -291,7 +634,7 @@ Major features/changes: * ec2 module now accepts 'exact_count' and 'count_tag' as a way to enforce a running number of nodes by tags. * all ec2 modules that work with Eucalyptus also now support a 'validate_certs' option, which can be set to 'off' for installations using self-signed certs. * Start of new integration test infrastructure (WIP, more details TBD) -* if repoquery is unavailble, the yum module will automatically attempt to install yum-utils +* if repoquery is unavailable, the yum module will automatically attempt to install yum-utils * ansible-vault: a framework for encrypting your playbooks and variable files * added support for privilege escalation via 'su' into bin/ansible and bin/ansible-playbook and associated keywords 'su', 'su_user', 'su_pass' for tasks/plays @@ -754,7 +1097,7 @@ Bugfixes and Misc Changes: * misc fixes to the Riak module * make template module slightly more efficient * base64encode / decode filters are now available to templates -* libvirt module can now work with multiple different libvirt connecton URIs +* libvirt module can now work with multiple different libvirt connection URIs * fix for postgresql password escaping * unicode fix for shlex.split in some cases * apt module upgrade logic improved @@ -789,7 +1132,7 @@ the variable is still registered for the host, with the attribute skipped: True. * service pattern argument now correctly read for BSD services * fetch location can now be controlled more directly via the 'flat' parameter. * added basename and dirname as Jinja2 filters available to all templates -* pip works better when sudoing from unpriveledged users +* pip works better when sudoing from unprivileged users * fix for user creation with groups specification reporting 'changed' incorrectly in some cases * fix for some unicode encoding errors in outputing some data in verbose mode * improved FreeBSD, NetBSD and Solaris facts @@ -959,7 +1302,7 @@ New playbook/language features: * task includes can now be of infinite depth * when_set and when_unset can take more than one var (when_set: $a and $b and $c) * added the with_sequence lookup plugin -* can override "connection:" on an indvidual task +* can override "connection:" on an individual task * parameterized playbook includes can now define complex variables (not just all on one line) * making inventory variables available for use in vars_files paths * messages when skipping plays are now more clear diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 8ce40348ca..094501db90 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,11 +1,22 @@ ##### Issue Type: -Can you help us out in labelling this by telling us what kind of ticket this this? You can say “Bug Report”, “Feature Idea”, “Feature Pull Request”, “New Module Pull Request”, “Bugfix Pull Request”, “Documentation Report”, or “Docs Pull Request”. +Can you help us out in labelling this by telling us what kind of ticket this this? You can say: + - Bug Report + - Feature Idea + - Feature Pull Request + - New Module Pull Request + - Bugfix Pull Request + - Documentation Report + - Docs Pull Request ##### Ansible Version: Let us know what version of Ansible you are using. Please supply the verbatim output from running “ansible --version”. If you're filing a ticket on a version of Ansible which is not the latest, we'd greatly appreciate it if you could retest on the latest version first. We don't expect you to test against the development branch most of the time, but we may ask for that if you have cycles. Thanks! +##### Ansible Configuration: + +What have you changed about your Ansible installation? What configuration settings have you changed/added/removed? Compare your /etc/ansible/ansible.cfg against a clean version from Github and let us know what's different. + ##### Environment: What OS are you running Ansible from and what OS are you managing? Examples include RHEL 5/6, Centos 5/6, Ubuntu 12.04/13.10, *BSD, Solaris. If this is a generic feature request or it doesn’t apply, just say “N/A”. Not all tickets may be about operating system related things and we understand that. diff --git a/MANIFEST.in b/MANIFEST.in index 5fdfe50f34..b9bf5f4276 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,9 @@ +prune v2 +prune docsite +prune ticket_stubs +prune packaging +prune test +prune hacking include README.md packaging/rpm/ansible.spec COPYING include examples/hosts include examples/ansible.cfg @@ -7,3 +13,9 @@ recursive-include docs * include Makefile include VERSION include MANIFEST.in +include contrib/README.md +include contrib/inventory * +exclude lib/ansible/modules/core/.git* +exclude lib/ansible/modules/extras/.git* +prune lib/ansible/modules/core/.git +prune lib/ansible/modules/extras/.git diff --git a/Makefile b/Makefile index 5ac0e26d40..7533e648c5 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ # make deb-src -------------- produce a DEB source # make deb ------------------ produce a DEB # make docs ----------------- rebuild the manpages (results are checked in) -# make tests ---------------- run the tests +# make tests ---------------- run the tests (see test/README.md for requirements) # make pyflakes, make pep8 -- source code checks ######################################################## @@ -34,7 +34,8 @@ PYTHON=python SITELIB = $(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print get_python_lib()") # VERSION file provides one place to update the software version -VERSION := $(shell cat VERSION) +VERSION := $(shell cat VERSION | cut -f1 -d' ') +RELEASE := $(shell cat VERSION | cut -f2 -d' ') # Get the branch information from git ifneq ($(shell which git),) @@ -52,8 +53,9 @@ DEBUILD_BIN ?= debuild DEBUILD_OPTS = --source-option="-I" DPUT_BIN ?= dput DPUT_OPTS ?= +DEB_DATE := $(shell date +"%a, %d %b %Y %T %z") ifeq ($(OFFICIAL),yes) - DEB_RELEASE = 1ppa + DEB_RELEASE = $(RELEASE)ppa # Sign OFFICIAL builds using 'DEBSIGN_KEYID' # DEBSIGN_KEYID is required when signing ifneq ($(DEBSIGN_KEYID),) @@ -74,7 +76,7 @@ DEB_DIST ?= unstable RPMSPECDIR= packaging/rpm RPMSPEC = $(RPMSPECDIR)/ansible.spec RPMDIST = $(shell rpm --eval '%{?dist}') -RPMRELEASE = 1 +RPMRELEASE = $(RELEASE) ifneq ($(OFFICIAL),yes) RPMRELEASE = 0.git$(DATE) endif @@ -93,7 +95,7 @@ NOSETESTS3 ?= nosetests-3.3 all: clean python tests: - PYTHONPATH=./lib $(NOSETESTS) -d -w test/units -v # Could do: --with-coverage --cover-package=ansible + PYTHONPATH=./lib $(NOSETESTS) -d -w test/units -v --with-coverage --cover-package=ansible --cover-branches newtests: PYTHONPATH=./v2:./lib $(NOSETESTS) -d -w v2/test -v --with-coverage --cover-package=ansible --cover-branches @@ -134,7 +136,7 @@ clean: @echo "Cleaning up byte compiled python stuff" find . -type f -regex ".*\.py[co]$$" -delete @echo "Cleaning up editor backup files" - find . -type f \( -name "*~" -or -name "#*" \) -delete + find . -type f \( -name "*~" -or -name "#*" \) |grep -v test/units/inventory_test_data/group_vars/noparse/all.yml~ |xargs -n 1024 -r rm find . -type f \( -name "*.swp" \) -delete @echo "Cleaning up manpage stuff" find ./docs/man -type f -name "*.xml" -delete @@ -151,6 +153,7 @@ clean: rm -rf docs/js @echo "Cleaning up authors file" rm -f AUTHORS.TXT + find . -type f -name '*.pyc' -delete python: $(PYTHON) setup.py build @@ -215,7 +218,7 @@ debian: sdist mkdir -p deb-build/$${DIST} ; \ tar -C deb-build/$${DIST} -xvf dist/$(NAME)-$(VERSION).tar.gz ; \ cp -a packaging/debian deb-build/$${DIST}/$(NAME)-$(VERSION)/ ; \ - sed -ie "s#^$(NAME) (\([^)]*\)) \([^;]*\);#ansible (\1-$(DEB_RELEASE)~$${DIST}) $${DIST};#" deb-build/$${DIST}/$(NAME)-$(VERSION)/debian/changelog ; \ + sed -ie "s|%VERSION%|$(VERSION)|g;s|%RELEASE%|$(DEB_RELEASE)|;s|%DIST%|$${DIST}|g;s|%DATE%|$(DEB_DATE)|g" deb-build/$${DIST}/$(NAME)-$(VERSION)/debian/changelog ; \ done deb: debian diff --git a/README.md b/README.md index ab08cf027f..8bfc18c7ca 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -[![PyPI version](https://badge.fury.io/py/ansible.png)](http://badge.fury.io/py/ansible) [![PyPI downloads](https://pypip.in/d/ansible/badge.png)](https://pypi.python.org/pypi/ansible) +[![PyPI version](https://badge.fury.io/py/ansible.png)](http://badge.fury.io/py/ansible) +[![PyPI downloads](https://pypip.in/d/ansible/badge.png)](https://pypi.python.org/pypi/ansible) +[![Build Status](https://travis-ci.org/ansible/ansible.svg?branch=devel)](https://travis-ci.org/ansible/ansible) Ansible @@ -49,8 +51,7 @@ Branch Info Authors ======= -Ansible was created by [Michael DeHaan](github.com/mpdehaan) (michael@ansible.com) and has contributions from over -800 users (and growing). Thanks everyone! +Ansible was created by [Michael DeHaan](https://github.com/mpdehaan) (michael.dehaan/gmail/com) and has contributions from over 1000 users (and growing). Thanks everyone! Ansible is sponsored by [Ansible, Inc](http://ansible.com) diff --git a/RELEASES.txt b/RELEASES.txt index 72323a146f..c147deddf3 100644 --- a/RELEASES.txt +++ b/RELEASES.txt @@ -4,11 +4,17 @@ Ansible Releases at a Glance Active Development ++++++++++++++++++ -1.8 "You Really Got Me" ---- FALL 2014 +2.0 "TBD" - in progress Released ++++++++ +1.9.1 "Dancing In the Streets" 04-27-2015 +1.9.0 "Dancing In the Streets" 03-25-2015 +1.8.4 "You Really Got Me" ---- 02-19-2015 +1.8.3 "You Really Got Me" ---- 02-17-2015 +1.8.2 "You Really Got Me" ---- 12-04-2014 +1.8.1 "You Really Got Me" ---- 11-26-2014 1.7.2 "Summer Nights" -------- 09-24-2014 1.7.1 "Summer Nights" -------- 08-14-2014 1.7 "Summer Nights" -------- 08-06-2014 diff --git a/VERSION b/VERSION index 6259340971..a4b5d82d9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.8 +2.0.0 0.0.pre diff --git a/bin/ansible b/bin/ansible index b82a18d3d3..209b235c88 100755 --- a/bin/ansible +++ b/bin/ansible @@ -18,187 +18,93 @@ # along with Ansible. If not, see . ######################################################## +from __future__ import (absolute_import, print_function) +__metaclass__ = type + +__requires__ = ['ansible'] +try: + import pkg_resources +except Exception: + # Use pkg_resources to find the correct versions of libraries and set + # sys.path appropriately when there are multiversion installs. But we + # have code that better expresses the errors in the places where the code + # is actually used (the deps are optional for many code paths) so we don't + # want to fail here. + pass import os import sys +import traceback -from ansible.runner import Runner -import ansible.constants as C -from ansible import utils -from ansible import errors -from ansible import callbacks -from ansible import inventory -######################################################## +from ansible.errors import AnsibleError, AnsibleOptionsError, AnsibleParserError +from ansible.utils.display import Display -class Cli(object): - ''' code behind bin/ansible ''' +######################################## +### OUTPUT OF LAST RESORT ### +class LastResort(object): + def display(self, msg): + print(msg, file=sys.stderr) - # ---------------------------------------------- - - def __init__(self): - self.stats = callbacks.AggregateStats() - self.callbacks = callbacks.CliRunnerCallbacks() - if C.DEFAULT_LOAD_CALLBACK_PLUGINS: - callbacks.load_callback_plugins() - - # ---------------------------------------------- - - def parse(self): - ''' create an options parser for bin/ansible ''' - - parser = utils.base_parser( - constants=C, - runas_opts=True, - subset_opts=True, - async_opts=True, - output_opts=True, - connect_opts=True, - check_opts=True, - diff_opts=False, - usage='%prog [options]' - ) - - parser.add_option('-a', '--args', dest='module_args', - help="module arguments", default=C.DEFAULT_MODULE_ARGS) - parser.add_option('-m', '--module-name', dest='module_name', - help="module name to execute (default=%s)" % C.DEFAULT_MODULE_NAME, - default=C.DEFAULT_MODULE_NAME) - - options, args = parser.parse_args() - self.callbacks.options = options - - if len(args) == 0 or len(args) > 1: - parser.print_help() - sys.exit(1) - - # su and sudo command line arguments need to be mutually exclusive - if (options.su or options.su_user or options.ask_su_pass) and \ - (options.sudo or options.sudo_user or options.ask_sudo_pass): - parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') " - "and su arguments ('-su', '--su-user', and '--ask-su-pass') are " - "mutually exclusive") - - if (options.ask_vault_pass and options.vault_password_file): - parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive") - - return (options, args) - - # ---------------------------------------------- - - def run(self, options, args): - ''' use Runner lib to do SSH things ''' - - pattern = args[0] - - sshpass = None - sudopass = None - su_pass = None - vault_pass = None - - options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS - # Never ask for an SSH password when we run with local connection - if options.connection == "local": - options.ask_pass = False - options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS - options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS - options.ask_vault_pass = options.ask_vault_pass or C.DEFAULT_ASK_VAULT_PASS - - (sshpass, sudopass, su_pass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass, ask_vault_pass=options.ask_vault_pass) - - # read vault_pass from a file - if not options.ask_vault_pass and options.vault_password_file: - vault_pass = utils.read_vault_file(options.vault_password_file) - - inventory_manager = inventory.Inventory(options.inventory, vault_password=vault_pass) - if options.subset: - inventory_manager.subset(options.subset) - hosts = inventory_manager.list_hosts(pattern) - if len(hosts) == 0: - callbacks.display("No hosts matched", stderr=True) - sys.exit(0) - - if options.listhosts: - for host in hosts: - callbacks.display(' %s' % host) - sys.exit(0) - - if ((options.module_name == 'command' or options.module_name == 'shell') - and not options.module_args): - callbacks.display("No argument passed to %s module" % options.module_name, color='red', stderr=True) - sys.exit(1) + def error(self, msg, wrap_text=None): + print(msg, file=sys.stderr) - if options.su_user or options.ask_su_pass: - options.su = True - options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER - options.su_user = options.su_user or C.DEFAULT_SU_USER - if options.tree: - utils.prepare_writeable_dir(options.tree) - - runner = Runner( - module_name=options.module_name, - module_path=options.module_path, - module_args=options.module_args, - remote_user=options.remote_user, - remote_pass=sshpass, - inventory=inventory_manager, - timeout=options.timeout, - private_key_file=options.private_key_file, - forks=options.forks, - pattern=pattern, - callbacks=self.callbacks, - sudo=options.sudo, - sudo_pass=sudopass, - sudo_user=options.sudo_user, - transport=options.connection, - subset=options.subset, - check=options.check, - diff=options.check, - su=options.su, - su_pass=su_pass, - su_user=options.su_user, - vault_pass=vault_pass - ) - - if options.seconds: - callbacks.display("background launch...\n\n", color='cyan') - results, poller = runner.run_async(options.seconds) - results = self.poll_while_needed(poller, options) - else: - results = runner.run() - - return (runner, results) - - # ---------------------------------------------- - - def poll_while_needed(self, poller, options): - ''' summarize results from Runner ''' - - # BACKGROUND POLL LOGIC when -B and -P are specified - if options.seconds and options.poll_interval > 0: - poller.wait(options.seconds, options.poll_interval) - - return poller.results - - -######################################################## +######################################## if __name__ == '__main__': - callbacks.display("", log_only=True) - callbacks.display(" ".join(sys.argv), log_only=True) - callbacks.display("", log_only=True) - cli = Cli() - (options, args) = cli.parse() + display = LastResort() + cli = None + me = os.path.basename(sys.argv[0]) + try: - (runner, results) = cli.run(options, args) - for result in results['contacted'].values(): - if 'failed' in result or result.get('rc', 0) != 0: - sys.exit(2) - if results['dark']: - sys.exit(3) - except errors.AnsibleError, e: - # Generic handler for ansible specific errors - callbacks.display("ERROR: %s" % str(e), stderr=True, color='red') - sys.exit(1) + display = Display() + if me == 'ansible-playbook': + from ansible.cli.playbook import PlaybookCLI as mycli + elif me == 'ansible': + from ansible.cli.adhoc import AdHocCLI as mycli + elif me == 'ansible-pull': + from ansible.cli.pull import PullCLI as mycli + elif me == 'ansible-doc': + from ansible.cli.doc import DocCLI as mycli + elif me == 'ansible-vault': + from ansible.cli.vault import VaultCLI as mycli + elif me == 'ansible-galaxy': + from ansible.cli.galaxy import GalaxyCLI as mycli + + cli = mycli(sys.argv, display=display) + if cli: + cli.parse() + sys.exit(cli.run()) + else: + raise AnsibleError("Program not implemented: %s" % me) + + except AnsibleOptionsError as e: + cli.parser.print_help() + display.error(str(e), wrap_text=False) + sys.exit(5) + except AnsibleParserError as e: + display.error(str(e), wrap_text=False) + sys.exit(4) +# TQM takes care of these, but leaving comment to reserve the exit codes +# except AnsibleHostUnreachable as e: +# display.error(str(e)) +# sys.exit(3) +# except AnsibleHostFailed as e: +# display.error(str(e)) +# sys.exit(2) + except AnsibleError as e: + display.error(str(e), wrap_text=False) + sys.exit(1) + except KeyboardInterrupt: + display.error("User interrupted execution") + sys.exit(99) + except Exception as e: + have_cli_options = cli is not None and cli.options is not None + display.error("Unexpected Exception: %s" % str(e), wrap_text=False) + if not have_cli_options or have_cli_options and cli.options.verbosity > 2: + display.display("the full traceback was:\n\n%s" % traceback.format_exc()) + else: + display.display("to see the full traceback, use -vvv") + sys.exit(250) diff --git a/bin/ansible-doc b/bin/ansible-doc deleted file mode 100755 index d5143f33a1..0000000000 --- a/bin/ansible-doc +++ /dev/null @@ -1,295 +0,0 @@ -#!/usr/bin/env python - -# (c) 2012, Jan-Piet Mens -# -# This file is part of Ansible -# -# Ansible 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 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . -# - -import os -import sys -import textwrap -import re -import optparse -import datetime -import subprocess -from ansible import utils -from ansible.utils import module_docs -import ansible.constants as C -from ansible.utils import version -import traceback - -MODULEDIR = C.DEFAULT_MODULE_PATH - -BLACKLIST_EXTS = ('.pyc', '.swp', '.bak', '~', '.rpm') - -_ITALIC = re.compile(r"I\(([^)]+)\)") -_BOLD = re.compile(r"B\(([^)]+)\)") -_MODULE = re.compile(r"M\(([^)]+)\)") -_URL = re.compile(r"U\(([^)]+)\)") -_CONST = re.compile(r"C\(([^)]+)\)") -PAGER = 'less' -LESS_OPTS = 'FRSX' # -F (quit-if-one-screen) -R (allow raw ansi control chars) - # -S (chop long lines) -X (disable termcap init and de-init) - -def pager_print(text): - ''' just print text ''' - print text - -def pager_pipe(text, cmd): - ''' pipe text through a pager ''' - if 'LESS' not in os.environ: - os.environ['LESS'] = LESS_OPTS - try: - cmd = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout) - cmd.communicate(input=text) - except IOError: - pass - except KeyboardInterrupt: - pass - -def pager(text): - ''' find reasonable way to display text ''' - # this is a much simpler form of what is in pydoc.py - if not sys.stdout.isatty(): - pager_print(text) - elif 'PAGER' in os.environ: - if sys.platform == 'win32': - pager_print(text) - else: - pager_pipe(text, os.environ['PAGER']) - elif hasattr(os, 'system') and os.system('(less) 2> /dev/null') == 0: - pager_pipe(text, 'less') - else: - pager_print(text) - -def tty_ify(text): - - t = _ITALIC.sub("`" + r"\1" + "'", text) # I(word) => `word' - t = _BOLD.sub("*" + r"\1" + "*", t) # B(word) => *word* - t = _MODULE.sub("[" + r"\1" + "]", t) # M(word) => [word] - t = _URL.sub(r"\1", t) # U(word) => word - t = _CONST.sub("`" + r"\1" + "'", t) # C(word) => `word' - - return t - -def get_man_text(doc): - - opt_indent=" " - text = [] - text.append("> %s\n" % doc['module'].upper()) - - desc = " ".join(doc['description']) - - text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=" ", subsequent_indent=" ")) - - if 'option_keys' in doc and len(doc['option_keys']) > 0: - text.append("Options (= is mandatory):\n") - - for o in sorted(doc['option_keys']): - opt = doc['options'][o] - - if opt.get('required', False): - opt_leadin = "=" - else: - opt_leadin = "-" - - text.append("%s %s" % (opt_leadin, o)) - - desc = " ".join(opt['description']) - - if 'choices' in opt: - choices = ", ".join(str(i) for i in opt['choices']) - desc = desc + " (Choices: " + choices + ")" - if 'default' in opt: - default = str(opt['default']) - desc = desc + " [Default: " + default + "]" - text.append("%s\n" % textwrap.fill(tty_ify(desc), initial_indent=opt_indent, - subsequent_indent=opt_indent)) - - if 'notes' in doc and len(doc['notes']) > 0: - notes = " ".join(doc['notes']) - text.append("Notes:%s\n" % textwrap.fill(tty_ify(notes), initial_indent=" ", - subsequent_indent=opt_indent)) - - - if 'requirements' in doc and doc['requirements'] is not None and len(doc['requirements']) > 0: - req = ", ".join(doc['requirements']) - text.append("Requirements:%s\n" % textwrap.fill(tty_ify(req), initial_indent=" ", - subsequent_indent=opt_indent)) - - if 'examples' in doc and len(doc['examples']) > 0: - text.append("Example%s:\n" % ('' if len(doc['examples']) < 2 else 's')) - for ex in doc['examples']: - text.append("%s\n" % (ex['code'])) - - if 'plainexamples' in doc and doc['plainexamples'] is not None: - text.append(doc['plainexamples']) - text.append('') - - return "\n".join(text) - - -def get_snippet_text(doc): - - text = [] - desc = tty_ify(" ".join(doc['short_description'])) - text.append("- name: %s" % (desc)) - text.append(" action: %s" % (doc['module'])) - - for o in sorted(doc['options'].keys()): - opt = doc['options'][o] - desc = tty_ify(" ".join(opt['description'])) - - if opt.get('required', False): - s = o + "=" - else: - s = o - - text.append(" %-20s # %s" % (s, desc)) - text.append('') - - return "\n".join(text) - -def get_module_list_text(module_list): - text = [] - for module in sorted(set(module_list)): - - if module in module_docs.BLACKLIST_MODULES: - continue - - filename = utils.plugins.module_finder.find_plugin(module) - - if filename is None: - continue - if filename.endswith(".ps1"): - continue - if os.path.isdir(filename): - continue - - try: - doc, plainexamples = module_docs.get_docstring(filename) - desc = tty_ify(doc.get('short_description', '?')) - if len(desc) > 55: - desc = desc + '...' - text.append("%-20s %-60.60s" % (module, desc)) - except: - traceback.print_exc() - sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module) - return "\n".join(text) - -def main(): - - p = optparse.OptionParser( - version=version("%prog"), - usage='usage: %prog [options] [module...]', - description='Show Ansible module documentation', - ) - - p.add_option("-M", "--module-path", - action="store", - dest="module_path", - default=MODULEDIR, - help="Ansible modules/ directory") - p.add_option("-l", "--list", - action="store_true", - default=False, - dest='list_dir', - help='List available modules') - p.add_option("-s", "--snippet", - action="store_true", - default=False, - dest='show_snippet', - help='Show playbook snippet for specified module(s)') - p.add_option('-v', action='version', help='Show version number and exit') - - (options, args) = p.parse_args() - - if options.module_path is not None: - for i in options.module_path.split(os.pathsep): - utils.plugins.module_finder.add_directory(i) - - if options.list_dir: - # list all modules - paths = utils.plugins.module_finder._get_paths() - module_list = [] - for path in paths: - # os.system("ls -C %s" % (path)) - if os.path.isdir(path): - for module in os.listdir(path): - if any(module.endswith(x) for x in BLACKLIST_EXTS): - continue - module_list.append(module) - - pager(get_module_list_text(module_list)) - sys.exit() - - if len(args) == 0: - p.print_help() - - def print_paths(finder): - ''' Returns a string suitable for printing of the search path ''' - - # Uses a list to get the order right - ret = [] - for i in finder._get_paths(): - if i not in ret: - ret.append(i) - return os.pathsep.join(ret) - - text = '' - for module in args: - - filename = utils.plugins.module_finder.find_plugin(module) - if filename is None: - sys.stderr.write("module %s not found in %s\n" % (module, - print_paths(utils.plugins.module_finder))) - continue - - if any(filename.endswith(x) for x in BLACKLIST_EXTS): - continue - - try: - doc, plainexamples = module_docs.get_docstring(filename) - except: - traceback.print_exc() - sys.stderr.write("ERROR: module %s has a documentation error formatting or is missing documentation\n" % module) - continue - - if doc is not None: - - all_keys = [] - for (k,v) in doc['options'].iteritems(): - all_keys.append(k) - all_keys = sorted(all_keys) - doc['option_keys'] = all_keys - - doc['filename'] = filename - doc['docuri'] = doc['module'].replace('_', '-') - doc['now_date'] = datetime.date.today().strftime('%Y-%m-%d') - doc['plainexamples'] = plainexamples - - if options.show_snippet: - text += get_snippet_text(doc) - else: - text += get_man_text(doc) - else: - # this typically means we couldn't even parse the docstring, not just that the YAML is busted, - # probably a quoting issue. - sys.stderr.write("ERROR: module %s missing documentation (or could not parse documentation)\n" % module) - pager(text) - -if __name__ == '__main__': - main() diff --git a/bin/ansible-doc b/bin/ansible-doc new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/bin/ansible-doc @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/bin/ansible-galaxy b/bin/ansible-galaxy deleted file mode 100755 index 9018e6c205..0000000000 --- a/bin/ansible-galaxy +++ /dev/null @@ -1,901 +0,0 @@ -#!/usr/bin/env python - -######################################################################## -# -# (C) 2013, James Cammarata -# -# This file is part of Ansible -# -# Ansible 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 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . -# -######################################################################## - -import datetime -import json -import os -import os.path -import shutil -import subprocess -import sys -import tarfile -import tempfile -import urllib -import urllib2 -import yaml - -from collections import defaultdict -from distutils.version import LooseVersion -from jinja2 import Environment -from optparse import OptionParser - -import ansible.constants as C -import ansible.utils -from ansible.errors import AnsibleError - -default_meta_template = """--- -galaxy_info: - author: {{ author }} - description: {{description}} - company: {{ company }} - # Some suggested licenses: - # - BSD (default) - # - MIT - # - GPLv2 - # - GPLv3 - # - Apache - # - CC-BY - license: {{ license }} - min_ansible_version: {{ min_ansible_version }} - # - # Below are all platforms currently available. Just uncomment - # the ones that apply to your role. If you don't see your - # platform on this list, let us know and we'll get it added! - # - #platforms: - {%- for platform,versions in platforms.iteritems() %} - #- name: {{ platform }} - # versions: - # - all - {%- for version in versions %} - # - {{ version }} - {%- endfor %} - {%- endfor %} - # - # Below are all categories currently available. Just as with - # the platforms above, uncomment those that apply to your role. - # - #categories: - {%- for category in categories %} - #- {{ category.name }} - {%- endfor %} -dependencies: [] - # List your role dependencies here, one per line. Only - # dependencies available via galaxy should be listed here. - # Be sure to remove the '[]' above if you add dependencies - # to this list. - {% for dependency in dependencies %} - #- {{ dependency }} - {% endfor %} - -""" - -default_readme_template = """Role Name -========= - -A brief description of the role goes here. - -Requirements ------------- - -Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. - -Role Variables --------------- - -A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. - -Dependencies ------------- - -A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. - -Example Playbook ----------------- - -Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: - - - hosts: servers - roles: - - { role: username.rolename, x: 42 } - -License -------- - -BSD - -Author Information ------------------- - -An optional section for the role authors to include contact information, or a website (HTML is not allowed). -""" - -#------------------------------------------------------------------------------------- -# Utility functions for parsing actions/options -#------------------------------------------------------------------------------------- - -VALID_ACTIONS = ("init", "info", "install", "list", "remove") - -def get_action(args): - """ - Get the action the user wants to execute from the - sys argv list. - """ - for i in range(0,len(args)): - arg = args[i] - if arg in VALID_ACTIONS: - del args[i] - return arg - return None - -def build_option_parser(action): - """ - Builds an option parser object based on the action - the user wants to execute. - """ - - usage = "usage: %%prog [%s] [--help] [options] ..." % "|".join(VALID_ACTIONS) - epilog = "\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) - OptionParser.format_epilog = lambda self, formatter: self.epilog - parser = OptionParser(usage=usage, epilog=epilog) - - if not action: - parser.print_help() - sys.exit() - - # options for all actions - # - none yet - - # options specific to actions - if action == "info": - parser.set_usage("usage: %prog info [options] role_name[,version]") - elif action == "init": - parser.set_usage("usage: %prog init [options] role_name") - parser.add_option( - '-p', '--init-path', dest='init_path', default="./", - help='The path in which the skeleton role will be created. ' - 'The default is the current working directory.') - parser.add_option( - '--offline', dest='offline', default=False, action='store_true', - help="Don't query the galaxy API when creating roles") - elif action == "install": - parser.set_usage("usage: %prog install [options] [-r FILE | role_name(s)[,version] | scm+role_repo_url[,version] | tar_file(s)]") - parser.add_option( - '-i', '--ignore-errors', dest='ignore_errors', action='store_true', default=False, - help='Ignore errors and continue with the next specified role.') - parser.add_option( - '-n', '--no-deps', dest='no_deps', action='store_true', default=False, - help='Don\'t download roles listed as dependencies') - parser.add_option( - '-r', '--role-file', dest='role_file', - help='A file containing a list of roles to be imported') - elif action == "remove": - parser.set_usage("usage: %prog remove role1 role2 ...") - elif action == "list": - parser.set_usage("usage: %prog list [role_name]") - - # options that apply to more than one action - if action != "init": - parser.add_option( - '-p', '--roles-path', dest='roles_path', default=C.DEFAULT_ROLES_PATH, - help='The path to the directory containing your roles. ' - 'The default is the roles_path configured in your ' - 'ansible.cfg file (/etc/ansible/roles if not configured)') - - if action in ("info","init","install"): - parser.add_option( - '-s', '--server', dest='api_server', default="galaxy.ansible.com", - help='The API server destination') - - if action in ("init","install"): - parser.add_option( - '-f', '--force', dest='force', action='store_true', default=False, - help='Force overwriting an existing role') - # done, return the parser - return parser - -def get_opt(options, k, defval=""): - """ - Returns an option from an Optparse values instance. - """ - try: - data = getattr(options, k) - except: - return defval - if k == "roles_path": - if os.pathsep in data: - data = data.split(os.pathsep)[0] - return data - -def exit_without_ignore(options, rc=1): - """ - Exits with the specified return code unless the - option --ignore-errors was specified - """ - - if not get_opt(options, "ignore_errors", False): - print '- you can use --ignore-errors to skip failed roles.' - sys.exit(rc) - -#------------------------------------------------------------------------------------- -# Galaxy API functions -#------------------------------------------------------------------------------------- - -def api_get_config(api_server): - """ - Fetches the Galaxy API current version to ensure - the API server is up and reachable. - """ - - try: - url = 'https://%s/api/' % api_server - data = json.load(urllib2.urlopen(url)) - if not data.get("current_version",None): - return None - else: - return data - except: - return None - -def api_lookup_role_by_name(api_server, role_name): - """ - Uses the Galaxy API to do a lookup on the role owner/name. - """ - - role_name = urllib.quote(role_name) - - try: - parts = role_name.split(".") - user_name = ".".join(parts[0:-1]) - role_name = parts[-1] - print "- downloading role '%s', owned by %s" % (role_name, user_name) - except: - parser.print_help() - print "- invalid role name (%s). Specify role as format: username.rolename" % role_name - sys.exit(1) - - url = 'https://%s/api/v1/roles/?owner__username=%s&name=%s' % (api_server,user_name,role_name) - try: - data = json.load(urllib2.urlopen(url)) - if len(data["results"]) == 0: - return None - else: - return data["results"][0] - except: - return None - -def api_fetch_role_related(api_server, related, role_id): - """ - Uses the Galaxy API to fetch the list of related items for - the given role. The url comes from the 'related' field of - the role. - """ - - try: - url = 'https://%s/api/v1/roles/%d/%s/?page_size=50' % (api_server, int(role_id), related) - data = json.load(urllib2.urlopen(url)) - results = data['results'] - done = (data.get('next', None) == None) - while not done: - url = 'https://%s%s' % (api_server, data['next']) - print url - data = json.load(urllib2.urlopen(url)) - results += data['results'] - done = (data.get('next', None) == None) - return results - except: - return None - -def api_get_list(api_server, what): - """ - Uses the Galaxy API to fetch the list of items specified. - """ - - try: - url = 'https://%s/api/v1/%s/?page_size' % (api_server, what) - data = json.load(urllib2.urlopen(url)) - if "results" in data: - results = data['results'] - else: - results = data - done = True - if "next" in data: - done = (data.get('next', None) == None) - while not done: - url = 'https://%s%s' % (api_server, data['next']) - print url - data = json.load(urllib2.urlopen(url)) - results += data['results'] - done = (data.get('next', None) == None) - return results - except: - print "- failed to download the %s list" % what - return None - -#------------------------------------------------------------------------------------- -# scm repo utility functions -#------------------------------------------------------------------------------------- - -def scm_archive_role(scm, role_url, role_version, role_name): - if scm not in ['hg', 'git']: - print "- scm %s is not currently supported" % scm - return False - tempdir = tempfile.mkdtemp() - clone_cmd = [scm, 'clone', role_url, role_name] - with open('/dev/null', 'w') as devnull: - try: - print "- executing: %s" % " ".join(clone_cmd) - popen = subprocess.Popen(clone_cmd, cwd=tempdir, stdout=devnull, stderr=devnull) - except: - raise AnsibleError("error executing: %s" % " ".join(clone_cmd)) - rc = popen.wait() - if rc != 0: - print "- command %s failed" % ' '.join(clone_cmd) - print " in directory %s" % tempdir - return False - - temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.tar') - if scm == 'hg': - archive_cmd = ['hg', 'archive', '--prefix', "%s/" % role_name] - if role_version: - archive_cmd.extend(['-r', role_version]) - archive_cmd.append(temp_file.name) - if scm == 'git': - archive_cmd = ['git', 'archive', '--prefix=%s/' % role_name, '--output=%s' % temp_file.name] - if role_version: - archive_cmd.append(role_version) - else: - archive_cmd.append('HEAD') - - with open('/dev/null', 'w') as devnull: - print "- executing: %s" % " ".join(archive_cmd) - popen = subprocess.Popen(archive_cmd, cwd=os.path.join(tempdir, role_name), - stderr=devnull, stdout=devnull) - rc = popen.wait() - if rc != 0: - print "- command %s failed" % ' '.join(archive_cmd) - print " in directory %s" % tempdir - return False - - shutil.rmtree(tempdir) - - return temp_file.name - - -#------------------------------------------------------------------------------------- -# Role utility functions -#------------------------------------------------------------------------------------- - -def get_role_path(role_name, options): - """ - Returns the role path based on the roles_path option - and the role name. - """ - roles_path = get_opt(options,'roles_path') - roles_path = os.path.join(roles_path, role_name) - roles_path = os.path.expanduser(roles_path) - return roles_path - -def get_role_metadata(role_name, options): - """ - Returns the metadata as YAML, if the file 'meta/main.yml' - exists in the specified role_path - """ - role_path = os.path.join(get_role_path(role_name, options), 'meta/main.yml') - try: - if os.path.isfile(role_path): - f = open(role_path, 'r') - meta_data = yaml.safe_load(f) - f.close() - return meta_data - else: - return None - except: - return None - -def get_galaxy_install_info(role_name, options): - """ - Returns the YAML data contained in 'meta/.galaxy_install_info', - if it exists. - """ - - try: - info_path = os.path.join(get_role_path(role_name, options), 'meta/.galaxy_install_info') - if os.path.isfile(info_path): - f = open(info_path, 'r') - info_data = yaml.safe_load(f) - f.close() - return info_data - else: - return None - except: - return None - -def write_galaxy_install_info(role_name, role_version, options): - """ - Writes a YAML-formatted file to the role's meta/ directory - (named .galaxy_install_info) which contains some information - we can use later for commands like 'list' and 'info'. - """ - - info = dict( - version = role_version, - install_date = datetime.datetime.utcnow().strftime("%c"), - ) - try: - info_path = os.path.join(get_role_path(role_name, options), 'meta/.galaxy_install_info') - f = open(info_path, 'w+') - info_data = yaml.safe_dump(info, f) - f.close() - except: - return False - return True - - -def remove_role(role_name, options): - """ - Removes the specified role from the roles path. There is a - sanity check to make sure there's a meta/main.yml file at this - path so the user doesn't blow away random directories - """ - if get_role_metadata(role_name, options): - role_path = get_role_path(role_name, options) - shutil.rmtree(role_path) - return True - else: - return False - -def fetch_role(role_name, target, role_data, options): - """ - Downloads the archived role from github to a temp location, extracts - it, and then copies the extracted role to the role library path. - """ - - # first grab the file and save it to a temp location - if '://' in role_name: - archive_url = role_name - else: - archive_url = 'https://github.com/%s/%s/archive/%s.tar.gz' % (role_data["github_user"], role_data["github_repo"], target) - print "- downloading role from %s" % archive_url - - try: - url_file = urllib2.urlopen(archive_url) - temp_file = tempfile.NamedTemporaryFile(delete=False) - data = url_file.read() - while data: - temp_file.write(data) - data = url_file.read() - temp_file.close() - return temp_file.name - except Exception, e: - # TODO: better urllib2 error handling for error - # messages that are more exact - print "- error: failed to download the file." - return False - -def install_role(role_name, role_version, role_filename, options): - # the file is a tar, so open it that way and extract it - # to the specified (or default) roles directory - - if not tarfile.is_tarfile(role_filename): - print "- error: the file downloaded was not a tar.gz" - return False - else: - if role_filename.endswith('.gz'): - role_tar_file = tarfile.open(role_filename, "r:gz") - else: - role_tar_file = tarfile.open(role_filename, "r") - # verify the role's meta file - meta_file = None - members = role_tar_file.getmembers() - # next find the metadata file - for member in members: - if "/meta/main.yml" in member.name: - meta_file = member - break - if not meta_file: - print "- error: this role does not appear to have a meta/main.yml file." - return False - else: - try: - meta_file_data = yaml.safe_load(role_tar_file.extractfile(meta_file)) - except: - print "- error: this role does not appear to have a valid meta/main.yml file." - return False - - # we strip off the top-level directory for all of the files contained within - # the tar file here, since the default is 'github_repo-target', and change it - # to the specified role's name - role_path = os.path.join(get_opt(options, 'roles_path'), role_name) - role_path = os.path.expanduser(role_path) - print "- extracting %s to %s" % (role_name, role_path) - try: - if os.path.exists(role_path): - if not os.path.isdir(role_path): - print "- error: the specified roles path exists and is not a directory." - return False - elif not get_opt(options, "force", False): - print "- error: the specified role %s appears to already exist. Use --force to replace it." % role_name - return False - else: - # using --force, remove the old path - if not remove_role(role_name, options): - print "- error: %s doesn't appear to contain a role." % role_path - print " please remove this directory manually if you really want to put the role here." - return False - else: - os.makedirs(role_path) - - # now we do the actual extraction to the role_path - for member in members: - # we only extract files, and remove any relative path - # bits that might be in the file for security purposes - # and drop the leading directory, as mentioned above - if member.isreg(): - parts = member.name.split("/")[1:] - final_parts = [] - for part in parts: - if part != '..' and '~' not in part and '$' not in part: - final_parts.append(part) - member.name = os.path.join(*final_parts) - role_tar_file.extract(member, role_path) - - # write out the install info file for later use - write_galaxy_install_info(role_name, role_version, options) - except OSError, e: - print "- error: you do not have permission to modify files in %s" % role_path - return False - - # return the parsed yaml metadata - print "- %s was installed successfully" % role_name - return meta_file_data - -#------------------------------------------------------------------------------------- -# Action functions -#------------------------------------------------------------------------------------- - -def execute_init(args, options, parser): - """ - Executes the init action, which creates the skeleton framework - of a role that complies with the galaxy metadata format. - """ - - init_path = get_opt(options, 'init_path', './') - api_server = get_opt(options, "api_server", "galaxy.ansible.com") - force = get_opt(options, 'force', False) - offline = get_opt(options, 'offline', False) - - if not offline: - api_config = api_get_config(api_server) - if not api_config: - print "- the API server (%s) is not responding, please try again later." % api_server - sys.exit(1) - - try: - role_name = args.pop(0).strip() - if role_name == "": - raise Exception("") - role_path = os.path.join(init_path, role_name) - if os.path.exists(role_path): - if os.path.isfile(role_path): - print "- the path %s already exists, but is a file - aborting" % role_path - sys.exit(1) - elif not force: - print "- the directory %s already exists." % role_path - print " you can use --force to re-initialize this directory,\n" + \ - " however it will reset any main.yml files that may have\n" + \ - " been modified there already." - sys.exit(1) - except Exception, e: - parser.print_help() - print "- no role name specified for init" - sys.exit(1) - - ROLE_DIRS = ('defaults','files','handlers','meta','tasks','templates','vars') - - # create the default README.md - if not os.path.exists(role_path): - os.makedirs(role_path) - readme_path = os.path.join(role_path, "README.md") - f = open(readme_path, "wb") - f.write(default_readme_template) - f.close - - for dir in ROLE_DIRS: - dir_path = os.path.join(init_path, role_name, dir) - main_yml_path = os.path.join(dir_path, 'main.yml') - # create the directory if it doesn't exist already - if not os.path.exists(dir_path): - os.makedirs(dir_path) - - # now create the main.yml file for that directory - if dir == "meta": - # create a skeleton meta/main.yml with a valid galaxy_info - # datastructure in place, plus with all of the available - # tags/platforms included (but commented out) and the - # dependencies section - platforms = [] - if not offline: - platforms = api_get_list(api_server, "platforms") or [] - categories = [] - if not offline: - categories = api_get_list(api_server, "categories") or [] - - # group the list of platforms from the api based - # on their names, with the release field being - # appended to a list of versions - platform_groups = defaultdict(list) - for platform in platforms: - platform_groups[platform['name']].append(platform['release']) - platform_groups[platform['name']].sort() - - inject = dict( - author = 'your name', - company = 'your company (optional)', - license = 'license (GPLv2, CC-BY, etc)', - min_ansible_version = '1.2', - platforms = platform_groups, - categories = categories, - ) - rendered_meta = Environment().from_string(default_meta_template).render(inject) - f = open(main_yml_path, 'w') - f.write(rendered_meta) - f.close() - pass - elif dir not in ('files','templates'): - # just write a (mostly) empty YAML file for main.yml - f = open(main_yml_path, 'w') - f.write('---\n# %s file for %s\n' % (dir,role_name)) - f.close() - print "- %s was created successfully" % role_name - -def execute_info(args, options, parser): - """ - Executes the info action. This action prints out detailed - information about an installed role as well as info available - from the galaxy API. - """ - - pass - -def execute_install(args, options, parser): - """ - Executes the installation action. The args list contains the - roles to be installed, unless -f was specified. The list of roles - can be a name (which will be downloaded via the galaxy API and github), - or it can be a local .tar.gz file. - """ - - role_file = get_opt(options, "role_file", None) - api_server = get_opt(options, "api_server", "galaxy.ansible.com") - no_deps = get_opt(options, "no_deps", False) - roles_path = get_opt(options, "roles_path") - - if len(args) == 0 and not role_file: - # the user needs to specify one of either --role-file - # or specify a single user/role name - parser.print_help() - print "- you must specify a user/role name or a roles file" - sys.exit() - elif len(args) == 1 and role_file: - # using a role file is mutually exclusive of specifying - # the role name on the command line - parser.print_help() - print "- please specify a user/role name, or a roles file, but not both" - sys.exit(1) - - roles_done = [] - if role_file: - f = open(role_file, 'r') - if role_file.endswith('.yaml') or role_file.endswith('.yml'): - roles_left = map(ansible.utils.role_yaml_parse, yaml.safe_load(f)) - else: - # roles listed in a file, one per line - roles_left = map(ansible.utils.role_spec_parse, f.readlines()) - f.close() - else: - # roles were specified directly, so we'll just go out grab them - # (and their dependencies, unless the user doesn't want us to). - roles_left = map(ansible.utils.role_spec_parse, args) - - while len(roles_left) > 0: - # query the galaxy API for the role data - role_data = None - role = roles_left.pop(0) - role_src = role.get("src") - role_scm = role.get("scm") - role_path = role.get("path") - - if role_path: - options.roles_path = role_path - else: - options.roles_path = roles_path - - if os.path.isfile(role_src): - # installing a local tar.gz - tmp_file = role_src - else: - if role_scm: - # create tar file from scm url - tmp_file = scm_archive_role(role_scm, role_src, role.get("version"), role.get("name")) - elif '://' in role_src: - # just download a URL - version will probably be in the URL - tmp_file = fetch_role(role_src, None, None, options) - else: - # installing from galaxy - api_config = api_get_config(api_server) - if not api_config: - print "- the API server (%s) is not responding, please try again later." % api_server - sys.exit(1) - - role_data = api_lookup_role_by_name(api_server, role_src) - if not role_data: - print "- sorry, %s was not found on %s." % (role_src, api_server) - exit_without_ignore(options) - continue - - role_versions = api_fetch_role_related(api_server, 'versions', role_data['id']) - if "version" not in role or role['version'] == '': - # convert the version names to LooseVersion objects - # and sort them to get the latest version. If there - # are no versions in the list, we'll grab the head - # of the master branch - if len(role_versions) > 0: - loose_versions = [LooseVersion(a.get('name',None)) for a in role_versions] - loose_versions.sort() - role["version"] = str(loose_versions[-1]) - else: - role["version"] = 'master' - elif role['version'] != 'master': - if role_versions and role["version"] not in [a.get('name', None) for a in role_versions]: - print 'role is %s' % role - print "- the specified version (%s) was not found in the list of available versions (%s)." % (role['version'], role_versions) - exit_without_ignore(options) - continue - - # download the role. if --no-deps was specified, we stop here, - # otherwise we recursively grab roles and all of their deps. - tmp_file = fetch_role(role_src, role["version"], role_data, options) - installed = False - if tmp_file: - installed = install_role(role.get("name"), role.get("version"), tmp_file, options) - # we're done with the temp file, clean it up - if tmp_file != role_src: - os.unlink(tmp_file) - # install dependencies, if we want them - if not no_deps and installed: - if not role_data: - role_data = get_role_metadata(role.get("name"), options) - role_dependencies = role_data['dependencies'] - else: - role_dependencies = role_data['summary_fields']['dependencies'] # api_fetch_role_related(api_server, 'dependencies', role_data['id']) - for dep in role_dependencies: - if isinstance(dep, basestring): - dep = ansible.utils.role_spec_parse(dep) - else: - dep = ansible.utils.role_yaml_parse(dep) - if not get_role_metadata(dep["name"], options): - if dep not in roles_left: - print '- adding dependency: %s' % dep["name"] - roles_left.append(dep) - else: - print '- dependency %s already pending installation.' % dep["name"] - else: - print '- dependency %s is already installed, skipping.' % dep["name"] - if not tmp_file or not installed: - print "- %s was NOT installed successfully." % role.get("name") - exit_without_ignore(options) - sys.exit(0) - -def execute_remove(args, options, parser): - """ - Executes the remove action. The args list contains the list - of roles to be removed. This list can contain more than one role. - """ - - if len(args) == 0: - parser.print_help() - print '- you must specify at least one role to remove.' - sys.exit() - - for role in args: - if get_role_metadata(role, options): - if remove_role(role, options): - print '- successfully removed %s' % role - else: - print "- failed to remove role: %s" % role - else: - print '- %s is not installed, skipping.' % role - sys.exit(0) - -def execute_list(args, options, parser): - """ - Executes the list action. The args list can contain zero - or one role. If one is specified, only that role will be - shown, otherwise all roles in the specified directory will - be shown. - """ - - if len(args) > 1: - print "- please specify only one role to list, or specify no roles to see a full list" - sys.exit(1) - - if len(args) == 1: - # show only the request role, if it exists - role_name = args[0] - metadata = get_role_metadata(role_name, options) - if metadata: - install_info = get_galaxy_install_info(role_name, options) - version = None - if install_info: - version = install_info.get("version", None) - if not version: - version = "(unknown version)" - # show some more info about single roles here - print "- %s, %s" % (role_name, version) - else: - print "- the role %s was not found" % role_name - else: - # show all valid roles in the roles_path directory - roles_path = get_opt(options, 'roles_path') - roles_path = os.path.expanduser(roles_path) - if not os.path.exists(roles_path): - parser.print_help() - print "- the path %s does not exist. Please specify a valid path with --roles-path" % roles_path - sys.exit(1) - elif not os.path.isdir(roles_path): - print "- %s exists, but it is not a directory. Please specify a valid path with --roles-path" % roles_path - parser.print_help() - sys.exit(1) - path_files = os.listdir(roles_path) - for path_file in path_files: - if get_role_metadata(path_file, options): - install_info = get_galaxy_install_info(path_file, options) - version = None - if install_info: - version = install_info.get("version", None) - if not version: - version = "(unknown version)" - print "- %s, %s" % (path_file, version) - sys.exit(0) - -#------------------------------------------------------------------------------------- -# The main entry point -#------------------------------------------------------------------------------------- - -def main(): - # parse the CLI options - action = get_action(sys.argv) - parser = build_option_parser(action) - (options, args) = parser.parse_args() - - # execute the desired action - if 1: #try: - fn = globals()["execute_%s" % action] - fn(args, options, parser) - #except KeyError, e: - # print "- error: %s is not a valid action. Valid actions are: %s" % (action, ", ".join(VALID_ACTIONS)) - # sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/bin/ansible-galaxy b/bin/ansible-galaxy new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/bin/ansible-galaxy @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/bin/ansible-playbook b/bin/ansible-playbook deleted file mode 100755 index 96e87de3eb..0000000000 --- a/bin/ansible-playbook +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env python -# (C) 2012, Michael DeHaan, - -# This file is part of Ansible -# -# Ansible 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 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . - -####################################################### - -#__requires__ = ['ansible'] -#import pkg_resources - -import sys -import os -import stat - -# Augment PYTHONPATH to find Python modules relative to this file path -# This is so that we can find the modules when running from a local checkout -# installed as editable with `pip install -e ...` or `python setup.py develop` -local_module_path = os.path.abspath( - os.path.join(os.path.dirname(__file__), '..', 'lib') -) -sys.path.append(local_module_path) - -import ansible.playbook -import ansible.constants as C -import ansible.utils.template -from ansible import errors -from ansible import callbacks -from ansible import utils -from ansible.color import ANSIBLE_COLOR, stringc -from ansible.callbacks import display - -def colorize(lead, num, color): - """ Print 'lead' = 'num' in 'color' """ - if num != 0 and ANSIBLE_COLOR and color is not None: - return "%s%s%-15s" % (stringc(lead, color), stringc("=", color), stringc(str(num), color)) - else: - return "%s=%-4s" % (lead, str(num)) - -def hostcolor(host, stats, color=True): - if ANSIBLE_COLOR and color: - if stats['failures'] != 0 or stats['unreachable'] != 0: - return "%-37s" % stringc(host, 'red') - elif stats['changed'] != 0: - return "%-37s" % stringc(host, 'yellow') - else: - return "%-37s" % stringc(host, 'green') - return "%-26s" % host - - -def main(args): - ''' run ansible-playbook operations ''' - - # create parser for CLI options - parser = utils.base_parser( - constants=C, - usage = "%prog playbook.yml", - connect_opts=True, - runas_opts=True, - subset_opts=True, - check_opts=True, - diff_opts=True - ) - #parser.add_option('--vault-password', dest="vault_password", - # help="password for vault encrypted files") - parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append", - help="set additional variables as key=value or YAML/JSON", default=[]) - parser.add_option('-t', '--tags', dest='tags', default='all', - help="only run plays and tasks tagged with these values") - parser.add_option('--skip-tags', dest='skip_tags', - help="only run plays and tasks whose tags do not match these values") - parser.add_option('--syntax-check', dest='syntax', action='store_true', - help="perform a syntax check on the playbook, but do not execute it") - parser.add_option('--list-tasks', dest='listtasks', action='store_true', - help="list all tasks that would be executed") - parser.add_option('--step', dest='step', action='store_true', - help="one-step-at-a-time: confirm each task before running") - parser.add_option('--start-at-task', dest='start_at', - help="start the playbook at the task matching this name") - parser.add_option('--force-handlers', dest='force_handlers', action='store_true', - help="run handlers even if a task fails") - parser.add_option('--flush-cache', dest='flush_cache', action='store_true', - help="clear the fact cache") - - options, args = parser.parse_args(args) - - if len(args) == 0: - parser.print_help(file=sys.stderr) - return 1 - - # su and sudo command line arguments need to be mutually exclusive - if (options.su or options.su_user or options.ask_su_pass) and \ - (options.sudo or options.sudo_user or options.ask_sudo_pass): - parser.error("Sudo arguments ('--sudo', '--sudo-user', and '--ask-sudo-pass') " - "and su arguments ('-su', '--su-user', and '--ask-su-pass') are " - "mutually exclusive") - - if (options.ask_vault_pass and options.vault_password_file): - parser.error("--ask-vault-pass and --vault-password-file are mutually exclusive") - - sshpass = None - sudopass = None - su_pass = None - vault_pass = None - - options.ask_vault_pass = options.ask_vault_pass or C.DEFAULT_ASK_VAULT_PASS - - if options.listhosts or options.syntax or options.listtasks: - (_, _, _, vault_pass) = utils.ask_passwords(ask_vault_pass=options.ask_vault_pass) - else: - options.ask_pass = options.ask_pass or C.DEFAULT_ASK_PASS - # Never ask for an SSH password when we run with local connection - if options.connection == "local": - options.ask_pass = False - options.ask_sudo_pass = options.ask_sudo_pass or C.DEFAULT_ASK_SUDO_PASS - options.ask_su_pass = options.ask_su_pass or C.DEFAULT_ASK_SU_PASS - (sshpass, sudopass, su_pass, vault_pass) = utils.ask_passwords(ask_pass=options.ask_pass, ask_sudo_pass=options.ask_sudo_pass, ask_su_pass=options.ask_su_pass, ask_vault_pass=options.ask_vault_pass) - options.sudo_user = options.sudo_user or C.DEFAULT_SUDO_USER - options.su_user = options.su_user or C.DEFAULT_SU_USER - - # read vault_pass from a file - if not options.ask_vault_pass and options.vault_password_file: - vault_pass = utils.read_vault_file(options.vault_password_file) - - extra_vars = {} - for extra_vars_opt in options.extra_vars: - if extra_vars_opt.startswith("@"): - # Argument is a YAML file (JSON is a subset of YAML) - extra_vars = utils.combine_vars(extra_vars, utils.parse_yaml_from_file(extra_vars_opt[1:], vault_password=vault_pass)) - elif extra_vars_opt and extra_vars_opt[0] in '[{': - # Arguments as YAML - extra_vars = utils.combine_vars(extra_vars, utils.parse_yaml(extra_vars_opt)) - else: - # Arguments as Key-value - extra_vars = utils.combine_vars(extra_vars, utils.parse_kv(extra_vars_opt)) - - only_tags = options.tags.split(",") - skip_tags = options.skip_tags - if options.skip_tags is not None: - skip_tags = options.skip_tags.split(",") - - for playbook in args: - if not os.path.exists(playbook): - raise errors.AnsibleError("the playbook: %s could not be found" % playbook) - if not (os.path.isfile(playbook) or stat.S_ISFIFO(os.stat(playbook).st_mode)): - raise errors.AnsibleError("the playbook: %s does not appear to be a file" % playbook) - - inventory = ansible.inventory.Inventory(options.inventory, vault_password=vault_pass) - inventory.subset(options.subset) - if len(inventory.list_hosts()) == 0: - raise errors.AnsibleError("provided hosts list is empty") - - # run all playbooks specified on the command line - for playbook in args: - - stats = callbacks.AggregateStats() - playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY) - if options.step: - playbook_cb.step = options.step - if options.start_at: - playbook_cb.start_at = options.start_at - runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY) - - pb = ansible.playbook.PlayBook( - playbook=playbook, - module_path=options.module_path, - inventory=inventory, - forks=options.forks, - remote_user=options.remote_user, - remote_pass=sshpass, - callbacks=playbook_cb, - runner_callbacks=runner_cb, - stats=stats, - timeout=options.timeout, - transport=options.connection, - sudo=options.sudo, - sudo_user=options.sudo_user, - sudo_pass=sudopass, - extra_vars=extra_vars, - private_key_file=options.private_key_file, - only_tags=only_tags, - skip_tags=skip_tags, - check=options.check, - diff=options.diff, - su=options.su, - su_pass=su_pass, - su_user=options.su_user, - vault_password=vault_pass, - force_handlers=options.force_handlers - ) - - if options.flush_cache: - display(callbacks.banner("FLUSHING FACT CACHE")) - pb.SETUP_CACHE.flush() - - if options.listhosts or options.listtasks or options.syntax: - print '' - print 'playbook: %s' % playbook - print '' - playnum = 0 - for (play_ds, play_basedir) in zip(pb.playbook, pb.play_basedirs): - playnum += 1 - play = ansible.playbook.Play(pb, play_ds, play_basedir, - vault_password=pb.vault_password) - label = play.name - hosts = pb.inventory.list_hosts(play.hosts) - - # Filter all tasks by given tags - if pb.only_tags != 'all': - if options.subset and not hosts: - continue - matched_tags, unmatched_tags = play.compare_tags(pb.only_tags) - - # Remove skipped tasks - matched_tags = matched_tags - set(pb.skip_tags) - - unmatched_tags.discard('all') - unknown_tags = ((set(pb.only_tags) | set(pb.skip_tags)) - - (matched_tags | unmatched_tags)) - - if unknown_tags: - continue - - if options.listhosts: - print ' play #%d (%s): host count=%d' % (playnum, label, len(hosts)) - for host in hosts: - print ' %s' % host - - if options.listtasks: - print ' play #%d (%s):' % (playnum, label) - - for task in play.tasks(): - if (set(task.tags).intersection(pb.only_tags) and not - set(task.tags).intersection(pb.skip_tags)): - if getattr(task, 'name', None) is not None: - # meta tasks have no names - print ' %s' % task.name - if options.listhosts or options.listtasks: - print '' - continue - - if options.syntax: - # if we've not exited by now then we are fine. - print 'Playbook Syntax is fine' - return 0 - - failed_hosts = [] - unreachable_hosts = [] - - try: - - pb.run() - - hosts = sorted(pb.stats.processed.keys()) - display(callbacks.banner("PLAY RECAP")) - playbook_cb.on_stats(pb.stats) - - for h in hosts: - t = pb.stats.summarize(h) - if t['failures'] > 0: - failed_hosts.append(h) - if t['unreachable'] > 0: - unreachable_hosts.append(h) - - retries = failed_hosts + unreachable_hosts - - if len(retries) > 0: - filename = pb.generate_retry_inventory(retries) - if filename: - display(" to retry, use: --limit @%s\n" % filename) - - for h in hosts: - t = pb.stats.summarize(h) - - display("%s : %s %s %s %s" % ( - hostcolor(h, t), - colorize('ok', t['ok'], 'green'), - colorize('changed', t['changed'], 'yellow'), - colorize('unreachable', t['unreachable'], 'red'), - colorize('failed', t['failures'], 'red')), - screen_only=True - ) - - display("%s : %s %s %s %s" % ( - hostcolor(h, t, False), - colorize('ok', t['ok'], None), - colorize('changed', t['changed'], None), - colorize('unreachable', t['unreachable'], None), - colorize('failed', t['failures'], None)), - log_only=True - ) - - - print "" - if len(failed_hosts) > 0: - return 2 - if len(unreachable_hosts) > 0: - return 3 - - except errors.AnsibleError, e: - display("ERROR: %s" % e, color='red') - return 1 - - return 0 - - -if __name__ == "__main__": - display(" ", log_only=True) - display(" ".join(sys.argv), log_only=True) - display(" ", log_only=True) - try: - sys.exit(main(sys.argv[1:])) - except errors.AnsibleError, e: - display("ERROR: %s" % e, color='red', stderr=True) - sys.exit(1) - except KeyboardInterrupt, ke: - display("ERROR: interrupted", color='red', stderr=True) - sys.exit(1) diff --git a/bin/ansible-playbook b/bin/ansible-playbook new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/bin/ansible-playbook @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/bin/ansible-pull b/bin/ansible-pull deleted file mode 100755 index 4f4da24d83..0000000000 --- a/bin/ansible-pull +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python - -# (c) 2012, Stephen Fromm -# -# Ansible 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 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . -# -# ansible-pull is a script that runs ansible in local mode -# after checking out a playbooks directory from source repo. There is an -# example playbook to bootstrap this script in the examples/ dir which -# installs ansible and sets it up to run on cron. - -# usage: -# ansible-pull -d /var/lib/ansible \ -# -U http://example.net/content.git [-C production] \ -# [path/playbook.yml] -# -# the -d and -U arguments are required; the -C argument is optional. -# -# ansible-pull accepts an optional argument to specify a playbook -# location underneath the workdir and then searches the source repo -# for playbooks in the following order, stopping at the first match: -# -# 1. $workdir/path/playbook.yml, if specified -# 2. $workdir/$fqdn.yml -# 3. $workdir/$hostname.yml -# 4. $workdir/local.yml -# -# the source repo must contain at least one of these playbooks. - -import os -import shutil -import subprocess -import sys -import datetime -import socket -import random -import time -from ansible import utils -from ansible.utils import cmd_functions -from ansible import errors -from ansible import inventory - -DEFAULT_REPO_TYPE = 'git' -DEFAULT_PLAYBOOK = 'local.yml' -PLAYBOOK_ERRORS = {1: 'File does not exist', - 2: 'File is not readable'} - -VERBOSITY=0 - -def increment_debug(option, opt, value, parser): - global VERBOSITY - VERBOSITY += 1 - -def try_playbook(path): - if not os.path.exists(path): - return 1 - if not os.access(path, os.R_OK): - return 2 - return 0 - - -def select_playbook(path, args): - playbook = None - if len(args) > 0 and args[0] is not None: - playbook = "%s/%s" % (path, args[0]) - rc = try_playbook(playbook) - if rc != 0: - print >>sys.stderr, "%s: %s" % (playbook, PLAYBOOK_ERRORS[rc]) - return None - return playbook - else: - fqdn = socket.getfqdn() - hostpb = "%s/%s.yml" % (path, fqdn) - shorthostpb = "%s/%s.yml" % (path, fqdn.split('.')[0]) - localpb = "%s/%s" % (path, DEFAULT_PLAYBOOK) - errors = [] - for pb in [hostpb, shorthostpb, localpb]: - rc = try_playbook(pb) - if rc == 0: - playbook = pb - break - else: - errors.append("%s: %s" % (pb, PLAYBOOK_ERRORS[rc])) - if playbook is None: - print >>sys.stderr, "\n".join(errors) - return playbook - - -def main(args): - """ Set up and run a local playbook """ - usage = "%prog [options] [playbook.yml]" - parser = utils.SortedOptParser(usage=usage) - parser.add_option('--purge', default=False, action='store_true', - help='purge checkout after playbook run') - parser.add_option('-o', '--only-if-changed', dest='ifchanged', default=False, action='store_true', - help='only run the playbook if the repository has been updated') - parser.add_option('-s', '--sleep', dest='sleep', default=None, - help='sleep for random interval (between 0 and n number of seconds) before starting. this is a useful way to disperse git requests') - parser.add_option('-f', '--force', dest='force', default=False, - action='store_true', - help='run the playbook even if the repository could ' - 'not be updated') - parser.add_option('-d', '--directory', dest='dest', default=None, - help='directory to checkout repository to') - #parser.add_option('-l', '--live', default=True, action='store_live', - # help='Print the ansible-playbook output while running') - parser.add_option('-U', '--url', dest='url', default=None, - help='URL of the playbook repository') - parser.add_option('-C', '--checkout', dest='checkout', - help='branch/tag/commit to checkout. ' - 'Defaults to behavior of repository module.') - parser.add_option('-i', '--inventory-file', dest='inventory', - help="location of the inventory host file") - parser.add_option('-e', '--extra-vars', dest="extra_vars", action="append", - help="set additional variables as key=value or YAML/JSON", default=[]) - parser.add_option('-v', '--verbose', default=False, action="callback", - callback=increment_debug, - help='Pass -vvvv to ansible-playbook') - parser.add_option('-m', '--module-name', dest='module_name', - default=DEFAULT_REPO_TYPE, - help='Module name used to check out repository. ' - 'Default is %s.' % DEFAULT_REPO_TYPE) - parser.add_option('--vault-password-file', dest='vault_password_file', - help="vault password file") - parser.add_option('-K', '--ask-sudo-pass', default=False, dest='ask_sudo_pass', action='store_true', - help='ask for sudo password') - options, args = parser.parse_args(args) - - hostname = socket.getfqdn() - if not options.dest: - # use a hostname dependent directory, in case of $HOME on nfs - options.dest = utils.prepare_writeable_dir('~/.ansible/pull/%s' % hostname) - - options.dest = os.path.abspath(options.dest) - - if not options.url: - parser.error("URL for repository not specified, use -h for help") - return 1 - - now = datetime.datetime.now() - print >>sys.stderr, now.strftime("Starting ansible-pull at %F %T") - - # Attempt to use the inventory passed in as an argument - # It might not yet have been downloaded so use localhost if note - if not options.inventory or not os.path.exists(options.inventory): - inv_opts = 'localhost,' - else: - inv_opts = options.inventory - limit_opts = 'localhost:%s:127.0.0.1' % hostname - repo_opts = "name=%s dest=%s" % (options.url, options.dest) - - if VERBOSITY == 0: - base_opts = '-c local --limit "%s"' % limit_opts - elif VERBOSITY > 0: - debug_level = ''.join([ "v" for x in range(0, VERBOSITY) ]) - base_opts = '-%s -c local --limit "%s"' % (debug_level, limit_opts) - - if options.checkout: - repo_opts += ' version=%s' % options.checkout - path = utils.plugins.module_finder.find_plugin(options.module_name) - if path is None: - sys.stderr.write("module '%s' not found.\n" % options.module_name) - return 1 - cmd = 'ansible localhost -i "%s" %s -m %s -a "%s"' % ( - inv_opts, base_opts, options.module_name, repo_opts - ) - - if options.sleep: - try: - secs = random.randint(0,int(options.sleep)); - except ValueError: - parser.error("%s is not a number." % options.sleep) - return 1 - - print >>sys.stderr, "Sleeping for %d seconds..." % secs - time.sleep(secs); - - - # RUN THe CHECKOUT COMMAND - rc, out, err = cmd_functions.run_cmd(cmd, live=True) - - if rc != 0: - if options.force: - print "Unable to update repository. Continuing with (forced) run of playbook." - else: - return rc - elif options.ifchanged and '"changed": true' not in out: - print "Repository has not changed, quitting." - return 0 - - playbook = select_playbook(options.dest, args) - - if playbook is None: - print >>sys.stderr, "Could not find a playbook to run." - return 1 - - cmd = 'ansible-playbook %s %s' % (base_opts, playbook) - if options.vault_password_file: - cmd += " --vault-password-file=%s" % options.vault_password_file - if options.inventory: - cmd += ' -i "%s"' % options.inventory - for ev in options.extra_vars: - cmd += ' -e "%s"' % ev - if options.ask_sudo_pass: - cmd += ' -K' - os.chdir(options.dest) - - # RUN THE PLAYBOOK COMMAND - rc, out, err = cmd_functions.run_cmd(cmd, live=True) - - if options.purge: - os.chdir('/') - try: - shutil.rmtree(options.dest) - except Exception, e: - print >>sys.stderr, "Failed to remove %s: %s" % (options.dest, str(e)) - - return rc - -if __name__ == '__main__': - try: - sys.exit(main(sys.argv[1:])) - except KeyboardInterrupt, e: - print >>sys.stderr, "Exit on user request.\n" - sys.exit(1) diff --git a/bin/ansible-pull b/bin/ansible-pull new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/bin/ansible-pull @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/bin/ansible-vault b/bin/ansible-vault deleted file mode 100755 index 10da6a9ea7..0000000000 --- a/bin/ansible-vault +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python - -# (c) 2014, James Tanner -# -# Ansible 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 3 of the License, or -# (at your option) any later version. -# -# Ansible 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 Ansible. If not, see . -# -# ansible-vault is a script that encrypts/decrypts YAML files. See -# http://docs.ansible.com/playbooks_vault.html for more details. - -#__requires__ = ['ansible'] -#import pkg_resources - -import os -import sys -import traceback - -import ansible.constants as C - -from ansible import utils -from ansible import errors -from ansible.utils.vault import VaultEditor - -from optparse import OptionParser - -#------------------------------------------------------------------------------------- -# Utility functions for parsing actions/options -#------------------------------------------------------------------------------------- - -VALID_ACTIONS = ("create", "decrypt", "edit", "encrypt", "rekey", "view") - -def build_option_parser(action): - """ - Builds an option parser object based on the action - the user wants to execute. - """ - - usage = "usage: %%prog [%s] [--help] [options] file_name" % "|".join(VALID_ACTIONS) - epilog = "\nSee '%s --help' for more information on a specific command.\n\n" % os.path.basename(sys.argv[0]) - OptionParser.format_epilog = lambda self, formatter: self.epilog - parser = OptionParser(usage=usage, epilog=epilog) - - if not action: - parser.print_help() - sys.exit() - - # options for all actions - #parser.add_option('-c', '--cipher', dest='cipher', default="AES256", help="cipher to use") - parser.add_option('--debug', dest='debug', action="store_true", help="debug") - parser.add_option('--vault-password-file', dest='password_file', - help="vault password file", default=C.DEFAULT_VAULT_PASSWORD_FILE) - - # options specific to actions - if action == "create": - parser.set_usage("usage: %prog create [options] file_name") - elif action == "decrypt": - parser.set_usage("usage: %prog decrypt [options] file_name") - elif action == "edit": - parser.set_usage("usage: %prog edit [options] file_name") - elif action == "view": - parser.set_usage("usage: %prog view [options] file_name") - elif action == "encrypt": - parser.set_usage("usage: %prog encrypt [options] file_name") - elif action == "rekey": - parser.set_usage("usage: %prog rekey [options] file_name") - - # done, return the parser - return parser - -def get_action(args): - """ - Get the action the user wants to execute from the - sys argv list. - """ - for i in range(0,len(args)): - arg = args[i] - if arg in VALID_ACTIONS: - del args[i] - return arg - return None - -def get_opt(options, k, defval=""): - """ - Returns an option from an Optparse values instance. - """ - try: - data = getattr(options, k) - except: - return defval - if k == "roles_path": - if os.pathsep in data: - data = data.split(os.pathsep)[0] - return data - -#------------------------------------------------------------------------------------- -# Command functions -#------------------------------------------------------------------------------------- - -def execute_create(args, options, parser): - if len(args) > 1: - raise errors.AnsibleError("'create' does not accept more than one filename") - - if not options.password_file: - password, new_password = utils.ask_vault_passwords(ask_vault_pass=True, confirm_vault=True) - else: - password = utils.read_vault_file(options.password_file) - - cipher = 'AES256' - if hasattr(options, 'cipher'): - cipher = options.cipher - - this_editor = VaultEditor(cipher, password, args[0]) - this_editor.create_file() - -def execute_decrypt(args, options, parser): - - if not options.password_file: - password, new_password = utils.ask_vault_passwords(ask_vault_pass=True) - else: - password = utils.read_vault_file(options.password_file) - - cipher = 'AES256' - if hasattr(options, 'cipher'): - cipher = options.cipher - - for f in args: - this_editor = VaultEditor(cipher, password, f) - this_editor.decrypt_file() - - print "Decryption successful" - -def execute_edit(args, options, parser): - - if len(args) > 1: - raise errors.AnsibleError("edit does not accept more than one filename") - - if not options.password_file: - password, new_password = utils.ask_vault_passwords(ask_vault_pass=True) - else: - password = utils.read_vault_file(options.password_file) - - cipher = None - - for f in args: - this_editor = VaultEditor(cipher, password, f) - this_editor.edit_file() - -def execute_view(args, options, parser): - - if len(args) > 1: - raise errors.AnsibleError("view does not accept more than one filename") - - if not options.password_file: - password, new_password = utils.ask_vault_passwords(ask_vault_pass=True) - else: - password = utils.read_vault_file(options.password_file) - - cipher = None - - for f in args: - this_editor = VaultEditor(cipher, password, f) - this_editor.view_file() - -def execute_encrypt(args, options, parser): - - if not options.password_file: - password, new_password = utils.ask_vault_passwords(ask_vault_pass=True, confirm_vault=True) - else: - password = utils.read_vault_file(options.password_file) - - cipher = 'AES256' - if hasattr(options, 'cipher'): - cipher = options.cipher - - for f in args: - this_editor = VaultEditor(cipher, password, f) - this_editor.encrypt_file() - - print "Encryption successful" - -def execute_rekey(args, options, parser): - - if not options.password_file: - password, __ = utils.ask_vault_passwords(ask_vault_pass=True) - else: - password = utils.read_vault_file(options.password_file) - - __, new_password = utils.ask_vault_passwords(ask_vault_pass=False, ask_new_vault_pass=True, confirm_new=True) - - cipher = None - for f in args: - this_editor = VaultEditor(cipher, password, f) - this_editor.rekey_file(new_password) - - print "Rekey successful" - -#------------------------------------------------------------------------------------- -# MAIN -#------------------------------------------------------------------------------------- - -def main(): - - action = get_action(sys.argv) - parser = build_option_parser(action) - (options, args) = parser.parse_args() - - if not len(args): - raise errors.AnsibleError( - "The '%s' command requires a filename as the first argument" % action - ) - - # execute the desired action - try: - fn = globals()["execute_%s" % action] - fn(args, options, parser) - except Exception, err: - if options.debug: - print traceback.format_exc() - print "ERROR:",err - sys.exit(1) - -if __name__ == "__main__": - main() diff --git a/bin/ansible-vault b/bin/ansible-vault new file mode 120000 index 0000000000..cabb1f519a --- /dev/null +++ b/bin/ansible-vault @@ -0,0 +1 @@ +ansible \ No newline at end of file diff --git a/contrib/README.md b/contrib/README.md new file mode 100644 index 0000000000..dab0da4ba7 --- /dev/null +++ b/contrib/README.md @@ -0,0 +1,17 @@ +inventory +========= + +Inventory scripts allow you to store your hosts, groups, and variables in any way +you like. Examples include discovering inventory from EC2 or pulling it from +Cobbler. These could also be used to interface with LDAP or database. + +chmod +x an inventory plugin and either name it /etc/ansible/hosts or use ansible +with -i to designate the path to the script. You might also need to copy a configuration +file with the same name and/or set environment variables, the scripts or configuration +files have more details. + +contributions welcome +===================== + +Send in pull requests to add plugins of your own. The sky is the limit! + diff --git a/plugins/inventory/abiquo.ini b/contrib/inventory/abiquo.ini similarity index 100% rename from plugins/inventory/abiquo.ini rename to contrib/inventory/abiquo.ini diff --git a/plugins/inventory/abiquo.py b/contrib/inventory/abiquo.py similarity index 100% rename from plugins/inventory/abiquo.py rename to contrib/inventory/abiquo.py diff --git a/plugins/inventory/apache-libcloud.py b/contrib/inventory/apache-libcloud.py similarity index 96% rename from plugins/inventory/apache-libcloud.py rename to contrib/inventory/apache-libcloud.py index 95804095da..151daeefe0 100755 --- a/plugins/inventory/apache-libcloud.py +++ b/contrib/inventory/apache-libcloud.py @@ -222,12 +222,17 @@ class LibcloudInventory(object): self.push(self.inventory, self.to_safe('type_' + node.instance_type), dest) ''' # Inventory: Group by key pair - if node.extra['keyname']: - self.push(self.inventory, self.to_safe('key_' + node.extra['keyname']), dest) + if node.extra['key_name']: + self.push(self.inventory, self.to_safe('key_' + node.extra['key_name']), dest) # Inventory: Group by security group, quick thing to handle single sg - if node.extra['securitygroup']: - self.push(self.inventory, self.to_safe('sg_' + node.extra['securitygroup'][0]), dest) + if node.extra['security_group']: + self.push(self.inventory, self.to_safe('sg_' + node.extra['security_group'][0]), dest) + + # Inventory: Group by tag + if node.extra['tags']: + for tagkey in node.extra['tags'].keys(): + self.push(self.inventory, self.to_safe('tag_' + tagkey + '_' + node.extra['tags'][tagkey]), dest) def get_host_info(self): ''' diff --git a/contrib/inventory/cloudstack.ini b/contrib/inventory/cloudstack.ini new file mode 100644 index 0000000000..43777b593f --- /dev/null +++ b/contrib/inventory/cloudstack.ini @@ -0,0 +1,5 @@ +[cloudstack] +#endpoint = https://api.exoscale.ch/compute +endpoint = https://cloud.example.com/client/api +key = cloudstack api key +secret = cloudstack api secret diff --git a/contrib/inventory/cloudstack.py b/contrib/inventory/cloudstack.py new file mode 100755 index 0000000000..426cf163fd --- /dev/null +++ b/contrib/inventory/cloudstack.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# (c) 2015, René Moser +# +# This file is part of Ansible, +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +###################################################################### + +""" +Ansible CloudStack external inventory script. +============================================= + +Generates Ansible inventory from CloudStack. Configuration is read from +'cloudstack.ini'. If you need to pass the project, write a simple wrapper +script, e.g. project_cloudstack.sh: + + #!/bin/bash + cloudstack.py --project $@ + + +When run against a specific host, this script returns the following attributes +based on the data obtained from CloudStack API: + + "web01": { + "cpu_number": 2, + "nic": [ + { + "ip": "10.102.76.98", + "mac": "02:00:50:99:00:01", + "type": "Isolated", + "netmask": "255.255.255.0", + "gateway": "10.102.76.1" + }, + { + "ip": "10.102.138.63", + "mac": "06:b7:5a:00:14:84", + "type": "Shared", + "netmask": "255.255.255.0", + "gateway": "10.102.138.1" + } + ], + "default_ip": "10.102.76.98", + "zone": "ZUERICH", + "created": "2014-07-02T07:53:50+0200", + "hypervisor": "VMware", + "memory": 2048, + "state": "Running", + "tags": [], + "cpu_speed": 1800, + "affinity_group": [], + "service_offering": "Small", + "cpu_used": "62%" + } + + +usage: cloudstack.py [--list] [--host HOST] [--project PROJECT] +""" + +import os +import sys +import argparse + +try: + import json +except: + import simplejson as json + + +try: + from cs import CloudStack, CloudStackException, read_config +except ImportError: + print >> sys.stderr, "Error: CloudStack library must be installed: pip install cs." + sys.exit(1) + + +class CloudStackInventory(object): + def __init__(self): + + parser = argparse.ArgumentParser() + parser.add_argument('--host') + parser.add_argument('--list', action='store_true') + parser.add_argument('--project') + + options = parser.parse_args() + try: + self.cs = CloudStack(**read_config()) + except CloudStackException, e: + print >> sys.stderr, "Error: Could not connect to CloudStack API" + + project_id = '' + if options.project: + project_id = self.get_project_id(options.project) + + if options.host: + data = self.get_host(options.host) + print json.dumps(data, indent=2) + + elif options.list: + data = self.get_list() + print json.dumps(data, indent=2) + else: + print >> sys.stderr, "usage: --list | --host [--project ]" + sys.exit(1) + + + def get_project_id(self, project): + projects = self.cs.listProjects() + if projects: + for p in projects['project']: + if p['name'] == project or p['id'] == project: + return p['id'] + print >> sys.stderr, "Error: Project %s not found." % project + sys.exit(1) + + + def get_host(self, name, project_id=''): + hosts = self.cs.listVirtualMachines(projectid=project_id) + data = {} + if not hosts: + return data + for host in hosts['virtualmachine']: + host_name = host['displayname'] + if name == host_name: + data['zone'] = host['zonename'] + if 'group' in host: + data['group'] = host['group'] + data['state'] = host['state'] + data['service_offering'] = host['serviceofferingname'] + data['affinity_group'] = host['affinitygroup'] + data['security_group'] = host['securitygroup'] + data['cpu_number'] = host['cpunumber'] + data['cpu_speed'] = host['cpuspeed'] + if 'cpuused' in host: + data['cpu_used'] = host['cpuused'] + data['memory'] = host['memory'] + data['tags'] = host['tags'] + data['hypervisor'] = host['hypervisor'] + data['created'] = host['created'] + data['nic'] = [] + for nic in host['nic']: + data['nic'].append({ + 'ip': nic['ipaddress'], + 'mac': nic['macaddress'], + 'netmask': nic['netmask'], + 'gateway': nic['gateway'], + 'type': nic['type'], + }) + if nic['isdefault']: + data['default_ip'] = nic['ipaddress'] + break; + return data + + + def get_list(self, project_id=''): + data = { + 'all': { + 'hosts': [], + }, + '_meta': { + 'hostvars': {}, + }, + } + + groups = self.cs.listInstanceGroups(projectid=project_id) + if groups: + for group in groups['instancegroup']: + group_name = group['name'] + if group_name and not group_name in data: + data[group_name] = { + 'hosts': [] + } + + hosts = self.cs.listVirtualMachines(projectid=project_id) + if not hosts: + return data + for host in hosts['virtualmachine']: + host_name = host['displayname'] + data['all']['hosts'].append(host_name) + data['_meta']['hostvars'][host_name] = {} + data['_meta']['hostvars'][host_name]['zone'] = host['zonename'] + if 'group' in host: + data['_meta']['hostvars'][host_name]['group'] = host['group'] + data['_meta']['hostvars'][host_name]['state'] = host['state'] + data['_meta']['hostvars'][host_name]['service_offering'] = host['serviceofferingname'] + data['_meta']['hostvars'][host_name]['affinity_group'] = host['affinitygroup'] + data['_meta']['hostvars'][host_name]['security_group'] = host['securitygroup'] + data['_meta']['hostvars'][host_name]['cpu_number'] = host['cpunumber'] + data['_meta']['hostvars'][host_name]['cpu_speed'] = host['cpuspeed'] + if 'cpuused' in host: + data['_meta']['hostvars'][host_name]['cpu_used'] = host['cpuused'] + data['_meta']['hostvars'][host_name]['created'] = host['created'] + data['_meta']['hostvars'][host_name]['memory'] = host['memory'] + data['_meta']['hostvars'][host_name]['tags'] = host['tags'] + data['_meta']['hostvars'][host_name]['hypervisor'] = host['hypervisor'] + data['_meta']['hostvars'][host_name]['created'] = host['created'] + data['_meta']['hostvars'][host_name]['nic'] = [] + for nic in host['nic']: + data['_meta']['hostvars'][host_name]['nic'].append({ + 'ip': nic['ipaddress'], + 'mac': nic['macaddress'], + 'netmask': nic['netmask'], + 'gateway': nic['gateway'], + 'type': nic['type'], + }) + if nic['isdefault']: + data['_meta']['hostvars'][host_name]['default_ip'] = nic['ipaddress'] + + group_name = '' + if 'group' in host: + group_name = host['group'] + + if group_name and group_name in data: + data[group_name]['hosts'].append(host_name) + return data + + +if __name__ == '__main__': + CloudStackInventory() diff --git a/plugins/inventory/cobbler.ini b/contrib/inventory/cobbler.ini similarity index 100% rename from plugins/inventory/cobbler.ini rename to contrib/inventory/cobbler.ini diff --git a/plugins/inventory/cobbler.py b/contrib/inventory/cobbler.py similarity index 99% rename from plugins/inventory/cobbler.py rename to contrib/inventory/cobbler.py index f607da2dc2..f352c8cf9d 100755 --- a/plugins/inventory/cobbler.py +++ b/contrib/inventory/cobbler.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python """ Cobbler external inventory script diff --git a/plugins/inventory/collins.ini b/contrib/inventory/collins.ini similarity index 100% rename from plugins/inventory/collins.ini rename to contrib/inventory/collins.ini diff --git a/plugins/inventory/collins.py b/contrib/inventory/collins.py similarity index 97% rename from plugins/inventory/collins.py rename to contrib/inventory/collins.py index 1561fcf5c7..775beecb1d 100755 --- a/plugins/inventory/collins.py +++ b/contrib/inventory/collins.py @@ -41,7 +41,9 @@ that will be used instead of the configured values if they are set: If errors are encountered during operation, this script will return an exit code of 255; otherwise, it will return an exit code of 0. -Tested against Ansible 1.6.6 and Collins 1.2.4. +Collins attributes are accessible as variables in ansible via the COLLINS['attribute_name']. + +Tested against Ansible 1.8.2 and Collins 1.3.0. """ # (c) 2014, Steve Salevan @@ -305,6 +307,8 @@ class CollinsInventory(object): else: ip_index = self.ip_address_index + asset['COLLINS'] = {} + # Attempts to locate the asset's primary identifier (hostname or IP address), # which will be used to index the asset throughout the Ansible inventory. if self.prefer_hostnames and self._asset_has_attribute(asset, 'HOSTNAME'): @@ -332,8 +336,8 @@ class CollinsInventory(object): if 'ATTRIBS' in asset: for attrib_block in asset['ATTRIBS'].keys(): for attrib in asset['ATTRIBS'][attrib_block].keys(): - attrib_key = self.to_safe( - '%s-%s' % (attrib, asset['ATTRIBS'][attrib_block][attrib])) + asset['COLLINS'][attrib] = asset['ATTRIBS'][attrib_block][attrib] + attrib_key = self.to_safe('%s-%s' % (attrib, asset['ATTRIBS'][attrib_block][attrib])) self.push(self.inventory, attrib_key, asset_identifier) # Indexes asset by all built-in Collins attributes. diff --git a/contrib/inventory/consul.ini b/contrib/inventory/consul.ini new file mode 100644 index 0000000000..8761494ab9 --- /dev/null +++ b/contrib/inventory/consul.ini @@ -0,0 +1,37 @@ +# Ansible Consul external inventory script settings. + +[consul] + +# restrict included nodes to those from this datacenter +#datacenter = nyc1 + +# url of the the consul cluster to query +#url = http://demo.consul.io +url = http://localhost:8500 + +# suffix added to each service to create a group name e.g Service of 'redis' and +# a suffix of '_servers' will add each address to the group name 'redis_servers' +servers_suffix = _servers + +# if specified then the inventory will generate domain names that will resolve +# via Consul's inbuilt DNS. +#domain=consul + +# make groups from service tags. the name of the group is derived from the +# service name and the tag name e.g. a service named nginx with tags ['master', 'v1'] +# will create groups nginx_master and nginx_v1 +tags = true + +# looks up the node name at the given path for a list of groups to which the +# node should be added. +kv_groups=ansible/groups + +# looks up the node name at the given path for a json dictionary of metadata that +# should be attached as metadata for the node +kv_metadata=ansible/metadata + +# looks up the health of each service and adds the node to 'up' and 'down' groups +# based on the service availibility +availability = true +available_suffix = _up +unavailable_suffix = _down diff --git a/contrib/inventory/consul_io.py b/contrib/inventory/consul_io.py new file mode 100755 index 0000000000..4e40f96873 --- /dev/null +++ b/contrib/inventory/consul_io.py @@ -0,0 +1,428 @@ +#!/usr/bin/env python + +# +# (c) 2015, Steve Gargan +# +# This file is part of Ansible, +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +###################################################################### + +''' +Consul.io inventory script (http://consul.io) +====================================== + +Generates Ansible inventory from nodes in a Consul cluster. This script will +group nodes by: + - datacenter, + - registered service + - service tags + - service status + - values from the k/v store + +This script can be run with the switches +--list as expected groups all the nodes in all datacenters +--datacenter, to restrict the nodes to a single datacenter +--host to restrict the inventory to a single named node. (requires datacenter config) + +The configuration for this plugin is read from a consul.ini file located in the +same directory as this inventory script. All config options in the config file +are optional except the host and port, which must point to a valid agent or +server running the http api. For more information on enabling the endpoint see. + +http://www.consul.io/docs/agent/options.html + +Other options include: + +'datacenter': + +which restricts the included nodes to those from the given datacenter + +'domain': + +if specified then the inventory will generate domain names that will resolve +via Consul's inbuilt DNS. The name is derived from the node name, datacenter +and domain .node... Note that you will need to +have consul hooked into your DNS server for these to resolve. See the consul +DNS docs for more info. + +which restricts the included nodes to those from the given datacenter + +'servers_suffix': + +defining the a suffix to add to the service name when creating the service +group. e.g Service name of 'redis' and a suffix of '_servers' will add +each nodes address to the group name 'redis_servers'. No suffix is added +if this is not set + +'tags': + +boolean flag defining if service tags should be used to create Inventory +groups e.g. an nginx service with the tags ['master', 'v1'] will create +groups nginx_master and nginx_v1 to which the node running the service +will be added. No tag groups are created if this is missing. + +'token': + +ACL token to use to authorize access to the key value store. May be required +to retrieve the kv_groups and kv_metadata based on your consul configuration. + +'kv_groups': + +This is used to lookup groups for a node in the key value store. It specifies a +path to which each discovered node's name will be added to create a key to query +the key/value store. There it expects to find a comma separated list of group +names to which the node should be added e.g. if the inventory contains +'nyc-web-1' and kv_groups = 'ansible/groups' then the key +'v1/kv/ansible/groups/nyc-web-1' will be queried for a group list. If this query + returned 'test,honeypot' then the node address to both groups. + +'kv_metadata': + +kv_metadata is used to lookup metadata for each discovered node. Like kv_groups +above it is used to build a path to lookup in the kv store where it expects to +find a json dictionary of metadata entries. If found, each key/value pair in the +dictionary is added to the metadata for the node. + +'availability': + +if true then availability groups will be created for each service. The node will +be added to one of the groups based on the health status of the service. The +group name is derived from the service name and the configurable availability +suffixes + +'available_suffix': + +suffix that should be appended to the service availability groups for available +services e.g. if the suffix is '_up' and the service is nginx, then nodes with +healthy nginx services will be added to the nginix_up group. Defaults to +'_available' + +'unavailable_suffix': + +as above but for unhealthy services, defaults to '_unavailable' + +Note that if the inventory discovers an 'ssh' service running on a node it will +register the port as ansible_ssh_port in the node's metadata and this port will +be used to access the machine. +``` + +''' + +import os +import re +import argparse +from time import time +import sys +import ConfigParser +import urllib, urllib2, base64 + +try: + import json +except ImportError: + import simplejson as json + +try: + import consul +except ImportError, e: + print """failed=True msg='python-consul required for this module. see + http://python-consul.readthedocs.org/en/latest/#installation'""" + sys.exit(1) + + + +class ConsulInventory(object): + + def __init__(self): + ''' Create an inventory based on the catalog of nodes and services + registered in a consul cluster''' + self.node_metadata = {} + self.nodes = {} + self.nodes_by_service = {} + self.nodes_by_tag = {} + self.nodes_by_datacenter = {} + self.nodes_by_kv = {} + self.nodes_by_availability = {} + self.current_dc = None + + config = ConsulConfig() + self.config = config + + self.consul_api = config.get_consul_api() + + if config.has_config('datacenter'): + if config.has_config('host'): + self.load_data_for_node(config.host, config.datacenter) + else: + self.load_data_for_datacenter(config.datacenter) + else: + self.load_all_data_consul() + + self.combine_all_results() + print json.dumps(self.inventory, sort_keys=True, indent=2) + + def load_all_data_consul(self): + ''' cycle through each of the datacenters in the consul catalog and process + the nodes in each ''' + self.datacenters = self.consul_api.catalog.datacenters() + for datacenter in self.datacenters: + self.current_dc = datacenter + self.load_data_for_datacenter(datacenter) + + + def load_availability_groups(self, node, datacenter): + '''check the health of each service on a node and add add the node to either + an 'available' or 'unavailable' grouping. The suffix for each group can be + controlled from the config''' + if self.config.has_config('availability'): + for service_name, service in node['Services'].iteritems(): + for node in self.consul_api.health.service(service_name)[1]: + for check in node['Checks']: + if check['ServiceName'] == service_name: + ok = 'passing' == check['Status'] + if ok: + suffix = self.config.get_availability_suffix( + 'available_suffix', '_available') + else: + suffix = self.config.get_availability_suffix( + 'unavailable_suffix', '_unavailable') + self.add_node_to_map(self.nodes_by_availability, + service_name + suffix, node['Node']) + + + def load_data_for_datacenter(self, datacenter): + '''processes all the nodes in a particular datacenter''' + index, nodes = self.consul_api.catalog.nodes(dc=datacenter) + for node in nodes: + self.add_node_to_map(self.nodes_by_datacenter, datacenter, node) + self.load_data_for_node(node['Node'], datacenter) + + def load_data_for_node(self, node, datacenter): + '''loads the data for a sinle node adding it to various groups based on + metadata retrieved from the kv store and service availability''' + + index, node_data = self.consul_api.catalog.node(node, dc=datacenter) + node = node_data['Node'] + self.add_node_to_map(self.nodes, 'all', node) + self.add_metadata(node_data, "consul_datacenter", datacenter) + self.add_metadata(node_data, "consul_nodename", node['Node']) + + self.load_groups_from_kv(node_data) + self.load_node_metadata_from_kv(node_data) + self.load_availability_groups(node_data, datacenter) + + for name, service in node_data['Services'].items(): + self.load_data_from_service(name, service, node_data) + + def load_node_metadata_from_kv(self, node_data): + ''' load the json dict at the metadata path defined by the kv_metadata value + and the node name add each entry in the dictionary to the the node's + metadata ''' + node = node_data['Node'] + if self.config.has_config('kv_metadata'): + key = "%s/%s/%s" % (self.config.kv_metadata, self.current_dc, node['Node']) + index, metadata = self.consul_api.kv.get(key) + if metadata and metadata['Value']: + try: + metadata = json.loads(metadata['Value']) + for k,v in metadata.items(): + self.add_metadata(node_data, k, v) + except: + pass + + def load_groups_from_kv(self, node_data): + ''' load the comma separated list of groups at the path defined by the + kv_groups config value and the node name add the node address to each + group found ''' + node = node_data['Node'] + if self.config.has_config('kv_groups'): + key = "%s/%s/%s" % (self.config.kv_groups, self.current_dc, node['Node']) + index, groups = self.consul_api.kv.get(key) + if groups and groups['Value']: + for group in groups['Value'].split(','): + self.add_node_to_map(self.nodes_by_kv, group.strip(), node) + + def load_data_from_service(self, service_name, service, node_data): + '''process a service registered on a node, adding the node to a group with + the service name. Each service tag is extracted and the node is added to a + tag grouping also''' + self.add_metadata(node_data, "consul_services", service_name, True) + + if self.is_service("ssh", service_name): + self.add_metadata(node_data, "ansible_ssh_port", service['Port']) + + if self.config.has_config('servers_suffix'): + service_name = service_name + self.config.servers_suffix + + self.add_node_to_map(self.nodes_by_service, service_name, node_data['Node']) + self.extract_groups_from_tags(service_name, service, node_data) + + def is_service(self, target, name): + return name and (name.lower() == target.lower()) + + def extract_groups_from_tags(self, service_name, service, node_data): + '''iterates each service tag and adds the node to groups derived from the + service and tag names e.g. nginx_master''' + if self.config.has_config('tags') and service['Tags']: + tags = service['Tags'] + self.add_metadata(node_data, "consul_%s_tags" % service_name, tags) + for tag in service['Tags']: + tagname = service_name +'_'+tag + self.add_node_to_map(self.nodes_by_tag, tagname, node_data['Node']) + + def combine_all_results(self): + '''prunes and sorts all groupings for combination into the final map''' + self.inventory = {"_meta": { "hostvars" : self.node_metadata}} + groupings = [self.nodes, self.nodes_by_datacenter, self.nodes_by_service, + self.nodes_by_tag, self.nodes_by_kv, self.nodes_by_availability] + for grouping in groupings: + for name, addresses in grouping.items(): + self.inventory[name] = sorted(list(set(addresses))) + + def add_metadata(self, node_data, key, value, is_list = False): + ''' Pushed an element onto a metadata dict for the node, creating + the dict if it doesn't exist ''' + key = self.to_safe(key) + node = self.get_inventory_name(node_data['Node']) + + if node in self.node_metadata: + metadata = self.node_metadata[node] + else: + metadata = {} + self.node_metadata[node] = metadata + if is_list: + self.push(metadata, key, value) + else: + metadata[key] = value + + def get_inventory_name(self, node_data): + '''return the ip or a node name that can be looked up in consul's dns''' + domain = self.config.domain + if domain: + node_name = node_data['Node'] + if self.current_dc: + return '%s.node.%s.%s' % ( node_name, self.current_dc, domain) + else: + return '%s.node.%s' % ( node_name, domain) + else: + return node_data['Address'] + + def add_node_to_map(self, map, name, node): + self.push(map, name, self.get_inventory_name(node)) + + + def push(self, my_dict, key, element): + ''' Pushed an element onto an array that may not have been defined in the + dict ''' + key = self.to_safe(key) + if key in my_dict: + my_dict[key].append(element) + else: + my_dict[key] = [element] + + def to_safe(self, word): + ''' Converts 'bad' characters in a string to underscores so they can be used + as Ansible groups ''' + return re.sub('[^A-Za-z0-9\-\.]', '_', word) + + def sanitize_dict(self, d): + + new_dict = {} + for k, v in d.items(): + if v != None: + new_dict[self.to_safe(str(k))] = self.to_safe(str(v)) + return new_dict + + def sanitize_list(self, seq): + new_seq = [] + for d in seq: + new_seq.append(self.sanitize_dict(d)) + return new_seq + + +class ConsulConfig(dict): + + def __init__(self): + self.read_settings() + self.read_cli_args() + + def has_config(self, name): + if hasattr(self, name): + return getattr(self, name) + else: + return False + + def read_settings(self): + ''' Reads the settings from the consul.ini file ''' + config = ConfigParser.SafeConfigParser() + config.read(os.path.dirname(os.path.realpath(__file__)) + '/consul.ini') + + config_options = ['host', 'token', 'datacenter', 'servers_suffix', + 'tags', 'kv_metadata', 'kv_groups', 'availability', + 'unavailable_suffix', 'available_suffix', 'url', + 'domain'] + for option in config_options: + value = None + if config.has_option('consul', option): + value = config.get('consul', option) + setattr(self, option, value) + + def read_cli_args(self): + ''' Command line argument processing ''' + parser = argparse.ArgumentParser(description= + 'Produce an Ansible Inventory file based nodes in a Consul cluster') + + parser.add_argument('--list', action='store_true', + help='Get all inventory variables from all nodes in the consul cluster') + parser.add_argument('--host', action='store', + help='Get all inventory variables about a specific consul node, \ + requires datacenter set in consul.ini.') + parser.add_argument('--datacenter', action='store', + help='Get all inventory about a specific consul datacenter') + + args = parser.parse_args() + arg_names = ['host', 'datacenter'] + + for arg in arg_names: + if getattr(args, arg): + setattr(self, arg, getattr(args, arg)) + + def get_availability_suffix(self, suffix, default): + if self.has_config(suffix): + return self.has_config(suffix) + return default + + + def get_consul_api(self): + '''get an instance of the api based on the supplied configuration''' + host = 'localhost' + port = 8500 + token = None + + if hasattr(self, 'url'): + from urlparse import urlparse + o = urlparse(self.url) + if o.hostname: + host = o.hostname + if o.port: + port = o.port + + if hasattr(self, 'token'): + token = self.token + if not token: + token = 'anonymous' + return consul.Consul(host=host, port=port, token=token) + +ConsulInventory() diff --git a/plugins/inventory/digital_ocean.ini b/contrib/inventory/digital_ocean.ini similarity index 66% rename from plugins/inventory/digital_ocean.ini rename to contrib/inventory/digital_ocean.ini index c4e3fe2141..021899731c 100644 --- a/plugins/inventory/digital_ocean.ini +++ b/contrib/inventory/digital_ocean.ini @@ -3,12 +3,11 @@ [digital_ocean] -# The module needs your DigitalOcean Client ID and API Key. -# These may also be specified on the command line via --client-id and --api-key -# or via the environment variables DO_CLIENT_ID and DO_API_KEY +# The module needs your DigitalOcean API Token. +# It may also be specified on the command line via --api-token +# or via the environment variables DO_API_TOKEN or DO_API_KEY # -#client_id = abcdefg123456 -#api_key = 123456abcdefg +#api_token = 123456abcdefg # API calls to DigitalOcean may be slow. For this reason, we cache the results diff --git a/plugins/inventory/digital_ocean.py b/contrib/inventory/digital_ocean.py similarity index 55% rename from plugins/inventory/digital_ocean.py rename to contrib/inventory/digital_ocean.py index 1c3eccd21e..4f312e7c24 100755 --- a/plugins/inventory/digital_ocean.py +++ b/contrib/inventory/digital_ocean.py @@ -24,12 +24,12 @@ found. You can force this script to use the cache with --force-cache. Configuration is read from `digital_ocean.ini`, then from environment variables, then and command-line arguments. -Most notably, the DigitalOcean Client ID and API Key must be specified. They -can be specified in the INI file or with the following environment variables: - export DO_CLIENT_ID='DO123' DO_API_KEY='abc123' +Most notably, the DigitalOcean API Token must be specified. It can be specified +in the INI file or with the following environment variables: + export DO_API_TOKEN='abc123' or + export DO_API_KEY='abc123' -Alternatively, they can be passed on the command-line with --client-id and ---api-key. +Alternatively, it can be passed on the command-line with --api-token. If you specify DigitalOcean credentials in the INI file, a handy way to get them into your environment (e.g., to use the digital_ocean module) @@ -43,25 +43,30 @@ The following groups are generated from --list: - image_ID - image_NAME - distro_NAME (distribution NAME from image) - - region_ID - region_NAME - - size_ID - size_NAME - status_STATUS When run against a specific host, this script returns the following variables: + - do_backup_ids - do_created_at - - do_distroy + - do_disk + - do_features - list - do_id - - do_image - - do_image_id + - do_image - object - do_ip_address + - do_kernel - object + - do_locked + - de_memory - do_name - - do_region - - do_region_id - - do_size - - do_size_id + - do_networks - object + - do_next_backup_window + - do_region - object + - do_size - object + - do_size_slug + - do_snapshot_ids - list - do_status + - do_vcpus ----- ``` @@ -70,8 +75,9 @@ usage: digital_ocean.py [-h] [--list] [--host HOST] [--all] [--ssh-keys] [--domains] [--pretty] [--cache-path CACHE_PATH] [--cache-max_age CACHE_MAX_AGE] - [--refresh-cache] [--client-id CLIENT_ID] - [--api-key API_KEY] + [--force-cache] + [--refresh-cache] + [--api-token API_TOKEN] Produce an Ansible Inventory file based on DigitalOcean credentials @@ -93,12 +99,11 @@ optional arguments: Path to the cache files (default: .) --cache-max_age CACHE_MAX_AGE Maximum age of the cached items (default: 0) + --force-cache Only use data from the cache --refresh-cache Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files) - --client-id CLIENT_ID, -c CLIENT_ID - DigitalOcean Client ID - --api-key API_KEY, -a API_KEY - DigitalOcean API Key + --api-token API_TOKEN, -a API_TOKEN + DigitalOcean API Token ``` ''' @@ -106,7 +111,7 @@ optional arguments: # (c) 2013, Evan Wies # # Inspired by the EC2 inventory plugin: -# https://github.com/ansible/ansible/blob/devel/plugins/inventory/ec2.py +# https://github.com/ansible/ansible/blob/devel/contrib/inventory/ec2.py # # This file is part of Ansible, # @@ -157,7 +162,6 @@ class DigitalOceanInventory(object): # DigitalOceanInventory data self.data = {} # All DigitalOcean data self.inventory = {} # Ansible Inventory - self.index = {} # Various indices of Droplet metadata # Define defaults self.cache_path = '.' @@ -169,49 +173,61 @@ class DigitalOceanInventory(object): self.read_cli_args() # Verify credentials were set - if not hasattr(self, 'client_id') or not hasattr(self, 'api_key'): - print '''Could not find values for DigitalOcean client_id and api_key. -They must be specified via either ini file, command line argument (--client-id and --api-key), -or environment variables (DO_CLIENT_ID and DO_API_KEY)''' + if not hasattr(self, 'api_token'): + print '''Could not find values for DigitalOcean api_token. +They must be specified via either ini file, command line argument (--api-token), +or environment variables (DO_API_TOKEN)''' sys.exit(-1) # env command, show DigitalOcean credentials if self.args.env: - print "DO_CLIENT_ID=%s DO_API_KEY=%s" % (self.client_id, self.api_key) + print "DO_API_TOKEN=%s" % self.api_token sys.exit(0) # Manage cache self.cache_filename = self.cache_path + "/ansible-digital_ocean.cache" self.cache_refreshed = False - if not self.args.force_cache and self.args.refresh_cache or not self.is_cache_valid(): - self.load_all_data_from_digital_ocean() - else: + if self.is_cache_valid: self.load_from_cache() if len(self.data) == 0: if self.args.force_cache: print '''Cache is empty and --force-cache was specified''' sys.exit(-1) - self.load_all_data_from_digital_ocean() - else: - # We always get fresh droplets for --list, --host, --all, and --droplets - # unless --force-cache is specified - if not self.args.force_cache and ( - self.args.list or self.args.host or self.args.all or self.args.droplets): - self.load_droplets_from_digital_ocean() + + self.manager = DoManager(None, self.api_token, api_version=2) # Pick the json_data to print based on the CLI command - if self.args.droplets: json_data = { 'droplets': self.data['droplets'] } - elif self.args.regions: json_data = { 'regions': self.data['regions'] } - elif self.args.images: json_data = { 'images': self.data['images'] } - elif self.args.sizes: json_data = { 'sizes': self.data['sizes'] } - elif self.args.ssh_keys: json_data = { 'ssh_keys': self.data['ssh_keys'] } - elif self.args.domains: json_data = { 'domains': self.data['domains'] } - elif self.args.all: json_data = self.data - - elif self.args.host: json_data = self.load_droplet_variables_for_host() + if self.args.droplets: + self.load_from_digital_ocean('droplets') + json_data = {'droplets': self.data['droplets']} + elif self.args.regions: + self.load_from_digital_ocean('regions') + json_data = {'regions': self.data['regions']} + elif self.args.images: + self.load_from_digital_ocean('images') + json_data = {'images': self.data['images']} + elif self.args.sizes: + self.load_from_digital_ocean('sizes') + json_data = {'sizes': self.data['sizes']} + elif self.args.ssh_keys: + self.load_from_digital_ocean('ssh_keys') + json_data = {'ssh_keys': self.data['ssh_keys']} + elif self.args.domains: + self.load_from_digital_ocean('domains') + json_data = {'domains': self.data['domains']} + elif self.args.all: + self.load_from_digital_ocean() + json_data = self.data + elif self.args.host: + json_data = self.load_droplet_variables_for_host() else: # '--list' this is last to make it default - json_data = self.inventory + self.load_from_digital_ocean('droplets') + self.build_inventory() + json_data = self.inventory + + if self.cache_refreshed: + self.write_to_cache() if self.args.pretty: print json.dumps(json_data, sort_keys=True, indent=2) @@ -230,10 +246,8 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' config.read(os.path.dirname(os.path.realpath(__file__)) + '/digital_ocean.ini') # Credentials - if config.has_option('digital_ocean', 'client_id'): - self.client_id = config.get('digital_ocean', 'client_id') - if config.has_option('digital_ocean', 'api_key'): - self.api_key = config.get('digital_ocean', 'api_key') + if config.has_option('digital_ocean', 'api_token'): + self.api_token = config.get('digital_ocean', 'api_token') # Cache related if config.has_option('digital_ocean', 'cache_path'): @@ -245,8 +259,10 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' def read_environment(self): ''' Reads the settings from environment variables ''' # Setup credentials - if os.getenv("DO_CLIENT_ID"): self.client_id = os.getenv("DO_CLIENT_ID") - if os.getenv("DO_API_KEY"): self.api_key = os.getenv("DO_API_KEY") + if os.getenv("DO_API_TOKEN"): + self.api_token = os.getenv("DO_API_TOKEN") + if os.getenv("DO_API_KEY"): + self.api_token = os.getenv("DO_API_KEY") def read_cli_args(self): @@ -269,70 +285,57 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' parser.add_argument('--cache-path', action='store', help='Path to the cache files (default: .)') parser.add_argument('--cache-max_age', action='store', help='Maximum age of the cached items (default: 0)') parser.add_argument('--force-cache', action='store_true', default=False, help='Only use data from the cache') - parser.add_argument('--refresh-cache','-r', action='store_true', default=False, help='Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files)') + parser.add_argument('--refresh-cache','-r', action='store_true', default=False, + help='Force refresh of cache by making API requests to DigitalOcean (default: False - use cache files)') - parser.add_argument('--env','-e', action='store_true', help='Display DO_CLIENT_ID and DO_API_KEY') - parser.add_argument('--client-id','-c', action='store', help='DigitalOcean Client ID') - parser.add_argument('--api-key','-a', action='store', help='DigitalOcean API Key') + parser.add_argument('--env','-e', action='store_true', help='Display DO_API_TOKEN') + parser.add_argument('--api-token','-a', action='store', help='DigitalOcean API Token') self.args = parser.parse_args() - if self.args.client_id: self.client_id = self.args.client_id - if self.args.api_key: self.api_key = self.args.api_key - if self.args.cache_path: self.cache_path = self.args.cache_path - if self.args.cache_max_age: self.cache_max_age = self.args.cache_max_age + if self.args.api_token: + self.api_token = self.args.api_token # Make --list default if none of the other commands are specified - if (not self.args.droplets and not self.args.regions and not self.args.images and - not self.args.sizes and not self.args.ssh_keys and not self.args.domains and - not self.args.all and not self.args.host): - self.args.list = True + if (not self.args.droplets and not self.args.regions and + not self.args.images and not self.args.sizes and + not self.args.ssh_keys and not self.args.domains and + not self.args.all and not self.args.host): + self.args.list = True ########################################################################### # Data Management ########################################################################### - def load_all_data_from_digital_ocean(self): - ''' Use dopy to get all the information from DigitalOcean and save data in cache files ''' - manager = DoManager(self.client_id, self.api_key) + def load_from_digital_ocean(self, resource=None): + '''Get JSON from DigitalOcean API''' + if self.args.force_cache: + return + # We always get fresh droplets + if self.is_cache_valid() and not (resource=='droplets' or resource is None): + return + if self.args.refresh_cache: + resource=None - self.data = {} - self.data['droplets'] = self.sanitize_list(manager.all_active_droplets()) - self.data['regions'] = self.sanitize_list(manager.all_regions()) - self.data['images'] = self.sanitize_list(manager.all_images(filter=None)) - self.data['sizes'] = self.sanitize_list(manager.sizes()) - self.data['ssh_keys'] = self.sanitize_list(manager.all_ssh_keys()) - self.data['domains'] = self.sanitize_list(manager.all_domains()) - - self.index = {} - self.index['region_to_name'] = self.build_index(self.data['regions'], 'id', 'name') - self.index['size_to_name'] = self.build_index(self.data['sizes'], 'id', 'name') - self.index['image_to_name'] = self.build_index(self.data['images'], 'id', 'name') - self.index['image_to_distro'] = self.build_index(self.data['images'], 'id', 'distribution') - self.index['host_to_droplet'] = self.build_index(self.data['droplets'], 'ip_address', 'id', False) - - self.build_inventory() - - self.write_to_cache() - - - def load_droplets_from_digital_ocean(self): - ''' Use dopy to get droplet information from DigitalOcean and save data in cache files ''' - manager = DoManager(self.client_id, self.api_key) - self.data['droplets'] = self.sanitize_list(manager.all_active_droplets()) - self.index['host_to_droplet'] = self.build_index(self.data['droplets'], 'ip_address', 'id', False) - self.build_inventory() - self.write_to_cache() - - - def build_index(self, source_seq, key_from, key_to, use_slug=True): - dest_dict = {} - for item in source_seq: - name = (use_slug and item.has_key('slug')) and item['slug'] or item[key_to] - key = item[key_from] - dest_dict[key] = name - return dest_dict + if resource == 'droplets' or resource is None: + self.data['droplets'] = self.manager.all_active_droplets() + self.cache_refreshed = True + if resource == 'regions' or resource is None: + self.data['regions'] = self.manager.all_regions() + self.cache_refreshed = True + if resource == 'images' or resource is None: + self.data['images'] = self.manager.all_images(filter=None) + self.cache_refreshed = True + if resource == 'sizes' or resource is None: + self.data['sizes'] = self.manager.sizes() + self.cache_refreshed = True + if resource == 'ssh_keys' or resource is None: + self.data['ssh_keys'] = self.manager.all_ssh_keys() + self.cache_refreshed = True + if resource == 'domains' or resource is None: + self.data['domains'] = self.manager.all_domains() + self.cache_refreshed = True def build_inventory(self): @@ -345,70 +348,34 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' self.inventory[droplet['id']] = [dest] self.push(self.inventory, droplet['name'], dest) - self.push(self.inventory, 'region_'+droplet['region_id'], dest) - self.push(self.inventory, 'image_' +droplet['image_id'], dest) - self.push(self.inventory, 'size_' +droplet['size_id'], dest) - self.push(self.inventory, 'status_'+droplet['status'], dest) + self.push(self.inventory, 'region_' + droplet['region']['slug'], dest) + self.push(self.inventory, 'image_' + str(droplet['image']['id']), dest) + self.push(self.inventory, 'size_' + droplet['size']['slug'], dest) - region_name = self.index['region_to_name'].get(droplet['region_id']) - if region_name: - self.push(self.inventory, 'region_'+region_name, dest) + image_slug = droplet['image']['slug'] + if image_slug: + self.push(self.inventory, 'image_' + self.to_safe(image_slug), dest) + else: + image_name = droplet['image']['name'] + if image_name: + self.push(self.inventory, 'image_' + self.to_safe(image_name), dest) - size_name = self.index['size_to_name'].get(droplet['size_id']) - if size_name: - self.push(self.inventory, 'size_'+size_name, dest) - - image_name = self.index['image_to_name'].get(droplet['image_id']) - if image_name: - self.push(self.inventory, 'image_'+image_name, dest) - - distro_name = self.index['image_to_distro'].get(droplet['image_id']) - if distro_name: - self.push(self.inventory, 'distro_'+distro_name, dest) + self.push(self.inventory, 'distro_' + self.to_safe(droplet['image']['distribution']), dest) + self.push(self.inventory, 'status_' + droplet['status'], dest) def load_droplet_variables_for_host(self): '''Generate a JSON response to a --host call''' - host = self.to_safe(str(self.args.host)) + host = int(self.args.host) - if not host in self.index['host_to_droplet']: - # try updating cache - if not self.args.force_cache: - self.load_all_data_from_digital_ocean() - if not host in self.index['host_to_droplet']: - # host might not exist anymore - return {} - - droplet = None - if self.cache_refreshed: - for drop in self.data['droplets']: - if drop['ip_address'] == host: - droplet = self.sanitize_dict(drop) - break - else: - # Cache wasn't refreshed this run, so hit DigitalOcean API - manager = DoManager(self.client_id, self.api_key) - droplet_id = self.index['host_to_droplet'][host] - droplet = self.sanitize_dict(manager.show_droplet(droplet_id)) - - if not droplet: - return {} + droplet = self.manager.show_droplet(host) # Put all the information in a 'do_' namespace info = {} for k, v in droplet.items(): info['do_'+k] = v - # Generate user-friendly variables (i.e. not the ID's) - if droplet.has_key('region_id'): - info['do_region'] = self.index['region_to_name'].get(droplet['region_id']) - if droplet.has_key('size_id'): - info['do_size'] = self.index['size_to_name'].get(droplet['size_id']) - if droplet.has_key('image_id'): - info['do_image'] = self.index['image_to_name'].get(droplet['image_id']) - info['do_distro'] = self.index['image_to_distro'].get(droplet['image_id']) - - return info + return {'droplet': info} @@ -428,19 +395,21 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' def load_from_cache(self): ''' Reads the data from the cache file and assigns it to member variables as Python Objects''' - cache = open(self.cache_filename, 'r') - json_data = cache.read() - cache.close() - data = json.loads(json_data) + try: + cache = open(self.cache_filename, 'r') + json_data = cache.read() + cache.close() + data = json.loads(json_data) + except IOError: + data = {'data': {}, 'inventory': {}} self.data = data['data'] self.inventory = data['inventory'] - self.index = data['index'] def write_to_cache(self): ''' Writes data in JSON format to a file ''' - data = { 'data': self.data, 'index': self.index, 'inventory': self.inventory } + data = { 'data': self.data, 'inventory': self.inventory } json_data = json.dumps(data, sort_keys=True, indent=2) cache = open(self.cache_filename, 'w') @@ -448,7 +417,6 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' cache.close() - ########################################################################### # Utilities ########################################################################### @@ -456,7 +424,7 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' def push(self, my_dict, key, element): ''' Pushed an element onto an array that may not have been defined in the dict ''' if key in my_dict: - my_dict[key].append(element); + my_dict[key].append(element) else: my_dict[key] = [element] @@ -466,21 +434,6 @@ or environment variables (DO_CLIENT_ID and DO_API_KEY)''' return re.sub("[^A-Za-z0-9\-\.]", "_", word) - def sanitize_dict(self, d): - new_dict = {} - for k, v in d.items(): - if v != None: - new_dict[self.to_safe(str(k))] = self.to_safe(str(v)) - return new_dict - - - def sanitize_list(self, seq): - new_seq = [] - for d in seq: - new_seq.append(self.sanitize_dict(d)) - return new_seq - - ########################################################################### # Run the script diff --git a/plugins/inventory/docker.py b/contrib/inventory/docker.py similarity index 100% rename from plugins/inventory/docker.py rename to contrib/inventory/docker.py diff --git a/plugins/inventory/docker.yml b/contrib/inventory/docker.yml similarity index 100% rename from plugins/inventory/docker.yml rename to contrib/inventory/docker.yml diff --git a/plugins/inventory/ec2.ini b/contrib/inventory/ec2.ini similarity index 65% rename from plugins/inventory/ec2.ini rename to contrib/inventory/ec2.ini index c66bf309b1..a1d9b1d805 100644 --- a/plugins/inventory/ec2.ini +++ b/contrib/inventory/ec2.ini @@ -24,14 +24,20 @@ regions_exclude = us-gov-west-1,cn-north-1 # This is the normal destination variable to use. If you are running Ansible # from outside EC2, then 'public_dns_name' makes the most sense. If you are # running Ansible from within EC2, then perhaps you want to use the internal -# address, and should set this to 'private_dns_name'. +# address, and should set this to 'private_dns_name'. The key of an EC2 tag +# may optionally be used; however the boto instance variables hold precedence +# in the event of a collision. destination_variable = public_dns_name # For server inside a VPC, using DNS names may not make sense. When an instance # has 'subnet_id' set, this variable is used. If the subnet is public, setting # this to 'ip_address' will return the public IP address. For instances in a # private subnet, this should be set to 'private_ip_address', and Ansible must -# be run from with EC2. +# be run from within EC2. The key of an EC2 tag may optionally be used; however +# the boto instance variables hold precedence in the event of a collision. +# WARNING: - instances that are in the private vpc, _without_ public ip address +# will not be listed in the inventory until You set: +# vpc_destination_variable = 'private_ip_address' vpc_destination_variable = ip_address # To tag instances on EC2 with the resource records that point to them from @@ -41,6 +47,9 @@ route53 = False # To exclude RDS instances from the inventory, uncomment and set to False. #rds = False +# To exclude ElastiCache instances from the inventory, uncomment and set to False. +#elasticache = False + # Additionally, you can specify the list of zones to exclude looking up in # 'route53_excluded_zones' as a comma-separated list. # route53_excluded_zones = samplezone1.com, samplezone2.com @@ -53,6 +62,18 @@ all_instances = False # 'all_rds_instances' to True return all RDS instances regardless of state. all_rds_instances = False +# By default, only ElastiCache clusters and nodes in the 'available' state +# are returned. Set 'all_elasticache_clusters' and/or 'all_elastic_nodes' +# to True return all ElastiCache clusters and nodes, regardless of state. +# +# Note that all_elasticache_nodes only applies to listed clusters. That means +# if you set all_elastic_clusters to false, no node will be return from +# unavailable clusters, regardless of the state and to what you set for +# all_elasticache_nodes. +all_elasticache_replication_groups = False +all_elasticache_clusters = False +all_elasticache_nodes = False + # API calls to EC2 are slow. For this reason, we cache the results of an API # call. Set this to the path you want cache files to be written to. Two files # will be written to this directory: @@ -68,11 +89,31 @@ cache_max_age = 300 # Organize groups into a nested/hierarchy instead of a flat namespace. nested_groups = False +# The EC2 inventory output can become very large. To manage its size, +# configure which groups should be created. +group_by_instance_id = True +group_by_region = True +group_by_availability_zone = True +group_by_ami_id = True +group_by_instance_type = True +group_by_key_pair = True +group_by_vpc_id = True +group_by_security_group = True +group_by_tag_keys = True +group_by_tag_none = True +group_by_route53_names = True +group_by_rds_engine = True +group_by_rds_parameter_group = True +group_by_elasticache_engine = True +group_by_elasticache_cluster = True +group_by_elasticache_parameter_group = True +group_by_elasticache_replication_group = True + # If you only want to include hosts that match a certain regular expression -# pattern_include = stage-* +# pattern_include = staging-* # If you want to exclude any hosts that match a certain regular expression -# pattern_exclude = stage-* +# pattern_exclude = staging-* # Instance filters can be used to control which instances are retrieved for # inventory. For the full list of possible filters, please read the EC2 API @@ -80,14 +121,14 @@ nested_groups = False # Filters are key/value pairs separated by '=', to list multiple filters use # a list separated by commas. See examples below. -# Retrieve only instances with (key=value) env=stage tag -# instance_filters = tag:env=stage +# Retrieve only instances with (key=value) env=staging tag +# instance_filters = tag:env=staging # Retrieve only instances with role=webservers OR role=dbservers tag # instance_filters = tag:role=webservers,tag:role=dbservers -# Retrieve only t1.micro instances OR instances with tag env=stage -# instance_filters = instance-type=t1.micro,tag:env=stage +# Retrieve only t1.micro instances OR instances with tag env=staging +# instance_filters = instance-type=t1.micro,tag:env=staging # You can use wildcards in filter values also. Below will list instances which # tag Name value matches webservers1* diff --git a/contrib/inventory/ec2.py b/contrib/inventory/ec2.py new file mode 100755 index 0000000000..a8e042e3f4 --- /dev/null +++ b/contrib/inventory/ec2.py @@ -0,0 +1,1237 @@ +#!/usr/bin/env python + +''' +EC2 external inventory script +================================= + +Generates inventory that Ansible can understand by making API request to +AWS EC2 using the Boto library. + +NOTE: This script assumes Ansible is being executed where the environment +variables needed for Boto have already been set: + export AWS_ACCESS_KEY_ID='AK123' + export AWS_SECRET_ACCESS_KEY='abc123' + +This script also assumes there is an ec2.ini file alongside it. To specify a +different path to ec2.ini, define the EC2_INI_PATH environment variable: + + export EC2_INI_PATH=/path/to/my_ec2.ini + +If you're using eucalyptus you need to set the above variables and +you need to define: + + export EC2_URL=http://hostname_of_your_cc:port/services/Eucalyptus + +For more details, see: http://docs.pythonboto.org/en/latest/boto_config_tut.html + +When run against a specific host, this script returns the following variables: + - ec2_ami_launch_index + - ec2_architecture + - ec2_association + - ec2_attachTime + - ec2_attachment + - ec2_attachmentId + - ec2_client_token + - ec2_deleteOnTermination + - ec2_description + - ec2_deviceIndex + - ec2_dns_name + - ec2_eventsSet + - ec2_group_name + - ec2_hypervisor + - ec2_id + - ec2_image_id + - ec2_instanceState + - ec2_instance_type + - ec2_ipOwnerId + - ec2_ip_address + - ec2_item + - ec2_kernel + - ec2_key_name + - ec2_launch_time + - ec2_monitored + - ec2_monitoring + - ec2_networkInterfaceId + - ec2_ownerId + - ec2_persistent + - ec2_placement + - ec2_platform + - ec2_previous_state + - ec2_private_dns_name + - ec2_private_ip_address + - ec2_publicIp + - ec2_public_dns_name + - ec2_ramdisk + - ec2_reason + - ec2_region + - ec2_requester_id + - ec2_root_device_name + - ec2_root_device_type + - ec2_security_group_ids + - ec2_security_group_names + - ec2_shutdown_state + - ec2_sourceDestCheck + - ec2_spot_instance_request_id + - ec2_state + - ec2_state_code + - ec2_state_reason + - ec2_status + - ec2_subnet_id + - ec2_tenancy + - ec2_virtualization_type + - ec2_vpc_id + +These variables are pulled out of a boto.ec2.instance object. There is a lack of +consistency with variable spellings (camelCase and underscores) since this +just loops through all variables the object exposes. It is preferred to use the +ones with underscores when multiple exist. + +In addition, if an instance has AWS Tags associated with it, each tag is a new +variable named: + - ec2_tag_[Key] = [Value] + +Security groups are comma-separated in 'ec2_security_group_ids' and +'ec2_security_group_names'. +''' + +# (c) 2012, Peter Sankauskas +# +# This file is part of Ansible, +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +###################################################################### + +import sys +import os +import argparse +import re +from time import time +import boto +from boto import ec2 +from boto import rds +from boto import elasticache +from boto import route53 +import six + +from six.moves import configparser +from collections import defaultdict + +try: + import json +except ImportError: + import simplejson as json + + +class Ec2Inventory(object): + def _empty_inventory(self): + return {"_meta" : {"hostvars" : {}}} + + def __init__(self): + ''' Main execution path ''' + + # Inventory grouped by instance IDs, tags, security groups, regions, + # and availability zones + self.inventory = self._empty_inventory() + + # Index of hostname (address) to instance ID + self.index = {} + + # Read settings and parse CLI arguments + self.read_settings() + self.parse_cli_args() + + # Cache + if self.args.refresh_cache: + self.do_api_calls_update_cache() + elif not self.is_cache_valid(): + self.do_api_calls_update_cache() + + # Data to print + if self.args.host: + data_to_print = self.get_host_info() + + elif self.args.list: + # Display list of instances for inventory + if self.inventory == self._empty_inventory(): + data_to_print = self.get_inventory_from_cache() + else: + data_to_print = self.json_format_dict(self.inventory, True) + + print(data_to_print) + + + def is_cache_valid(self): + ''' Determines if the cache files have expired, or if it is still valid ''' + + if os.path.isfile(self.cache_path_cache): + mod_time = os.path.getmtime(self.cache_path_cache) + current_time = time() + if (mod_time + self.cache_max_age) > current_time: + if os.path.isfile(self.cache_path_index): + return True + + return False + + + def read_settings(self): + ''' Reads the settings from the ec2.ini file ''' + if six.PY2: + config = configparser.SafeConfigParser() + else: + config = configparser.ConfigParser() + ec2_default_ini_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ec2.ini') + ec2_ini_path = os.path.expanduser(os.path.expandvars(os.environ.get('EC2_INI_PATH', ec2_default_ini_path))) + config.read(ec2_ini_path) + + # is eucalyptus? + self.eucalyptus_host = None + self.eucalyptus = False + if config.has_option('ec2', 'eucalyptus'): + self.eucalyptus = config.getboolean('ec2', 'eucalyptus') + if self.eucalyptus and config.has_option('ec2', 'eucalyptus_host'): + self.eucalyptus_host = config.get('ec2', 'eucalyptus_host') + + # Regions + self.regions = [] + configRegions = config.get('ec2', 'regions') + configRegions_exclude = config.get('ec2', 'regions_exclude') + if (configRegions == 'all'): + if self.eucalyptus_host: + self.regions.append(boto.connect_euca(host=self.eucalyptus_host).region.name) + else: + for regionInfo in ec2.regions(): + if regionInfo.name not in configRegions_exclude: + self.regions.append(regionInfo.name) + else: + self.regions = configRegions.split(",") + + # Destination addresses + self.destination_variable = config.get('ec2', 'destination_variable') + self.vpc_destination_variable = config.get('ec2', 'vpc_destination_variable') + + # Route53 + self.route53_enabled = config.getboolean('ec2', 'route53') + self.route53_excluded_zones = [] + if config.has_option('ec2', 'route53_excluded_zones'): + self.route53_excluded_zones.extend( + config.get('ec2', 'route53_excluded_zones', '').split(',')) + + # Include RDS instances? + self.rds_enabled = True + if config.has_option('ec2', 'rds'): + self.rds_enabled = config.getboolean('ec2', 'rds') + + # Include ElastiCache instances? + self.elasticache_enabled = True + if config.has_option('ec2', 'elasticache'): + self.elasticache_enabled = config.getboolean('ec2', 'elasticache') + + # Return all EC2 instances? + if config.has_option('ec2', 'all_instances'): + self.all_instances = config.getboolean('ec2', 'all_instances') + else: + self.all_instances = False + + # Return all RDS instances? (if RDS is enabled) + if config.has_option('ec2', 'all_rds_instances') and self.rds_enabled: + self.all_rds_instances = config.getboolean('ec2', 'all_rds_instances') + else: + self.all_rds_instances = False + + # Return all ElastiCache replication groups? (if ElastiCache is enabled) + if config.has_option('ec2', 'all_elasticache_replication_groups') and self.elasticache_enabled: + self.all_elasticache_replication_groups = config.getboolean('ec2', 'all_elasticache_replication_groups') + else: + self.all_elasticache_replication_groups = False + + # Return all ElastiCache clusters? (if ElastiCache is enabled) + if config.has_option('ec2', 'all_elasticache_clusters') and self.elasticache_enabled: + self.all_elasticache_clusters = config.getboolean('ec2', 'all_elasticache_clusters') + else: + self.all_elasticache_clusters = False + + # Return all ElastiCache nodes? (if ElastiCache is enabled) + if config.has_option('ec2', 'all_elasticache_nodes') and self.elasticache_enabled: + self.all_elasticache_nodes = config.getboolean('ec2', 'all_elasticache_nodes') + else: + self.all_elasticache_nodes = False + + # Cache related + cache_dir = os.path.expanduser(config.get('ec2', 'cache_path')) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + + self.cache_path_cache = cache_dir + "/ansible-ec2.cache" + self.cache_path_index = cache_dir + "/ansible-ec2.index" + self.cache_max_age = config.getint('ec2', 'cache_max_age') + + # Configure nested groups instead of flat namespace. + if config.has_option('ec2', 'nested_groups'): + self.nested_groups = config.getboolean('ec2', 'nested_groups') + else: + self.nested_groups = False + + # Configure which groups should be created. + group_by_options = [ + 'group_by_instance_id', + 'group_by_region', + 'group_by_availability_zone', + 'group_by_ami_id', + 'group_by_instance_type', + 'group_by_key_pair', + 'group_by_vpc_id', + 'group_by_security_group', + 'group_by_tag_keys', + 'group_by_tag_none', + 'group_by_route53_names', + 'group_by_rds_engine', + 'group_by_rds_parameter_group', + 'group_by_elasticache_engine', + 'group_by_elasticache_cluster', + 'group_by_elasticache_parameter_group', + 'group_by_elasticache_replication_group', + ] + for option in group_by_options: + if config.has_option('ec2', option): + setattr(self, option, config.getboolean('ec2', option)) + else: + setattr(self, option, True) + + # Do we need to just include hosts that match a pattern? + try: + pattern_include = config.get('ec2', 'pattern_include') + if pattern_include and len(pattern_include) > 0: + self.pattern_include = re.compile(pattern_include) + else: + self.pattern_include = None + except configparser.NoOptionError as e: + self.pattern_include = None + + # Do we need to exclude hosts that match a pattern? + try: + pattern_exclude = config.get('ec2', 'pattern_exclude'); + if pattern_exclude and len(pattern_exclude) > 0: + self.pattern_exclude = re.compile(pattern_exclude) + else: + self.pattern_exclude = None + except configparser.NoOptionError as e: + self.pattern_exclude = None + + # Instance filters (see boto and EC2 API docs). Ignore invalid filters. + self.ec2_instance_filters = defaultdict(list) + if config.has_option('ec2', 'instance_filters'): + for instance_filter in config.get('ec2', 'instance_filters', '').split(','): + instance_filter = instance_filter.strip() + if not instance_filter or '=' not in instance_filter: + continue + filter_key, filter_value = [x.strip() for x in instance_filter.split('=', 1)] + if not filter_key: + continue + self.ec2_instance_filters[filter_key].append(filter_value) + + def parse_cli_args(self): + ''' Command line argument processing ''' + + parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on EC2') + parser.add_argument('--list', action='store_true', default=True, + help='List instances (default: True)') + parser.add_argument('--host', action='store', + help='Get all the variables about a specific instance') + parser.add_argument('--refresh-cache', action='store_true', default=False, + help='Force refresh of cache by making API requests to EC2 (default: False - use cache files)') + self.args = parser.parse_args() + + + def do_api_calls_update_cache(self): + ''' Do API calls to each region, and save data in cache files ''' + + if self.route53_enabled: + self.get_route53_records() + + for region in self.regions: + self.get_instances_by_region(region) + if self.rds_enabled: + self.get_rds_instances_by_region(region) + if self.elasticache_enabled: + self.get_elasticache_clusters_by_region(region) + self.get_elasticache_replication_groups_by_region(region) + + self.write_to_cache(self.inventory, self.cache_path_cache) + self.write_to_cache(self.index, self.cache_path_index) + + def connect(self, region): + ''' create connection to api server''' + if self.eucalyptus: + conn = boto.connect_euca(host=self.eucalyptus_host) + conn.APIVersion = '2010-08-31' + else: + conn = ec2.connect_to_region(region) + # connect_to_region will fail "silently" by returning None if the region name is wrong or not supported + if conn is None: + self.fail_with_error("region name: %s likely not supported, or AWS is down. connection to region failed." % region) + return conn + + def get_instances_by_region(self, region): + ''' Makes an AWS EC2 API call to the list of instances in a particular + region ''' + + try: + conn = self.connect(region) + reservations = [] + if self.ec2_instance_filters: + for filter_key, filter_values in self.ec2_instance_filters.items(): + reservations.extend(conn.get_all_instances(filters = { filter_key : filter_values })) + else: + reservations = conn.get_all_instances() + + for reservation in reservations: + for instance in reservation.instances: + self.add_instance(instance, region) + + except boto.exception.BotoServerError as e: + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + else: + backend = 'Eucalyptus' if self.eucalyptus else 'AWS' + error = "Error connecting to %s backend.\n%s" % (backend, e.message) + self.fail_with_error(error) + + def get_rds_instances_by_region(self, region): + ''' Makes an AWS API call to the list of RDS instances in a particular + region ''' + + try: + conn = rds.connect_to_region(region) + if conn: + instances = conn.get_all_dbinstances() + for instance in instances: + self.add_rds_instance(instance, region) + except boto.exception.BotoServerError as e: + error = e.reason + + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + if not e.reason == "Forbidden": + error = "Looks like AWS RDS is down:\n%s" % e.message + self.fail_with_error(error) + + def get_elasticache_clusters_by_region(self, region): + ''' Makes an AWS API call to the list of ElastiCache clusters (with + nodes' info) in a particular region.''' + + # ElastiCache boto module doesn't provide a get_all_intances method, + # that's why we need to call describe directly (it would be called by + # the shorthand method anyway...) + try: + conn = elasticache.connect_to_region(region) + if conn: + # show_cache_node_info = True + # because we also want nodes' information + response = conn.describe_cache_clusters(None, None, None, True) + + except boto.exception.BotoServerError as e: + error = e.reason + + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + if not e.reason == "Forbidden": + error = "Looks like AWS ElastiCache is down:\n%s" % e.message + self.fail_with_error(error) + + try: + # Boto also doesn't provide wrapper classes to CacheClusters or + # CacheNodes. Because of that wo can't make use of the get_list + # method in the AWSQueryConnection. Let's do the work manually + clusters = response['DescribeCacheClustersResponse']['DescribeCacheClustersResult']['CacheClusters'] + + except KeyError as e: + error = "ElastiCache query to AWS failed (unexpected format)." + self.fail_with_error(error) + + for cluster in clusters: + self.add_elasticache_cluster(cluster, region) + + def get_elasticache_replication_groups_by_region(self, region): + ''' Makes an AWS API call to the list of ElastiCache replication groups + in a particular region.''' + + # ElastiCache boto module doesn't provide a get_all_intances method, + # that's why we need to call describe directly (it would be called by + # the shorthand method anyway...) + try: + conn = elasticache.connect_to_region(region) + if conn: + response = conn.describe_replication_groups() + + except boto.exception.BotoServerError as e: + error = e.reason + + if e.error_code == 'AuthFailure': + error = self.get_auth_error_message() + if not e.reason == "Forbidden": + error = "Looks like AWS ElastiCache [Replication Groups] is down:\n%s" % e.message + self.fail_with_error(error) + + try: + # Boto also doesn't provide wrapper classes to ReplicationGroups + # Because of that wo can't make use of the get_list method in the + # AWSQueryConnection. Let's do the work manually + replication_groups = response['DescribeReplicationGroupsResponse']['DescribeReplicationGroupsResult']['ReplicationGroups'] + + except KeyError as e: + error = "ElastiCache [Replication Groups] query to AWS failed (unexpected format)." + self.fail_with_error(error) + + for replication_group in replication_groups: + self.add_elasticache_replication_group(replication_group, region) + + def get_auth_error_message(self): + ''' create an informative error message if there is an issue authenticating''' + errors = ["Authentication error retrieving ec2 inventory."] + if None in [os.environ.get('AWS_ACCESS_KEY_ID'), os.environ.get('AWS_SECRET_ACCESS_KEY')]: + errors.append(' - No AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY environment vars found') + else: + errors.append(' - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment vars found but may not be correct') + + boto_paths = ['/etc/boto.cfg', '~/.boto', '~/.aws/credentials'] + boto_config_found = list(p for p in boto_paths if os.path.isfile(os.path.expanduser(p))) + if len(boto_config_found) > 0: + errors.append(" - Boto configs found at '%s', but the credentials contained may not be correct" % ', '.join(boto_config_found)) + else: + errors.append(" - No Boto config found at any expected location '%s'" % ', '.join(boto_paths)) + + return '\n'.join(errors) + + def fail_with_error(self, err_msg): + '''log an error to std err for ansible-playbook to consume and exit''' + sys.stderr.write(err_msg) + sys.exit(1) + + def get_instance(self, region, instance_id): + conn = self.connect(region) + + reservations = conn.get_all_instances([instance_id]) + for reservation in reservations: + for instance in reservation.instances: + return instance + + def add_instance(self, instance, region): + ''' Adds an instance to the inventory and index, as long as it is + addressable ''' + + # Only want running instances unless all_instances is True + if not self.all_instances and instance.state != 'running': + return + + # Select the best destination address + if instance.subnet_id: + dest = getattr(instance, self.vpc_destination_variable, None) + if dest is None: + dest = getattr(instance, 'tags').get(self.vpc_destination_variable, None) + else: + dest = getattr(instance, self.destination_variable, None) + if dest is None: + dest = getattr(instance, 'tags').get(self.destination_variable, None) + + if not dest: + # Skip instances we cannot address (e.g. private VPC subnet) + return + + # if we only want to include hosts that match a pattern, skip those that don't + if self.pattern_include and not self.pattern_include.match(dest): + return + + # if we need to exclude hosts that match a pattern, skip those + if self.pattern_exclude and self.pattern_exclude.match(dest): + return + + # Add to index + self.index[dest] = [region, instance.id] + + # Inventory: Group by instance ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[instance.id] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', instance.id) + + # Inventory: Group by region + if self.group_by_region: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone + if self.group_by_availability_zone: + self.push(self.inventory, instance.placement, dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, instance.placement) + self.push_group(self.inventory, 'zones', instance.placement) + + # Inventory: Group by Amazon Machine Image (AMI) ID + if self.group_by_ami_id: + ami_id = self.to_safe(instance.image_id) + self.push(self.inventory, ami_id, dest) + if self.nested_groups: + self.push_group(self.inventory, 'images', ami_id) + + # Inventory: Group by instance type + if self.group_by_instance_type: + type_name = self.to_safe('type_' + instance.instance_type) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by key pair + if self.group_by_key_pair and instance.key_name: + key_name = self.to_safe('key_' + instance.key_name) + self.push(self.inventory, key_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'keys', key_name) + + # Inventory: Group by VPC + if self.group_by_vpc_id and instance.vpc_id: + vpc_id_name = self.to_safe('vpc_id_' + instance.vpc_id) + self.push(self.inventory, vpc_id_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'vpcs', vpc_id_name) + + # Inventory: Group by security group + if self.group_by_security_group: + try: + for group in instance.groups: + key = self.to_safe("security_group_" + group.name) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + except AttributeError: + self.fail_with_error('\n'.join(['Package boto seems a bit older.', + 'Please upgrade boto >= 2.3.0.'])) + + # Inventory: Group by tag keys + if self.group_by_tag_keys: + for k, v in instance.tags.items(): + if v: + key = self.to_safe("tag_" + k + "=" + v) + else: + key = self.to_safe("tag_" + k) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'tags', self.to_safe("tag_" + k)) + self.push_group(self.inventory, self.to_safe("tag_" + k), key) + + # Inventory: Group by Route53 domain names if enabled + if self.route53_enabled and self.group_by_route53_names: + route53_names = self.get_instance_route53_names(instance) + for name in route53_names: + self.push(self.inventory, name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'route53', name) + + # Global Tag: instances without tags + if self.group_by_tag_none and len(instance.tags) == 0: + self.push(self.inventory, 'tag_none', dest) + if self.nested_groups: + self.push_group(self.inventory, 'tags', 'tag_none') + + # Global Tag: tag all EC2 instances + self.push(self.inventory, 'ec2', dest) + + self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) + + + def add_rds_instance(self, instance, region): + ''' Adds an RDS instance to the inventory and index, as long as it is + addressable ''' + + # Only want available instances unless all_rds_instances is True + if not self.all_rds_instances and instance.status != 'available': + return + + # Select the best destination address + dest = instance.endpoint[0] + + if not dest: + # Skip instances we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, instance.id] + + # Inventory: Group by instance ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[instance.id] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', instance.id) + + # Inventory: Group by region + if self.group_by_region: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone + if self.group_by_availability_zone: + self.push(self.inventory, instance.availability_zone, dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, instance.availability_zone) + self.push_group(self.inventory, 'zones', instance.availability_zone) + + # Inventory: Group by instance type + if self.group_by_instance_type: + type_name = self.to_safe('type_' + instance.instance_class) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by VPC + if self.group_by_vpc_id and instance.subnet_group and instance.subnet_group.vpc_id: + vpc_id_name = self.to_safe('vpc_id_' + instance.subnet_group.vpc_id) + self.push(self.inventory, vpc_id_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'vpcs', vpc_id_name) + + # Inventory: Group by security group + if self.group_by_security_group: + try: + if instance.security_group: + key = self.to_safe("security_group_" + instance.security_group.name) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + + except AttributeError: + self.fail_with_error('\n'.join(['Package boto seems a bit older.', + 'Please upgrade boto >= 2.3.0.'])) + + + # Inventory: Group by engine + if self.group_by_rds_engine: + self.push(self.inventory, self.to_safe("rds_" + instance.engine), dest) + if self.nested_groups: + self.push_group(self.inventory, 'rds_engines', self.to_safe("rds_" + instance.engine)) + + # Inventory: Group by parameter group + if self.group_by_rds_parameter_group: + self.push(self.inventory, self.to_safe("rds_parameter_group_" + instance.parameter_group.name), dest) + if self.nested_groups: + self.push_group(self.inventory, 'rds_parameter_groups', self.to_safe("rds_parameter_group_" + instance.parameter_group.name)) + + # Global Tag: all RDS instances + self.push(self.inventory, 'rds', dest) + + self.inventory["_meta"]["hostvars"][dest] = self.get_host_info_dict_from_instance(instance) + + def add_elasticache_cluster(self, cluster, region): + ''' Adds an ElastiCache cluster to the inventory and index, as long as + it's nodes are addressable ''' + + # Only want available clusters unless all_elasticache_clusters is True + if not self.all_elasticache_clusters and cluster['CacheClusterStatus'] != 'available': + return + + # Select the best destination address + if 'ConfigurationEndpoint' in cluster and cluster['ConfigurationEndpoint']: + # Memcached cluster + dest = cluster['ConfigurationEndpoint']['Address'] + is_redis = False + else: + # Redis sigle node cluster + # Because all Redis clusters are single nodes, we'll merge the + # info from the cluster with info about the node + dest = cluster['CacheNodes'][0]['Endpoint']['Address'] + is_redis = True + + if not dest: + # Skip clusters we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, cluster['CacheClusterId']] + + # Inventory: Group by instance ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[cluster['CacheClusterId']] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', cluster['CacheClusterId']) + + # Inventory: Group by region + if self.group_by_region and not is_redis: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone + if self.group_by_availability_zone and not is_redis: + self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone']) + self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone']) + + # Inventory: Group by node type + if self.group_by_instance_type and not is_redis: + type_name = self.to_safe('type_' + cluster['CacheNodeType']) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by VPC (information not available in the current + # AWS API version for ElastiCache) + + # Inventory: Group by security group + if self.group_by_security_group and not is_redis: + + # Check for the existence of the 'SecurityGroups' key and also if + # this key has some value. When the cluster is not placed in a SG + # the query can return None here and cause an error. + if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None: + for security_group in cluster['SecurityGroups']: + key = self.to_safe("security_group_" + security_group['SecurityGroupId']) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + + # Inventory: Group by engine + if self.group_by_elasticache_engine and not is_redis: + self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_engines', self.to_safe(cluster['Engine'])) + + # Inventory: Group by parameter group + if self.group_by_elasticache_parameter_group: + self.push(self.inventory, self.to_safe("elasticache_parameter_group_" + cluster['CacheParameterGroup']['CacheParameterGroupName']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_parameter_groups', self.to_safe(cluster['CacheParameterGroup']['CacheParameterGroupName'])) + + # Inventory: Group by replication group + if self.group_by_elasticache_replication_group and 'ReplicationGroupId' in cluster and cluster['ReplicationGroupId']: + self.push(self.inventory, self.to_safe("elasticache_replication_group_" + cluster['ReplicationGroupId']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_replication_groups', self.to_safe(cluster['ReplicationGroupId'])) + + # Global Tag: all ElastiCache clusters + self.push(self.inventory, 'elasticache_clusters', cluster['CacheClusterId']) + + host_info = self.get_host_info_dict_from_describe_dict(cluster) + + self.inventory["_meta"]["hostvars"][dest] = host_info + + # Add the nodes + for node in cluster['CacheNodes']: + self.add_elasticache_node(node, cluster, region) + + def add_elasticache_node(self, node, cluster, region): + ''' Adds an ElastiCache node to the inventory and index, as long as + it is addressable ''' + + # Only want available nodes unless all_elasticache_nodes is True + if not self.all_elasticache_nodes and node['CacheNodeStatus'] != 'available': + return + + # Select the best destination address + dest = node['Endpoint']['Address'] + + if not dest: + # Skip nodes we cannot address (e.g. private VPC subnet) + return + + node_id = self.to_safe(cluster['CacheClusterId'] + '_' + node['CacheNodeId']) + + # Add to index + self.index[dest] = [region, node_id] + + # Inventory: Group by node ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[node_id] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', node_id) + + # Inventory: Group by region + if self.group_by_region: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone + if self.group_by_availability_zone: + self.push(self.inventory, cluster['PreferredAvailabilityZone'], dest) + if self.nested_groups: + if self.group_by_region: + self.push_group(self.inventory, region, cluster['PreferredAvailabilityZone']) + self.push_group(self.inventory, 'zones', cluster['PreferredAvailabilityZone']) + + # Inventory: Group by node type + if self.group_by_instance_type: + type_name = self.to_safe('type_' + cluster['CacheNodeType']) + self.push(self.inventory, type_name, dest) + if self.nested_groups: + self.push_group(self.inventory, 'types', type_name) + + # Inventory: Group by VPC (information not available in the current + # AWS API version for ElastiCache) + + # Inventory: Group by security group + if self.group_by_security_group: + + # Check for the existence of the 'SecurityGroups' key and also if + # this key has some value. When the cluster is not placed in a SG + # the query can return None here and cause an error. + if 'SecurityGroups' in cluster and cluster['SecurityGroups'] is not None: + for security_group in cluster['SecurityGroups']: + key = self.to_safe("security_group_" + security_group['SecurityGroupId']) + self.push(self.inventory, key, dest) + if self.nested_groups: + self.push_group(self.inventory, 'security_groups', key) + + # Inventory: Group by engine + if self.group_by_elasticache_engine: + self.push(self.inventory, self.to_safe("elasticache_" + cluster['Engine']), dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_engines', self.to_safe("elasticache_" + cluster['Engine'])) + + # Inventory: Group by parameter group (done at cluster level) + + # Inventory: Group by replication group (done at cluster level) + + # Inventory: Group by ElastiCache Cluster + if self.group_by_elasticache_cluster: + self.push(self.inventory, self.to_safe("elasticache_cluster_" + cluster['CacheClusterId']), dest) + + # Global Tag: all ElastiCache nodes + self.push(self.inventory, 'elasticache_nodes', dest) + + host_info = self.get_host_info_dict_from_describe_dict(node) + + if dest in self.inventory["_meta"]["hostvars"]: + self.inventory["_meta"]["hostvars"][dest].update(host_info) + else: + self.inventory["_meta"]["hostvars"][dest] = host_info + + def add_elasticache_replication_group(self, replication_group, region): + ''' Adds an ElastiCache replication group to the inventory and index ''' + + # Only want available clusters unless all_elasticache_replication_groups is True + if not self.all_elasticache_replication_groups and replication_group['Status'] != 'available': + return + + # Select the best destination address (PrimaryEndpoint) + dest = replication_group['NodeGroups'][0]['PrimaryEndpoint']['Address'] + + if not dest: + # Skip clusters we cannot address (e.g. private VPC subnet) + return + + # Add to index + self.index[dest] = [region, replication_group['ReplicationGroupId']] + + # Inventory: Group by ID (always a group of 1) + if self.group_by_instance_id: + self.inventory[replication_group['ReplicationGroupId']] = [dest] + if self.nested_groups: + self.push_group(self.inventory, 'instances', replication_group['ReplicationGroupId']) + + # Inventory: Group by region + if self.group_by_region: + self.push(self.inventory, region, dest) + if self.nested_groups: + self.push_group(self.inventory, 'regions', region) + + # Inventory: Group by availability zone (doesn't apply to replication groups) + + # Inventory: Group by node type (doesn't apply to replication groups) + + # Inventory: Group by VPC (information not available in the current + # AWS API version for replication groups + + # Inventory: Group by security group (doesn't apply to replication groups) + # Check this value in cluster level + + # Inventory: Group by engine (replication groups are always Redis) + if self.group_by_elasticache_engine: + self.push(self.inventory, 'elasticache_redis', dest) + if self.nested_groups: + self.push_group(self.inventory, 'elasticache_engines', 'redis') + + # Global Tag: all ElastiCache clusters + self.push(self.inventory, 'elasticache_replication_groups', replication_group['ReplicationGroupId']) + + host_info = self.get_host_info_dict_from_describe_dict(replication_group) + + self.inventory["_meta"]["hostvars"][dest] = host_info + + def get_route53_records(self): + ''' Get and store the map of resource records to domain names that + point to them. ''' + + r53_conn = route53.Route53Connection() + all_zones = r53_conn.get_zones() + + route53_zones = [ zone for zone in all_zones if zone.name[:-1] + not in self.route53_excluded_zones ] + + self.route53_records = {} + + for zone in route53_zones: + rrsets = r53_conn.get_all_rrsets(zone.id) + + for record_set in rrsets: + record_name = record_set.name + + if record_name.endswith('.'): + record_name = record_name[:-1] + + for resource in record_set.resource_records: + self.route53_records.setdefault(resource, set()) + self.route53_records[resource].add(record_name) + + + def get_instance_route53_names(self, instance): + ''' Check if an instance is referenced in the records we have from + Route53. If it is, return the list of domain names pointing to said + instance. If nothing points to it, return an empty list. ''' + + instance_attributes = [ 'public_dns_name', 'private_dns_name', + 'ip_address', 'private_ip_address' ] + + name_list = set() + + for attrib in instance_attributes: + try: + value = getattr(instance, attrib) + except AttributeError: + continue + + if value in self.route53_records: + name_list.update(self.route53_records[value]) + + return list(name_list) + + def get_host_info_dict_from_instance(self, instance): + instance_vars = {} + for key in vars(instance): + value = getattr(instance, key) + key = self.to_safe('ec2_' + key) + + # Handle complex types + # state/previous_state changed to properties in boto in https://github.com/boto/boto/commit/a23c379837f698212252720d2af8dec0325c9518 + if key == 'ec2__state': + instance_vars['ec2_state'] = instance.state or '' + instance_vars['ec2_state_code'] = instance.state_code + elif key == 'ec2__previous_state': + instance_vars['ec2_previous_state'] = instance.previous_state or '' + instance_vars['ec2_previous_state_code'] = instance.previous_state_code + elif type(value) in [int, bool]: + instance_vars[key] = value + elif isinstance(value, six.string_types): + instance_vars[key] = value.strip() + elif type(value) == type(None): + instance_vars[key] = '' + elif key == 'ec2_region': + instance_vars[key] = value.name + elif key == 'ec2__placement': + instance_vars['ec2_placement'] = value.zone + elif key == 'ec2_tags': + for k, v in value.items(): + key = self.to_safe('ec2_tag_' + k) + instance_vars[key] = v + elif key == 'ec2_groups': + group_ids = [] + group_names = [] + for group in value: + group_ids.append(group.id) + group_names.append(group.name) + instance_vars["ec2_security_group_ids"] = ','.join([str(i) for i in group_ids]) + instance_vars["ec2_security_group_names"] = ','.join([str(i) for i in group_names]) + else: + pass + # TODO Product codes if someone finds them useful + #print key + #print type(value) + #print value + + return instance_vars + + def get_host_info_dict_from_describe_dict(self, describe_dict): + ''' Parses the dictionary returned by the API call into a flat list + of parameters. This method should be used only when 'describe' is + used directly because Boto doesn't provide specific classes. ''' + + # I really don't agree with prefixing everything with 'ec2' + # because EC2, RDS and ElastiCache are different services. + # I'm just following the pattern used until now to not break any + # compatibility. + + host_info = {} + for key in describe_dict: + value = describe_dict[key] + key = self.to_safe('ec2_' + self.uncammelize(key)) + + # Handle complex types + + # Target: Memcached Cache Clusters + if key == 'ec2_configuration_endpoint' and value: + host_info['ec2_configuration_endpoint_address'] = value['Address'] + host_info['ec2_configuration_endpoint_port'] = value['Port'] + + # Target: Cache Nodes and Redis Cache Clusters (single node) + if key == 'ec2_endpoint' and value: + host_info['ec2_endpoint_address'] = value['Address'] + host_info['ec2_endpoint_port'] = value['Port'] + + # Target: Redis Replication Groups + if key == 'ec2_node_groups' and value: + host_info['ec2_endpoint_address'] = value[0]['PrimaryEndpoint']['Address'] + host_info['ec2_endpoint_port'] = value[0]['PrimaryEndpoint']['Port'] + replica_count = 0 + for node in value[0]['NodeGroupMembers']: + if node['CurrentRole'] == 'primary': + host_info['ec2_primary_cluster_address'] = node['ReadEndpoint']['Address'] + host_info['ec2_primary_cluster_port'] = node['ReadEndpoint']['Port'] + host_info['ec2_primary_cluster_id'] = node['CacheClusterId'] + elif node['CurrentRole'] == 'replica': + host_info['ec2_replica_cluster_address_'+ str(replica_count)] = node['ReadEndpoint']['Address'] + host_info['ec2_replica_cluster_port_'+ str(replica_count)] = node['ReadEndpoint']['Port'] + host_info['ec2_replica_cluster_id_'+ str(replica_count)] = node['CacheClusterId'] + replica_count += 1 + + # Target: Redis Replication Groups + if key == 'ec2_member_clusters' and value: + host_info['ec2_member_clusters'] = ','.join([str(i) for i in value]) + + # Target: All Cache Clusters + elif key == 'ec2_cache_parameter_group': + host_info["ec2_cache_node_ids_to_reboot"] = ','.join([str(i) for i in value['CacheNodeIdsToReboot']]) + host_info['ec2_cache_parameter_group_name'] = value['CacheParameterGroupName'] + host_info['ec2_cache_parameter_apply_status'] = value['ParameterApplyStatus'] + + # Target: Almost everything + elif key == 'ec2_security_groups': + + # Skip if SecurityGroups is None + # (it is possible to have the key defined but no value in it). + if value is not None: + sg_ids = [] + for sg in value: + sg_ids.append(sg['SecurityGroupId']) + host_info["ec2_security_group_ids"] = ','.join([str(i) for i in sg_ids]) + + # Target: Everything + # Preserve booleans and integers + elif type(value) in [int, bool]: + host_info[key] = value + + # Target: Everything + # Sanitize string values + elif isinstance(value, six.string_types): + host_info[key] = value.strip() + + # Target: Everything + # Replace None by an empty string + elif type(value) == type(None): + host_info[key] = '' + + else: + # Remove non-processed complex types + pass + + return host_info + + def get_host_info(self): + ''' Get variables about a specific host ''' + + if len(self.index) == 0: + # Need to load index from cache + self.load_index_from_cache() + + if not self.args.host in self.index: + # try updating the cache + self.do_api_calls_update_cache() + if not self.args.host in self.index: + # host might not exist anymore + return self.json_format_dict({}, True) + + (region, instance_id) = self.index[self.args.host] + + instance = self.get_instance(region, instance_id) + return self.json_format_dict(self.get_host_info_dict_from_instance(instance), True) + + def push(self, my_dict, key, element): + ''' Push an element onto an array that may not have been defined in + the dict ''' + group_info = my_dict.setdefault(key, []) + if isinstance(group_info, dict): + host_list = group_info.setdefault('hosts', []) + host_list.append(element) + else: + group_info.append(element) + + def push_group(self, my_dict, key, element): + ''' Push a group as a child of another group. ''' + parent_group = my_dict.setdefault(key, {}) + if not isinstance(parent_group, dict): + parent_group = my_dict[key] = {'hosts': parent_group} + child_groups = parent_group.setdefault('children', []) + if element not in child_groups: + child_groups.append(element) + + def get_inventory_from_cache(self): + ''' Reads the inventory from the cache file and returns it as a JSON + object ''' + + cache = open(self.cache_path_cache, 'r') + json_inventory = cache.read() + return json_inventory + + + def load_index_from_cache(self): + ''' Reads the index from the cache file sets self.index ''' + + cache = open(self.cache_path_index, 'r') + json_index = cache.read() + self.index = json.loads(json_index) + + + def write_to_cache(self, data, filename): + ''' Writes data in JSON format to a file ''' + + json_data = self.json_format_dict(data, True) + cache = open(filename, 'w') + cache.write(json_data) + cache.close() + + def uncammelize(self, key): + temp = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', key) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', temp).lower() + + def to_safe(self, word): + ''' Converts 'bad' characters in a string to underscores so they can be + used as Ansible groups ''' + + return re.sub("[^A-Za-z0-9\_]", "_", word) + + def json_format_dict(self, data, pretty=False): + ''' Converts a dict to a JSON object and dumps it as a formatted + string ''' + + if pretty: + return json.dumps(data, sort_keys=True, indent=2) + else: + return json.dumps(data) + + +# Run the script +Ec2Inventory() diff --git a/contrib/inventory/fleet.py b/contrib/inventory/fleet.py new file mode 100755 index 0000000000..3267aeb2ea --- /dev/null +++ b/contrib/inventory/fleet.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python +""" +fleetctl base external inventory script. Automatically finds the IPs of the booted coreos instances and +returns it under the host group 'coreos' +""" + +# Copyright (C) 2014 Andrew Rothstein +# +# 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 3 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, see . + +# +# Thanks to the vagrant.py inventory script for giving me the basic structure +# of this. +# + +import sys +import subprocess +import re +import string +from optparse import OptionParser +try: + import json +except: + import simplejson as json + +# Options +#------------------------------ + +parser = OptionParser(usage="%prog [options] --list | --host ") +parser.add_option('--list', default=False, dest="list", action="store_true", + help="Produce a JSON consumable grouping of servers in your fleet") +parser.add_option('--host', default=None, dest="host", + help="Generate additional host specific details for given host for Ansible") +(options, args) = parser.parse_args() + +# +# helper functions +# + +def get_ssh_config(): + configs = [] + for box in list_running_boxes(): + config = get_a_ssh_config(box) + configs.append(config) + return configs + +#list all the running instances in the fleet +def list_running_boxes(): + boxes = [] + for line in subprocess.check_output(["fleetctl", "list-machines"]).split('\n'): + matcher = re.search("[^\s]+[\s]+([^\s]+).+", line) + if matcher and matcher.group(1) != "IP": + boxes.append(matcher.group(1)) + + return boxes + +def get_a_ssh_config(box_name): + config = {} + config['Host'] = box_name + config['ansible_ssh_user'] = 'core' + config['ansible_python_interpreter'] = '/opt/bin/python' + return config + +# List out servers that vagrant has running +#------------------------------ +if options.list: + ssh_config = get_ssh_config() + hosts = { 'coreos': []} + + for data in ssh_config: + hosts['coreos'].append(data['Host']) + + print json.dumps(hosts) + sys.exit(1) + +# Get out the host details +#------------------------------ +elif options.host: + result = {} + ssh_config = get_ssh_config() + + details = filter(lambda x: (x['Host'] == options.host), ssh_config) + if len(details) > 0: + #pass through the port, in case it's non standard. + result = details[0] + result + + print json.dumps(result) + sys.exit(1) + + +# Print out help +#------------------------------ +else: + parser.print_help() + sys.exit(1) diff --git a/contrib/inventory/freeipa.py b/contrib/inventory/freeipa.py new file mode 100755 index 0000000000..05a8dba356 --- /dev/null +++ b/contrib/inventory/freeipa.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python + +import argparse +from ipalib import api +import json + +def initialize(): + ''' + This function initializes the FreeIPA/IPA API. This function requires + no arguments. A kerberos key must be present in the users keyring in + order for this to work. + ''' + + api.bootstrap(context='cli') + api.finalize() + try: + api.Backend.rpcclient.connect() + except AttributeError: + #FreeIPA < 4.0 compatibility + api.Backend.xmlclient.connect() + + return api + +def list_groups(api): + ''' + This function returns a list of all host groups. This function requires + one argument, the FreeIPA/IPA API object. + ''' + + inventory = {} + hostvars={} + meta={} + + result = api.Command.hostgroup_find()['result'] + + for hostgroup in result: + inventory[hostgroup['cn'][0]] = { 'hosts': [host for host in hostgroup['member_host']]} + + for host in hostgroup['member_host']: + hostvars[host] = {} + + inventory['_meta'] = {'hostvars': hostvars} + inv_string = json.dumps(inventory, indent=1, sort_keys=True) + print inv_string + + return None + +def parse_args(): + ''' + This function parses the arguments that were passed in via the command line. + This function expects no arguments. + ''' + + parser = argparse.ArgumentParser(description='Ansible FreeIPA/IPA ' + 'inventory module') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specified host') + + return parser.parse_args() + +def print_host(host): + ''' + This function is really a stub, it could return variables to be used in + a playbook. However, at this point there are no variables stored in + FreeIPA/IPA. + + This function expects one string, this hostname to lookup variables for. + ''' + + print json.dumps({}) + + return None + +if __name__ == '__main__': + args = parse_args() + + if args.host: + print_host(args.host) + elif args.list: + api = initialize() + list_groups(api) diff --git a/plugins/inventory/gce.ini b/contrib/inventory/gce.ini similarity index 100% rename from plugins/inventory/gce.ini rename to contrib/inventory/gce.ini diff --git a/plugins/inventory/gce.py b/contrib/inventory/gce.py similarity index 92% rename from plugins/inventory/gce.py rename to contrib/inventory/gce.py index 08a21f12f2..740e112332 100755 --- a/plugins/inventory/gce.py +++ b/contrib/inventory/gce.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python # Copyright 2013 Google Inc. # # This file is part of Ansible @@ -66,12 +66,22 @@ Examples: $ ansible -i gce.py us-central1-a -m shell -a "/bin/uname -a" Use the GCE inventory script to print out instance specific information - $ plugins/inventory/gce.py --host my_instance + $ contrib/inventory/gce.py --host my_instance Author: Eric Johnson Version: 0.0.1 ''' +__requires__ = ['pycrypto>=2.6'] +try: + import pkg_resources +except ImportError: + # Use pkg_resources to find the correct versions of libraries and set + # sys.path appropriately when there are multiversion installs. We don't + # fail here as there is code that better expresses the errors where the + # library is used. + pass + USER_AGENT_PRODUCT="Ansible-gce_inventory_plugin" USER_AGENT_VERSION="v1" @@ -211,7 +221,7 @@ class GceInventory(object): 'gce_image': inst.image, 'gce_machine_type': inst.size, 'gce_private_ip': inst.private_ips[0], - 'gce_public_ip': inst.public_ips[0], + 'gce_public_ip': inst.public_ips[0] if len(inst.public_ips) >= 1 else None, 'gce_name': inst.name, 'gce_description': inst.extra['description'], 'gce_status': inst.extra['status'], @@ -220,7 +230,7 @@ class GceInventory(object): 'gce_metadata': md, 'gce_network': net, # Hosts don't have a public name, so we add an IP - 'ansible_ssh_host': inst.public_ips[0] + 'ansible_ssh_host': inst.public_ips[0] if len(inst.public_ips) >= 1 else inst.private_ips[0] } def get_instance(self, instance_name): @@ -247,7 +257,10 @@ class GceInventory(object): tags = node.extra['tags'] for t in tags: - tag = 'tag_%s' % t + if t.startswith('group-'): + tag = t[6:] + else: + tag = 'tag_%s' % t if groups.has_key(tag): groups[tag].append(name) else: groups[tag] = [name] diff --git a/plugins/inventory/jail.py b/contrib/inventory/jail.py similarity index 100% rename from plugins/inventory/jail.py rename to contrib/inventory/jail.py diff --git a/contrib/inventory/landscape.py b/contrib/inventory/landscape.py new file mode 100755 index 0000000000..4b53171c34 --- /dev/null +++ b/contrib/inventory/landscape.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python + +# (c) 2015, Marc Abramowitz +# +# This file is part of Ansible. +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +# Dynamic inventory script which lets you use nodes discovered by Canonical's +# Landscape (http://www.ubuntu.com/management/landscape-features). +# +# Requires the `landscape_api` Python module +# See: +# - https://landscape.canonical.com/static/doc/api/api-client-package.html +# - https://landscape.canonical.com/static/doc/api/python-api.html +# +# Environment variables +# --------------------- +# - `LANDSCAPE_API_URI` +# - `LANDSCAPE_API_KEY` +# - `LANDSCAPE_API_SECRET` +# - `LANDSCAPE_API_SSL_CA_FILE` (optional) + + +import argparse +import collections +import os +import sys + +from landscape_api.base import API, HTTPError + +try: + import json +except ImportError: + import simplejson as json + +_key = 'landscape' + + +class EnvironmentConfig(object): + uri = os.getenv('LANDSCAPE_API_URI') + access_key = os.getenv('LANDSCAPE_API_KEY') + secret_key = os.getenv('LANDSCAPE_API_SECRET') + ssl_ca_file = os.getenv('LANDSCAPE_API_SSL_CA_FILE') + + +def _landscape_client(): + env = EnvironmentConfig() + return API( + uri=env.uri, + access_key=env.access_key, + secret_key=env.secret_key, + ssl_ca_file=env.ssl_ca_file) + + +def get_landscape_members_data(): + return _landscape_client().get_computers() + + +def get_nodes(data): + return [node['hostname'] for node in data] + + +def get_groups(data): + groups = collections.defaultdict(list) + + for node in data: + for value in node['tags']: + groups[value].append(node['hostname']) + + return groups + + +def get_meta(data): + meta = {'hostvars': {}} + for node in data: + meta['hostvars'][node['hostname']] = {'tags': node['tags']} + return meta + + +def print_list(): + data = get_landscape_members_data() + nodes = get_nodes(data) + groups = get_groups(data) + meta = get_meta(data) + inventory_data = {_key: nodes, '_meta': meta} + inventory_data.update(groups) + print(json.dumps(inventory_data)) + + +def print_host(host): + data = get_landscape_members_data() + meta = get_meta(data) + print(json.dumps(meta['hostvars'][host])) + + +def get_args(args_list): + parser = argparse.ArgumentParser( + description='ansible inventory script reading from landscape cluster') + mutex_group = parser.add_mutually_exclusive_group(required=True) + help_list = 'list all hosts from landscape cluster' + mutex_group.add_argument('--list', action='store_true', help=help_list) + help_host = 'display variables for a host' + mutex_group.add_argument('--host', help=help_host) + return parser.parse_args(args_list) + + +def main(args_list): + args = get_args(args_list) + if args.list: + print_list() + if args.host: + print_host(args.host) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/plugins/inventory/libcloud.ini b/contrib/inventory/libcloud.ini similarity index 100% rename from plugins/inventory/libcloud.ini rename to contrib/inventory/libcloud.ini diff --git a/plugins/inventory/libvirt_lxc.py b/contrib/inventory/libvirt_lxc.py similarity index 100% rename from plugins/inventory/libvirt_lxc.py rename to contrib/inventory/libvirt_lxc.py diff --git a/plugins/inventory/linode.ini b/contrib/inventory/linode.ini similarity index 100% rename from plugins/inventory/linode.ini rename to contrib/inventory/linode.ini diff --git a/plugins/inventory/linode.py b/contrib/inventory/linode.py similarity index 100% rename from plugins/inventory/linode.py rename to contrib/inventory/linode.py diff --git a/contrib/inventory/nova.ini b/contrib/inventory/nova.ini new file mode 100644 index 0000000000..c5cfeef810 --- /dev/null +++ b/contrib/inventory/nova.ini @@ -0,0 +1,48 @@ +# Ansible OpenStack external inventory script +# DEPRECATED: please use openstack.py inventory which is configured for +# auth using the os-client-config library and either clouds.yaml or standard +# openstack environment variables + +[openstack] + +#------------------------------------------------------------------------- +# Required settings +#------------------------------------------------------------------------- + +# API version +version = 2 + +# OpenStack nova username +username = + +# OpenStack nova api_key or password +api_key = + +# OpenStack nova auth_url +auth_url = + +# OpenStack nova project_id or tenant name +project_id = + +#------------------------------------------------------------------------- +# Optional settings +#------------------------------------------------------------------------- + +# Authentication system +# auth_system = keystone + +# Serverarm region name to use +# region_name = + +# Specify a preference for public or private IPs (public is default) +# prefer_private = False + +# What service type (required for newer nova client) +# service_type = compute + + +# TODO: Some other options +# insecure = +# endpoint_type = +# extensions = +# service_name = diff --git a/contrib/inventory/nova.py b/contrib/inventory/nova.py new file mode 100755 index 0000000000..af2e7a0760 --- /dev/null +++ b/contrib/inventory/nova.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python + +# (c) 2012, Marco Vito Moscaritolo +# +# This file is part of Ansible, +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +# WARNING: This file is deprecated. New work should focus on the openstack.py +# inventory module, which properly handles multiple clouds as well as keystone +# v3 and keystone auth plugins + +import sys +import re +import os +import ConfigParser +from novaclient import client as nova_client + +try: + import json +except ImportError: + import simplejson as json + + +sys.stderr.write("WARNING: this inventory module is deprecated. please migrate usage to openstack.py\n") + +################################################### +# executed with no parameters, return the list of +# all groups and hosts + +NOVA_CONFIG_FILES = [os.getcwd() + "/nova.ini", + os.path.expanduser(os.environ.get('ANSIBLE_CONFIG', "~/nova.ini")), + "/etc/ansible/nova.ini"] + +NOVA_DEFAULTS = { + 'auth_system': None, + 'region_name': None, + 'service_type': 'compute', +} + + +def nova_load_config_file(): + p = ConfigParser.SafeConfigParser(NOVA_DEFAULTS) + + for path in NOVA_CONFIG_FILES: + if os.path.exists(path): + p.read(path) + return p + + return None + + +def get_fallback(config, value, section="openstack"): + """ + Get value from config object and return the value + or false + """ + try: + return config.get(section, value) + except ConfigParser.NoOptionError: + return False + + +def push(data, key, element): + """ + Assist in items to a dictionary of lists + """ + if (not element) or (not key): + return + + if key in data: + data[key].append(element) + else: + data[key] = [element] + + +def to_safe(word): + ''' + Converts 'bad' characters in a string to underscores so they can + be used as Ansible groups + ''' + return re.sub(r"[^A-Za-z0-9\-]", "_", word) + + +def get_ips(server, access_ip=True): + """ + Returns a list of the server's IPs, or the preferred + access IP + """ + private = [] + public = [] + address_list = [] + # Iterate through each servers network(s), get addresses and get type + addresses = getattr(server, 'addresses', {}) + if len(addresses) > 0: + for network in addresses.itervalues(): + for address in network: + if address.get('OS-EXT-IPS:type', False) == 'fixed': + private.append(address['addr']) + elif address.get('OS-EXT-IPS:type', False) == 'floating': + public.append(address['addr']) + + if not access_ip: + address_list.append(server.accessIPv4) + address_list.extend(private) + address_list.extend(public) + return address_list + + access_ip = None + # Append group to list + if server.accessIPv4: + access_ip = server.accessIPv4 + if (not access_ip) and public and not (private and prefer_private): + access_ip = public[0] + if private and not access_ip: + access_ip = private[0] + + return access_ip + + +def get_metadata(server): + """Returns dictionary of all host metadata""" + get_ips(server, False) + results = {} + for key in vars(server): + # Extract value + value = getattr(server, key) + + # Generate sanitized key + key = 'os_' + re.sub(r"[^A-Za-z0-9\-]", "_", key).lower() + + # Att value to instance result (exclude manager class) + #TODO: maybe use value.__class__ or similar inside of key_name + if key != 'os_manager': + results[key] = value + return results + +config = nova_load_config_file() +if not config: + sys.exit('Unable to find configfile in %s' % ', '.join(NOVA_CONFIG_FILES)) + +# Load up connections info based on config and then environment +# variables +username = (get_fallback(config, 'username') or + os.environ.get('OS_USERNAME', None)) +api_key = (get_fallback(config, 'api_key') or + os.environ.get('OS_PASSWORD', None)) +auth_url = (get_fallback(config, 'auth_url') or + os.environ.get('OS_AUTH_URL', None)) +project_id = (get_fallback(config, 'project_id') or + os.environ.get('OS_TENANT_NAME', None)) +region_name = (get_fallback(config, 'region_name') or + os.environ.get('OS_REGION_NAME', None)) +auth_system = (get_fallback(config, 'auth_system') or + os.environ.get('OS_AUTH_SYSTEM', None)) + +# Determine what type of IP is preferred to return +prefer_private = False +try: + prefer_private = config.getboolean('openstack', 'prefer_private') +except ConfigParser.NoOptionError: + pass + +client = nova_client.Client( + version=config.get('openstack', 'version'), + username=username, + api_key=api_key, + auth_url=auth_url, + region_name=region_name, + project_id=project_id, + auth_system=auth_system, + service_type=config.get('openstack', 'service_type'), +) + +# Default or added list option +if (len(sys.argv) == 2 and sys.argv[1] == '--list') or len(sys.argv) == 1: + groups = {'_meta': {'hostvars': {}}} + # Cycle on servers + for server in client.servers.list(): + access_ip = get_ips(server) + + # Push to name group of 1 + push(groups, server.name, access_ip) + + # Run through each metadata item and add instance to it + for key, value in server.metadata.iteritems(): + composed_key = to_safe('tag_{0}_{1}'.format(key, value)) + push(groups, composed_key, access_ip) + + # Do special handling of group for backwards compat + # inventory groups + group = server.metadata['group'] if 'group' in server.metadata else 'undefined' + push(groups, group, access_ip) + + # Add vars to _meta key for performance optimization in + # Ansible 1.3+ + groups['_meta']['hostvars'][access_ip] = get_metadata(server) + + # Return server list + print(json.dumps(groups, sort_keys=True, indent=2)) + sys.exit(0) + +##################################################### +# executed with a hostname as a parameter, return the +# variables for that host + +elif len(sys.argv) == 3 and (sys.argv[1] == '--host'): + results = {} + ips = [] + for server in client.servers.list(): + if sys.argv[2] in (get_ips(server) or []): + results = get_metadata(server) + print(json.dumps(results, sort_keys=True, indent=2)) + sys.exit(0) + +else: + print "usage: --list ..OR.. --host " + sys.exit(1) diff --git a/plugins/inventory/openshift.py b/contrib/inventory/openshift.py similarity index 100% rename from plugins/inventory/openshift.py rename to contrib/inventory/openshift.py diff --git a/contrib/inventory/openstack.py b/contrib/inventory/openstack.py new file mode 100755 index 0000000000..819380c796 --- /dev/null +++ b/contrib/inventory/openstack.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python + +# Copyright (c) 2012, Marco Vito Moscaritolo +# Copyright (c) 2013, Jesse Keating +# Copyright (c) 2014, Hewlett-Packard Development Company, L.P. +# +# This module 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 3 of the License, or +# (at your option) any later version. +# +# This software 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 software. If not, see . + +# The OpenStack Inventory module uses os-client-config for configuration. +# https://github.com/stackforge/os-client-config +# This means it will either: +# - Respect normal OS_* environment variables like other OpenStack tools +# - Read values from a clouds.yaml file. +# If you want to configure via clouds.yaml, you can put the file in: +# - Current directory +# - ~/.config/openstack/clouds.yaml +# - /etc/openstack/clouds.yaml +# - /etc/ansible/openstack.yml +# The clouds.yaml file can contain entries for multiple clouds and multiple +# regions of those clouds. If it does, this inventory module will connect to +# all of them and present them as one contiguous inventory. +# +# See the adjacent openstack.yml file for an example config file + +import argparse +import collections +import os +import sys +import time + +try: + import json +except: + import simplejson as json + +import os_client_config +import shade + + +class OpenStackInventory(object): + + def __init__(self, private=False, refresh=False): + self.openstack_config = os_client_config.config.OpenStackConfig( + os_client_config.config.CONFIG_FILES.append( + '/etc/ansible/openstack.yml'), + private) + self.clouds = shade.openstack_clouds(self.openstack_config) + self.refresh = refresh + + self.cache_max_age = self.openstack_config.get_cache_max_age() + cache_path = self.openstack_config.get_cache_path() + + # Cache related + if not os.path.exists(cache_path): + os.makedirs(cache_path) + self.cache_file = os.path.join(cache_path, "ansible-inventory.cache") + + def is_cache_stale(self): + ''' Determines if cache file has expired, or if it is still valid ''' + if os.path.isfile(self.cache_file): + mod_time = os.path.getmtime(self.cache_file) + current_time = time.time() + if (mod_time + self.cache_max_age) > current_time: + return False + return True + + def get_host_groups(self): + if self.refresh or self.is_cache_stale(): + groups = self.get_host_groups_from_cloud() + self.write_cache(groups) + else: + return json.load(open(self.cache_file, 'r')) + return groups + + def write_cache(self, groups): + with open(self.cache_file, 'w') as cache_file: + cache_file.write(self.json_format_dict(groups)) + + def get_host_groups_from_cloud(self): + groups = collections.defaultdict(list) + hostvars = collections.defaultdict(dict) + + for cloud in self.clouds: + + # Cycle on servers + for server in cloud.list_servers(): + + meta = cloud.get_server_meta(server) + + if 'interface_ip' not in meta['server_vars']: + # skip this host if it doesn't have a network address + continue + + server_vars = meta['server_vars'] + hostvars[server.name][ + 'ansible_ssh_host'] = server_vars['interface_ip'] + hostvars[server.name]['openstack'] = server_vars + + for group in meta['groups']: + groups[group].append(server.name) + + if hostvars: + groups['_meta'] = {'hostvars': hostvars} + return groups + + def json_format_dict(self, data): + return json.dumps(data, sort_keys=True, indent=2) + + def list_instances(self): + groups = self.get_host_groups() + # Return server list + print(self.json_format_dict(groups)) + + def get_host(self, hostname): + groups = self.get_host_groups() + hostvars = groups['_meta']['hostvars'] + if hostname in hostvars: + print(self.json_format_dict(hostvars[hostname])) + + +def parse_args(): + parser = argparse.ArgumentParser(description='OpenStack Inventory Module') + parser.add_argument('--private', + action='store_true', + help='Use private address for ansible host') + parser.add_argument('--refresh', action='store_true', + help='Refresh cached information') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specific host') + return parser.parse_args() + + +def main(): + args = parse_args() + try: + inventory = OpenStackInventory(args.private, args.refresh) + if args.list: + inventory.list_instances() + elif args.host: + inventory.get_host(args.host) + except shade.OpenStackCloudException as e: + print(e.message) + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/contrib/inventory/openstack.yml b/contrib/inventory/openstack.yml new file mode 100644 index 0000000000..a99bb02058 --- /dev/null +++ b/contrib/inventory/openstack.yml @@ -0,0 +1,28 @@ +clouds: + mordred: + cloud: hp + auth: + username: mordred@example.com + password: my-wonderful-password + project_name: mordred-tenant + region_name: region-b.geo-1 + monty: + cloud: hp + auth: + username: monty.taylor@example.com + password: another-wonderful-password + project_name: monty.taylor@example.com-default-tenant + region_name: region-b.geo-1 + rax: + cloud: rackspace + auth: + username: example + password: spectacular-password + project_id: 2352426 + region_name: DFW,ORD,IAD + devstack: + auth: + auth_url: http://127.0.0.1:35357/v2.0/ + username: stack + password: stack + project_name: stack diff --git a/contrib/inventory/ovirt.ini b/contrib/inventory/ovirt.ini new file mode 100644 index 0000000000..a52f9d63ff --- /dev/null +++ b/contrib/inventory/ovirt.ini @@ -0,0 +1,33 @@ +# Copyright 2013 Google Inc. +# +# This file is part of Ansible +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + + +# Author: Josha Inglis based on the gce.ini by Eric Johnson + +[ovirt] +# ovirt Service Account configuration information can be stored in the +# libcloud 'secrets.py' file. Ideally, the 'secrets.py' file will already +# exist in your PYTHONPATH and be picked up automatically with an import +# statement in the inventory script. However, you can specify an absolute +# path to the secrets.py file with 'libcloud_secrets' parameter. +ovirt_api_secrets = + +# If you are not going to use a 'secrets.py' file, you can set the necessary +# authorization parameters here. +ovirt_url = +ovirt_username = +ovirt_password = diff --git a/contrib/inventory/ovirt.py b/contrib/inventory/ovirt.py new file mode 100755 index 0000000000..dc022c5dfd --- /dev/null +++ b/contrib/inventory/ovirt.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# Copyright 2015 IIX Inc. +# +# This file is part of Ansible +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +""" +ovirt external inventory script +================================= + +Generates inventory that Ansible can understand by making API requests to +oVirt via the ovirt-engine-sdk-python library. + +When run against a specific host, this script returns the following variables +based on the data obtained from the ovirt_sdk Node object: + - ovirt_uuid + - ovirt_id + - ovirt_image + - ovirt_machine_type + - ovirt_ips + - ovirt_name + - ovirt_description + - ovirt_status + - ovirt_zone + - ovirt_tags + - ovirt_stats + +When run in --list mode, instances are grouped by the following categories: + + - zone: + zone group name. + - instance tags: + An entry is created for each tag. For example, if you have two instances + with a common tag called 'foo', they will both be grouped together under + the 'tag_foo' name. + - network name: + the name of the network is appended to 'network_' (e.g. the 'default' + network will result in a group named 'network_default') + - running status: + group name prefixed with 'status_' (e.g. status_up, status_down,..) + +Examples: + Execute uname on all instances in the us-central1-a zone + $ ansible -i ovirt.py us-central1-a -m shell -a "/bin/uname -a" + + Use the ovirt inventory script to print out instance specific information + $ contrib/inventory/ovirt.py --host my_instance + +Author: Josha Inglis based on the gce.py by Eric Johnson +Version: 0.0.1 +""" + +USER_AGENT_PRODUCT = "Ansible-ovirt_inventory_plugin" +USER_AGENT_VERSION = "v1" + +import sys +import os +import argparse +import ConfigParser +from collections import defaultdict + +try: + import json +except ImportError: + # noinspection PyUnresolvedReferences,PyPackageRequirements + import simplejson as json + +try: + # noinspection PyUnresolvedReferences + from ovirtsdk.api import API + # noinspection PyUnresolvedReferences + from ovirtsdk.xml import params +except ImportError: + print("ovirt inventory script requires ovirt-engine-sdk-python") + sys.exit(1) + + +class OVirtInventory(object): + def __init__(self): + # Read settings and parse CLI arguments + self.args = self.parse_cli_args() + self.driver = self.get_ovirt_driver() + + # Just display data for specific host + if self.args.host: + print self.json_format_dict( + self.node_to_dict(self.get_instance(self.args.host)), + pretty=self.args.pretty + ) + sys.exit(0) + + # Otherwise, assume user wants all instances grouped + print( + self.json_format_dict( + data=self.group_instances(), + pretty=self.args.pretty + ) + ) + sys.exit(0) + + @staticmethod + def get_ovirt_driver(): + """ + Determine the ovirt authorization settings and return a ovirt_sdk driver. + + :rtype : ovirtsdk.api.API + """ + kwargs = {} + + ovirt_ini_default_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "ovirt.ini") + ovirt_ini_path = os.environ.get('OVIRT_INI_PATH', ovirt_ini_default_path) + + # Create a ConfigParser. + # This provides empty defaults to each key, so that environment + # variable configuration (as opposed to INI configuration) is able + # to work. + config = ConfigParser.SafeConfigParser(defaults={ + 'ovirt_url': '', + 'ovirt_username': '', + 'ovirt_password': '', + 'ovirt_api_secrets': '', + }) + if 'ovirt' not in config.sections(): + config.add_section('ovirt') + config.read(ovirt_ini_path) + + # Attempt to get ovirt params from a configuration file, if one + # exists. + secrets_path = config.get('ovirt', 'ovirt_api_secrets') + secrets_found = False + try: + # noinspection PyUnresolvedReferences,PyPackageRequirements + import secrets + + kwargs = getattr(secrets, 'OVIRT_KEYWORD_PARAMS', {}) + secrets_found = True + except ImportError: + pass + + if not secrets_found and secrets_path: + if not secrets_path.endswith('secrets.py'): + err = "Must specify ovirt_sdk secrets file as /absolute/path/to/secrets.py" + print(err) + sys.exit(1) + sys.path.append(os.path.dirname(secrets_path)) + try: + # noinspection PyUnresolvedReferences,PyPackageRequirements + import secrets + + kwargs = getattr(secrets, 'OVIRT_KEYWORD_PARAMS', {}) + except ImportError: + pass + if not secrets_found: + kwargs = { + 'url': config.get('ovirt', 'ovirt_url'), + 'username': config.get('ovirt', 'ovirt_username'), + 'password': config.get('ovirt', 'ovirt_password'), + } + + # If the appropriate environment variables are set, they override + # other configuration; process those into our args and kwargs. + kwargs['url'] = os.environ.get('OVIRT_URL') + kwargs['username'] = os.environ.get('OVIRT_EMAIL') + kwargs['password'] = os.environ.get('OVIRT_PASS') + + # Retrieve and return the ovirt driver. + return API(insecure=True, **kwargs) + + @staticmethod + def parse_cli_args(): + """ + Command line argument processing + + :rtype : argparse.Namespace + """ + + parser = argparse.ArgumentParser(description='Produce an Ansible Inventory file based on ovirt') + parser.add_argument('--list', action='store_true', default=True, help='List instances (default: True)') + parser.add_argument('--host', action='store', help='Get all information about an instance') + parser.add_argument('--pretty', action='store_true', default=False, help='Pretty format (default: False)') + return parser.parse_args() + + def node_to_dict(self, inst): + """ + :type inst: params.VM + """ + if inst is None: + return {} + + inst.get_custom_properties() + ips = [ip.get_address() for ip in inst.get_guest_info().get_ips().get_ip()] \ + if inst.get_guest_info() is not None else [] + stats = {} + for stat in inst.get_statistics().list(): + stats[stat.get_name()] = stat.get_values().get_value()[0].get_datum() + + return { + 'ovirt_uuid': inst.get_id(), + 'ovirt_id': inst.get_id(), + 'ovirt_image': inst.get_os().get_type(), + 'ovirt_machine_type': inst.get_instance_type(), + 'ovirt_ips': ips, + 'ovirt_name': inst.get_name(), + 'ovirt_description': inst.get_description(), + 'ovirt_status': inst.get_status().get_state(), + 'ovirt_zone': inst.get_cluster().get_id(), + 'ovirt_tags': self.get_tags(inst), + 'ovirt_stats': stats, + # Hosts don't have a public name, so we add an IP + 'ansible_ssh_host': ips[0] if len(ips) > 0 else None + } + + @staticmethod + def get_tags(inst): + """ + :type inst: params.VM + """ + return [x.get_name() for x in inst.get_tags().list()] + + # noinspection PyBroadException,PyUnusedLocal + def get_instance(self, instance_name): + """Gets details about a specific instance """ + try: + return self.driver.vms.get(name=instance_name) + except Exception as e: + return None + + def group_instances(self): + """Group all instances""" + groups = defaultdict(list) + meta = {"hostvars": {}} + + for node in self.driver.vms.list(): + assert isinstance(node, params.VM) + name = node.get_name() + + meta["hostvars"][name] = self.node_to_dict(node) + + zone = node.get_cluster().get_name() + groups[zone].append(name) + + tags = self.get_tags(node) + for t in tags: + tag = 'tag_%s' % t + groups[tag].append(name) + + nets = [x.get_name() for x in node.get_nics().list()] + for net in nets: + net = 'network_%s' % net + groups[net].append(name) + + status = node.get_status().get_state() + stat = 'status_%s' % status.lower() + if stat in groups: + groups[stat].append(name) + else: + groups[stat] = [name] + + groups["_meta"] = meta + + return groups + + @staticmethod + def json_format_dict(data, pretty=False): + """ Converts a dict to a JSON object and dumps it as a formatted + string """ + + if pretty: + return json.dumps(data, sort_keys=True, indent=2) + else: + return json.dumps(data) + +# Run the script +OVirtInventory() diff --git a/contrib/inventory/rax.ini b/contrib/inventory/rax.ini new file mode 100644 index 0000000000..5a269e16a3 --- /dev/null +++ b/contrib/inventory/rax.ini @@ -0,0 +1,57 @@ +# Ansible Rackspace external inventory script settings +# + +[rax] + +# Environment Variable: RAX_CREDS_FILE +# +# An optional configuration that points to a pyrax-compatible credentials +# file. +# +# If not supplied, rax.py will look for a credentials file +# at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK, +# and therefore requires a file formatted per the SDK's specifications. +# +# https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md +# creds_file = ~/.rackspace_cloud_credentials + +# Environment Variable: RAX_REGION +# +# An optional environment variable to narrow inventory search +# scope. If used, needs a value like ORD, DFW, SYD (a Rackspace +# datacenter) and optionally accepts a comma-separated list. +# regions = IAD,ORD,DFW + +# Environment Variable: RAX_ENV +# +# A configuration that will use an environment as configured in +# ~/.pyrax.cfg, see +# https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md +# env = prod + +# Environment Variable: RAX_META_PREFIX +# Default: meta +# +# A configuration that changes the prefix used for meta key/value groups. +# For compatibility with ec2.py set to "tag" +# meta_prefix = meta + +# Environment Variable: RAX_ACCESS_NETWORK +# Default: public +# +# A configuration that will tell the inventory script to use a specific +# server network to determine the ansible_ssh_host value. If no address +# is found, ansible_ssh_host will not be set. Accepts a comma-separated +# list of network names, the first found wins. +# access_network = public + +# Environment Variable: RAX_ACCESS_IP_VERSION +# Default: 4 +# +# A configuration related to "access_network" that will attempt to +# determine the ansible_ssh_host value for either IPv4 or IPv6. If no +# address is found, ansible_ssh_host will not be set. +# Acceptable values are: 4 or 6. Values other than 4 or 6 +# will be ignored, and 4 will be used. Accepts a comma separated list, +# the first found wins. +# access_ip_version = 4 diff --git a/contrib/inventory/rax.py b/contrib/inventory/rax.py new file mode 100755 index 0000000000..10b72d322b --- /dev/null +++ b/contrib/inventory/rax.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python + +# (c) 2013, Jesse Keating , +# Matt Martz +# +# This file is part of Ansible. +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +""" +Rackspace Cloud Inventory + +Authors: + Jesse Keating , + Matt Martz + + +Description: + Generates inventory that Ansible can understand by making API request to + Rackspace Public Cloud API + + When run against a specific host, this script returns variables similar to: + rax_os-ext-sts_task_state + rax_addresses + rax_links + rax_image + rax_os-ext-sts_vm_state + rax_flavor + rax_id + rax_rax-bandwidth_bandwidth + rax_user_id + rax_os-dcf_diskconfig + rax_accessipv4 + rax_accessipv6 + rax_progress + rax_os-ext-sts_power_state + rax_metadata + rax_status + rax_updated + rax_hostid + rax_name + rax_created + rax_tenant_id + rax_loaded + +Configuration: + rax.py can be configured using a rax.ini file or via environment + variables. The rax.ini file should live in the same directory along side + this script. + + The section header for configuration values related to this + inventory plugin is [rax] + + [rax] + creds_file = ~/.rackspace_cloud_credentials + regions = IAD,ORD,DFW + env = prod + meta_prefix = meta + access_network = public + access_ip_version = 4 + + Each of these configurations also has a corresponding environment variable. + An environment variable will override a configuration file value. + + creds_file: + Environment Variable: RAX_CREDS_FILE + + An optional configuration that points to a pyrax-compatible credentials + file. + + If not supplied, rax.py will look for a credentials file + at ~/.rackspace_cloud_credentials. It uses the Rackspace Python SDK, + and therefore requires a file formatted per the SDK's specifications. + + https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + + regions: + Environment Variable: RAX_REGION + + An optional environment variable to narrow inventory search + scope. If used, needs a value like ORD, DFW, SYD (a Rackspace + datacenter) and optionally accepts a comma-separated list. + + environment: + Environment Variable: RAX_ENV + + A configuration that will use an environment as configured in + ~/.pyrax.cfg, see + https://github.com/rackspace/pyrax/blob/master/docs/getting_started.md + + meta_prefix: + Environment Variable: RAX_META_PREFIX + Default: meta + + A configuration that changes the prefix used for meta key/value groups. + For compatibility with ec2.py set to "tag" + + access_network: + Environment Variable: RAX_ACCESS_NETWORK + Default: public + + A configuration that will tell the inventory script to use a specific + server network to determine the ansible_ssh_host value. If no address + is found, ansible_ssh_host will not be set. Accepts a comma-separated + list of network names, the first found wins. + + access_ip_version: + Environment Variable: RAX_ACCESS_IP_VERSION + Default: 4 + + A configuration related to "access_network" that will attempt to + determine the ansible_ssh_host value for either IPv4 or IPv6. If no + address is found, ansible_ssh_host will not be set. + Acceptable values are: 4 or 6. Values other than 4 or 6 + will be ignored, and 4 will be used. Accepts a comma-separated list, + the first found wins. + +Examples: + List server instances + $ RAX_CREDS_FILE=~/.raxpub rax.py --list + + List servers in ORD datacenter only + $ RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD rax.py --list + + List servers in ORD and DFW datacenters + $ RAX_CREDS_FILE=~/.raxpub RAX_REGION=ORD,DFW rax.py --list + + Get server details for server named "server.example.com" + $ RAX_CREDS_FILE=~/.raxpub rax.py --host server.example.com + + Use the instance private IP to connect (instead of public IP) + $ RAX_CREDS_FILE=~/.raxpub RAX_ACCESS_NETWORK=private rax.py --list +""" + +import os +import re +import sys +import argparse +import warnings +import collections +import ConfigParser + +from ansible.constants import get_config, mk_boolean + +try: + import json +except ImportError: + import simplejson as json + +try: + import pyrax + from pyrax.utils import slugify +except ImportError: + print('pyrax is required for this module') + sys.exit(1) + +NON_CALLABLES = (basestring, bool, dict, int, list, type(None)) + + +def load_config_file(): + p = ConfigParser.ConfigParser() + config_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), + 'rax.ini') + try: + p.read(config_file) + except ConfigParser.Error: + return None + else: + return p +p = load_config_file() + + +def rax_slugify(value): + return 'rax_%s' % (re.sub('[^\w-]', '_', value).lower().lstrip('_')) + + +def to_dict(obj): + instance = {} + for key in dir(obj): + value = getattr(obj, key) + if isinstance(value, NON_CALLABLES) and not key.startswith('_'): + key = rax_slugify(key) + instance[key] = value + + return instance + + +def host(regions, hostname): + hostvars = {} + + for region in regions: + # Connect to the region + cs = pyrax.connect_to_cloudservers(region=region) + for server in cs.servers.list(): + if server.name == hostname: + for key, value in to_dict(server).items(): + hostvars[key] = value + + # And finally, add an IP address + hostvars['ansible_ssh_host'] = server.accessIPv4 + print(json.dumps(hostvars, sort_keys=True, indent=4)) + + +def _list(regions): + groups = collections.defaultdict(list) + hostvars = collections.defaultdict(dict) + images = {} + cbs_attachments = collections.defaultdict(dict) + + prefix = get_config(p, 'rax', 'meta_prefix', 'RAX_META_PREFIX', 'meta') + + networks = get_config(p, 'rax', 'access_network', 'RAX_ACCESS_NETWORK', + 'public', islist=True) + try: + ip_versions = map(int, get_config(p, 'rax', 'access_ip_version', + 'RAX_ACCESS_IP_VERSION', 4, + islist=True)) + except: + ip_versions = [4] + else: + ip_versions = [v for v in ip_versions if v in [4, 6]] + if not ip_versions: + ip_versions = [4] + + # Go through all the regions looking for servers + for region in regions: + # Connect to the region + cs = pyrax.connect_to_cloudservers(region=region) + if cs is None: + warnings.warn( + 'Connecting to Rackspace region "%s" has caused Pyrax to ' + 'return a NoneType. Is this a valid region?' % region, + RuntimeWarning) + continue + for server in cs.servers.list(): + # Create a group on region + groups[region].append(server.name) + + # Check if group metadata key in servers' metadata + group = server.metadata.get('group') + if group: + groups[group].append(server.name) + + for extra_group in server.metadata.get('groups', '').split(','): + if extra_group: + groups[extra_group].append(server.name) + + # Add host metadata + for key, value in to_dict(server).items(): + hostvars[server.name][key] = value + + hostvars[server.name]['rax_region'] = region + + for key, value in server.metadata.iteritems(): + groups['%s_%s_%s' % (prefix, key, value)].append(server.name) + + groups['instance-%s' % server.id].append(server.name) + groups['flavor-%s' % server.flavor['id']].append(server.name) + + # Handle boot from volume + if not server.image: + if not cbs_attachments[region]: + cbs = pyrax.connect_to_cloud_blockstorage(region) + for vol in cbs.list(): + if mk_boolean(vol.bootable): + for attachment in vol.attachments: + metadata = vol.volume_image_metadata + server_id = attachment['server_id'] + cbs_attachments[region][server_id] = { + 'id': metadata['image_id'], + 'name': slugify(metadata['image_name']) + } + image = cbs_attachments[region].get(server.id) + if image: + server.image = {'id': image['id']} + hostvars[server.name]['rax_image'] = server.image + hostvars[server.name]['rax_boot_source'] = 'volume' + images[image['id']] = image['name'] + else: + hostvars[server.name]['rax_boot_source'] = 'local' + + try: + imagegroup = 'image-%s' % images[server.image['id']] + groups[imagegroup].append(server.name) + groups['image-%s' % server.image['id']].append(server.name) + except KeyError: + try: + image = cs.images.get(server.image['id']) + except cs.exceptions.NotFound: + groups['image-%s' % server.image['id']].append(server.name) + else: + images[image.id] = image.human_id + groups['image-%s' % image.human_id].append(server.name) + groups['image-%s' % server.image['id']].append(server.name) + + # And finally, add an IP address + ansible_ssh_host = None + # use accessIPv[46] instead of looping address for 'public' + for network_name in networks: + if ansible_ssh_host: + break + if network_name == 'public': + for version_name in ip_versions: + if ansible_ssh_host: + break + if version_name == 6 and server.accessIPv6: + ansible_ssh_host = server.accessIPv6 + elif server.accessIPv4: + ansible_ssh_host = server.accessIPv4 + if not ansible_ssh_host: + addresses = server.addresses.get(network_name, []) + for address in addresses: + for version_name in ip_versions: + if ansible_ssh_host: + break + if address.get('version') == version_name: + ansible_ssh_host = address.get('addr') + break + if ansible_ssh_host: + hostvars[server.name]['ansible_ssh_host'] = ansible_ssh_host + + if hostvars: + groups['_meta'] = {'hostvars': hostvars} + print(json.dumps(groups, sort_keys=True, indent=4)) + + +def parse_args(): + parser = argparse.ArgumentParser(description='Ansible Rackspace Cloud ' + 'inventory module') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specific host') + return parser.parse_args() + + +def setup(): + default_creds_file = os.path.expanduser('~/.rackspace_cloud_credentials') + + env = get_config(p, 'rax', 'environment', 'RAX_ENV', None) + if env: + pyrax.set_environment(env) + + keyring_username = pyrax.get_setting('keyring_username') + + # Attempt to grab credentials from environment first + creds_file = get_config(p, 'rax', 'creds_file', + 'RAX_CREDS_FILE', None) + if creds_file is not None: + creds_file = os.path.expanduser(creds_file) + else: + # But if that fails, use the default location of + # ~/.rackspace_cloud_credentials + if os.path.isfile(default_creds_file): + creds_file = default_creds_file + elif not keyring_username: + sys.stderr.write('No value in environment variable %s and/or no ' + 'credentials file at %s\n' + % ('RAX_CREDS_FILE', default_creds_file)) + sys.exit(1) + + identity_type = pyrax.get_setting('identity_type') + pyrax.set_setting('identity_type', identity_type or 'rackspace') + + region = pyrax.get_setting('region') + + try: + if keyring_username: + pyrax.keyring_auth(keyring_username, region=region) + else: + pyrax.set_credential_file(creds_file, region=region) + except Exception, e: + sys.stderr.write("%s: %s\n" % (e, e.message)) + sys.exit(1) + + regions = [] + if region: + regions.append(region) + else: + region_list = get_config(p, 'rax', 'regions', 'RAX_REGION', 'all', + islist=True) + for region in region_list: + region = region.strip().upper() + if region == 'ALL': + regions = pyrax.regions + break + elif region not in pyrax.regions: + sys.stderr.write('Unsupported region %s' % region) + sys.exit(1) + elif region not in regions: + regions.append(region) + + return regions + + +def main(): + args = parse_args() + regions = setup() + if args.list: + _list(regions) + elif args.host: + host(regions, args.host) + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/contrib/inventory/serf.py b/contrib/inventory/serf.py new file mode 100755 index 0000000000..e1340da92d --- /dev/null +++ b/contrib/inventory/serf.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +# (c) 2015, Marc Abramowitz +# +# This file is part of Ansible. +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +# Dynamic inventory script which lets you use nodes discovered by Serf +# (https://serfdom.io/). +# +# Requires the `serfclient` Python module from +# https://pypi.python.org/pypi/serfclient +# +# Environment variables +# --------------------- +# - `SERF_RPC_ADDR` +# - `SERF_RPC_AUTH` +# +# These variables are described at https://www.serfdom.io/docs/commands/members.html#_rpc_addr + +import argparse +import collections +import os +import sys + +# https://pypi.python.org/pypi/serfclient +from serfclient import SerfClient, EnvironmentConfig + +try: + import json +except ImportError: + import simplejson as json + +_key = 'serf' + + +def _serf_client(): + env = EnvironmentConfig() + return SerfClient(host=env.host, port=env.port, rpc_auth=env.auth_key) + + +def get_serf_members_data(): + return _serf_client().members().body['Members'] + + +def get_nodes(data): + return [node['Name'] for node in data] + + +def get_groups(data): + groups = collections.defaultdict(list) + + for node in data: + for key, value in node['Tags'].items(): + groups[value].append(node['Name']) + + return groups + + +def get_meta(data): + meta = {'hostvars': {}} + for node in data: + meta['hostvars'][node['Name']] = node['Tags'] + return meta + + +def print_list(): + data = get_serf_members_data() + nodes = get_nodes(data) + groups = get_groups(data) + meta = get_meta(data) + inventory_data = {_key: nodes, '_meta': meta} + inventory_data.update(groups) + print(json.dumps(inventory_data)) + + +def print_host(host): + data = get_serf_members_data() + meta = get_meta(data) + print(json.dumps(meta['hostvars'][host])) + + +def get_args(args_list): + parser = argparse.ArgumentParser( + description='ansible inventory script reading from serf cluster') + mutex_group = parser.add_mutually_exclusive_group(required=True) + help_list = 'list all hosts from serf cluster' + mutex_group.add_argument('--list', action='store_true', help=help_list) + help_host = 'display variables for a host' + mutex_group.add_argument('--host', help=help_host) + return parser.parse_args(args_list) + + +def main(args_list): + args = get_args(args_list) + if args.list: + print_list() + if args.host: + print_host(args.host) + + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/plugins/inventory/softlayer.py b/contrib/inventory/softlayer.py similarity index 99% rename from plugins/inventory/softlayer.py rename to contrib/inventory/softlayer.py index ef8a2f6a74..d2a15b1218 100755 --- a/plugins/inventory/softlayer.py +++ b/contrib/inventory/softlayer.py @@ -55,7 +55,7 @@ class SoftLayerInventory(object): self.get_all_servers() print self.json_format_dict(self.inventory, True) elif self.args.host: - self.get_virtual_servers(client) + self.get_virtual_servers() print self.json_format_dict(self.inventory["_meta"]["hostvars"][self.args.host], True) def to_safe(self, word): diff --git a/plugins/inventory/spacewalk.py b/contrib/inventory/spacewalk.py similarity index 100% rename from plugins/inventory/spacewalk.py rename to contrib/inventory/spacewalk.py diff --git a/plugins/inventory/ssh_config.py b/contrib/inventory/ssh_config.py similarity index 100% rename from plugins/inventory/ssh_config.py rename to contrib/inventory/ssh_config.py diff --git a/plugins/inventory/vagrant.py b/contrib/inventory/vagrant.py similarity index 64% rename from plugins/inventory/vagrant.py rename to contrib/inventory/vagrant.py index ea59a7bc02..10dc61cdb2 100755 --- a/plugins/inventory/vagrant.py +++ b/contrib/inventory/vagrant.py @@ -13,6 +13,7 @@ Example Vagrant configuration using this script: """ # Copyright (C) 2013 Mark Mandel +# 2015 Igor Khomyakov # # 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 @@ -33,17 +34,26 @@ Example Vagrant configuration using this script: # import sys +import os.path import subprocess import re -import string +from paramiko import SSHConfig +from cStringIO import StringIO from optparse import OptionParser +from collections import defaultdict try: import json except: import simplejson as json +_group = 'vagrant' # a default group +_ssh_to_ansible = [('user', 'ansible_ssh_user'), + ('hostname', 'ansible_ssh_host'), + ('identityfile', 'ansible_ssh_private_key_file'), + ('port', 'ansible_ssh_port')] + # Options -#------------------------------ +# ------------------------------ parser = OptionParser(usage="%prog [options] --list | --host ") parser.add_option('--list', default=False, dest="list", action="store_true", @@ -56,19 +66,13 @@ parser.add_option('--host', default=None, dest="host", # helper functions # + # get all the ssh configs for all boxes in an array of dictionaries. def get_ssh_config(): - configs = [] + return {k: get_a_ssh_config(k) for k in list_running_boxes()} - boxes = list_running_boxes() - for box in boxes: - config = get_a_ssh_config(box) - configs.append(config) - - return configs - -#list all the running boxes +# list all the running boxes def list_running_boxes(): output = subprocess.check_output(["vagrant", "status"]).split('\n') @@ -79,54 +83,47 @@ def list_running_boxes(): if matcher: boxes.append(matcher.group(1)) - return boxes -#get the ssh config for a single box + +# get the ssh config for a single box def get_a_ssh_config(box_name): """Gives back a map of all the machine's ssh configurations""" - output = subprocess.check_output(["vagrant", "ssh-config", box_name]).split('\n') + output = subprocess.check_output(["vagrant", "ssh-config", box_name]) + config = SSHConfig() + config.parse(StringIO(output)) + host_config = config.lookup(box_name) - config = {} - for line in output: - if line.strip() != '': - matcher = re.search("( )?([a-zA-Z]+) (.*)", line) - config[matcher.group(2)] = matcher.group(3) - - return config + # man 5 ssh_config: + # > It is possible to have multiple identity files ... + # > all these identities will be tried in sequence. + for id in host_config['identityfile']: + if os.path.isfile(id): + host_config['identityfile'] = id + return {v: host_config[k] for k, v in _ssh_to_ansible} # List out servers that vagrant has running -#------------------------------ +# ------------------------------ if options.list: ssh_config = get_ssh_config() - hosts = { 'vagrant': []} + meta = defaultdict(dict) - for data in ssh_config: - hosts['vagrant'].append(data['HostName']) + for host in ssh_config: + meta['hostvars'][host] = ssh_config[host] - print json.dumps(hosts) - sys.exit(1) + print json.dumps({_group: list(ssh_config.keys()), '_meta': meta}) + sys.exit(0) # Get out the host details -#------------------------------ +# ------------------------------ elif options.host: - result = {} - ssh_config = get_ssh_config() - - details = filter(lambda x: (x['HostName'] == options.host), ssh_config) - if len(details) > 0: - #pass through the port, in case it's non standard. - result = details[0] - result['ansible_ssh_port'] = result['Port'] - - print json.dumps(result) - sys.exit(1) - + print json.dumps(get_a_ssh_config(options.host)) + sys.exit(0) # Print out help -#------------------------------ +# ------------------------------ else: parser.print_help() - sys.exit(1) \ No newline at end of file + sys.exit(0) diff --git a/contrib/inventory/vbox.py b/contrib/inventory/vbox.py new file mode 100755 index 0000000000..ff31785d7e --- /dev/null +++ b/contrib/inventory/vbox.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +# This file is part of Ansible, +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +import sys +from subprocess import Popen,PIPE + +try: + import json +except ImportError: + import simplejson as json + + +VBOX="VBoxManage" + + +def get_hosts(host=None): + + returned = {} + try: + if host: + p = Popen([VBOX, 'showvminfo', host], stdout=PIPE) + else: + returned = { 'all': set(), '_metadata': {} } + p = Popen([VBOX, 'list', '-l', 'vms'], stdout=PIPE) + except: + sys.exit(1) + + hostvars = {} + prevkey = pref_k = '' + + for line in p.stdout.readlines(): + + try: + k,v = line.split(':',1) + except: + continue + + if k == '': + continue + + v = v.strip() + if k.startswith('Name'): + if v not in hostvars: + curname = v + hostvars[curname] = {} + try: # try to get network info + x = Popen([VBOX, 'guestproperty', 'get', curname,"/VirtualBox/GuestInfo/Net/0/V4/IP"],stdout=PIPE) + ipinfo = x.stdout.read() + if 'Value' in ipinfo: + a,ip = ipinfo.split(':',1) + hostvars[curname]['ansible_ssh_host'] = ip.strip() + except: + pass + + continue + + if not host: + if k == 'Groups': + for group in v.split('/'): + if group: + if group not in returned: + returned[group] = set() + returned[group].add(curname) + returned['all'].add(curname) + continue + + pref_k = 'vbox_' + k.strip().replace(' ','_') + if k.startswith(' '): + if prevkey not in hostvars[curname]: + hostvars[curname][prevkey] = {} + hostvars[curname][prevkey][pref_k]= v + else: + if v != '': + hostvars[curname][pref_k] = v + + prevkey = pref_k + + if not host: + returned['_metadata']['hostvars'] = hostvars + else: + returned = hostvars[host] + return returned + + +if __name__ == '__main__': + + inventory = {} + hostname = None + + if len(sys.argv) > 1: + if sys.argv[1] == "--host": + hostname = sys.argv[2] + + if hostname: + inventory = get_hosts(hostname) + else: + inventory = get_hosts() + + import pprint + print pprint.pprint(inventory) diff --git a/contrib/inventory/vmware.ini b/contrib/inventory/vmware.ini new file mode 100644 index 0000000000..5097735fd0 --- /dev/null +++ b/contrib/inventory/vmware.ini @@ -0,0 +1,43 @@ +# Ansible VMware external inventory script settings + +[defaults] + +# If true (the default), return only guest VMs. If false, also return host +# systems in the results. +guests_only = True + +# Specify an alternate group name for guest VMs. If not defined, defaults to +# the basename of the inventory script + "_vm", e.g. "vmware_vm". +#vm_group = vm_group_name + +# Specify an alternate group name for host systems when guests_only=false. +# If not defined, defaults to the basename of the inventory script + "_hw", +# e.g. "vmware_hw". +#hw_group = hw_group_name + +# Specify the number of seconds to use the inventory cache before it is +# considered stale. If not defined, defaults to 0 seconds. +#cache_max_age = 3600 + +# Specify the directory used for storing the inventory cache. If not defined, +# caching will be disabled. +#cache_dir = ~/.cache/ansible + +# Specify a prefix filter. Any VMs with names beginning with this string will +# not be returned. +# prefix_filter = test_ + +[auth] + +# Specify hostname or IP address of vCenter/ESXi server. A port may be +# included with the hostname, e.g.: vcenter.example.com:8443. This setting +# may also be defined via the VMWARE_HOST environment variable. +host = vcenter.example.com + +# Specify a username to access the vCenter host. This setting may also be +# defined with the VMWARE_USER environment variable. +user = ihasaccess + +# Specify a password to access the vCenter host. This setting may also be +# defined with the VMWARE_PASSWORD environment variable. +password = ssshverysecret diff --git a/contrib/inventory/vmware.py b/contrib/inventory/vmware.py new file mode 100755 index 0000000000..b708d59994 --- /dev/null +++ b/contrib/inventory/vmware.py @@ -0,0 +1,432 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +''' +VMware Inventory Script +======================= + +Retrieve information about virtual machines from a vCenter server or +standalone ESX host. When `group_by=false` (in the INI file), host systems +are also returned in addition to VMs. + +This script will attempt to read configuration from an INI file with the same +base filename if present, or `vmware.ini` if not. It is possible to create +symlinks to the inventory script to support multiple configurations, e.g.: + +* `vmware.py` (this script) +* `vmware.ini` (default configuration, will be read by `vmware.py`) +* `vmware_test.py` (symlink to `vmware.py`) +* `vmware_test.ini` (test configuration, will be read by `vmware_test.py`) +* `vmware_other.py` (symlink to `vmware.py`, will read `vmware.ini` since no + `vmware_other.ini` exists) + +The path to an INI file may also be specified via the `VMWARE_INI` environment +variable, in which case the filename matching rules above will not apply. + +Host and authentication parameters may be specified via the `VMWARE_HOST`, +`VMWARE_USER` and `VMWARE_PASSWORD` environment variables; these options will +take precedence over options present in the INI file. An INI file is not +required if these options are specified using environment variables. +''' + +import collections +import json +import logging +import optparse +import os +import sys +import time +import ConfigParser + +# Disable logging message trigged by pSphere/suds. +try: + from logging import NullHandler +except ImportError: + from logging import Handler + class NullHandler(Handler): + def emit(self, record): + pass +logging.getLogger('psphere').addHandler(NullHandler()) +logging.getLogger('suds').addHandler(NullHandler()) + +from psphere.client import Client +from psphere.errors import ObjectNotFoundError +from psphere.managedobjects import HostSystem, VirtualMachine, ManagedObject, Network +from suds.sudsobject import Object as SudsObject + + +class VMwareInventory(object): + + def __init__(self, guests_only=None): + self.config = ConfigParser.SafeConfigParser() + if os.environ.get('VMWARE_INI', ''): + config_files = [os.environ['VMWARE_INI']] + else: + config_files = [os.path.abspath(sys.argv[0]).rstrip('.py') + '.ini', 'vmware.ini'] + for config_file in config_files: + if os.path.exists(config_file): + self.config.read(config_file) + break + + # Retrieve only guest VMs, or include host systems? + if guests_only is not None: + self.guests_only = guests_only + elif self.config.has_option('defaults', 'guests_only'): + self.guests_only = self.config.getboolean('defaults', 'guests_only') + else: + self.guests_only = True + + # Read authentication information from VMware environment variables + # (if set), otherwise from INI file. + auth_host = os.environ.get('VMWARE_HOST') + if not auth_host and self.config.has_option('auth', 'host'): + auth_host = self.config.get('auth', 'host') + auth_user = os.environ.get('VMWARE_USER') + if not auth_user and self.config.has_option('auth', 'user'): + auth_user = self.config.get('auth', 'user') + auth_password = os.environ.get('VMWARE_PASSWORD') + if not auth_password and self.config.has_option('auth', 'password'): + auth_password = self.config.get('auth', 'password') + + # Create the VMware client connection. + self.client = Client(auth_host, auth_user, auth_password) + + def _put_cache(self, name, value): + ''' + Saves the value to cache with the name given. + ''' + if self.config.has_option('defaults', 'cache_dir'): + cache_dir = os.path.expanduser(self.config.get('defaults', 'cache_dir')) + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + cache_file = os.path.join(cache_dir, name) + with open(cache_file, 'w') as cache: + json.dump(value, cache) + + def _get_cache(self, name, default=None): + ''' + Retrieves the value from cache for the given name. + ''' + if self.config.has_option('defaults', 'cache_dir'): + cache_dir = self.config.get('defaults', 'cache_dir') + cache_file = os.path.join(cache_dir, name) + if os.path.exists(cache_file): + if self.config.has_option('defaults', 'cache_max_age'): + cache_max_age = self.config.getint('defaults', 'cache_max_age') + else: + cache_max_age = 0 + cache_stat = os.stat(cache_file) + if (cache_stat.st_mtime + cache_max_age) >= time.time(): + with open(cache_file) as cache: + return json.load(cache) + return default + + def _flatten_dict(self, d, parent_key='', sep='_'): + ''' + Flatten nested dicts by combining keys with a separator. Lists with + only string items are included as is; any other lists are discarded. + ''' + items = [] + for k, v in d.items(): + if k.startswith('_'): + continue + new_key = parent_key + sep + k if parent_key else k + if isinstance(v, collections.MutableMapping): + items.extend(self._flatten_dict(v, new_key, sep).items()) + elif isinstance(v, (list, tuple)): + if all([isinstance(x, basestring) for x in v]): + items.append((new_key, v)) + else: + items.append((new_key, v)) + return dict(items) + + def _get_obj_info(self, obj, depth=99, seen=None): + ''' + Recursively build a data structure for the given pSphere object (depth + only applies to ManagedObject instances). + ''' + seen = seen or set() + if isinstance(obj, ManagedObject): + try: + obj_unicode = unicode(getattr(obj, 'name')) + except AttributeError: + obj_unicode = () + if obj in seen: + return obj_unicode + seen.add(obj) + if depth <= 0: + return obj_unicode + d = {} + for attr in dir(obj): + if attr.startswith('_'): + continue + try: + val = getattr(obj, attr) + obj_info = self._get_obj_info(val, depth - 1, seen) + if obj_info != (): + d[attr] = obj_info + except Exception, e: + pass + return d + elif isinstance(obj, SudsObject): + d = {} + for key, val in iter(obj): + obj_info = self._get_obj_info(val, depth, seen) + if obj_info != (): + d[key] = obj_info + return d + elif isinstance(obj, (list, tuple)): + l = [] + for val in iter(obj): + obj_info = self._get_obj_info(val, depth, seen) + if obj_info != (): + l.append(obj_info) + return l + elif isinstance(obj, (type(None), bool, int, long, float, basestring)): + return obj + else: + return () + + def _get_host_info(self, host, prefix='vmware'): + ''' + Return a flattened dict with info about the given host system. + ''' + host_info = { + 'name': host.name, + } + for attr in ('datastore', 'network', 'vm'): + try: + value = getattr(host, attr) + host_info['%ss' % attr] = self._get_obj_info(value, depth=0) + except AttributeError: + host_info['%ss' % attr] = [] + for k, v in self._get_obj_info(host.summary, depth=0).items(): + if isinstance(v, collections.MutableMapping): + for k2, v2 in v.items(): + host_info[k2] = v2 + elif k != 'host': + host_info[k] = v + try: + host_info['ipAddress'] = host.config.network.vnic[0].spec.ip.ipAddress + except Exception, e: + print >> sys.stderr, e + host_info = self._flatten_dict(host_info, prefix) + if ('%s_ipAddress' % prefix) in host_info: + host_info['ansible_ssh_host'] = host_info['%s_ipAddress' % prefix] + return host_info + + def _get_vm_info(self, vm, prefix='vmware'): + ''' + Return a flattened dict with info about the given virtual machine. + ''' + vm_info = { + 'name': vm.name, + } + for attr in ('datastore', 'network'): + try: + value = getattr(vm, attr) + vm_info['%ss' % attr] = self._get_obj_info(value, depth=0) + except AttributeError: + vm_info['%ss' % attr] = [] + try: + vm_info['resourcePool'] = self._get_obj_info(vm.resourcePool, depth=0) + except AttributeError: + vm_info['resourcePool'] = '' + try: + vm_info['guestState'] = vm.guest.guestState + except AttributeError: + vm_info['guestState'] = '' + for k, v in self._get_obj_info(vm.summary, depth=0).items(): + if isinstance(v, collections.MutableMapping): + for k2, v2 in v.items(): + if k2 == 'host': + k2 = 'hostSystem' + vm_info[k2] = v2 + elif k != 'vm': + vm_info[k] = v + vm_info = self._flatten_dict(vm_info, prefix) + if ('%s_ipAddress' % prefix) in vm_info: + vm_info['ansible_ssh_host'] = vm_info['%s_ipAddress' % prefix] + return vm_info + + def _add_host(self, inv, parent_group, host_name): + ''' + Add the host to the parent group in the given inventory. + ''' + p_group = inv.setdefault(parent_group, []) + if isinstance(p_group, dict): + group_hosts = p_group.setdefault('hosts', []) + else: + group_hosts = p_group + if host_name not in group_hosts: + group_hosts.append(host_name) + + def _add_child(self, inv, parent_group, child_group): + ''' + Add a child group to a parent group in the given inventory. + ''' + if parent_group != 'all': + p_group = inv.setdefault(parent_group, {}) + if not isinstance(p_group, dict): + inv[parent_group] = {'hosts': p_group} + p_group = inv[parent_group] + group_children = p_group.setdefault('children', []) + if child_group not in group_children: + group_children.append(child_group) + inv.setdefault(child_group, []) + + def get_inventory(self, meta_hostvars=True): + ''' + Reads the inventory from cache or VMware API via pSphere. + ''' + # Use different cache names for guests only vs. all hosts. + if self.guests_only: + cache_name = '__inventory_guests__' + else: + cache_name = '__inventory_all__' + + inv = self._get_cache(cache_name, None) + if inv is not None: + return inv + + inv = {'all': {'hosts': []}} + if meta_hostvars: + inv['_meta'] = {'hostvars': {}} + + default_group = os.path.basename(sys.argv[0]).rstrip('.py') + + if not self.guests_only: + if self.config.has_option('defaults', 'hw_group'): + hw_group = self.config.get('defaults', 'hw_group') + else: + hw_group = default_group + '_hw' + + if self.config.has_option('defaults', 'vm_group'): + vm_group = self.config.get('defaults', 'vm_group') + else: + vm_group = default_group + '_vm' + + if self.config.has_option('defaults', 'prefix_filter'): + prefix_filter = self.config.get('defaults', 'prefix_filter') + else: + prefix_filter = None + + # Loop through physical hosts: + for host in HostSystem.all(self.client): + + if not self.guests_only: + self._add_host(inv, 'all', host.name) + self._add_host(inv, hw_group, host.name) + host_info = self._get_host_info(host) + if meta_hostvars: + inv['_meta']['hostvars'][host.name] = host_info + self._put_cache(host.name, host_info) + + # Loop through all VMs on physical host. + for vm in host.vm: + if prefix_filter: + if vm.name.startswith( prefix_filter ): + continue + self._add_host(inv, 'all', vm.name) + self._add_host(inv, vm_group, vm.name) + vm_info = self._get_vm_info(vm) + if meta_hostvars: + inv['_meta']['hostvars'][vm.name] = vm_info + self._put_cache(vm.name, vm_info) + + # Group by resource pool. + vm_resourcePool = vm_info.get('vmware_resourcePool', None) + if vm_resourcePool: + self._add_child(inv, vm_group, 'resource_pools') + self._add_child(inv, 'resource_pools', vm_resourcePool) + self._add_host(inv, vm_resourcePool, vm.name) + + # Group by datastore. + for vm_datastore in vm_info.get('vmware_datastores', []): + self._add_child(inv, vm_group, 'datastores') + self._add_child(inv, 'datastores', vm_datastore) + self._add_host(inv, vm_datastore, vm.name) + + # Group by network. + for vm_network in vm_info.get('vmware_networks', []): + self._add_child(inv, vm_group, 'networks') + self._add_child(inv, 'networks', vm_network) + self._add_host(inv, vm_network, vm.name) + + # Group by guest OS. + vm_guestId = vm_info.get('vmware_guestId', None) + if vm_guestId: + self._add_child(inv, vm_group, 'guests') + self._add_child(inv, 'guests', vm_guestId) + self._add_host(inv, vm_guestId, vm.name) + + # Group all VM templates. + vm_template = vm_info.get('vmware_template', False) + if vm_template: + self._add_child(inv, vm_group, 'templates') + self._add_host(inv, 'templates', vm.name) + + self._put_cache(cache_name, inv) + return inv + + def get_host(self, hostname): + ''' + Read info about a specific host or VM from cache or VMware API. + ''' + inv = self._get_cache(hostname, None) + if inv is not None: + return inv + + if not self.guests_only: + try: + host = HostSystem.get(self.client, name=hostname) + inv = self._get_host_info(host) + except ObjectNotFoundError: + pass + + if inv is None: + try: + vm = VirtualMachine.get(self.client, name=hostname) + inv = self._get_vm_info(vm) + except ObjectNotFoundError: + pass + + if inv is not None: + self._put_cache(hostname, inv) + return inv or {} + + +def main(): + parser = optparse.OptionParser() + parser.add_option('--list', action='store_true', dest='list', + default=False, help='Output inventory groups and hosts') + parser.add_option('--host', dest='host', default=None, metavar='HOST', + help='Output variables only for the given hostname') + # Additional options for use when running the script standalone, but never + # used by Ansible. + parser.add_option('--pretty', action='store_true', dest='pretty', + default=False, help='Output nicely-formatted JSON') + parser.add_option('--include-host-systems', action='store_true', + dest='include_host_systems', default=False, + help='Include host systems in addition to VMs') + parser.add_option('--no-meta-hostvars', action='store_false', + dest='meta_hostvars', default=True, + help='Exclude [\'_meta\'][\'hostvars\'] with --list') + options, args = parser.parse_args() + + if options.include_host_systems: + vmware_inventory = VMwareInventory(guests_only=False) + else: + vmware_inventory = VMwareInventory() + if options.host is not None: + inventory = vmware_inventory.get_host(options.host) + else: + inventory = vmware_inventory.get_inventory(options.meta_hostvars) + + json_kwargs = {} + if options.pretty: + json_kwargs.update({'indent': 4, 'sort_keys': True}) + json.dump(inventory, sys.stdout, **json_kwargs) + + +if __name__ == '__main__': + main() diff --git a/plugins/inventory/windows_azure.ini b/contrib/inventory/windows_azure.ini similarity index 100% rename from plugins/inventory/windows_azure.ini rename to contrib/inventory/windows_azure.ini diff --git a/plugins/inventory/windows_azure.py b/contrib/inventory/windows_azure.py similarity index 100% rename from plugins/inventory/windows_azure.py rename to contrib/inventory/windows_azure.py diff --git a/plugins/inventory/zabbix.ini b/contrib/inventory/zabbix.ini similarity index 100% rename from plugins/inventory/zabbix.ini rename to contrib/inventory/zabbix.ini diff --git a/plugins/inventory/zabbix.py b/contrib/inventory/zabbix.py similarity index 96% rename from plugins/inventory/zabbix.py rename to contrib/inventory/zabbix.py index 68cc5cc57b..2bc1e2e1cc 100755 --- a/plugins/inventory/zabbix.py +++ b/contrib/inventory/zabbix.py @@ -20,7 +20,7 @@ ###################################################################### """ -Zabbix Server external inventory script. +Zabbix Server external inventory script. ======================================== Returns hosts and hostgroups from Zabbix Server. @@ -31,7 +31,6 @@ Tested with Zabbix Server 2.0.6. """ import os, sys -import json import argparse import ConfigParser @@ -55,7 +54,7 @@ class ZabbixInventory(object): if config.has_option('zabbix', 'server'): self.zabbix_server = config.get('zabbix', 'server') - # login + # login if config.has_option('zabbix', 'username'): self.zabbix_username = config.get('zabbix', 'username') if config.has_option('zabbix', 'password'): @@ -84,7 +83,7 @@ class ZabbixInventory(object): for host in hostsData: hostname = host['name'] - data[self.defaultgroup]['hosts'].append(hostname) + data[self.defaultgroup]['hosts'].append(hostname) for group in host['groups']: groupname = group['name'] diff --git a/contrib/inventory/zone.py b/contrib/inventory/zone.py new file mode 100755 index 0000000000..163912c7f4 --- /dev/null +++ b/contrib/inventory/zone.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + +# (c) 2015, Dagobert Michelsen +# +# This file is part of Ansible, +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + +from subprocess import Popen,PIPE +import sys +import json + +result = {} +result['all'] = {} + +pipe = Popen(['zoneadm', 'list', '-ip'], stdout=PIPE, universal_newlines=True) +result['all']['hosts'] = [] +for l in pipe.stdout.readlines(): + # 1:work:running:/zones/work:3126dc59-9a07-4829-cde9-a816e4c5040e:native:shared + s = l.split(':') + if s[1] != 'global': + result['all']['hosts'].append(s[1]) + +result['all']['vars'] = {} +result['all']['vars']['ansible_connection'] = 'zone' + +if len(sys.argv) == 2 and sys.argv[1] == '--list': + print json.dumps(result) +elif len(sys.argv) == 3 and sys.argv[1] == '--host': + print json.dumps({'ansible_connection': 'zone'}) +else: + print "Need an argument, either --list or --host " diff --git a/docs/man/man1/ansible-doc.1 b/docs/man/man1/ansible-doc.1 index 041cf48099..2d5068d0d3 100644 --- a/docs/man/man1/ansible-doc.1 +++ b/docs/man/man1/ansible-doc.1 @@ -1,13 +1,13 @@ '\" t .\" Title: ansible-doc .\" Author: :doctype:manpage -.\" Generator: DocBook XSL Stylesheets v1.76.1 -.\" Date: 05/26/2014 +.\" Generator: DocBook XSL Stylesheets v1.78.1 +.\" Date: 12/09/2014 .\" Manual: System administration commands -.\" Source: Ansible 1.7 +.\" Source: Ansible 1.9 .\" Language: English .\" -.TH "ANSIBLE\-DOC" "1" "05/26/2014" "Ansible 1\&.7" "System administration commands" +.TH "ANSIBLE\-DOC" "1" "12/09/2014" "Ansible 1\&.9" "System administration commands" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -64,9 +64,3 @@ Ansible is released under the terms of the GPLv3 License\&. \fBansible\-playbook\fR(1), \fBansible\fR(1), \fBansible\-pull\fR(1) .sp Extensive documentation is available in the documentation site: http://docs\&.ansible\&.com\&. IRC and mailing list info can be found in file CONTRIBUTING\&.md, available in: https://github\&.com/ansible/ansible -.SH "AUTHOR" -.PP -\fB:doctype:manpage\fR -.RS 4 -Author. -.RE diff --git a/docs/man/man1/ansible-galaxy.1 b/docs/man/man1/ansible-galaxy.1 index 5bac353505..f8486c75f4 100644 --- a/docs/man/man1/ansible-galaxy.1 +++ b/docs/man/man1/ansible-galaxy.1 @@ -1,13 +1,13 @@ '\" t .\" Title: ansible-galaxy .\" Author: [see the "AUTHOR" section] -.\" Generator: DocBook XSL Stylesheets v1.76.1 -.\" Date: 05/26/2014 +.\" Generator: DocBook XSL Stylesheets v1.78.1 +.\" Date: 05/05/2015 .\" Manual: System administration commands -.\" Source: Ansible 1.7 +.\" Source: Ansible 2.0.0 .\" Language: English .\" -.TH "ANSIBLE\-GALAXY" "1" "05/26/2014" "Ansible 1\&.7" "System administration commands" +.TH "ANSIBLE\-GALAXY" "1" "05/05/2015" "Ansible 2\&.0\&.0" "System administration commands" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -149,6 +149,11 @@ Force overwriting an existing role\&. .RS 4 The path in which the skeleton role will be created\&.The default is the current working directory\&. .RE +.PP +\fB\-\-offline\fR +.RS 4 +Don\(cqt query the galaxy API when creating roles +.RE .SH "LIST" .sp The \fBlist\fR sub\-command is used to show what roles are currently instaled\&. You can specify a role name, and if installed only that role will be shown\&. diff --git a/docs/man/man1/ansible-galaxy.1.asciidoc.in b/docs/man/man1/ansible-galaxy.1.asciidoc.in index b8a80e6b2c..3d59e31706 100644 --- a/docs/man/man1/ansible-galaxy.1.asciidoc.in +++ b/docs/man/man1/ansible-galaxy.1.asciidoc.in @@ -122,6 +122,10 @@ Force overwriting an existing role. The path in which the skeleton role will be created.The default is the current working directory. +*--offline*:: + +Don't query the galaxy API when creating roles + LIST ---- diff --git a/docs/man/man1/ansible-playbook.1 b/docs/man/man1/ansible-playbook.1 index 63f8904f0c..0c820b72e3 100644 --- a/docs/man/man1/ansible-playbook.1 +++ b/docs/man/man1/ansible-playbook.1 @@ -1,13 +1,13 @@ '\" t .\" Title: ansible-playbook .\" Author: :doctype:manpage -.\" Generator: DocBook XSL Stylesheets v1.76.1 -.\" Date: 05/26/2014 +.\" Generator: DocBook XSL Stylesheets v1.78.1 +.\" Date: 07/23/2015 .\" Manual: System administration commands -.\" Source: Ansible 1.7 +.\" Source: Ansible %VERSION% .\" Language: English .\" -.TH "ANSIBLE\-PLAYBOOK" "1" "05/26/2014" "Ansible 1\&.7" "System administration commands" +.TH "ANSIBLE\-PLAYBOOK" "1" "07/23/2015" "Ansible %VERSION%" "System administration commands" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -43,39 +43,6 @@ The names of one or more YAML format files to run as ansible playbooks\&. .RE .SH "OPTIONS" .PP -\fB\-v\fR, \fB\-\-verbose\fR -.RS 4 -Verbose mode, more output from successful actions will be shown\&. Give up to three times for more output\&. -.RE -.PP -\fB\-i\fR \fIPATH\fR, \fB\-\-inventory=\fR\fIPATH\fR -.RS 4 -The -\fIPATH\fR -to the inventory hosts file, which defaults to -\fI/etc/ansible/hosts\fR\&. -.RE -.PP -\fB\-M\fR \fIDIRECTORY\fR, \fB\-\-module\-path=\fR\fIDIRECTORY\fR -.RS 4 -The -\fIDIRECTORY\fR -search path to load modules from\&. The default is -\fI/usr/share/ansible\fR\&. This can also be set with the ANSIBLE_LIBRARY environment variable\&. -.RE -.PP -\fB\-e\fR \fIVARS\fR, \fB\-\-extra\-vars=\fR\fIVARS\fR -.RS 4 -Extra variables to inject into a playbook, in key=value key=value format or as quoted JSON (hashes and arrays)\&. -.RE -.PP -\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR -.RS 4 -Level of parallelism\&. -\fINUM\fR -is specified as an integer, the default is 5\&. -.RE -.PP \fB\-k\fR, \fB\-\-ask\-pass\fR .RS 4 Prompt for the SSH password instead of assuming key\-based authentication with ssh\-agent\&. @@ -86,52 +53,26 @@ Prompt for the SSH password instead of assuming key\-based authentication with s Prompt for the password to use for playbook plays that request sudo access, if any\&. .RE .PP -\fB\-U\fR, \fISUDO_USER\fR, \fB\-\-sudo\-user=\fR\fISUDO_USER\fR +\fB\-b\fR, \fB\-\-become\fR .RS 4 -Desired sudo user (default=root)\&. +Run operations with become (nopasswd implied) .RE .PP -\fB\-t\fR, \fITAGS\fR, \fB\-\-tags=\fR\fITAGS\fR +\fB\-\-become\-method=BECOME_METHOD\fR .RS 4 -Only run plays and tasks tagged with these values\&. +Privilege escalation method to use (default=sudo), valid choices: [ sudo | su | pbrun | pfexec | runas ] .RE .PP -\fB\-\-skip\-tags=\fR\fISKIP_TAGS\fR +\fB\-\-become\-user=BECOME_USER\fR .RS 4 -Only run plays and tasks whose tags do not match these values\&. +Run operations as this user (default=None)\&. .RE .PP -\fB\-\-syntax\-check\fR -.RS 4 -Look for syntax errors in the playbook, but don\(cqt run anything -.RE -.PP -\fB\-\-check\fR +\fB\-C\fR, \fB\-\-check\fR .RS 4 Do not make any changes on the remote system, but test resources to see what might have changed\&. Note this can not scan all possible resource types and is only a simulation\&. .RE .PP -\fB\-\-diff\fR -.RS 4 -When changing any templated files, show the unified diffs of how they changed\&. When used with \-\-check, shows how the files would have changed if \-\-check were not used\&. -.RE -.PP -\fB\-T\fR \fISECONDS\fR, \fB\-\-timeout=\fR\fISECONDS\fR -.RS 4 -Connection timeout to use when trying to talk to hosts, in -\fISECONDS\fR\&. -.RE -.PP -\fB\-s\fR, \fB\-\-sudo\fR -.RS 4 -Force all plays to use sudo, even if not marked as such\&. -.RE -.PP -\fB\-u\fR \fIUSERNAME\fR, \fB\-\-user=\fR\fIUSERNAME\fR -.RS 4 -Use this remote user name on playbook steps that do not indicate a user name to run as\&. -.RE -.PP \fB\-c\fR \fICONNECTION\fR, \fB\-\-connection=\fR\fICONNECTION\fR .RS 4 Connection type to use\&. Possible options are @@ -143,15 +84,159 @@ Connection type to use\&. Possible options are is mostly useful for crontab or kickstarts\&. .RE .PP +\fB\-D\fR, \fB\-\-diff\fR +.RS 4 +When changing any templated files, show the unified diffs of how they changed\&. When used with \-\-check, shows how the files would have changed if \-\-check were not used\&. +.RE +.PP +\fB\-e\fR \fIVARS\fR, \fB\-\-extra\-vars=\fR\fIVARS\fR +.RS 4 +Extra variables to inject into a playbook, in key=value key=value format or as quoted JSON (hashes and arrays)\&. To load variables from a file, specify the file preceded by @ (e\&.g\&. @vars\&.yml)\&. +.RE +.PP +\fB\-\-flush\-cache\fR +.RS 4 +Clear the fact cache\&. +.RE +.PP +\fB\-\-force\-handlers\fR +.RS 4 +Run handlers even if a task fails\&. +.RE +.PP +\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR +.RS 4 +Level of parallelism\&. +\fINUM\fR +is specified as an integer, the default is 5\&. +.RE +.PP +\fB\-h\fR, \fB\-\-help\fR +.RS 4 +Show help page and exit +.RE +.PP +\fB\-i\fR \fIPATH\fR, \fB\-\-inventory=\fR\fIPATH\fR +.RS 4 +The +\fIPATH\fR +to the inventory hosts file, which defaults to +\fI/etc/ansible/hosts\fR\&. +.RE +.PP \fB\-l\fR \fISUBSET\fR, \fB\-\-limit=\fR\fISUBSET\fR .RS 4 Further limits the selected host/group patterns\&. .RE +.PP +\fB\-\-list\-hosts\fR +.RS 4 +Outputs a list of matching hosts; does not execute anything else\&. +.RE +.PP +\fB\-\-list\-tags\fR +.RS 4 +List all available tags\&. +.RE +.PP +\fB\-\-list\-tasks\fR +.RS 4 +List all tasks that would be executed +.RE +.PP +\fB\-M\fR \fIDIRECTORY\fR, \fB\-\-module\-path=\fR\fIDIRECTORY\fR +.RS 4 +The +\fIDIRECTORY\fR +search path to load modules from\&. The default is +\fI/usr/share/ansible\fR\&. This can also be set with the ANSIBLE_LIBRARY environment variable\&. +.RE +.PP +\fB\-\-private\-key=\fR\fIPRIVATE_KEY_FILE\fR +.RS 4 +Use this file to authenticate the connection +.RE +.PP +\fB\-\-skip\-tages=\fR\fISKIP_TAGS\fR +.RS 4 +Only run plays and tasks whose tags do not match these values\&. +.RE +.PP +\fB\-\-start\-at\-task=\fR\fISTART_AT\fR +.RS 4 +Start the playbook at the task matching this name\&. +.RE +.PP +\fB\-\-step\fR +.RS 4 +One\-step\-at\-a\-time: confirm each task before running\&. +.RE +.PP +\fB\-S\fR, \-\-su* +.RS 4 +Run operations with su (deprecated, use become) +.RE +.PP +\fB\-R SU\-USER\fR, \fB\-\-su\-user=\fR\fISU_USER\fR +.RS 4 +run operations with su as this user (default=root) (deprecated, use become) +.RE +.PP +\fB\-s\fR, \fB\-\-sudo\fR +.RS 4 +Run operations with sudo (nopasswd) (deprecated, use become) +.RE +.PP +\fB\-U\fR, \fISUDO_USER\fR, \fB\-\-sudo\-user=\fR\fISUDO_USER\fR +.RS 4 +Desired sudo user (default=root) (deprecated, use become)\&. +.RE +.PP +\fB\-\-skip\-tags=\fR\fISKIP_TAGS\fR +.RS 4 +Only run plays and tasks whose tags do not match these values\&. +.RE +.PP +\fB\-\-syntax\-check\fR +.RS 4 +Look for syntax errors in the playbook, but don\(cqt run anything +.RE +.PP +\fB\-t\fR, \fITAGS\fR, \fB\-\-tags=\fR\fITAGS\fR +.RS 4 +Only run plays and tasks tagged with these values\&. +.RE +.PP +\fB\-T\fR \fISECONDS\fR, \fB\-\-timeout=\fR\fISECONDS\fR +.RS 4 +Connection timeout to use when trying to talk to hosts, in +\fISECONDS\fR\&. +.RE +.PP +\fB\-u\fR \fIUSERNAME\fR, \fB\-\-user=\fR\fIUSERNAME\fR +.RS 4 +Use this remote user name on playbook steps that do not indicate a user name to run as\&. +.RE +.PP +\fB\-\-vault\-password\-file=\fR\fIVAULT_PASSWORD_FILE\fR +.RS 4 +Vault password file\&. +.RE +.PP +\fB\-v\fR, \fB\-\-verbose\fR +.RS 4 +Verbose mode, more output from successful actions will be shown\&. Give up to three times for more output\&. +.RE +.PP +\fB\-\-version\fR +.RS 4 +Show program\(cqs version number and exit\&. +.RE .SH "ENVIRONMENT" .sp The following environment variables may be specified\&. .sp -ANSIBLE_HOSTS \(em Override the default ansible hosts file +ANSIBLE_INVENTORY \(em Override the default ansible inventory file .sp ANSIBLE_LIBRARY \(em Override the default ansible module library path .SH "FILES" diff --git a/docs/man/man1/ansible-playbook.1.asciidoc.in b/docs/man/man1/ansible-playbook.1.asciidoc.in index 23fe37a2c0..8b8ba9c468 100644 --- a/docs/man/man1/ansible-playbook.1.asciidoc.in +++ b/docs/man/man1/ansible-playbook.1.asciidoc.in @@ -34,16 +34,87 @@ The names of one or more YAML format files to run as ansible playbooks. OPTIONS ------- -*-v*, *--verbose*:: +*-k*, *--ask-pass*:: -Verbose mode, more output from successful actions will be shown. Give -up to three times for more output. +Prompt for the SSH password instead of assuming key-based +authentication with ssh-agent. + +*-K*, *--ask-sudo-pass*:: + +Prompt for the password to use for playbook plays that request sudo +access, if any. + +*-b*, *--become*:: + +Run operations with become (nopasswd implied) + +*--become-method=BECOME_METHOD*:: + +Privilege escalation method to use (default=sudo), +valid choices: [ sudo | su | pbrun | pfexec | runas ] + +*--become-user=BECOME_USER*:: + +Run operations as this user (default=None). + +*-C*, *--check*:: + +Do not make any changes on the remote system, but test resources to see what might +have changed. Note this can not scan all possible resource types and is only +a simulation. + +*-c* 'CONNECTION', *--connection=*'CONNECTION':: + +Connection type to use. Possible options are 'paramiko' (SSH), 'ssh', +and 'local'. 'local' is mostly useful for crontab or kickstarts. + +*-D*, *--diff*:: + +When changing any templated files, show the unified diffs of how they changed. When +used with --check, shows how the files would have changed if --check were not used. + +*-e* 'VARS', *--extra-vars=*'VARS':: + +Extra variables to inject into a playbook, in key=value key=value format or +as quoted JSON (hashes and arrays). To load variables from a file, specify +the file preceded by @ (e.g. @vars.yml). + +*--flush-cache*:: + +Clear the fact cache. + +*--force-handlers*:: + +Run handlers even if a task fails. + +*-f* 'NUM', *--forks=*'NUM':: + +Level of parallelism. 'NUM' is specified as an integer, the default is 5. + +*-h*, *--help*:: + +Show help page and exit *-i* 'PATH', *--inventory=*'PATH':: The 'PATH' to the inventory hosts file, which defaults to '/etc/ansible/hosts'. +*-l* 'SUBSET', *--limit=*'SUBSET':: + +Further limits the selected host/group patterns. + +*--list-hosts*:: + +Outputs a list of matching hosts; does not execute anything else. + +*--list-tags*:: + +List all available tags. + +*--list-tasks*:: + +List all tasks that would be executed *-M* 'DIRECTORY', *--module-path=*'DIRECTORY':: @@ -51,34 +122,38 @@ The 'DIRECTORY' search path to load modules from. The default is '/usr/share/ansible'. This can also be set with the ANSIBLE_LIBRARY environment variable. -*-e* 'VARS', *--extra-vars=*'VARS':: +*--private-key=*'PRIVATE_KEY_FILE':: -Extra variables to inject into a playbook, in key=value key=value format or -as quoted JSON (hashes and arrays). +Use this file to authenticate the connection -*-f* 'NUM', *--forks=*'NUM':: +*--skip-tages=*'SKIP_TAGS':: -Level of parallelism. 'NUM' is specified as an integer, the default is 5. +Only run plays and tasks whose tags do not match these values. +*--start-at-task=*'START_AT':: -*-k*, *--ask-pass*:: +Start the playbook at the task matching this name. -Prompt for the SSH password instead of assuming key-based -authentication with ssh-agent. +*--step*:: +One-step-at-a-time: confirm each task before running. -*-K*, *--ask-sudo-pass*:: +*-S*, --su*:: -Prompt for the password to use for playbook plays that request sudo -access, if any. +Run operations with su (deprecated, use become) + +*-R SU-USER*, *--su-user=*'SU_USER':: + +run operations with su as this user (default=root) +(deprecated, use become) + +*-s*, *--sudo*:: + +Run operations with sudo (nopasswd) (deprecated, use become) *-U*, 'SUDO_USER', *--sudo-user=*'SUDO_USER':: -Desired sudo user (default=root). - -*-t*, 'TAGS', *--tags=*'TAGS':: - -Only run plays and tasks tagged with these values. +Desired sudo user (default=root) (deprecated, use become). *--skip-tags=*'SKIP_TAGS':: @@ -88,48 +163,38 @@ Only run plays and tasks whose tags do not match these values. Look for syntax errors in the playbook, but don't run anything -*--check*:: +*-t*, 'TAGS', *--tags=*'TAGS':: -Do not make any changes on the remote system, but test resources to see what might -have changed. Note this can not scan all possible resource types and is only -a simulation. - -*--diff*:: - -When changing any templated files, show the unified diffs of how they changed. When -used with --check, shows how the files would have changed if --check were not used. +Only run plays and tasks tagged with these values. *-T* 'SECONDS', *--timeout=*'SECONDS':: Connection timeout to use when trying to talk to hosts, in 'SECONDS'. - -*-s*, *--sudo*:: - -Force all plays to use sudo, even if not marked as such. - - *-u* 'USERNAME', *--user=*'USERNAME':: Use this remote user name on playbook steps that do not indicate a user name to run as. -*-c* 'CONNECTION', *--connection=*'CONNECTION':: +*--vault-password-file=*'VAULT_PASSWORD_FILE':: -Connection type to use. Possible options are 'paramiko' (SSH), 'ssh', -and 'local'. 'local' is mostly useful for crontab or kickstarts. +Vault password file. -*-l* 'SUBSET', *--limit=*'SUBSET':: +*-v*, *--verbose*:: -Further limits the selected host/group patterns. +Verbose mode, more output from successful actions will be shown. Give +up to three times for more output. +*--version*:: + +Show program's version number and exit. ENVIRONMENT ----------- The following environment variables may be specified. -ANSIBLE_HOSTS -- Override the default ansible hosts file +ANSIBLE_INVENTORY -- Override the default ansible inventory file ANSIBLE_LIBRARY -- Override the default ansible module library path diff --git a/docs/man/man1/ansible-pull.1 b/docs/man/man1/ansible-pull.1 index 58029eabb8..8e9bc6a8f5 100644 --- a/docs/man/man1/ansible-pull.1 +++ b/docs/man/man1/ansible-pull.1 @@ -1,13 +1,13 @@ '\" t .\" Title: ansible .\" Author: :doctype:manpage -.\" Generator: DocBook XSL Stylesheets v1.76.1 -.\" Date: 05/26/2014 +.\" Generator: DocBook XSL Stylesheets v1.78.1 +.\" Date: 07/22/2015 .\" Manual: System administration commands -.\" Source: Ansible 1.7 +.\" Source: Ansible %VERSION% .\" Language: English .\" -.TH "ANSIBLE" "1" "05/26/2014" "Ansible 1\&.7" "System administration commands" +.TH "ANSIBLE" "1" "07/22/2015" "Ansible %VERSION%" "System administration commands" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -31,7 +31,7 @@ ansible-pull \- set up a remote copy of ansible on each managed node .SH "SYNOPSIS" .sp -ansible \-d DEST \-U URL [options] [ ] +ansible\-pull \-d DEST \-U URL [options] [ ] .SH "DESCRIPTION" .sp \fBAnsible\fR is an extra\-simple tool/framework/API for doing \*(Aqremote things\*(Aq over SSH\&. @@ -50,14 +50,14 @@ The name of one the YAML format files to run as an ansible playbook\&. This can .RE .SH "OPTIONS" .PP -\fB\-d\fR \fIDEST\fR, \fB\-\-directory=\fR\fIDEST\fR +\fB\-\-accept\-host\-key\fR .RS 4 -Directory to checkout repository into\&. If not provided, a subdirectory of ~/\&.ansible/pull/ will be used\&. +Adds the hostkey for the repo URL if not already added\&. .RE .PP -\fB\-U\fR \fIURL\fR, \fB\-\-url=\fR\fIURL\fR +\fB\-K\fR, \fB\-\-ask\-sudo\-pass\fR .RS 4 -URL of the playbook repository to checkout\&. +Ask for sudo password\&. .RE .PP \fB\-C\fR \fICHECKOUT\fR, \fB\-\-checkout=\fR\fICHECKOUT\fR @@ -65,11 +65,26 @@ URL of the playbook repository to checkout\&. Branch/Tag/Commit to checkout\&. If not provided, uses default behavior of module used to check out playbook repository\&. .RE .PP +\fB\-d\fR \fIDEST\fR, \fB\-\-directory=\fR\fIDEST\fR +.RS 4 +Directory to checkout repository into\&. If not provided, a subdirectory of ~/\&.ansible/pull/ will be used\&. +.RE +.PP +\fB\-e\fR \fIEXTRA_VARS\fR, \fB\-\-extra\-vars=\fR\*(AqEXTRA_VARS* +.RS 4 +Set additional variables as key=value or YAML/JSON +.RE +.PP \fB\-f\fR, \fB\-\-force\fR .RS 4 Force running of playbook even if unable to update playbook repository\&. This can be useful, for example, to enforce run\-time state when a network connection may not always be up or possible\&. .RE .PP +\fB\-h\fR, \fB\-\-help\fR +.RS 4 +Show the help message and exit\&. +.RE +.PP \fB\-i\fR \fIPATH\fR, \fB\-\-inventory=\fR\fIPATH\fR .RS 4 The @@ -77,9 +92,11 @@ The to the inventory hosts file\&. This can be a relative path within the checkout\&. .RE .PP -\fB\-\-purge\fR +\fB\-\-key\-file=\fR\fIKEYFILE\fR .RS 4 -Purge the checkout after the playbook is run\&. +Pass +\fI\-i \fR +to the SSH arguments used by git\&. .RE .PP \fB\-m\fR \fINAME\fR, \fB\-\-module\-name=\fR\fINAME\fR @@ -89,7 +106,37 @@ Module used to checkout playbook repository\&. Defaults to git\&. .PP \fB\-o\fR, \fB\-\-only\-if\-changed\fR .RS 4 -Run the playbook only if the repository has changed +Only run the playbook if the repository has been updated\&. +.RE +.PP +\fB\-\-purge\fR +.RS 4 +Purge the checkout after the playbook is run\&. +.RE +.PP +\fB\-s\fR \fISLEEP\fR, \fB\-\-sleep=\fR\fISLEEP\fR +.RS 4 +Sleep for random interval (between 0 and SLEEP number of seconds) before starting\&. This is a useful way ot disperse git requests\&. +.RE +.PP +\fB\-t\fR \fITAGS\fR, \fB\-\-tags=\fR\fITAGS\fR +.RS 4 +Only run plays and tasks tagged with these values\&. +.RE +.PP +\fB\-U\fR \fIURL\fR, \fB\-\-url=\fR\fIURL\fR +.RS 4 +URL of the playbook repository to checkout\&. +.RE +.PP +\fB\-\-vault\-password\-file=\fR\fIVAULT_PASSWORD_FILE\fR +.RS 4 +Vault password file\&. +.RE +.PP +\fB\-v\fR, \fB\-\-verbose\fR +.RS 4 +Pass \-vvv to ansible\-playbook\&. .RE .SH "AUTHOR" .sp diff --git a/docs/man/man1/ansible-pull.1.asciidoc.in b/docs/man/man1/ansible-pull.1.asciidoc.in index d75fc63794..b78b7e67a2 100644 --- a/docs/man/man1/ansible-pull.1.asciidoc.in +++ b/docs/man/man1/ansible-pull.1.asciidoc.in @@ -50,19 +50,26 @@ host hostname and finally a playbook named *local.yml*. OPTIONS ------- +*--accept-host-key*:: + +Adds the hostkey for the repo URL if not already added. + +*-K*, *--ask-sudo-pass*:: + +Ask for sudo password. + +*-C* 'CHECKOUT', *--checkout=*'CHECKOUT':: + +Branch/Tag/Commit to checkout. If not provided, uses default behavior of module used to check out playbook repository. + *-d* 'DEST', *--directory=*'DEST':: Directory to checkout repository into. If not provided, a subdirectory of ~/.ansible/pull/ will be used. -*-U* 'URL', *--url=*'URL':: +*-e* 'EXTRA_VARS', *--extra-vars=*'EXTRA_VARS*:: -URL of the playbook repository to checkout. - -*-C* 'CHECKOUT', *--checkout=*'CHECKOUT':: - -Branch/Tag/Commit to checkout. If not provided, uses default behavior -of module used to check out playbook repository. +Set additional variables as key=value or YAML/JSON *-f*, *--force*:: @@ -70,14 +77,17 @@ Force running of playbook even if unable to update playbook repository. This can be useful, for example, to enforce run-time state when a network connection may not always be up or possible. +*-h*, *--help*:: + +Show the help message and exit. + *-i* 'PATH', *--inventory=*'PATH':: -The 'PATH' to the inventory hosts file. This can be a relative path within -the checkout. +The 'PATH' to the inventory hosts file. This can be a relative path within the checkout. -*--purge*:: +*--key-file=*'KEYFILE':: -Purge the checkout after the playbook is run. +Pass '-i ' to the SSH arguments used by git. *-m* 'NAME', *--module-name=*'NAME':: @@ -85,7 +95,32 @@ Module used to checkout playbook repository. Defaults to git. *-o*, *--only-if-changed*:: -Run the playbook only if the repository has changed +Only run the playbook if the repository has been updated. + +*--purge*:: + +Purge the checkout after the playbook is run. + +*-s* 'SLEEP', *--sleep=*'SLEEP':: + +Sleep for random interval (between 0 and SLEEP number of seconds) before starting. This is a useful way ot disperse git requests. + +*-t* 'TAGS', *--tags=*'TAGS':: + +Only run plays and tasks tagged with these values. + +*-U* 'URL', *--url=*'URL':: + +URL of the playbook repository to checkout. + +*--vault-password-file=*'VAULT_PASSWORD_FILE':: + +Vault password file. + +*-v*, *--verbose*:: + +Pass -vvv to ansible-playbook. + AUTHOR ------ diff --git a/docs/man/man1/ansible-vault.1 b/docs/man/man1/ansible-vault.1 index f353e3269f..286e642748 100644 --- a/docs/man/man1/ansible-vault.1 +++ b/docs/man/man1/ansible-vault.1 @@ -1,13 +1,13 @@ '\" t .\" Title: ansible-vault .\" Author: [see the "AUTHOR" section] -.\" Generator: DocBook XSL Stylesheets v1.76.1 -.\" Date: 05/26/2014 +.\" Generator: DocBook XSL Stylesheets v1.78.1 +.\" Date: 12/09/2014 .\" Manual: System administration commands -.\" Source: Ansible 1.7 +.\" Source: Ansible 1.9 .\" Language: English .\" -.TH "ANSIBLE\-VAULT" "1" "05/26/2014" "Ansible 1\&.7" "System administration commands" +.TH "ANSIBLE\-VAULT" "1" "12/09/2014" "Ansible 1\&.9" "System administration commands" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- diff --git a/docs/man/man1/ansible.1 b/docs/man/man1/ansible.1 index 233428782e..83bfc0500d 100644 --- a/docs/man/man1/ansible.1 +++ b/docs/man/man1/ansible.1 @@ -1,13 +1,13 @@ '\" t .\" Title: ansible .\" Author: :doctype:manpage -.\" Generator: DocBook XSL Stylesheets v1.76.1 -.\" Date: 05/26/2014 +.\" Generator: DocBook XSL Stylesheets v1.78.1 +.\" Date: 07/15/2015 .\" Manual: System administration commands -.\" Source: Ansible 1.7 +.\" Source: Ansible %VERSION% .\" Language: English .\" -.TH "ANSIBLE" "1" "05/26/2014" "Ansible 1\&.7" "System administration commands" +.TH "ANSIBLE" "1" "07/15/2015" "Ansible %VERSION%" "System administration commands" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- @@ -43,9 +43,86 @@ A name of a group in the inventory file, a shell\-like glob selecting hosts in i .RE .SH "OPTIONS" .PP -\fB\-v\fR, \fB\-\-verbose\fR +\fB\-a\fR \*(Aq\fIARGUMENTS\fR\*(Aq, \fB\-\-args=\fR\*(Aq\fIARGUMENTS\fR\*(Aq .RS 4 -Verbose mode, more output from successful actions will be shown\&. Give up to three times for more output\&. +The +\fIARGUMENTS\fR +to pass to the module\&. +.RE +.PP +\fB\-\-ask\-become\-pass\fR +.RS 4 +Ask for privilege escalation password\&. +.RE +.PP +\fB\-k\fR, \fB\-\-ask\-pass\fR +.RS 4 +Prompt for the SSH password instead of assuming key\-based authentication with ssh\-agent\&. +.RE +.PP +\fB\-\-ask\-su\-pass\fR +.RS 4 +Prompt for su password (deprecated, use become)\&. +.RE +.PP +\fB\-K\fR, \fB\-\-ask\-sudo\-pass\fR +.RS 4 +Prompt for the password to use with \-\-sudo, if any\&. +.RE +.PP +\fB\-\-ask\-vault\-pass\fR +.RS 4 +Prompt for vault password\&. +.RE +.PP +\fB\-B\fR \fINUM\fR, \fB\-\-background=\fR\fINUM\fR +.RS 4 +Run commands in the background, killing the task after +\fINUM\fR +seconds\&. +.RE +.PP +\fB\-\-become\-method=\fR\fIBECOME_METHOD\fR +.RS 4 +Privilege escalation method to use (default=sudo), valid choices: [ sudo | su | pbrun | pfexec | runas ] +.RE +.PP +\fB\-\-become\-user=\fR\fIBECOME_USER\fR +.RS 4 +Run operations as this user (default=None)\&. +.RE +.PP +\fB\-C\fR, \fB\-\-check\fR +.RS 4 +Don\(cqt make any changes; instead try to predict some of the changes that may occur\&. +.RE +.PP +\fB\-c\fR \fICONNECTION\fR, \fB\-\-connection=\fR\fICONNECTION\fR +.RS 4 +Connection type to use\&. Possible options are +\fIparamiko\fR +(SSH), +\fIssh\fR, and +\fIlocal\fR\&. +\fIlocal\fR +is mostly useful for crontab or kickstarts\&. +.RE +.PP +\fB\-e\fR \fIEXTRA_VARS*, \fR\fI\fB\-\-extra\-vars=\fR\fR\fI\*(AqEXTRA_VARS\fR +.RS 4 +Set additional variables as key=value or YAML/JSON\&. +.RE +.PP +\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR +.RS 4 +Level of parallelism\&. +\fINUM\fR +is specified as an integer, the default is 5\&. +.RE +.PP +\fB\-h\fR, \fB\-\-help\fR +.RS 4 +Show help message and exit\&. .RE .PP \fB\-i\fR \fIPATH\fR, \fB\-\-inventory=\fR\fIPATH\fR @@ -56,16 +133,19 @@ to the inventory hosts file, which defaults to \fI/etc/ansible/hosts\fR\&. .RE .PP -\fB\-f\fR \fINUM\fR, \fB\-\-forks=\fR\fINUM\fR +\fB\-l\fR \fISUBSET\fR, \fB\-\-limit=\fR\fISUBSET\fR .RS 4 -Level of parallelism\&. -\fINUM\fR -is specified as an integer, the default is 5\&. +Further limits the selected host/group patterns\&. .RE .PP -\fB\-\-private\-key=\fR\fIPRIVATE_KEY_FILE\fR +\fB\-l\fR \fI~REGEX\fR, \fB\-\-limit=\fR\fI~REGEX\fR .RS 4 -Use this file to authenticate the connection\&. +Further limits hosts with a regex pattern\&. +.RE +.PP +\fB\-\-list\-hosts\fR +.RS 4 +Outputs a list of matching hosts; does not execute anything else\&. .RE .PP \fB\-m\fR \fINAME\fR, \fB\-\-module\-name=\fR\fINAME\fR @@ -82,33 +162,46 @@ search path to load modules from\&. The default is \fI/usr/share/ansible\fR\&. This can also be set with the ANSIBLE_LIBRARY environment variable\&. .RE .PP -\fB\-a\fR \*(Aq\fIARGUMENTS\fR\*(Aq, \fB\-\-args=\fR\*(Aq\fIARGUMENTS\fR\*(Aq -.RS 4 -The -\fIARGUMENTS\fR -to pass to the module\&. -.RE -.PP -\fB\-k\fR, \fB\-\-ask\-pass\fR -.RS 4 -Prompt for the SSH password instead of assuming key\-based authentication with ssh\-agent\&. -.RE -.PP -\fB\-K\fR, \fB\-\-ask\-sudo\-pass\fR -.RS 4 -Prompt for the password to use with \-\-sudo, if any -.RE -.PP \fB\-o\fR, \fB\-\-one\-line\fR .RS 4 Try to output everything on one line\&. .RE .PP +\fB\-P\fR \fINUM\fR, \fB\-\-poll=\fR\fINUM\fR +.RS 4 +Poll a background job every +\fINUM\fR +seconds\&. Requires +\fB\-B\fR\&. +.RE +.PP +\fB\-\-private\-key=\fR\fIPRIVATE_KEY_FILE\fR +.RS 4 +Use this file to authenticate the connection\&. +.RE +.PP +\fB\-S\fR, \fB\-\-su\fR +.RS 4 +Run operations with su (deprecated, use become)\&. +.RE +.PP +\fB\-R\fR \fISU_USER\fR, \fB\-\-se\-user=\fR\fISUDO_USER\fR +.RS 4 +Run operations with su as this user (default=root) (deprecated, use become) +.RE +.PP \fB\-s\fR, \fB\-\-sudo\fR .RS 4 Run the command as the user given by \-u and sudo to root\&. .RE .PP +\fB\-U\fR \fISUDO_USERNAME\fR, \fB\-\-sudo\-user=\fR\fISUDO_USERNAME\fR +.RS 4 +Sudo to +\fISUDO_USERNAME\fR +instead of root\&. Implies \-\-sudo\&. +.RE +.PP \fB\-t\fR \fIDIRECTORY\fR, \fB\-\-tree=\fR\fIDIRECTORY\fR .RS 4 Save contents in this output @@ -121,21 +214,6 @@ Connection timeout to use when trying to talk to hosts, in \fISECONDS\fR\&. .RE .PP -\fB\-B\fR \fINUM\fR, \fB\-\-background=\fR\fINUM\fR -.RS 4 -Run commands in the background, killing the task after -\fINUM\fR -seconds\&. -.RE -.PP -\fB\-P\fR \fINUM\fR, \fB\-\-poll=\fR\fINUM\fR -.RS 4 -Poll a background job every -\fINUM\fR -seconds\&. Requires -\fB\-B\fR\&. -.RE -.PP \fB\-u\fR \fIUSERNAME\fR, \fB\-\-user=\fR\fIUSERNAME\fR .RS 4 Use this remote @@ -143,32 +221,19 @@ Use this remote instead of the current user\&. .RE .PP -\fB\-U\fR \fISUDO_USERNAME\fR, \fB\-\-sudo\-user=\fR\fISUDO_USERNAME\fR +\fB\-\-vault\-password\-file=\fR\fIVAULT_PASSWORD_FILE\fR .RS 4 -Sudo to -\fISUDO_USERNAME\fR -instead of root\&. Implies \-\-sudo\&. +Vault password file\&. .RE .PP -\fB\-c\fR \fICONNECTION\fR, \fB\-\-connection=\fR\fICONNECTION\fR +\fB\-v\fR, \fB\-\-verbose\fR .RS 4 -Connection type to use\&. Possible options are -\fIparamiko\fR -(SSH), -\fIssh\fR, and -\fIlocal\fR\&. -\fIlocal\fR -is mostly useful for crontab or kickstarts\&. +Verbose mode, more output from successful actions will be shown\&. Give up to three times for more output\&. .RE .PP -\fB\-l\fR \fISUBSET\fR, \fB\-\-limit=\fR\fISUBSET\fR +\fB\-\-version\fR .RS 4 -Further limits the selected host/group patterns\&. -.RE -.PP -\fB\-l\fR \fI~REGEX\fR, \fB\-\-limit=\fR\fI~REGEX\fR -.RS 4 -Further limits hosts with a regex pattern\&. +Show program version number and exit\&. .RE .SH "INVENTORY" .sp @@ -188,7 +253,7 @@ Ranges of hosts are also supported\&. For more information and additional option .sp The following environment variables may be specified\&. .sp -ANSIBLE_HOSTS \(em Override the default ansible hosts file +ANSIBLE_INVENTORY \(em Override the default ansible inventory file .sp ANSIBLE_LIBRARY \(em Override the default ansible module library path .sp diff --git a/docs/man/man1/ansible.1.asciidoc.in b/docs/man/man1/ansible.1.asciidoc.in index 3f0e0f0cbf..26bd0144d4 100644 --- a/docs/man/man1/ansible.1.asciidoc.in +++ b/docs/man/man1/ansible.1.asciidoc.in @@ -34,56 +34,119 @@ semicolons. OPTIONS ------- -*-v*, *--verbose*:: +*-a* \'_ARGUMENTS_', *--args=*\'_ARGUMENTS_':: -Verbose mode, more output from successful actions will be shown. Give -up to three times for more output. +The 'ARGUMENTS' to pass to the module. -*-i* 'PATH', *--inventory=*'PATH':: +*--ask-become-pass*:: -The 'PATH' to the inventory hosts file, which defaults to '/etc/ansible/hosts'. +Ask for privilege escalation password. +*-k*, *--ask-pass*:: + +Prompt for the SSH password instead of assuming key-based authentication with ssh-agent. + +*--ask-su-pass*:: + +Prompt for su password (deprecated, use become). + +*-K*, *--ask-sudo-pass*:: + +Prompt for the password to use with --sudo, if any. + +*--ask-vault-pass*:: + +Prompt for vault password. + +*-B* 'NUM', *--background=*'NUM':: + +Run commands in the background, killing the task after 'NUM' seconds. + +*--become-method=*'BECOME_METHOD':: + +Privilege escalation method to use (default=sudo), +valid choices: [ sudo | su | pbrun | pfexec | runas ] + +*--become-user=*'BECOME_USER':: + +Run operations as this user (default=None). + +*-C*, *--check*:: + +Don't make any changes; instead try to predict some of the changes that may occur. + +*-c* 'CONNECTION', *--connection=*'CONNECTION':: + +Connection type to use. Possible options are 'paramiko' (SSH), 'ssh', +and 'local'. 'local' is mostly useful for crontab or kickstarts. + +*-e* 'EXTRA_VARS*, *--extra-vars=*'EXTRA_VARS':: + +Set additional variables as key=value or YAML/JSON. *-f* 'NUM', *--forks=*'NUM':: Level of parallelism. 'NUM' is specified as an integer, the default is 5. -*--private-key=*'PRIVATE_KEY_FILE':: +*-h*, *--help*:: -Use this file to authenticate the connection. +Show help message and exit. +*-i* 'PATH', *--inventory=*'PATH':: + +The 'PATH' to the inventory hosts file, which defaults to '/etc/ansible/hosts'. + +*-l* 'SUBSET', *--limit=*'SUBSET':: + +Further limits the selected host/group patterns. + +*-l* '\~REGEX', *--limit=*'~REGEX':: + +Further limits hosts with a regex pattern. + +*--list-hosts*:: + +Outputs a list of matching hosts; does not execute anything else. *-m* 'NAME', *--module-name=*'NAME':: Execute the module called 'NAME'. - *-M* 'DIRECTORY', *--module-path=*'DIRECTORY':: The 'DIRECTORY' search path to load modules from. The default is '/usr/share/ansible'. This can also be set with the ANSIBLE_LIBRARY environment variable. -*-a* \'_ARGUMENTS_', *--args=*\'_ARGUMENTS_':: - -The 'ARGUMENTS' to pass to the module. - -*-k*, *--ask-pass*:: - -Prompt for the SSH password instead of assuming key-based authentication with ssh-agent. - -*-K*, *--ask-sudo-pass*:: - -Prompt for the password to use with --sudo, if any - *-o*, *--one-line*:: Try to output everything on one line. +*-P* 'NUM', *--poll=*'NUM':: + +Poll a background job every 'NUM' seconds. Requires *-B*. + +*--private-key=*'PRIVATE_KEY_FILE':: + +Use this file to authenticate the connection. + +*-S*, *--su*:: + +Run operations with su (deprecated, use become). + +*-R* 'SU_USER', *--se-user=*'SUDO_USER':: + +Run operations with su as this user (default=root) +(deprecated, use become) + *-s*, *--sudo*:: Run the command as the user given by -u and sudo to root. +*-U* 'SUDO_USERNAME', *--sudo-user=*'SUDO_USERNAME':: + +Sudo to 'SUDO_USERNAME' instead of root. Implies --sudo. + *-t* 'DIRECTORY', *--tree=*'DIRECTORY':: Save contents in this output 'DIRECTORY', with the results saved in a @@ -93,34 +156,22 @@ file named after each host. Connection timeout to use when trying to talk to hosts, in 'SECONDS'. -*-B* 'NUM', *--background=*'NUM':: - -Run commands in the background, killing the task after 'NUM' seconds. - -*-P* 'NUM', *--poll=*'NUM':: - -Poll a background job every 'NUM' seconds. Requires *-B*. - *-u* 'USERNAME', *--user=*'USERNAME':: Use this remote 'USERNAME' instead of the current user. -*-U* 'SUDO_USERNAME', *--sudo-user=*'SUDO_USERNAME':: +*--vault-password-file=*'VAULT_PASSWORD_FILE':: -Sudo to 'SUDO_USERNAME' instead of root. Implies --sudo. +Vault password file. -*-c* 'CONNECTION', *--connection=*'CONNECTION':: +*-v*, *--verbose*:: -Connection type to use. Possible options are 'paramiko' (SSH), 'ssh', -and 'local'. 'local' is mostly useful for crontab or kickstarts. +Verbose mode, more output from successful actions will be shown. Give +up to three times for more output. -*-l* 'SUBSET', *--limit=*'SUBSET':: +*--version*:: -Further limits the selected host/group patterns. - -*-l* '\~REGEX', *--limit=*'~REGEX':: - -Further limits hosts with a regex pattern. +Show program version number and exit. INVENTORY --------- @@ -149,7 +200,7 @@ ENVIRONMENT The following environment variables may be specified. -ANSIBLE_HOSTS -- Override the default ansible hosts file +ANSIBLE_INVENTORY -- Override the default ansible inventory file ANSIBLE_LIBRARY -- Override the default ansible module library path diff --git a/docsite/_themes/srtd/footer.html b/docsite/_themes/srtd/footer.html index d702ad3a5f..b70cfde7ad 100644 --- a/docsite/_themes/srtd/footer.html +++ b/docsite/_themes/srtd/footer.html @@ -13,13 +13,13 @@

- © Copyright 2014 Ansible, Inc.. + © Copyright 2015 Ansible, Inc.. {%- if last_updated %} {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %} {%- endif %}

-Ansible docs are generated from GitHub sources using Sphinx using a theme provided by Read the Docs. {% if pagename.endswith("_module") %}. Module documentation is not edited directly, but is generated from the source code for the modules. To submit an update to module docs, edit the 'DOCUMENTATION' metadata in the module source tree. {% endif %} +Ansible docs are generated from GitHub sources using Sphinx using a theme provided by Read the Docs. {% if pagename.endswith("_module") %}. Module documentation is not edited directly, but is generated from the source code for the modules. To submit an update to module docs, edit the 'DOCUMENTATION' metadata in the core and extras modules source repositories. {% endif %} diff --git a/docsite/_themes/srtd/layout.html b/docsite/_themes/srtd/layout.html index 1073cad40e..74fb6008ba 100644 --- a/docsite/_themes/srtd/layout.html +++ b/docsite/_themes/srtd/layout.html @@ -113,6 +113,9 @@ } + + @@ -122,16 +125,32 @@ + + +
+ Documentation +
+
+
{# SIDE NAV, TOGGLES ON MOBILE #}