mirror of https://github.com/ansible/ansible.git
Merge remote-tracking branch 'upstream/devel' into win_fw
commit
828482643a
@ -0,0 +1,16 @@
|
|||||||
|
sudo: false
|
||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "2.7"
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
sources:
|
||||||
|
- deadsnakes
|
||||||
|
packages:
|
||||||
|
- python2.4
|
||||||
|
- python2.6
|
||||||
|
script:
|
||||||
|
- python2.4 -m compileall -fq -x 'cloud/|monitoring/zabbix.*\.py|/layman\.py|/maven_artifact\.py|clustering/consul.*\.py|notification/pushbullet\.py' .
|
||||||
|
- python2.6 -m compileall -fq .
|
||||||
|
- python2.7 -m compileall -fq .
|
||||||
|
- ./test-docs.sh extras
|
@ -0,0 +1,160 @@
|
|||||||
|
New module reviewers
|
||||||
|
====================
|
||||||
|
The following list represents all current Github module reviewers. It's currently comprised of all Ansible module authors, past and present.
|
||||||
|
|
||||||
|
Two +1 votes by any of these module reviewers on a new module pull request will result in the inclusion of that module into Ansible Extras.
|
||||||
|
|
||||||
|
Active
|
||||||
|
======
|
||||||
|
"Adam Garside (@fabulops)"
|
||||||
|
"Adam Keech (@smadam813)"
|
||||||
|
"Adam Miller (@maxamillion)"
|
||||||
|
"Alex Coomans (@drcapulet)"
|
||||||
|
"Alexander Bulimov (@abulimov)"
|
||||||
|
"Alexander Saltanov (@sashka)"
|
||||||
|
"Alexander Winkler (@dermute)"
|
||||||
|
"Andrew de Quincey (@adq)"
|
||||||
|
"André Paramés (@andreparames)"
|
||||||
|
"Andy Hill (@andyhky)"
|
||||||
|
"Artūras `arturaz` Šlajus (@arturaz)"
|
||||||
|
"Augustus Kling (@AugustusKling)"
|
||||||
|
"BOURDEL Paul (@pb8226)"
|
||||||
|
"Balazs Pocze (@banyek)"
|
||||||
|
"Ben Whaley (@bwhaley)"
|
||||||
|
"Benno Joy (@bennojoy)"
|
||||||
|
"Bernhard Weitzhofer (@b6d)"
|
||||||
|
"Boyd Adamson (@brontitall)"
|
||||||
|
"Brad Olson (@bradobro)"
|
||||||
|
"Brian Coca (@bcoca)"
|
||||||
|
"Brice Burgess (@briceburg)"
|
||||||
|
"Bruce Pennypacker (@bpennypacker)"
|
||||||
|
"Carson Gee (@carsongee)"
|
||||||
|
"Chris Church (@cchurch)"
|
||||||
|
"Chris Hoffman (@chrishoffman)"
|
||||||
|
"Chris Long (@alcamie101)"
|
||||||
|
"Chris Schmidt (@chrisisbeef)"
|
||||||
|
"Christian Berendt (@berendt)"
|
||||||
|
"Christopher H. Laco (@claco)"
|
||||||
|
"Cristian van Ee (@DJMuggs)"
|
||||||
|
"Dag Wieers (@dagwieers)"
|
||||||
|
"Dane Summers (@dsummersl)"
|
||||||
|
"Daniel Jaouen (@danieljaouen)"
|
||||||
|
"Daniel Schep (@dschep)"
|
||||||
|
"Dariusz Owczarek (@dareko)"
|
||||||
|
"Darryl Stoflet (@dstoflet)"
|
||||||
|
"David CHANIAL (@davixx)"
|
||||||
|
"David Stygstra (@stygstra)"
|
||||||
|
"Derek Carter (@goozbach)"
|
||||||
|
"Dimitrios Tydeas Mengidis (@dmtrs)"
|
||||||
|
"Doug Luce (@dougluce)"
|
||||||
|
"Dylan Martin (@pileofrogs)"
|
||||||
|
"Elliott Foster (@elliotttf)"
|
||||||
|
"Eric Johnson (@erjohnso)"
|
||||||
|
"Evan Duffield (@scicoin-project)"
|
||||||
|
"Evan Kaufman (@EvanK)"
|
||||||
|
"Evgenii Terechkov (@evgkrsk)"
|
||||||
|
"Franck Cuny (@franckcuny)"
|
||||||
|
"Gareth Rushgrove (@garethr)"
|
||||||
|
"Hagai Kariti (@hkariti)"
|
||||||
|
"Hector Acosta (@hacosta)"
|
||||||
|
"Hiroaki Nakamura (@hnakamur)"
|
||||||
|
"Ivan Vanderbyl (@ivanvanderbyl)"
|
||||||
|
"Jakub Jirutka (@jirutka)"
|
||||||
|
"James Cammarata (@jimi-c)"
|
||||||
|
"James Laska (@jlaska)"
|
||||||
|
"James S. Martin (@jsmartin)"
|
||||||
|
"Jan-Piet Mens (@jpmens)"
|
||||||
|
"Jayson Vantuyl (@jvantuyl)"
|
||||||
|
"Jens Depuydt (@jensdepuydt)"
|
||||||
|
"Jeroen Hoekx (@jhoekx)"
|
||||||
|
"Jesse Keating (@j2sol)"
|
||||||
|
"Jim Dalton (@jsdalton)"
|
||||||
|
"Jim Richardson (@weaselkeeper)"
|
||||||
|
"Jimmy Tang (@jcftang)"
|
||||||
|
"Johan Wiren (@johanwiren)"
|
||||||
|
"John Dewey (@retr0h)"
|
||||||
|
"John Jarvis (@jarv)"
|
||||||
|
"John Whitbeck (@jwhitbeck)"
|
||||||
|
"Jon Hawkesworth (@jhawkesworth)"
|
||||||
|
"Jonas Pfenniger (@zimbatm)"
|
||||||
|
"Jonathan I. Davila (@defionscode)"
|
||||||
|
"Joseph Callen (@jcpowermac)"
|
||||||
|
"Kevin Carter (@cloudnull)"
|
||||||
|
"Lester Wade (@lwade)"
|
||||||
|
"Lorin Hochstein (@lorin)"
|
||||||
|
"Manuel Sousa (@manuel-sousa)"
|
||||||
|
"Mark Theunissen (@marktheunissen)"
|
||||||
|
"Matt Coddington (@mcodd)"
|
||||||
|
"Matt Hite (@mhite)"
|
||||||
|
"Matt Makai (@makaimc)"
|
||||||
|
"Matt Martz (@sivel)"
|
||||||
|
"Matt Wright (@mattupstate)"
|
||||||
|
"Matthew Vernon (@mcv21)"
|
||||||
|
"Matthew Williams (@mgwilliams)"
|
||||||
|
"Matthias Vogelgesang (@matze)"
|
||||||
|
"Max Riveiro (@kavu)"
|
||||||
|
"Michael Gregson (@mgregson)"
|
||||||
|
"Michael J. Schultz (@mjschultz)"
|
||||||
|
"Michael Warkentin (@mwarkentin)"
|
||||||
|
"Mischa Peters (@mischapeters)"
|
||||||
|
"Monty Taylor (@emonty)"
|
||||||
|
"Nandor Sivok (@dominis)"
|
||||||
|
"Nate Coraor (@natefoo)"
|
||||||
|
"Nate Kingsley (@nate-kingsley)"
|
||||||
|
"Nick Harring (@NickatEpic)"
|
||||||
|
"Patrick Callahan (@dirtyharrycallahan)"
|
||||||
|
"Patrick Ogenstad (@ogenstad)"
|
||||||
|
"Patrick Pelletier (@skinp)"
|
||||||
|
"Patrik Lundin (@eest)"
|
||||||
|
"Paul Durivage (@angstwad)"
|
||||||
|
"Pavel Antonov (@softzilla)"
|
||||||
|
"Pepe Barbe (@elventear)"
|
||||||
|
"Peter Mounce (@petemounce)"
|
||||||
|
"Peter Oliver (@mavit)"
|
||||||
|
"Peter Sprygada (@privateip)"
|
||||||
|
"Peter Tan (@tanpeter)"
|
||||||
|
"Philippe Makowski (@pmakowski)"
|
||||||
|
"Phillip Gentry, CX Inc (@pcgentry)"
|
||||||
|
"Quentin Stafford-Fraser (@quentinsf)"
|
||||||
|
"Ramon de la Fuente (@ramondelafuente)"
|
||||||
|
"Raul Melo (@melodous)"
|
||||||
|
"Ravi Bhure (@ravibhure)"
|
||||||
|
"René Moser (@resmo)"
|
||||||
|
"Richard Hoop (@rhoop)"
|
||||||
|
"Richard Isaacson (@risaacson)"
|
||||||
|
"Rick Mendes (@rickmendes)"
|
||||||
|
"Romeo Theriault (@romeotheriault)"
|
||||||
|
"Scott Anderson (@tastychutney)"
|
||||||
|
"Sebastian Kornehl (@skornehl)"
|
||||||
|
"Serge van Ginderachter (@srvg)"
|
||||||
|
"Sergei Antipov (@UnderGreen)"
|
||||||
|
"Seth Edwards (@sedward)"
|
||||||
|
"Silviu Dicu (@silviud)"
|
||||||
|
"Simon JAILLET (@jails)"
|
||||||
|
"Stephen Fromm (@sfromm)"
|
||||||
|
"Steve (@groks)"
|
||||||
|
"Steve Gargan (@sgargan)"
|
||||||
|
"Steve Smith (@tarka)"
|
||||||
|
"Takashi Someda (@tksmd)"
|
||||||
|
"Taneli Leppä (@rosmo)"
|
||||||
|
"Tim Bielawa (@tbielawa)"
|
||||||
|
"Tim Bielawa (@tbielawa)"
|
||||||
|
"Tim Mahoney (@timmahoney)"
|
||||||
|
"Timothy Appnel (@tima)"
|
||||||
|
"Tom Bamford (@tombamford)"
|
||||||
|
"Trond Hindenes (@trondhindenes)"
|
||||||
|
"Vincent Van der Kussen (@vincentvdk)"
|
||||||
|
"Vincent Viallet (@zbal)"
|
||||||
|
"WAKAYAMA Shirou (@shirou)"
|
||||||
|
"Will Thames (@willthames)"
|
||||||
|
"Willy Barro (@willybarro)"
|
||||||
|
"Xabier Larrakoetxea (@slok)"
|
||||||
|
"Yeukhon Wong (@yeukhon)"
|
||||||
|
"Zacharie Eakin (@zeekin)"
|
||||||
|
"berenddeboer (@berenddeboer)"
|
||||||
|
"bleader (@bleader)"
|
||||||
|
"curtis (@ccollicutt)"
|
||||||
|
|
||||||
|
Retired
|
||||||
|
=======
|
||||||
|
None yet :)
|
@ -0,0 +1,88 @@
|
|||||||
|
Guidelines for AWS modules
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Naming your module
|
||||||
|
==================
|
||||||
|
|
||||||
|
Base the name of the module on the part of AWS that
|
||||||
|
you actually use. (A good rule of thumb is to take
|
||||||
|
whatever module you use with boto as a starting point).
|
||||||
|
|
||||||
|
Don't further abbreviate names - if something is a well
|
||||||
|
known abbreviation due to it being a major component of
|
||||||
|
AWS, that's fine, but don't create new ones independently
|
||||||
|
(e.g. VPC, ELB, etc. are fine)
|
||||||
|
|
||||||
|
Using boto
|
||||||
|
==========
|
||||||
|
|
||||||
|
Wrap the `import` statements in a try block and fail the
|
||||||
|
module later on if the import fails
|
||||||
|
|
||||||
|
```
|
||||||
|
try:
|
||||||
|
import boto
|
||||||
|
import boto.module.that.you.use
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
<lots of code here>
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
module_specific_parameter=dict(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
)
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto required for this module')
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Try and keep backward compatibility with relatively recent
|
||||||
|
versions of boto. That means that if want to implement some
|
||||||
|
functionality that uses a new feature of boto, it should only
|
||||||
|
fail if that feature actually needs to be run, with a message
|
||||||
|
saying which version of boto is needed.
|
||||||
|
|
||||||
|
Use feature testing (e.g. `hasattr('boto.module', 'shiny_new_method')`)
|
||||||
|
to check whether boto supports a feature rather than version checking
|
||||||
|
|
||||||
|
e.g. from the `ec2` module:
|
||||||
|
```
|
||||||
|
if boto_supports_profile_name_arg(ec2):
|
||||||
|
params['instance_profile_name'] = instance_profile_name
|
||||||
|
else:
|
||||||
|
if instance_profile_name is not None:
|
||||||
|
module.fail_json(
|
||||||
|
msg="instance_profile_name parameter requires Boto version 2.5.0 or higher")
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
Connecting to AWS
|
||||||
|
=================
|
||||||
|
|
||||||
|
For EC2 you can just use
|
||||||
|
|
||||||
|
```
|
||||||
|
ec2 = ec2_connect(module)
|
||||||
|
```
|
||||||
|
|
||||||
|
For other modules, you should use `get_aws_connection_info` and then
|
||||||
|
`connect_to_aws`. To connect to an example `xyz` service:
|
||||||
|
|
||||||
|
```
|
||||||
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||||
|
xyz = connect_to_aws(boto.xyz, region, **aws_connect_params)
|
||||||
|
```
|
||||||
|
|
||||||
|
The reason for using `get_aws_connection_info` and `connect_to_aws`
|
||||||
|
(and even `ec2_connect` uses those under the hood) rather than doing it
|
||||||
|
yourself is that they handle some of the more esoteric connection
|
||||||
|
options such as security tokens and boto profiles.
|
@ -0,0 +1,229 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: cloudtrail
|
||||||
|
short_description: manage CloudTrail creation and deletion
|
||||||
|
description:
|
||||||
|
- Creates or deletes CloudTrail configuration. Ensures logging is also enabled.
|
||||||
|
version_added: "2.0"
|
||||||
|
author:
|
||||||
|
- "Ansible Core Team"
|
||||||
|
- "Ted Timmons"
|
||||||
|
requirements:
|
||||||
|
- "boto >= 2.21"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- add or remove CloudTrail configuration.
|
||||||
|
required: true
|
||||||
|
choices: ['enabled', 'disabled']
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- name for given CloudTrail configuration.
|
||||||
|
- This is a primary key and is used to identify the configuration.
|
||||||
|
s3_bucket_prefix:
|
||||||
|
description:
|
||||||
|
- bucket to place CloudTrail in.
|
||||||
|
- this bucket should exist and have the proper policy. See U(http://docs.aws.amazon.com/awscloudtrail/latest/userguide/aggregating_logs_regions_bucket_policy.html)
|
||||||
|
- required when state=enabled.
|
||||||
|
required: false
|
||||||
|
s3_key_prefix:
|
||||||
|
description:
|
||||||
|
- prefix to keys in bucket. A trailing slash is not necessary and will be removed.
|
||||||
|
required: false
|
||||||
|
include_global_events:
|
||||||
|
description:
|
||||||
|
- record API calls from global services such as IAM and STS?
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
choices: ["true", "false"]
|
||||||
|
|
||||||
|
aws_secret_key:
|
||||||
|
description:
|
||||||
|
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aliases: [ 'ec2_secret_key', 'secret_key' ]
|
||||||
|
version_added: "1.5"
|
||||||
|
aws_access_key:
|
||||||
|
description:
|
||||||
|
- AWS access key. If not set then the value of the AWS_ACCESS_KEY environment variable is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aliases: [ 'ec2_access_key', 'access_key' ]
|
||||||
|
version_added: "1.5"
|
||||||
|
region:
|
||||||
|
description:
|
||||||
|
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||||
|
required: false
|
||||||
|
aliases: ['aws_region', 'ec2_region']
|
||||||
|
version_added: "1.5"
|
||||||
|
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: enable cloudtrail
|
||||||
|
local_action: cloudtrail
|
||||||
|
state=enabled name=main s3_bucket_name=ourbucket
|
||||||
|
s3_key_prefix=cloudtrail region=us-east-1
|
||||||
|
|
||||||
|
- name: enable cloudtrail with different configuration
|
||||||
|
local_action: cloudtrail
|
||||||
|
state=enabled name=main s3_bucket_name=ourbucket2
|
||||||
|
s3_key_prefix='' region=us-east-1
|
||||||
|
|
||||||
|
- name: remove cloudtrail
|
||||||
|
local_action: cloudtrail state=disabled name=main region=us-east-1
|
||||||
|
"""
|
||||||
|
|
||||||
|
HAS_BOTO = False
|
||||||
|
try:
|
||||||
|
import boto
|
||||||
|
import boto.cloudtrail
|
||||||
|
from boto.regioninfo import RegionInfo
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
class CloudTrailManager:
|
||||||
|
"""Handles cloudtrail configuration"""
|
||||||
|
|
||||||
|
def __init__(self, module, region=None, **aws_connect_params):
|
||||||
|
self.module = module
|
||||||
|
self.region = region
|
||||||
|
self.aws_connect_params = aws_connect_params
|
||||||
|
self.changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.conn = connect_to_aws(boto.cloudtrail, self.region, **self.aws_connect_params)
|
||||||
|
except boto.exception.NoAuthHandlerFound, e:
|
||||||
|
self.module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
def view_status(self, name):
|
||||||
|
return self.conn.get_trail_status(name)
|
||||||
|
|
||||||
|
def view(self, name):
|
||||||
|
ret = self.conn.describe_trails(trail_name_list=[name])
|
||||||
|
trailList = ret.get('trailList', [])
|
||||||
|
if len(trailList) == 1:
|
||||||
|
return trailList[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def exists(self, name=None):
|
||||||
|
ret = self.view(name)
|
||||||
|
if ret:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def enable_logging(self, name):
|
||||||
|
'''Turn on logging for a cloudtrail that already exists. Throws Exception on error.'''
|
||||||
|
self.conn.start_logging(name)
|
||||||
|
|
||||||
|
|
||||||
|
def enable(self, **create_args):
|
||||||
|
return self.conn.create_trail(**create_args)
|
||||||
|
|
||||||
|
def update(self, **create_args):
|
||||||
|
return self.conn.update_trail(**create_args)
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
'''Delete a given cloudtrial configuration. Throws Exception on error.'''
|
||||||
|
self.conn.delete_trail(name)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(dict(
|
||||||
|
state={'required': True, 'choices': ['enabled', 'disabled'] },
|
||||||
|
name={'required': True, 'type': 'str' },
|
||||||
|
s3_bucket_name={'required': False, 'type': 'str' },
|
||||||
|
s3_key_prefix={'default':'', 'required': False, 'type': 'str' },
|
||||||
|
include_global_events={'default':True, 'required': False, 'type': 'bool' },
|
||||||
|
))
|
||||||
|
required_together = ( ['state', 's3_bucket_name'] )
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True, required_together=required_together)
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto is required.')
|
||||||
|
|
||||||
|
ec2_url, access_key, secret_key, region = get_ec2_creds(module)
|
||||||
|
aws_connect_params = dict(aws_access_key_id=access_key,
|
||||||
|
aws_secret_access_key=secret_key)
|
||||||
|
|
||||||
|
if not region:
|
||||||
|
module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file")
|
||||||
|
|
||||||
|
ct_name = module.params['name']
|
||||||
|
s3_bucket_name = module.params['s3_bucket_name']
|
||||||
|
# remove trailing slash from the key prefix, really messes up the key structure.
|
||||||
|
s3_key_prefix = module.params['s3_key_prefix'].rstrip('/')
|
||||||
|
include_global_events = module.params['include_global_events']
|
||||||
|
|
||||||
|
#if module.params['state'] == 'present' and 'ec2_elbs' not in module.params:
|
||||||
|
# module.fail_json(msg="ELBs are required for registration or viewing")
|
||||||
|
|
||||||
|
cf_man = CloudTrailManager(module, region=region, **aws_connect_params)
|
||||||
|
|
||||||
|
results = { 'changed': False }
|
||||||
|
if module.params['state'] == 'enabled':
|
||||||
|
results['exists'] = cf_man.exists(name=ct_name)
|
||||||
|
if results['exists']:
|
||||||
|
results['view'] = cf_man.view(ct_name)
|
||||||
|
# only update if the values have changed.
|
||||||
|
if results['view']['S3BucketName'] != s3_bucket_name or \
|
||||||
|
results['view']['S3KeyPrefix'] != s3_key_prefix or \
|
||||||
|
results['view']['IncludeGlobalServiceEvents'] != include_global_events:
|
||||||
|
if not module.check_mode:
|
||||||
|
results['update'] = cf_man.update(name=ct_name, s3_bucket_name=s3_bucket_name, s3_key_prefix=s3_key_prefix, include_global_service_events=include_global_events)
|
||||||
|
results['changed'] = True
|
||||||
|
else:
|
||||||
|
if not module.check_mode:
|
||||||
|
# doesn't exist. create it.
|
||||||
|
results['enable'] = cf_man.enable(name=ct_name, s3_bucket_name=s3_bucket_name, s3_key_prefix=s3_key_prefix, include_global_service_events=include_global_events)
|
||||||
|
results['changed'] = True
|
||||||
|
|
||||||
|
# given cloudtrail should exist now. Enable the logging.
|
||||||
|
results['view_status'] = cf_man.view_status(ct_name)
|
||||||
|
results['was_logging_enabled'] = results['view_status'].get('IsLogging', False)
|
||||||
|
if not results['was_logging_enabled']:
|
||||||
|
if not module.check_mode:
|
||||||
|
cf_man.enable_logging(ct_name)
|
||||||
|
results['logging_enabled'] = True
|
||||||
|
results['changed'] = True
|
||||||
|
|
||||||
|
# delete the cloudtrai
|
||||||
|
elif module.params['state'] == 'disabled':
|
||||||
|
# check to see if it exists before deleting.
|
||||||
|
results['exists'] = cf_man.exists(name=ct_name)
|
||||||
|
if results['exists']:
|
||||||
|
# it exists, so we should delete it and mark changed.
|
||||||
|
if not module.check_mode:
|
||||||
|
cf_man.delete(ct_name)
|
||||||
|
results['changed'] = True
|
||||||
|
|
||||||
|
module.exit_json(**results)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,286 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: dynamodb_table
|
||||||
|
short_description: Create, update or delete AWS Dynamo DB tables.
|
||||||
|
description:
|
||||||
|
- Create or delete AWS Dynamo DB tables.
|
||||||
|
- Can update the provisioned throughput on existing tables.
|
||||||
|
- Returns the status of the specified table.
|
||||||
|
version_added: "2.0"
|
||||||
|
author: Alan Loi (@loia)
|
||||||
|
version_added: "2.0"
|
||||||
|
requirements:
|
||||||
|
- "boto >= 2.13.2"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Create or delete the table
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: 'present'
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the table.
|
||||||
|
required: true
|
||||||
|
hash_key_name:
|
||||||
|
description:
|
||||||
|
- Name of the hash key.
|
||||||
|
- Required when C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
hash_key_type:
|
||||||
|
description:
|
||||||
|
- Type of the hash key.
|
||||||
|
required: false
|
||||||
|
choices: ['STRING', 'NUMBER', 'BINARY']
|
||||||
|
default: 'STRING'
|
||||||
|
range_key_name:
|
||||||
|
description:
|
||||||
|
- Name of the range key.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
range_key_type:
|
||||||
|
description:
|
||||||
|
- Type of the range key.
|
||||||
|
required: false
|
||||||
|
choices: ['STRING', 'NUMBER', 'BINARY']
|
||||||
|
default: 'STRING'
|
||||||
|
read_capacity:
|
||||||
|
description:
|
||||||
|
- Read throughput capacity (units) to provision.
|
||||||
|
required: false
|
||||||
|
default: 1
|
||||||
|
write_capacity:
|
||||||
|
description:
|
||||||
|
- Write throughput capacity (units) to provision.
|
||||||
|
required: false
|
||||||
|
default: 1
|
||||||
|
region:
|
||||||
|
description:
|
||||||
|
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||||
|
required: false
|
||||||
|
aliases: ['aws_region', 'ec2_region']
|
||||||
|
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create dynamo table with hash and range primary key
|
||||||
|
- dynamodb_table:
|
||||||
|
name: my-table
|
||||||
|
region: us-east-1
|
||||||
|
hash_key_name: id
|
||||||
|
hash_key_type: STRING
|
||||||
|
range_key_name: create_time
|
||||||
|
range_key_type: NUMBER
|
||||||
|
read_capacity: 2
|
||||||
|
write_capacity: 2
|
||||||
|
|
||||||
|
# Update capacity on existing dynamo table
|
||||||
|
- dynamodb_table:
|
||||||
|
name: my-table
|
||||||
|
region: us-east-1
|
||||||
|
read_capacity: 10
|
||||||
|
write_capacity: 10
|
||||||
|
|
||||||
|
# Delete dynamo table
|
||||||
|
- dynamodb_table:
|
||||||
|
name: my-table
|
||||||
|
region: us-east-1
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
table_status:
|
||||||
|
description: The current status of the table.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ACTIVE
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto
|
||||||
|
import boto.dynamodb2
|
||||||
|
from boto.dynamodb2.table import Table
|
||||||
|
from boto.dynamodb2.fields import HashKey, RangeKey
|
||||||
|
from boto.dynamodb2.types import STRING, NUMBER, BINARY
|
||||||
|
from boto.exception import BotoServerError, NoAuthHandlerFound, JSONResponseError
|
||||||
|
HAS_BOTO = True
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
|
||||||
|
DYNAMO_TYPE_MAP = {
|
||||||
|
'STRING': STRING,
|
||||||
|
'NUMBER': NUMBER,
|
||||||
|
'BINARY': BINARY
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_or_update_dynamo_table(connection, module):
|
||||||
|
table_name = module.params.get('name')
|
||||||
|
hash_key_name = module.params.get('hash_key_name')
|
||||||
|
hash_key_type = module.params.get('hash_key_type')
|
||||||
|
range_key_name = module.params.get('range_key_name')
|
||||||
|
range_key_type = module.params.get('range_key_type')
|
||||||
|
read_capacity = module.params.get('read_capacity')
|
||||||
|
write_capacity = module.params.get('write_capacity')
|
||||||
|
|
||||||
|
schema = [
|
||||||
|
HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)),
|
||||||
|
RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type))
|
||||||
|
]
|
||||||
|
throughput = {
|
||||||
|
'read': read_capacity,
|
||||||
|
'write': write_capacity
|
||||||
|
}
|
||||||
|
|
||||||
|
result = dict(
|
||||||
|
region=module.params.get('region'),
|
||||||
|
table_name=table_name,
|
||||||
|
hash_key_name=hash_key_name,
|
||||||
|
hash_key_type=hash_key_type,
|
||||||
|
range_key_name=range_key_name,
|
||||||
|
range_key_type=range_key_type,
|
||||||
|
read_capacity=read_capacity,
|
||||||
|
write_capacity=write_capacity,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
table = Table(table_name, connection=connection)
|
||||||
|
|
||||||
|
if dynamo_table_exists(table):
|
||||||
|
result['changed'] = update_dynamo_table(table, throughput=throughput, check_mode=module.check_mode)
|
||||||
|
else:
|
||||||
|
if not module.check_mode:
|
||||||
|
Table.create(table_name, connection=connection, schema=schema, throughput=throughput)
|
||||||
|
result['changed'] = True
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
result['table_status'] = table.describe()['Table']['TableStatus']
|
||||||
|
|
||||||
|
except BotoServerError:
|
||||||
|
result['msg'] = 'Failed to create/update dynamo table due to error: ' + traceback.format_exc()
|
||||||
|
module.fail_json(**result)
|
||||||
|
else:
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_dynamo_table(connection, module):
|
||||||
|
table_name = module.params.get('name')
|
||||||
|
|
||||||
|
result = dict(
|
||||||
|
region=module.params.get('region'),
|
||||||
|
table_name=table_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
table = Table(table_name, connection=connection)
|
||||||
|
|
||||||
|
if dynamo_table_exists(table):
|
||||||
|
if not module.check_mode:
|
||||||
|
table.delete()
|
||||||
|
result['changed'] = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
result['changed'] = False
|
||||||
|
|
||||||
|
except BotoServerError:
|
||||||
|
result['msg'] = 'Failed to delete dynamo table due to error: ' + traceback.format_exc()
|
||||||
|
module.fail_json(**result)
|
||||||
|
else:
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
|
def dynamo_table_exists(table):
|
||||||
|
try:
|
||||||
|
table.describe()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except JSONResponseError, e:
|
||||||
|
if e.message and e.message.startswith('Requested resource not found'):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def update_dynamo_table(table, throughput=None, check_mode=False):
|
||||||
|
table.describe() # populate table details
|
||||||
|
|
||||||
|
if has_throughput_changed(table, throughput):
|
||||||
|
if not check_mode:
|
||||||
|
return table.update(throughput=throughput)
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def has_throughput_changed(table, new_throughput):
|
||||||
|
if not new_throughput:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return new_throughput['read'] != table.throughput['read'] or \
|
||||||
|
new_throughput['write'] != table.throughput['write']
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(dict(
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
name=dict(required=True, type='str'),
|
||||||
|
hash_key_name=dict(required=True, type='str'),
|
||||||
|
hash_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']),
|
||||||
|
range_key_name=dict(type='str'),
|
||||||
|
range_key_type=dict(default='STRING', type='str', choices=['STRING', 'NUMBER', 'BINARY']),
|
||||||
|
read_capacity=dict(default=1, type='int'),
|
||||||
|
write_capacity=dict(default=1, type='int'),
|
||||||
|
))
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
supports_check_mode=True)
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto required for this module')
|
||||||
|
|
||||||
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||||
|
if not region:
|
||||||
|
module.fail_json(msg='region must be specified')
|
||||||
|
|
||||||
|
try:
|
||||||
|
connection = connect_to_aws(boto.dynamodb2, region, **aws_connect_params)
|
||||||
|
|
||||||
|
except (NoAuthHandlerFound, StandardError), e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state == 'present':
|
||||||
|
create_or_update_dynamo_table(connection, module)
|
||||||
|
elif state == 'absent':
|
||||||
|
delete_dynamo_table(connection, module)
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: ec2_ami_copy
|
||||||
|
short_description: copies AMI between AWS regions, return new image id
|
||||||
|
description:
|
||||||
|
- Copies AMI from a source region to a destination region. This module has a dependency on python-boto >= 2.5
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
source_region:
|
||||||
|
description:
|
||||||
|
- the source region that AMI should be copied from
|
||||||
|
required: true
|
||||||
|
region:
|
||||||
|
description:
|
||||||
|
- the destination region that AMI should be copied to
|
||||||
|
required: true
|
||||||
|
aliases: ['aws_region', 'ec2_region', 'dest_region']
|
||||||
|
source_image_id:
|
||||||
|
description:
|
||||||
|
- the id of the image in source region that should be copied
|
||||||
|
required: true
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of the new image to copy
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- An optional human-readable string describing the contents and purpose of the new AMI.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
wait:
|
||||||
|
description:
|
||||||
|
- wait for the copied AMI to be in state 'available' before returning.
|
||||||
|
required: false
|
||||||
|
default: "no"
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
wait_timeout:
|
||||||
|
description:
|
||||||
|
- how long before wait gives up, in seconds
|
||||||
|
required: false
|
||||||
|
default: 1200
|
||||||
|
tags:
|
||||||
|
description:
|
||||||
|
- a hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}'
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
|
||||||
|
author: Amir Moulavi <amir.moulavi@gmail.com>
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Basic AMI Copy
|
||||||
|
- local_action:
|
||||||
|
module: ec2_ami_copy
|
||||||
|
source_region: eu-west-1
|
||||||
|
dest_region: us-east-1
|
||||||
|
source_image_id: ami-xxxxxxx
|
||||||
|
name: SuperService-new-AMI
|
||||||
|
description: latest patch
|
||||||
|
tags: '{"Name":"SuperService-new-AMI", "type":"SuperService"}'
|
||||||
|
wait: yes
|
||||||
|
register: image_id
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto
|
||||||
|
import boto.ec2
|
||||||
|
from boto.vpc import VPCConnection
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto required for this module')
|
||||||
|
|
||||||
|
def copy_image(module, ec2):
|
||||||
|
"""
|
||||||
|
Copies an AMI
|
||||||
|
|
||||||
|
module : AnsibleModule object
|
||||||
|
ec2: authenticated ec2 connection object
|
||||||
|
"""
|
||||||
|
|
||||||
|
source_region = module.params.get('source_region')
|
||||||
|
source_image_id = module.params.get('source_image_id')
|
||||||
|
name = module.params.get('name')
|
||||||
|
description = module.params.get('description')
|
||||||
|
tags = module.params.get('tags')
|
||||||
|
wait_timeout = int(module.params.get('wait_timeout'))
|
||||||
|
wait = module.params.get('wait')
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = {'source_region': source_region,
|
||||||
|
'source_image_id': source_image_id,
|
||||||
|
'name': name,
|
||||||
|
'description': description
|
||||||
|
}
|
||||||
|
|
||||||
|
image_id = ec2.copy_image(**params).image_id
|
||||||
|
except boto.exception.BotoServerError, e:
|
||||||
|
module.fail_json(msg="%s: %s" % (e.error_code, e.error_message))
|
||||||
|
|
||||||
|
img = wait_until_image_is_recognized(module, ec2, wait_timeout, image_id, wait)
|
||||||
|
|
||||||
|
img = wait_until_image_is_copied(module, ec2, wait_timeout, img, image_id, wait)
|
||||||
|
|
||||||
|
register_tags_if_any(module, ec2, tags, image_id)
|
||||||
|
|
||||||
|
module.exit_json(msg="AMI copy operation complete", image_id=image_id, state=img.state, changed=True)
|
||||||
|
|
||||||
|
|
||||||
|
# register tags to the copied AMI in dest_region
|
||||||
|
def register_tags_if_any(module, ec2, tags, image_id):
|
||||||
|
if tags:
|
||||||
|
try:
|
||||||
|
ec2.create_tags([image_id], tags)
|
||||||
|
except Exception as e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# wait here until the image is copied (i.e. the state becomes available
|
||||||
|
def wait_until_image_is_copied(module, ec2, wait_timeout, img, image_id, wait):
|
||||||
|
wait_timeout = time.time() + wait_timeout
|
||||||
|
while wait and wait_timeout > time.time() and (img is None or img.state != 'available'):
|
||||||
|
img = ec2.get_image(image_id)
|
||||||
|
time.sleep(3)
|
||||||
|
if wait and wait_timeout <= time.time():
|
||||||
|
# waiting took too long
|
||||||
|
module.fail_json(msg="timed out waiting for image to be copied")
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# wait until the image is recognized.
|
||||||
|
def wait_until_image_is_recognized(module, ec2, wait_timeout, image_id, wait):
|
||||||
|
for i in range(wait_timeout):
|
||||||
|
try:
|
||||||
|
return ec2.get_image(image_id)
|
||||||
|
except boto.exception.EC2ResponseError, e:
|
||||||
|
# This exception we expect initially right after registering the copy with EC2 API
|
||||||
|
if 'InvalidAMIID.NotFound' in e.error_code and wait:
|
||||||
|
time.sleep(1)
|
||||||
|
else:
|
||||||
|
# On any other exception we should fail
|
||||||
|
module.fail_json(
|
||||||
|
msg="Error while trying to find the new image. Using wait=yes and/or a longer wait_timeout may help: " + str(
|
||||||
|
e))
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="timed out waiting for image to be recognized")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(dict(
|
||||||
|
source_region=dict(required=True),
|
||||||
|
source_image_id=dict(required=True),
|
||||||
|
name=dict(),
|
||||||
|
description=dict(default=""),
|
||||||
|
wait=dict(type='bool', default=False),
|
||||||
|
wait_timeout=dict(default=1200),
|
||||||
|
tags=dict(type='dict')))
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ec2 = ec2_connect(module)
|
||||||
|
except boto.exception.NoAuthHandlerFound, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
region, ec2_url, boto_params = get_aws_connection_info(module)
|
||||||
|
vpc = connect_to_aws(boto.vpc, region, **boto_params)
|
||||||
|
except boto.exception.NoAuthHandlerFound, e:
|
||||||
|
module.fail_json(msg = str(e))
|
||||||
|
|
||||||
|
if not region:
|
||||||
|
module.fail_json(msg="region must be specified")
|
||||||
|
|
||||||
|
copy_image(module, ec2)
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,404 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# This is a 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 Ansible library 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 library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: ec2_eni
|
||||||
|
short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance
|
||||||
|
description:
|
||||||
|
- Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID is provided, an attempt is made to update the existing ENI. By passing 'None' as the instance_id, an ENI can be detached from an instance.
|
||||||
|
version_added: "2.0"
|
||||||
|
author: Rob White, wimnat [at] gmail.com, @wimnat
|
||||||
|
options:
|
||||||
|
eni_id:
|
||||||
|
description:
|
||||||
|
- The ID of the ENI
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
instance_id:
|
||||||
|
description:
|
||||||
|
- Instance ID that you wish to attach ENI to. To detach an ENI from an instance, use 'None'.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
private_ip_address:
|
||||||
|
description:
|
||||||
|
- Private IP address.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
subnet_id:
|
||||||
|
description:
|
||||||
|
- ID of subnet in which to create the ENI. Only required when state=present.
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Optional description of the ENI.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
security_groups:
|
||||||
|
description:
|
||||||
|
- List of security groups associated with the interface. Only used when state=present.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Create or delete ENI.
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
device_index:
|
||||||
|
description:
|
||||||
|
- The index of the device for the network interface attachment on the instance.
|
||||||
|
required: false
|
||||||
|
default: 0
|
||||||
|
force_detach:
|
||||||
|
description:
|
||||||
|
- Force detachment of the interface. This applies either when explicitly detaching the interface by setting instance_id to None or when deleting an interface with state=absent.
|
||||||
|
required: false
|
||||||
|
default: no
|
||||||
|
delete_on_termination:
|
||||||
|
description:
|
||||||
|
- Delete the interface when the instance it is attached to is terminated. You can only specify this flag when the interface is being modified, not on creation.
|
||||||
|
required: false
|
||||||
|
source_dest_check:
|
||||||
|
description:
|
||||||
|
- By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. You can only specify this flag when the interface is being modified, not on creation.
|
||||||
|
required: false
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Note: These examples do not set authentication details, see the AWS Guide for details.
|
||||||
|
|
||||||
|
# Create an ENI. As no security group is defined, ENI will be created in default security group
|
||||||
|
- ec2_eni:
|
||||||
|
private_ip_address: 172.31.0.20
|
||||||
|
subnet_id: subnet-xxxxxxxx
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# Create an ENI and attach it to an instance
|
||||||
|
- ec2_eni:
|
||||||
|
instance_id: i-xxxxxxx
|
||||||
|
device_index: 1
|
||||||
|
private_ip_address: 172.31.0.20
|
||||||
|
subnet_id: subnet-xxxxxxxx
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# Destroy an ENI, detaching it from any instance if necessary
|
||||||
|
- ec2_eni:
|
||||||
|
eni_id: eni-xxxxxxx
|
||||||
|
force_detach: yes
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
# Update an ENI
|
||||||
|
- ec2_eni:
|
||||||
|
eni_id: eni-xxxxxxx
|
||||||
|
description: "My new description"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# Detach an ENI from an instance
|
||||||
|
- ec2_eni:
|
||||||
|
eni_id: eni-xxxxxxx
|
||||||
|
instance_id: None
|
||||||
|
state: present
|
||||||
|
|
||||||
|
### Delete an interface on termination
|
||||||
|
# First create the interface
|
||||||
|
- ec2_eni:
|
||||||
|
instance_id: i-xxxxxxx
|
||||||
|
device_index: 1
|
||||||
|
private_ip_address: 172.31.0.20
|
||||||
|
subnet_id: subnet-xxxxxxxx
|
||||||
|
state: present
|
||||||
|
register: eni
|
||||||
|
|
||||||
|
# Modify the interface to enable the delete_on_terminaton flag
|
||||||
|
- ec2_eni:
|
||||||
|
eni_id: {{ "eni.interface.id" }}
|
||||||
|
delete_on_termination: true
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import time
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
import re
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto.ec2
|
||||||
|
from boto.exception import BotoServerError
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_error_message(xml_string):
|
||||||
|
|
||||||
|
root = ET.fromstring(xml_string)
|
||||||
|
for message in root.findall('.//Message'):
|
||||||
|
return message.text
|
||||||
|
|
||||||
|
|
||||||
|
def get_eni_info(interface):
|
||||||
|
|
||||||
|
interface_info = {'id': interface.id,
|
||||||
|
'subnet_id': interface.subnet_id,
|
||||||
|
'vpc_id': interface.vpc_id,
|
||||||
|
'description': interface.description,
|
||||||
|
'owner_id': interface.owner_id,
|
||||||
|
'status': interface.status,
|
||||||
|
'mac_address': interface.mac_address,
|
||||||
|
'private_ip_address': interface.private_ip_address,
|
||||||
|
'source_dest_check': interface.source_dest_check,
|
||||||
|
'groups': dict((group.id, group.name) for group in interface.groups),
|
||||||
|
}
|
||||||
|
|
||||||
|
if interface.attachment is not None:
|
||||||
|
interface_info['attachment'] = {'attachment_id': interface.attachment.id,
|
||||||
|
'instance_id': interface.attachment.instance_id,
|
||||||
|
'device_index': interface.attachment.device_index,
|
||||||
|
'status': interface.attachment.status,
|
||||||
|
'attach_time': interface.attachment.attach_time,
|
||||||
|
'delete_on_termination': interface.attachment.delete_on_termination,
|
||||||
|
}
|
||||||
|
|
||||||
|
return interface_info
|
||||||
|
|
||||||
|
def wait_for_eni(eni, status):
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time.sleep(3)
|
||||||
|
eni.update()
|
||||||
|
# If the status is detached we just need attachment to disappear
|
||||||
|
if eni.attachment is None:
|
||||||
|
if status == "detached":
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
if status == "attached" and eni.attachment.status == "attached":
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def create_eni(connection, module):
|
||||||
|
|
||||||
|
instance_id = module.params.get("instance_id")
|
||||||
|
if instance_id == 'None':
|
||||||
|
instance_id = None
|
||||||
|
do_detach = True
|
||||||
|
else:
|
||||||
|
do_detach = False
|
||||||
|
device_index = module.params.get("device_index")
|
||||||
|
subnet_id = module.params.get('subnet_id')
|
||||||
|
private_ip_address = module.params.get('private_ip_address')
|
||||||
|
description = module.params.get('description')
|
||||||
|
security_groups = module.params.get('security_groups')
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
eni = compare_eni(connection, module)
|
||||||
|
if eni is None:
|
||||||
|
eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups)
|
||||||
|
if instance_id is not None:
|
||||||
|
try:
|
||||||
|
eni.attach(instance_id, device_index)
|
||||||
|
except BotoServerError as ex:
|
||||||
|
eni.delete()
|
||||||
|
raise
|
||||||
|
changed = True
|
||||||
|
# Wait to allow creation / attachment to finish
|
||||||
|
wait_for_eni(eni, "attached")
|
||||||
|
eni.update()
|
||||||
|
|
||||||
|
except BotoServerError as e:
|
||||||
|
module.fail_json(msg=get_error_message(e.args[2]))
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, interface=get_eni_info(eni))
|
||||||
|
|
||||||
|
|
||||||
|
def modify_eni(connection, module):
|
||||||
|
|
||||||
|
eni_id = module.params.get("eni_id")
|
||||||
|
instance_id = module.params.get("instance_id")
|
||||||
|
if instance_id == 'None':
|
||||||
|
instance_id = None
|
||||||
|
do_detach = True
|
||||||
|
else:
|
||||||
|
do_detach = False
|
||||||
|
device_index = module.params.get("device_index")
|
||||||
|
subnet_id = module.params.get('subnet_id')
|
||||||
|
private_ip_address = module.params.get('private_ip_address')
|
||||||
|
description = module.params.get('description')
|
||||||
|
security_groups = module.params.get('security_groups')
|
||||||
|
force_detach = module.params.get("force_detach")
|
||||||
|
source_dest_check = module.params.get("source_dest_check")
|
||||||
|
delete_on_termination = module.params.get("delete_on_termination")
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get the eni with the eni_id specified
|
||||||
|
eni_result_set = connection.get_all_network_interfaces(eni_id)
|
||||||
|
eni = eni_result_set[0]
|
||||||
|
if description is not None:
|
||||||
|
if eni.description != description:
|
||||||
|
connection.modify_network_interface_attribute(eni.id, "description", description)
|
||||||
|
changed = True
|
||||||
|
if security_groups is not None:
|
||||||
|
if sorted(get_sec_group_list(eni.groups)) != sorted(security_groups):
|
||||||
|
connection.modify_network_interface_attribute(eni.id, "groupSet", security_groups)
|
||||||
|
changed = True
|
||||||
|
if source_dest_check is not None:
|
||||||
|
if eni.source_dest_check != source_dest_check:
|
||||||
|
connection.modify_network_interface_attribute(eni.id, "sourceDestCheck", source_dest_check)
|
||||||
|
changed = True
|
||||||
|
if delete_on_termination is not None:
|
||||||
|
if eni.attachment is not None:
|
||||||
|
if eni.attachment.delete_on_termination is not delete_on_termination:
|
||||||
|
connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Can not modify delete_on_termination as the interface is not attached")
|
||||||
|
if eni.attachment is not None and instance_id is None and do_detach is True:
|
||||||
|
eni.detach(force_detach)
|
||||||
|
wait_for_eni(eni, "detached")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
if instance_id is not None:
|
||||||
|
eni.attach(instance_id, device_index)
|
||||||
|
wait_for_eni(eni, "attached")
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
except BotoServerError as e:
|
||||||
|
print e
|
||||||
|
module.fail_json(msg=get_error_message(e.args[2]))
|
||||||
|
|
||||||
|
eni.update()
|
||||||
|
module.exit_json(changed=changed, interface=get_eni_info(eni))
|
||||||
|
|
||||||
|
|
||||||
|
def delete_eni(connection, module):
|
||||||
|
|
||||||
|
eni_id = module.params.get("eni_id")
|
||||||
|
force_detach = module.params.get("force_detach")
|
||||||
|
|
||||||
|
try:
|
||||||
|
eni_result_set = connection.get_all_network_interfaces(eni_id)
|
||||||
|
eni = eni_result_set[0]
|
||||||
|
|
||||||
|
if force_detach is True:
|
||||||
|
if eni.attachment is not None:
|
||||||
|
eni.detach(force_detach)
|
||||||
|
# Wait to allow detachment to finish
|
||||||
|
wait_for_eni(eni, "detached")
|
||||||
|
eni.update()
|
||||||
|
eni.delete()
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
eni.delete()
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
except BotoServerError as e:
|
||||||
|
msg = get_error_message(e.args[2])
|
||||||
|
regex = re.compile('The networkInterface ID \'.*\' does not exist')
|
||||||
|
if regex.search(msg) is not None:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
else:
|
||||||
|
module.fail_json(msg=get_error_message(e.args[2]))
|
||||||
|
|
||||||
|
def compare_eni(connection, module):
|
||||||
|
|
||||||
|
eni_id = module.params.get("eni_id")
|
||||||
|
subnet_id = module.params.get('subnet_id')
|
||||||
|
private_ip_address = module.params.get('private_ip_address')
|
||||||
|
description = module.params.get('description')
|
||||||
|
security_groups = module.params.get('security_groups')
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_eni = connection.get_all_network_interfaces(eni_id)
|
||||||
|
|
||||||
|
for eni in all_eni:
|
||||||
|
remote_security_groups = get_sec_group_list(eni.groups)
|
||||||
|
if (eni.subnet_id == subnet_id) and (eni.private_ip_address == private_ip_address) and (eni.description == description) and (remote_security_groups == security_groups):
|
||||||
|
return eni
|
||||||
|
|
||||||
|
except BotoServerError as e:
|
||||||
|
module.fail_json(msg=get_error_message(e.args[2]))
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_sec_group_list(groups):
|
||||||
|
|
||||||
|
# Build list of remote security groups
|
||||||
|
remote_security_groups = []
|
||||||
|
for group in groups:
|
||||||
|
remote_security_groups.append(group.id.encode())
|
||||||
|
|
||||||
|
return remote_security_groups
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
eni_id = dict(default=None),
|
||||||
|
instance_id = dict(default=None),
|
||||||
|
private_ip_address = dict(),
|
||||||
|
subnet_id = dict(),
|
||||||
|
description = dict(),
|
||||||
|
security_groups = dict(type='list'),
|
||||||
|
device_index = dict(default=0, type='int'),
|
||||||
|
state = dict(default='present', choices=['present', 'absent']),
|
||||||
|
force_detach = dict(default='no', type='bool'),
|
||||||
|
source_dest_check = dict(default=None, type='bool'),
|
||||||
|
delete_on_termination = dict(default=None, type='bool')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto required for this module')
|
||||||
|
|
||||||
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||||
|
|
||||||
|
if region:
|
||||||
|
try:
|
||||||
|
connection = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
||||||
|
except (boto.exception.NoAuthHandlerFound, StandardError), e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="region must be specified")
|
||||||
|
|
||||||
|
state = module.params.get("state")
|
||||||
|
eni_id = module.params.get("eni_id")
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
if eni_id is None:
|
||||||
|
if module.params.get("subnet_id") is None:
|
||||||
|
module.fail_json(msg="subnet_id must be specified when state=present")
|
||||||
|
create_eni(connection, module)
|
||||||
|
else:
|
||||||
|
modify_eni(connection, module)
|
||||||
|
elif state == 'absent':
|
||||||
|
if eni_id is None:
|
||||||
|
module.fail_json(msg="eni_id must be specified")
|
||||||
|
else:
|
||||||
|
delete_eni(connection, module)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
# this is magic, see lib/ansible/module_common.py
|
||||||
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,135 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# This is a 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 Ansible library 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 library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: ec2_eni_facts
|
||||||
|
short_description: Gather facts about ec2 ENI interfaces in AWS
|
||||||
|
description:
|
||||||
|
- Gather facts about ec2 ENI interfaces in AWS
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Rob White (@wimnat)"
|
||||||
|
options:
|
||||||
|
eni_id:
|
||||||
|
description:
|
||||||
|
- The ID of the ENI. Pass this option to gather facts about a particular ENI, otherwise, all ENIs are returned.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Note: These examples do not set authentication details, see the AWS Guide for details.
|
||||||
|
|
||||||
|
# Gather facts about all ENIs
|
||||||
|
- ec2_eni_facts:
|
||||||
|
|
||||||
|
# Gather facts about a particular ENI
|
||||||
|
- ec2_eni_facts:
|
||||||
|
eni_id: eni-xxxxxxx
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto.ec2
|
||||||
|
from boto.exception import BotoServerError
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
|
||||||
|
def get_error_message(xml_string):
|
||||||
|
|
||||||
|
root = ET.fromstring(xml_string)
|
||||||
|
for message in root.findall('.//Message'):
|
||||||
|
return message.text
|
||||||
|
|
||||||
|
|
||||||
|
def get_eni_info(interface):
|
||||||
|
|
||||||
|
interface_info = {'id': interface.id,
|
||||||
|
'subnet_id': interface.subnet_id,
|
||||||
|
'vpc_id': interface.vpc_id,
|
||||||
|
'description': interface.description,
|
||||||
|
'owner_id': interface.owner_id,
|
||||||
|
'status': interface.status,
|
||||||
|
'mac_address': interface.mac_address,
|
||||||
|
'private_ip_address': interface.private_ip_address,
|
||||||
|
'source_dest_check': interface.source_dest_check,
|
||||||
|
'groups': dict((group.id, group.name) for group in interface.groups),
|
||||||
|
}
|
||||||
|
|
||||||
|
if interface.attachment is not None:
|
||||||
|
interface_info['attachment'] = {'attachment_id': interface.attachment.id,
|
||||||
|
'instance_id': interface.attachment.instance_id,
|
||||||
|
'device_index': interface.attachment.device_index,
|
||||||
|
'status': interface.attachment.status,
|
||||||
|
'attach_time': interface.attachment.attach_time,
|
||||||
|
'delete_on_termination': interface.attachment.delete_on_termination,
|
||||||
|
}
|
||||||
|
|
||||||
|
return interface_info
|
||||||
|
|
||||||
|
|
||||||
|
def list_eni(connection, module):
|
||||||
|
|
||||||
|
eni_id = module.params.get("eni_id")
|
||||||
|
interface_dict_array = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_eni = connection.get_all_network_interfaces(eni_id)
|
||||||
|
except BotoServerError as e:
|
||||||
|
module.fail_json(msg=get_error_message(e.args[2]))
|
||||||
|
|
||||||
|
for interface in all_eni:
|
||||||
|
interface_dict_array.append(get_eni_info(interface))
|
||||||
|
|
||||||
|
module.exit_json(interfaces=interface_dict_array)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
eni_id = dict(default=None)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto required for this module')
|
||||||
|
|
||||||
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||||
|
|
||||||
|
if region:
|
||||||
|
try:
|
||||||
|
connection = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
||||||
|
except (boto.exception.NoAuthHandlerFound, StandardError), e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="region must be specified")
|
||||||
|
|
||||||
|
list_eni(connection, module)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
# this is magic, see lib/ansible/module_common.py
|
||||||
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# This is a 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 Ansible library 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 library. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: ec2_vpc_igw
|
||||||
|
short_description: Manage an AWS VPC Internet gateway
|
||||||
|
description:
|
||||||
|
- Manage an AWS VPC Internet gateway
|
||||||
|
version_added: "2.0"
|
||||||
|
author: Robert Estelle, @erydo
|
||||||
|
options:
|
||||||
|
vpc_id:
|
||||||
|
description:
|
||||||
|
- The VPC ID for the VPC in which to manage the Internet Gateway.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Create or terminate the IGW
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Note: These examples do not set authentication details, see the AWS Guide for details.
|
||||||
|
|
||||||
|
# Ensure that the VPC has an Internet Gateway.
|
||||||
|
# The Internet Gateway ID is can be accessed via {{igw.gateway_id}} for use
|
||||||
|
# in setting up NATs etc.
|
||||||
|
local_action:
|
||||||
|
module: ec2_vpc_igw
|
||||||
|
vpc_id: {{vpc.vpc_id}}
|
||||||
|
region: {{vpc.vpc.region}}
|
||||||
|
state: present
|
||||||
|
register: igw
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
import sys # noqa
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto.ec2
|
||||||
|
import boto.vpc
|
||||||
|
from boto.exception import EC2ResponseError
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
if __name__ != '__main__':
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleIGWException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_igw_absent(vpc_conn, vpc_id, check_mode):
|
||||||
|
igws = vpc_conn.get_all_internet_gateways(
|
||||||
|
filters={'attachment.vpc-id': vpc_id})
|
||||||
|
|
||||||
|
if not igws:
|
||||||
|
return {'changed': False}
|
||||||
|
|
||||||
|
if check_mode:
|
||||||
|
return {'changed': True}
|
||||||
|
|
||||||
|
for igw in igws:
|
||||||
|
try:
|
||||||
|
vpc_conn.detach_internet_gateway(igw.id, vpc_id)
|
||||||
|
vpc_conn.delete_internet_gateway(igw.id)
|
||||||
|
except EC2ResponseError as e:
|
||||||
|
raise AnsibleIGWException(
|
||||||
|
'Unable to delete Internet Gateway, error: {0}'.format(e))
|
||||||
|
|
||||||
|
return {'changed': True}
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_igw_present(vpc_conn, vpc_id, check_mode):
|
||||||
|
igws = vpc_conn.get_all_internet_gateways(
|
||||||
|
filters={'attachment.vpc-id': vpc_id})
|
||||||
|
|
||||||
|
if len(igws) > 1:
|
||||||
|
raise AnsibleIGWException(
|
||||||
|
'EC2 returned more than one Internet Gateway for VPC {0}, aborting'
|
||||||
|
.format(vpc_id))
|
||||||
|
|
||||||
|
if igws:
|
||||||
|
return {'changed': False, 'gateway_id': igws[0].id}
|
||||||
|
else:
|
||||||
|
if check_mode:
|
||||||
|
return {'changed': True, 'gateway_id': None}
|
||||||
|
|
||||||
|
try:
|
||||||
|
igw = vpc_conn.create_internet_gateway()
|
||||||
|
vpc_conn.attach_internet_gateway(igw.id, vpc_id)
|
||||||
|
return {'changed': True, 'gateway_id': igw.id}
|
||||||
|
except EC2ResponseError as e:
|
||||||
|
raise AnsibleIGWException(
|
||||||
|
'Unable to create Internet Gateway, error: {0}'.format(e))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
vpc_id = dict(required=True),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
supports_check_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='boto is required for this module')
|
||||||
|
|
||||||
|
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
|
||||||
|
|
||||||
|
if region:
|
||||||
|
try:
|
||||||
|
connection = connect_to_aws(boto.ec2, region, **aws_connect_params)
|
||||||
|
except (boto.exception.NoAuthHandlerFound, StandardError), e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="region must be specified")
|
||||||
|
|
||||||
|
vpc_id = module.params.get('vpc_id')
|
||||||
|
state = module.params.get('state', 'present')
|
||||||
|
|
||||||
|
try:
|
||||||
|
if state == 'present':
|
||||||
|
result = ensure_igw_present(connection, vpc_id, check_mode=module.check_mode)
|
||||||
|
elif state == 'absent':
|
||||||
|
result = ensure_igw_absent(connection, vpc_id, check_mode=module.check_mode)
|
||||||
|
except AnsibleIGWException as e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import * # noqa
|
||||||
|
from ansible.module_utils.ec2 import * # noqa
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: ec2_win_password
|
||||||
|
short_description: gets the default administrator password for ec2 windows instances
|
||||||
|
description:
|
||||||
|
- Gets the default administrator password from any EC2 Windows instance. The instance is referenced by its id (e.g. i-XXXXXXX). This module has a dependency on python-boto.
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Rick Mendes (@rickmendes)"
|
||||||
|
options:
|
||||||
|
instance_id:
|
||||||
|
description:
|
||||||
|
- The instance id to get the password data from.
|
||||||
|
required: true
|
||||||
|
key_file:
|
||||||
|
description:
|
||||||
|
- path to the file containing the key pair used on the instance
|
||||||
|
required: true
|
||||||
|
region:
|
||||||
|
description:
|
||||||
|
- The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aliases: [ 'aws_region', 'ec2_region' ]
|
||||||
|
|
||||||
|
extends_documentation_fragment: aws
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Example of getting a password
|
||||||
|
tasks:
|
||||||
|
- name: get the Administrator password
|
||||||
|
ec2_win_password:
|
||||||
|
profile: my-boto-profile
|
||||||
|
instance_id: i-XXXXXX
|
||||||
|
region: us-east-1
|
||||||
|
key_file: "~/aws-creds/my_test_key.pem"
|
||||||
|
'''
|
||||||
|
|
||||||
|
from base64 import b64decode
|
||||||
|
from os.path import expanduser
|
||||||
|
from Crypto.Cipher import PKCS1_v1_5
|
||||||
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
|
try:
|
||||||
|
import boto.ec2
|
||||||
|
HAS_BOTO = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_BOTO = False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = ec2_argument_spec()
|
||||||
|
argument_spec.update(dict(
|
||||||
|
instance_id = dict(required=True),
|
||||||
|
key_file = dict(required=True),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec)
|
||||||
|
|
||||||
|
if not HAS_BOTO:
|
||||||
|
module.fail_json(msg='Boto required for this module.')
|
||||||
|
|
||||||
|
instance_id = module.params.get('instance_id')
|
||||||
|
key_file = expanduser(module.params.get('key_file'))
|
||||||
|
|
||||||
|
ec2 = ec2_connect(module)
|
||||||
|
|
||||||
|
data = ec2.get_password_data(instance_id)
|
||||||
|
decoded = b64decode(data)
|
||||||
|
|
||||||
|
f = open(key_file, 'r')
|
||||||
|
key = RSA.importKey(f.read())
|
||||||
|
cipher = PKCS1_v1_5.new(key)
|
||||||
|
sentinel = 'password decryption failed!!!'
|
||||||
|
|
||||||
|
try:
|
||||||
|
decrypted = cipher.decrypt(decoded, sentinel)
|
||||||
|
except ValueError as e:
|
||||||
|
decrypted = None
|
||||||
|
|
||||||
|
if decrypted == None:
|
||||||
|
module.exit_json(win_password='', changed=False)
|
||||||
|
else:
|
||||||
|
module.exit_json(win_password=decrypted, changed=True)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.ec2 import *
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,410 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_account
|
||||||
|
short_description: Manages account on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create, disable, lock, enable and remove accounts.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of account.
|
||||||
|
required: true
|
||||||
|
username:
|
||||||
|
description:
|
||||||
|
- Username of the user to be created if account did not exist.
|
||||||
|
- Required on C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- Password of the user to be created if account did not exist.
|
||||||
|
- Required on C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
first_name:
|
||||||
|
description:
|
||||||
|
- First name of the user to be created if account did not exist.
|
||||||
|
- Required on C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
last_name:
|
||||||
|
description:
|
||||||
|
- Last name of the user to be created if account did not exist.
|
||||||
|
- Required on C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
email:
|
||||||
|
description:
|
||||||
|
- Email of the user to be created if account did not exist.
|
||||||
|
- Required on C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
timezone:
|
||||||
|
description:
|
||||||
|
- Timezone of the user to be created if account did not exist.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
network_domain:
|
||||||
|
description:
|
||||||
|
- Network domain of the account.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account_type:
|
||||||
|
description:
|
||||||
|
- Type of the account.
|
||||||
|
required: false
|
||||||
|
default: 'user'
|
||||||
|
choices: [ 'user', 'root_admin', 'domain_admin' ]
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the account is related to.
|
||||||
|
required: false
|
||||||
|
default: 'ROOT'
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the account.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent', 'enabled', 'disabled', 'locked' ]
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# create an account in domain 'CUSTOMERS'
|
||||||
|
local_action:
|
||||||
|
module: cs_account
|
||||||
|
name: customer_xy
|
||||||
|
username: customer_xy
|
||||||
|
password: S3Cur3
|
||||||
|
last_name: Doe
|
||||||
|
first_name: John
|
||||||
|
email: john.doe@example.com
|
||||||
|
domain: CUSTOMERS
|
||||||
|
|
||||||
|
# Lock an existing account in domain 'CUSTOMERS'
|
||||||
|
local_action:
|
||||||
|
module: cs_account
|
||||||
|
name: customer_xy
|
||||||
|
domain: CUSTOMERS
|
||||||
|
state: locked
|
||||||
|
|
||||||
|
# Disable an existing account in domain 'CUSTOMERS'
|
||||||
|
local_action:
|
||||||
|
module: cs_account
|
||||||
|
name: customer_xy
|
||||||
|
domain: CUSTOMERS
|
||||||
|
state: disabled
|
||||||
|
|
||||||
|
# Enable an existing account in domain 'CUSTOMERS'
|
||||||
|
local_action:
|
||||||
|
module: cs_account
|
||||||
|
name: customer_xy
|
||||||
|
domain: CUSTOMERS
|
||||||
|
state: enabled
|
||||||
|
|
||||||
|
# Remove an account in domain 'CUSTOMERS'
|
||||||
|
local_action:
|
||||||
|
module: cs_account
|
||||||
|
name: customer_xy
|
||||||
|
domain: CUSTOMERS
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of the account.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: linus@example.com
|
||||||
|
account_type:
|
||||||
|
description: Type of the account.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: user
|
||||||
|
account_state:
|
||||||
|
description: State of the account.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: enabled
|
||||||
|
network_domain:
|
||||||
|
description: Network domain of the account.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example.local
|
||||||
|
domain:
|
||||||
|
description: Domain the account is related.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ROOT
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackAccount(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.account = None
|
||||||
|
self.account_types = {
|
||||||
|
'user': 0,
|
||||||
|
'root_admin': 1,
|
||||||
|
'domain_admin': 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_account_type(self):
|
||||||
|
account_type = self.module.params.get('account_type')
|
||||||
|
return self.account_types[account_type]
|
||||||
|
|
||||||
|
|
||||||
|
def get_account(self):
|
||||||
|
if not self.account:
|
||||||
|
args = {}
|
||||||
|
args['listall'] = True
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
accounts = self.cs.listAccounts(**args)
|
||||||
|
if accounts:
|
||||||
|
account_name = self.module.params.get('name')
|
||||||
|
for a in accounts['account']:
|
||||||
|
if account_name in [ a['name'] ]:
|
||||||
|
self.account = a
|
||||||
|
break
|
||||||
|
|
||||||
|
return self.account
|
||||||
|
|
||||||
|
|
||||||
|
def enable_account(self):
|
||||||
|
account = self.get_account()
|
||||||
|
if not account:
|
||||||
|
self.module.fail_json(msg="Failed: account not present")
|
||||||
|
|
||||||
|
if account['state'].lower() != 'enabled':
|
||||||
|
self.result['changed'] = True
|
||||||
|
args = {}
|
||||||
|
args['id'] = account['id']
|
||||||
|
args['account'] = self.module.params.get('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.enableAccount(**args)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
account = res['account']
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
def lock_account(self):
|
||||||
|
return self.lock_or_disable_account(lock=True)
|
||||||
|
|
||||||
|
|
||||||
|
def disable_account(self):
|
||||||
|
return self.lock_or_disable_account()
|
||||||
|
|
||||||
|
|
||||||
|
def lock_or_disable_account(self, lock=False):
|
||||||
|
account = self.get_account()
|
||||||
|
if not account:
|
||||||
|
self.module.fail_json(msg="Failed: account not present")
|
||||||
|
|
||||||
|
# we need to enable the account to lock it.
|
||||||
|
if lock and account['state'].lower() == 'disabled':
|
||||||
|
account = self.enable_account()
|
||||||
|
|
||||||
|
if lock and account['state'].lower() != 'locked' \
|
||||||
|
or not lock and account['state'].lower() != 'disabled':
|
||||||
|
self.result['changed'] = True
|
||||||
|
args = {}
|
||||||
|
args['id'] = account['id']
|
||||||
|
args['account'] = self.module.params.get('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['lock'] = lock
|
||||||
|
if not self.module.check_mode:
|
||||||
|
account = self.cs.disableAccount(**args)
|
||||||
|
|
||||||
|
if 'errortext' in account:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % account['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
account = self._poll_job(account, 'account')
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
def present_account(self):
|
||||||
|
missing_params = []
|
||||||
|
|
||||||
|
if not self.module.params.get('email'):
|
||||||
|
missing_params.append('email')
|
||||||
|
|
||||||
|
if not self.module.params.get('username'):
|
||||||
|
missing_params.append('username')
|
||||||
|
|
||||||
|
if not self.module.params.get('password'):
|
||||||
|
missing_params.append('password')
|
||||||
|
|
||||||
|
if not self.module.params.get('first_name'):
|
||||||
|
missing_params.append('first_name')
|
||||||
|
|
||||||
|
if not self.module.params.get('last_name'):
|
||||||
|
missing_params.append('last_name')
|
||||||
|
|
||||||
|
if missing_params:
|
||||||
|
self.module.fail_json(msg="missing required arguments: %s" % ','.join(missing_params))
|
||||||
|
|
||||||
|
account = self.get_account()
|
||||||
|
|
||||||
|
if not account:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.module.params.get('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['accounttype'] = self.get_account_type()
|
||||||
|
args['networkdomain'] = self.module.params.get('network_domain')
|
||||||
|
args['username'] = self.module.params.get('username')
|
||||||
|
args['password'] = self.module.params.get('password')
|
||||||
|
args['firstname'] = self.module.params.get('first_name')
|
||||||
|
args['lastname'] = self.module.params.get('last_name')
|
||||||
|
args['email'] = self.module.params.get('email')
|
||||||
|
args['timezone'] = self.module.params.get('timezone')
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createAccount(**args)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
account = res['account']
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
def absent_account(self):
|
||||||
|
account = self.get_account()
|
||||||
|
if account:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteAccount(id=account['id'])
|
||||||
|
|
||||||
|
if 'errortext' in account:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
res = self._poll_job(res, 'account')
|
||||||
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, account):
|
||||||
|
if account:
|
||||||
|
if 'name' in account:
|
||||||
|
self.result['name'] = account['name']
|
||||||
|
if 'accounttype' in account:
|
||||||
|
for key,value in self.account_types.items():
|
||||||
|
if value == account['accounttype']:
|
||||||
|
self.result['account_type'] = key
|
||||||
|
break
|
||||||
|
if 'state' in account:
|
||||||
|
self.result['account_state'] = account['state']
|
||||||
|
if 'domain' in account:
|
||||||
|
self.result['domain'] = account['domain']
|
||||||
|
if 'networkdomain' in account:
|
||||||
|
self.result['network_domain'] = account['networkdomain']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
state = dict(choices=['present', 'absent', 'enabled', 'disabled', 'locked' ], default='present'),
|
||||||
|
account_type = dict(choices=['user', 'root_admin', 'domain_admin'], default='user'),
|
||||||
|
network_domain = dict(default=None),
|
||||||
|
domain = dict(default='ROOT'),
|
||||||
|
email = dict(default=None),
|
||||||
|
first_name = dict(default=None),
|
||||||
|
last_name = dict(default=None),
|
||||||
|
username = dict(default=None),
|
||||||
|
password = dict(default=None),
|
||||||
|
timezone = dict(default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_acc = AnsibleCloudStackAccount(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
if state in ['absent']:
|
||||||
|
account = acs_acc.absent_account()
|
||||||
|
|
||||||
|
elif state in ['enabled']:
|
||||||
|
account = acs_acc.enable_account()
|
||||||
|
|
||||||
|
elif state in ['disabled']:
|
||||||
|
account = acs_acc.disable_account()
|
||||||
|
|
||||||
|
elif state in ['locked']:
|
||||||
|
account = acs_acc.lock_account()
|
||||||
|
|
||||||
|
else:
|
||||||
|
account = acs_acc.present_account()
|
||||||
|
|
||||||
|
result = acs_acc.get_result(account)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_affinitygroup
|
||||||
|
short_description: Manages affinity groups on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create and remove affinity groups.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the affinity group.
|
||||||
|
required: true
|
||||||
|
affinty_type:
|
||||||
|
description:
|
||||||
|
- Type of the affinity group. If not specified, first found affinity type is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Description of the affinity group.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the affinity group.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the affinity group is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the affinity group is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a affinity group
|
||||||
|
- local_action:
|
||||||
|
module: cs_affinitygroup
|
||||||
|
name: haproxy
|
||||||
|
affinty_type: host anti-affinity
|
||||||
|
|
||||||
|
# Remove a affinity group
|
||||||
|
- local_action:
|
||||||
|
module: cs_affinitygroup
|
||||||
|
name: haproxy
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of affinity group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: app
|
||||||
|
description:
|
||||||
|
description: Description of affinity group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: application affinity group
|
||||||
|
affinity_type:
|
||||||
|
description: Type of affinity group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: host anti-affinity
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackAffinityGroup(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.affinity_group = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_affinity_group(self):
|
||||||
|
if not self.affinity_group:
|
||||||
|
affinity_group = self.module.params.get('name')
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
|
||||||
|
affinity_groups = self.cs.listAffinityGroups(**args)
|
||||||
|
if affinity_groups:
|
||||||
|
for a in affinity_groups['affinitygroup']:
|
||||||
|
if affinity_group in [ a['name'], a['id'] ]:
|
||||||
|
self.affinity_group = a
|
||||||
|
break
|
||||||
|
return self.affinity_group
|
||||||
|
|
||||||
|
|
||||||
|
def get_affinity_type(self):
|
||||||
|
affinity_type = self.module.params.get('affinty_type')
|
||||||
|
|
||||||
|
affinity_types = self.cs.listAffinityGroupTypes()
|
||||||
|
if affinity_types:
|
||||||
|
if not affinity_type:
|
||||||
|
return affinity_types['affinityGroupType'][0]['type']
|
||||||
|
|
||||||
|
for a in affinity_types['affinityGroupType']:
|
||||||
|
if a['type'] == affinity_type:
|
||||||
|
return a['type']
|
||||||
|
self.module.fail_json(msg="affinity group type '%s' not found" % affinity_type)
|
||||||
|
|
||||||
|
|
||||||
|
def create_affinity_group(self):
|
||||||
|
affinity_group = self.get_affinity_group()
|
||||||
|
if not affinity_group:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['type'] = self.get_affinity_type()
|
||||||
|
args['description'] = self.module.params.get('description')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createAffinityGroup(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
affinity_group = self._poll_job(res, 'affinitygroup')
|
||||||
|
return affinity_group
|
||||||
|
|
||||||
|
|
||||||
|
def remove_affinity_group(self):
|
||||||
|
affinity_group = self.get_affinity_group()
|
||||||
|
if affinity_group:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteAffinityGroup(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
res = self._poll_job(res, 'affinitygroup')
|
||||||
|
return affinity_group
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, affinity_group):
|
||||||
|
if affinity_group:
|
||||||
|
if 'name' in affinity_group:
|
||||||
|
self.result['name'] = affinity_group['name']
|
||||||
|
if 'description' in affinity_group:
|
||||||
|
self.result['description'] = affinity_group['description']
|
||||||
|
if 'type' in affinity_group:
|
||||||
|
self.result['affinity_type'] = affinity_group['type']
|
||||||
|
if 'domain' in affinity_group:
|
||||||
|
self.result['domain'] = affinity_group['domain']
|
||||||
|
if 'account' in affinity_group:
|
||||||
|
self.result['account'] = affinity_group['account']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
affinty_type = dict(default=None),
|
||||||
|
description = dict(default=None),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_ag = AnsibleCloudStackAffinityGroup(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
affinity_group = acs_ag.remove_affinity_group()
|
||||||
|
else:
|
||||||
|
affinity_group = acs_ag.create_affinity_group()
|
||||||
|
|
||||||
|
result = acs_ag.get_result(affinity_group)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,221 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_facts
|
||||||
|
short_description: Gather facts on instances of Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- This module fetches data from the metadata API in CloudStack. The module must be called from within the instance itself.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
filter:
|
||||||
|
description:
|
||||||
|
- Filter for a specific fact.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
choices:
|
||||||
|
- cloudstack_service_offering
|
||||||
|
- cloudstack_availability_zone
|
||||||
|
- cloudstack_public_hostname
|
||||||
|
- cloudstack_public_ipv4
|
||||||
|
- cloudstack_local_hostname
|
||||||
|
- cloudstack_local_ipv4
|
||||||
|
- cloudstack_instance_id
|
||||||
|
- cloudstack_user_data
|
||||||
|
requirements: [ 'yaml' ]
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Gather all facts on instances
|
||||||
|
- name: Gather cloudstack facts
|
||||||
|
cs_facts:
|
||||||
|
|
||||||
|
# Gather specific fact on instances
|
||||||
|
- name: Gather cloudstack facts
|
||||||
|
cs_facts: filter=cloudstack_instance_id
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
cloudstack_availability_zone:
|
||||||
|
description: zone the instance is deployed in.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ch-gva-2
|
||||||
|
cloudstack_instance_id:
|
||||||
|
description: UUID of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ab4e80b0-3e7e-4936-bdc5-e334ba5b0139
|
||||||
|
cloudstack_local_hostname:
|
||||||
|
description: local hostname of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139
|
||||||
|
cloudstack_local_ipv4:
|
||||||
|
description: local IPv4 of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 185.19.28.35
|
||||||
|
cloudstack_public_hostname:
|
||||||
|
description: public hostname of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: VM-ab4e80b0-3e7e-4936-bdc5-e334ba5b0139
|
||||||
|
cloudstack_public_ipv4:
|
||||||
|
description: public IPv4 of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 185.19.28.35
|
||||||
|
cloudstack_service_offering:
|
||||||
|
description: service offering of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Micro 512mb 1cpu
|
||||||
|
cloudstack_user_data:
|
||||||
|
description: data of the instance provided by users.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: { "bla": "foo" }
|
||||||
|
'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
try:
|
||||||
|
import yaml
|
||||||
|
has_lib_yaml = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_yaml = False
|
||||||
|
|
||||||
|
CS_METADATA_BASE_URL = "http://%s/latest/meta-data"
|
||||||
|
CS_USERDATA_BASE_URL = "http://%s/latest/user-data"
|
||||||
|
|
||||||
|
class CloudStackFacts(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.facts = ansible_facts(module)
|
||||||
|
self.api_ip = None
|
||||||
|
self.fact_paths = {
|
||||||
|
'cloudstack_service_offering': 'service-offering',
|
||||||
|
'cloudstack_availability_zone': 'availability-zone',
|
||||||
|
'cloudstack_public_hostname': 'public-hostname',
|
||||||
|
'cloudstack_public_ipv4': 'public-ipv4',
|
||||||
|
'cloudstack_local_hostname': 'local-hostname',
|
||||||
|
'cloudstack_local_ipv4': 'local-ipv4',
|
||||||
|
'cloudstack_instance_id': 'instance-id'
|
||||||
|
}
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
result = {}
|
||||||
|
filter = module.params.get('filter')
|
||||||
|
if not filter:
|
||||||
|
for key,path in self.fact_paths.iteritems():
|
||||||
|
result[key] = self._fetch(CS_METADATA_BASE_URL + "/" + path)
|
||||||
|
result['cloudstack_user_data'] = self._get_user_data_json()
|
||||||
|
else:
|
||||||
|
if filter == 'cloudstack_user_data':
|
||||||
|
result['cloudstack_user_data'] = self._get_user_data_json()
|
||||||
|
elif filter in self.fact_paths:
|
||||||
|
result[filter] = self._fetch(CS_METADATA_BASE_URL + "/" + self.fact_paths[filter])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_user_data_json(self):
|
||||||
|
try:
|
||||||
|
# this data come form users, we try what we can to parse it...
|
||||||
|
return yaml.load(self._fetch(CS_USERDATA_BASE_URL))
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch(self, path):
|
||||||
|
api_ip = self._get_api_ip()
|
||||||
|
if not api_ip:
|
||||||
|
return None
|
||||||
|
api_url = path % api_ip
|
||||||
|
(response, info) = fetch_url(module, api_url, force=True)
|
||||||
|
if response:
|
||||||
|
data = response.read()
|
||||||
|
else:
|
||||||
|
data = None
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dhcp_lease_file(self):
|
||||||
|
"""Return the path of the lease file."""
|
||||||
|
default_iface = self.facts['default_ipv4']['interface']
|
||||||
|
dhcp_lease_file_locations = [
|
||||||
|
'/var/lib/dhcp/dhclient.%s.leases' % default_iface, # debian / ubuntu
|
||||||
|
'/var/lib/dhclient/dhclient-%s.leases' % default_iface, # centos 6
|
||||||
|
'/var/lib/dhclient/dhclient--%s.lease' % default_iface, # centos 7
|
||||||
|
'/var/db/dhclient.leases.%s' % default_iface, # openbsd
|
||||||
|
]
|
||||||
|
for file_path in dhcp_lease_file_locations:
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return file_path
|
||||||
|
module.fail_json(msg="Could not find dhclient leases file.")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_api_ip(self):
|
||||||
|
"""Return the IP of the DHCP server."""
|
||||||
|
if not self.api_ip:
|
||||||
|
dhcp_lease_file = self._get_dhcp_lease_file()
|
||||||
|
for line in open(dhcp_lease_file):
|
||||||
|
if 'dhcp-server-identifier' in line:
|
||||||
|
# get IP of string "option dhcp-server-identifier 185.19.28.176;"
|
||||||
|
line = line.translate(None, ';')
|
||||||
|
self.api_ip = line.split()[2]
|
||||||
|
break
|
||||||
|
if not self.api_ip:
|
||||||
|
module.fail_json(msg="No dhcp-server-identifier found in leases file.")
|
||||||
|
return self.api_ip
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
global module
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
filter = dict(default=None, choices=[
|
||||||
|
'cloudstack_service_offering',
|
||||||
|
'cloudstack_availability_zone',
|
||||||
|
'cloudstack_public_hostname',
|
||||||
|
'cloudstack_public_ipv4',
|
||||||
|
'cloudstack_local_hostname',
|
||||||
|
'cloudstack_local_ipv4',
|
||||||
|
'cloudstack_instance_id',
|
||||||
|
'cloudstack_user_data',
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
supports_check_mode=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_yaml:
|
||||||
|
module.fail_json(msg="missing python library: yaml")
|
||||||
|
|
||||||
|
cs_facts = CloudStackFacts().run()
|
||||||
|
cs_facts_result = dict(changed=False, ansible_facts=cs_facts)
|
||||||
|
module.exit_json(**cs_facts_result)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.urls import *
|
||||||
|
from ansible.module_utils.facts import *
|
||||||
|
main()
|
@ -0,0 +1,461 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_firewall
|
||||||
|
short_description: Manages firewall rules on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Creates and removes firewall rules.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
ip_address:
|
||||||
|
description:
|
||||||
|
- Public IP address the ingress rule is assigned to.
|
||||||
|
- Required if C(type=ingress).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
network:
|
||||||
|
description:
|
||||||
|
- Network the egress rule is related to.
|
||||||
|
- Required if C(type=egress).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the firewall rule.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- Type of the firewall rule.
|
||||||
|
required: false
|
||||||
|
default: 'ingress'
|
||||||
|
choices: [ 'ingress', 'egress' ]
|
||||||
|
protocol:
|
||||||
|
description:
|
||||||
|
- Protocol of the firewall rule.
|
||||||
|
- C(all) is only available if C(type=egress)
|
||||||
|
required: false
|
||||||
|
default: 'tcp'
|
||||||
|
choices: [ 'tcp', 'udp', 'icmp', 'all' ]
|
||||||
|
cidr:
|
||||||
|
description:
|
||||||
|
- CIDR (full notation) to be used for firewall rule.
|
||||||
|
required: false
|
||||||
|
default: '0.0.0.0/0'
|
||||||
|
start_port:
|
||||||
|
description:
|
||||||
|
- Start port for this rule. Considered if C(protocol=tcp) or C(protocol=udp).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aliases: [ 'port' ]
|
||||||
|
end_port:
|
||||||
|
description:
|
||||||
|
- End port for this rule. Considered if C(protocol=tcp) or C(protocol=udp). If not specified, equal C(start_port).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
icmp_type:
|
||||||
|
description:
|
||||||
|
- Type of the icmp message being sent. Considered if C(protocol=icmp).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
icmp_code:
|
||||||
|
description:
|
||||||
|
- Error code for this icmp message. Considered if C(protocol=icmp).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the firewall rule is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the firewall rule is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the firewall rule is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Allow inbound port 80/tcp from 1.2.3.4 to 4.3.2.1
|
||||||
|
- local_action:
|
||||||
|
module: cs_firewall
|
||||||
|
ip_address: 4.3.2.1
|
||||||
|
port: 80
|
||||||
|
cidr: 1.2.3.4/32
|
||||||
|
|
||||||
|
# Allow inbound tcp/udp port 53 to 4.3.2.1
|
||||||
|
- local_action:
|
||||||
|
module: cs_firewall
|
||||||
|
ip_address: 4.3.2.1
|
||||||
|
port: 53
|
||||||
|
protocol: '{{ item }}'
|
||||||
|
with_items:
|
||||||
|
- tcp
|
||||||
|
- udp
|
||||||
|
|
||||||
|
# Ensure firewall rule is removed
|
||||||
|
- local_action:
|
||||||
|
module: cs_firewall
|
||||||
|
ip_address: 4.3.2.1
|
||||||
|
start_port: 8000
|
||||||
|
end_port: 8888
|
||||||
|
cidr: 17.0.0.0/8
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
# Allow all outbound traffic
|
||||||
|
- local_action:
|
||||||
|
module: cs_firewall
|
||||||
|
network: my_network
|
||||||
|
type: egress
|
||||||
|
protocol: all
|
||||||
|
|
||||||
|
# Allow only HTTP outbound traffic for an IP
|
||||||
|
- local_action:
|
||||||
|
module: cs_firewall
|
||||||
|
network: my_network
|
||||||
|
type: egress
|
||||||
|
port: 80
|
||||||
|
cidr: 10.101.1.20
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
ip_address:
|
||||||
|
description: IP address of the rule if C(type=ingress)
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 10.100.212.10
|
||||||
|
type:
|
||||||
|
description: Type of the rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ingress
|
||||||
|
cidr:
|
||||||
|
description: CIDR of the rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 0.0.0.0/0
|
||||||
|
protocol:
|
||||||
|
description: Protocol of the rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: tcp
|
||||||
|
start_port:
|
||||||
|
description: Start port of the rule.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
end_port:
|
||||||
|
description: End port of the rule.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
icmp_code:
|
||||||
|
description: ICMP code of the rule.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 1
|
||||||
|
icmp_type:
|
||||||
|
description: ICMP type of the rule.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 1
|
||||||
|
network:
|
||||||
|
description: Name of the network if C(type=egress)
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: my_network
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackFirewall(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.firewall_rule = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_end_port(self):
|
||||||
|
if self.module.params.get('end_port'):
|
||||||
|
return self.module.params.get('end_port')
|
||||||
|
return self.module.params.get('start_port')
|
||||||
|
|
||||||
|
|
||||||
|
def get_firewall_rule(self):
|
||||||
|
if not self.firewall_rule:
|
||||||
|
cidr = self.module.params.get('cidr')
|
||||||
|
protocol = self.module.params.get('protocol')
|
||||||
|
start_port = self.module.params.get('start_port')
|
||||||
|
end_port = self.get_end_port()
|
||||||
|
icmp_code = self.module.params.get('icmp_code')
|
||||||
|
icmp_type = self.module.params.get('icmp_type')
|
||||||
|
fw_type = self.module.params.get('type')
|
||||||
|
|
||||||
|
if protocol in ['tcp', 'udp'] and not (start_port and end_port):
|
||||||
|
self.module.fail_json(msg="missing required argument for protocol '%s': start_port or end_port" % protocol)
|
||||||
|
|
||||||
|
if protocol == 'icmp' and not icmp_type:
|
||||||
|
self.module.fail_json(msg="missing required argument for protocol 'icmp': icmp_type")
|
||||||
|
|
||||||
|
if protocol == 'all' and fw_type != 'egress':
|
||||||
|
self.module.fail_json(msg="protocol 'all' could only be used for type 'egress'" )
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
|
||||||
|
if fw_type == 'egress':
|
||||||
|
args['networkid'] = self.get_network(key='id')
|
||||||
|
if not args['networkid']:
|
||||||
|
self.module.fail_json(msg="missing required argument for type egress: network")
|
||||||
|
firewall_rules = self.cs.listEgressFirewallRules(**args)
|
||||||
|
else:
|
||||||
|
args['ipaddressid'] = self.get_ip_address('id')
|
||||||
|
if not args['ipaddressid']:
|
||||||
|
self.module.fail_json(msg="missing required argument for type ingress: ip_address")
|
||||||
|
firewall_rules = self.cs.listFirewallRules(**args)
|
||||||
|
|
||||||
|
if firewall_rules and 'firewallrule' in firewall_rules:
|
||||||
|
for rule in firewall_rules['firewallrule']:
|
||||||
|
type_match = self._type_cidr_match(rule, cidr)
|
||||||
|
|
||||||
|
protocol_match = self._tcp_udp_match(rule, protocol, start_port, end_port) \
|
||||||
|
or self._icmp_match(rule, protocol, icmp_code, icmp_type) \
|
||||||
|
or self._egress_all_match(rule, protocol, fw_type)
|
||||||
|
|
||||||
|
if type_match and protocol_match:
|
||||||
|
self.firewall_rule = rule
|
||||||
|
break
|
||||||
|
return self.firewall_rule
|
||||||
|
|
||||||
|
|
||||||
|
def _tcp_udp_match(self, rule, protocol, start_port, end_port):
|
||||||
|
return protocol in ['tcp', 'udp'] \
|
||||||
|
and protocol == rule['protocol'] \
|
||||||
|
and start_port == int(rule['startport']) \
|
||||||
|
and end_port == int(rule['endport'])
|
||||||
|
|
||||||
|
|
||||||
|
def _egress_all_match(self, rule, protocol, fw_type):
|
||||||
|
return protocol in ['all'] \
|
||||||
|
and protocol == rule['protocol'] \
|
||||||
|
and fw_type == 'egress'
|
||||||
|
|
||||||
|
|
||||||
|
def _icmp_match(self, rule, protocol, icmp_code, icmp_type):
|
||||||
|
return protocol == 'icmp' \
|
||||||
|
and protocol == rule['protocol'] \
|
||||||
|
and icmp_code == rule['icmpcode'] \
|
||||||
|
and icmp_type == rule['icmptype']
|
||||||
|
|
||||||
|
|
||||||
|
def _type_cidr_match(self, rule, cidr):
|
||||||
|
return cidr == rule['cidrlist']
|
||||||
|
|
||||||
|
|
||||||
|
def get_network(self, key=None, network=None):
|
||||||
|
if not network:
|
||||||
|
network = self.module.params.get('network')
|
||||||
|
|
||||||
|
if not network:
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
|
||||||
|
networks = self.cs.listNetworks(**args)
|
||||||
|
if not networks:
|
||||||
|
self.module.fail_json(msg="No networks available")
|
||||||
|
|
||||||
|
for n in networks['network']:
|
||||||
|
if network in [ n['displaytext'], n['name'], n['id'] ]:
|
||||||
|
return self._get_by_key(key, n)
|
||||||
|
break
|
||||||
|
self.module.fail_json(msg="Network '%s' not found" % network)
|
||||||
|
|
||||||
|
|
||||||
|
def create_firewall_rule(self):
|
||||||
|
firewall_rule = self.get_firewall_rule()
|
||||||
|
if not firewall_rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['cidrlist'] = self.module.params.get('cidr')
|
||||||
|
args['protocol'] = self.module.params.get('protocol')
|
||||||
|
args['startport'] = self.module.params.get('start_port')
|
||||||
|
args['endport'] = self.get_end_port()
|
||||||
|
args['icmptype'] = self.module.params.get('icmp_type')
|
||||||
|
args['icmpcode'] = self.module.params.get('icmp_code')
|
||||||
|
|
||||||
|
fw_type = self.module.params.get('type')
|
||||||
|
if not self.module.check_mode:
|
||||||
|
if fw_type == 'egress':
|
||||||
|
args['networkid'] = self.get_network(key='id')
|
||||||
|
res = self.cs.createEgressFirewallRule(**args)
|
||||||
|
else:
|
||||||
|
args['ipaddressid'] = self.get_ip_address('id')
|
||||||
|
res = self.cs.createFirewallRule(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
firewall_rule = self._poll_job(res, 'firewallrule')
|
||||||
|
return firewall_rule
|
||||||
|
|
||||||
|
|
||||||
|
def remove_firewall_rule(self):
|
||||||
|
firewall_rule = self.get_firewall_rule()
|
||||||
|
if firewall_rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = firewall_rule['id']
|
||||||
|
|
||||||
|
fw_type = self.module.params.get('type')
|
||||||
|
if not self.module.check_mode:
|
||||||
|
if fw_type == 'egress':
|
||||||
|
res = self.cs.deleteEgressFirewallRule(**args)
|
||||||
|
else:
|
||||||
|
res = self.cs.deleteFirewallRule(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
res = self._poll_job(res, 'firewallrule')
|
||||||
|
return firewall_rule
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, firewall_rule):
|
||||||
|
if firewall_rule:
|
||||||
|
self.result['type'] = self.module.params.get('type')
|
||||||
|
if 'cidrlist' in firewall_rule:
|
||||||
|
self.result['cidr'] = firewall_rule['cidrlist']
|
||||||
|
if 'startport' in firewall_rule:
|
||||||
|
self.result['start_port'] = int(firewall_rule['startport'])
|
||||||
|
if 'endport' in firewall_rule:
|
||||||
|
self.result['end_port'] = int(firewall_rule['endport'])
|
||||||
|
if 'protocol' in firewall_rule:
|
||||||
|
self.result['protocol'] = firewall_rule['protocol']
|
||||||
|
if 'ipaddress' in firewall_rule:
|
||||||
|
self.result['ip_address'] = firewall_rule['ipaddress']
|
||||||
|
if 'icmpcode' in firewall_rule:
|
||||||
|
self.result['icmp_code'] = int(firewall_rule['icmpcode'])
|
||||||
|
if 'icmptype' in firewall_rule:
|
||||||
|
self.result['icmp_type'] = int(firewall_rule['icmptype'])
|
||||||
|
if 'networkid' in firewall_rule:
|
||||||
|
self.result['network'] = self.get_network(key='displaytext', network=firewall_rule['networkid'])
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
ip_address = dict(default=None),
|
||||||
|
network = dict(default=None),
|
||||||
|
cidr = dict(default='0.0.0.0/0'),
|
||||||
|
protocol = dict(choices=['tcp', 'udp', 'icmp', 'all'], default='tcp'),
|
||||||
|
type = dict(choices=['ingress', 'egress'], default='ingress'),
|
||||||
|
icmp_type = dict(type='int', default=None),
|
||||||
|
icmp_code = dict(type='int', default=None),
|
||||||
|
start_port = dict(type='int', aliases=['port'], default=None),
|
||||||
|
end_port = dict(type='int', default=None),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_one_of = (
|
||||||
|
['ip_address', 'network'],
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['icmp_type', 'icmp_code'],
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
mutually_exclusive = (
|
||||||
|
['icmp_type', 'start_port'],
|
||||||
|
['icmp_type', 'end_port'],
|
||||||
|
['ip_address', 'network'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_fw = AnsibleCloudStackFirewall(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
fw_rule = acs_fw.remove_firewall_rule()
|
||||||
|
else:
|
||||||
|
fw_rule = acs_fw.create_firewall_rule()
|
||||||
|
|
||||||
|
result = acs_fw.get_result(fw_rule)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,865 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_instance
|
||||||
|
short_description: Manages instances and virtual machines on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Deploy, start, restart, stop and destroy instances.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Host name of the instance. C(name) can only contain ASCII letters.
|
||||||
|
required: true
|
||||||
|
display_name:
|
||||||
|
description:
|
||||||
|
- Custom display name of the instances.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
group:
|
||||||
|
description:
|
||||||
|
- Group in where the new instance should be in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the instance.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'deployed', 'started', 'stopped', 'restarted', 'destroyed', 'expunged', 'present', 'absent' ]
|
||||||
|
service_offering:
|
||||||
|
description:
|
||||||
|
- Name or id of the service offering of the new instance.
|
||||||
|
- If not set, first found service offering is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
template:
|
||||||
|
description:
|
||||||
|
- Name or id of the template to be used for creating the new instance.
|
||||||
|
- Required when using C(state=present).
|
||||||
|
- Mutually exclusive with C(ISO) option.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
iso:
|
||||||
|
description:
|
||||||
|
- Name or id of the ISO to be used for creating the new instance.
|
||||||
|
- Required when using C(state=present).
|
||||||
|
- Mutually exclusive with C(template) option.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
hypervisor:
|
||||||
|
description:
|
||||||
|
- Name the hypervisor to be used for creating the new instance.
|
||||||
|
- Relevant when using C(state=present), but only considered if not set on ISO/template.
|
||||||
|
- If not set or found on ISO/template, first found hypervisor will be used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ]
|
||||||
|
keyboard:
|
||||||
|
description:
|
||||||
|
- Keyboard device type for the instance.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
choices: [ 'de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us' ]
|
||||||
|
networks:
|
||||||
|
description:
|
||||||
|
- List of networks to use for the new instance.
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
aliases: [ 'network' ]
|
||||||
|
ip_address:
|
||||||
|
description:
|
||||||
|
- IPv4 address for default instance's network during creation.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
ip6_address:
|
||||||
|
description:
|
||||||
|
- IPv6 address for default instance's network.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
disk_offering:
|
||||||
|
description:
|
||||||
|
- Name of the disk offering to be used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
disk_size:
|
||||||
|
description:
|
||||||
|
- Disk size in GByte required if deploying instance from ISO.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
security_groups:
|
||||||
|
description:
|
||||||
|
- List of security groups the instance to be applied to.
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
aliases: [ 'security_group' ]
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the instance is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the instance is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the instance to be deployed in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
zone:
|
||||||
|
description:
|
||||||
|
- Name of the zone in which the instance shoud be deployed.
|
||||||
|
- If not set, default zone is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
ssh_key:
|
||||||
|
description:
|
||||||
|
- Name of the SSH key to be deployed on the new instance.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
affinity_groups:
|
||||||
|
description:
|
||||||
|
- Affinity groups names to be applied to the new instance.
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
aliases: [ 'affinity_group' ]
|
||||||
|
user_data:
|
||||||
|
description:
|
||||||
|
- Optional data (ASCII) that can be sent to the instance upon a successful deployment.
|
||||||
|
- The data will be automatically base64 encoded.
|
||||||
|
- Consider switching to HTTP_POST by using C(CLOUDSTACK_METHOD=post) to increase the HTTP_GET size limit of 2KB to 32 KB.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- Force stop/start the instance if required to apply changes, otherwise a running instance will not be changed.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
tags:
|
||||||
|
description:
|
||||||
|
- List of tags. Tags are a list of dictionaries having keys C(key) and C(value).
|
||||||
|
- "If you want to delete all tags, set a empty list e.g. C(tags: [])."
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a instance from an ISO
|
||||||
|
# NOTE: Names of offerings and ISOs depending on the CloudStack configuration.
|
||||||
|
- local_action:
|
||||||
|
module: cs_instance
|
||||||
|
name: web-vm-1
|
||||||
|
iso: Linux Debian 7 64-bit
|
||||||
|
hypervisor: VMware
|
||||||
|
project: Integration
|
||||||
|
zone: ch-zrh-ix-01
|
||||||
|
service_offering: 1cpu_1gb
|
||||||
|
disk_offering: PerfPlus Storage
|
||||||
|
disk_size: 20
|
||||||
|
networks:
|
||||||
|
- Server Integration
|
||||||
|
- Sync Integration
|
||||||
|
- Storage Integration
|
||||||
|
|
||||||
|
# For changing a running instance, use the 'force' parameter
|
||||||
|
- local_action:
|
||||||
|
module: cs_instance
|
||||||
|
name: web-vm-1
|
||||||
|
display_name: web-vm-01.example.com
|
||||||
|
iso: Linux Debian 7 64-bit
|
||||||
|
service_offering: 2cpu_2gb
|
||||||
|
force: yes
|
||||||
|
|
||||||
|
# Create or update a instance on Exoscale's public cloud
|
||||||
|
- local_action:
|
||||||
|
module: cs_instance
|
||||||
|
name: web-vm-1
|
||||||
|
template: Linux Debian 7 64-bit
|
||||||
|
service_offering: Tiny
|
||||||
|
ssh_key: john@example.com
|
||||||
|
tags:
|
||||||
|
- { key: admin, value: john }
|
||||||
|
- { key: foo, value: bar }
|
||||||
|
|
||||||
|
# Ensure a instance has stopped
|
||||||
|
- local_action: cs_instance name=web-vm-1 state=stopped
|
||||||
|
|
||||||
|
# Ensure a instance is running
|
||||||
|
- local_action: cs_instance name=web-vm-1 state=started
|
||||||
|
|
||||||
|
# Remove a instance
|
||||||
|
- local_action: cs_instance name=web-vm-1 state=absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
id:
|
||||||
|
description: ID of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
|
||||||
|
name:
|
||||||
|
description: Name of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web-01
|
||||||
|
display_name:
|
||||||
|
description: Display name of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web-01
|
||||||
|
group:
|
||||||
|
description: Group name of the instance is related.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web
|
||||||
|
created:
|
||||||
|
description: Date of the instance was created.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2014-12-01T14:57:57+0100
|
||||||
|
password_enabled:
|
||||||
|
description: True if password setting is enabled.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
password:
|
||||||
|
description: The password of the instance if exists.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Ge2oe7Do
|
||||||
|
ssh_key:
|
||||||
|
description: Name of SSH key deployed to instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: key@work
|
||||||
|
domain:
|
||||||
|
description: Domain the instance is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example domain
|
||||||
|
account:
|
||||||
|
description: Account the instance is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
project:
|
||||||
|
description: Name of project the instance is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Production
|
||||||
|
default_ip:
|
||||||
|
description: Default IP address of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 10.23.37.42
|
||||||
|
public_ip:
|
||||||
|
description: Public IP address with instance via static NAT rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 1.2.3.4
|
||||||
|
iso:
|
||||||
|
description: Name of ISO the instance was deployed with.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Debian-8-64bit
|
||||||
|
template:
|
||||||
|
description: Name of template the instance was deployed with.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Debian-8-64bit
|
||||||
|
service_offering:
|
||||||
|
description: Name of the service offering the instance has.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2cpu_2gb
|
||||||
|
zone:
|
||||||
|
description: Name of zone the instance is in.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ch-gva-2
|
||||||
|
state:
|
||||||
|
description: State of the instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Running
|
||||||
|
security_groups:
|
||||||
|
description: Security groups the instance is in.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
sample: '[ "default" ]'
|
||||||
|
affinity_groups:
|
||||||
|
description: Affinity groups the instance is in.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
sample: '[ "webservers" ]'
|
||||||
|
tags:
|
||||||
|
description: List of resource tags associated with the instance.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: '[ { "key": "foo", "value": "bar" } ]'
|
||||||
|
hypervisor:
|
||||||
|
description: Hypervisor related to this instance.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: KVM
|
||||||
|
instance_name:
|
||||||
|
description: Internal name of the instance (ROOT admin only).
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: i-44-3992-VM
|
||||||
|
'''
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackInstance(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.instance = None
|
||||||
|
self.template = None
|
||||||
|
self.iso = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_offering_id(self):
|
||||||
|
service_offering = self.module.params.get('service_offering')
|
||||||
|
|
||||||
|
service_offerings = self.cs.listServiceOfferings()
|
||||||
|
if service_offerings:
|
||||||
|
if not service_offering:
|
||||||
|
return service_offerings['serviceoffering'][0]['id']
|
||||||
|
|
||||||
|
for s in service_offerings['serviceoffering']:
|
||||||
|
if service_offering in [ s['name'], s['id'] ]:
|
||||||
|
return s['id']
|
||||||
|
self.module.fail_json(msg="Service offering '%s' not found" % service_offering)
|
||||||
|
|
||||||
|
|
||||||
|
def get_template_or_iso(self, key=None):
|
||||||
|
template = self.module.params.get('template')
|
||||||
|
iso = self.module.params.get('iso')
|
||||||
|
|
||||||
|
if not template and not iso:
|
||||||
|
self.module.fail_json(msg="Template or ISO is required.")
|
||||||
|
|
||||||
|
if template and iso:
|
||||||
|
self.module.fail_json(msg="Template are ISO are mutually exclusive.")
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
|
||||||
|
if template:
|
||||||
|
if self.template:
|
||||||
|
return self._get_by_key(key, self.template)
|
||||||
|
|
||||||
|
args['templatefilter'] = 'executable'
|
||||||
|
templates = self.cs.listTemplates(**args)
|
||||||
|
if templates:
|
||||||
|
for t in templates['template']:
|
||||||
|
if template in [ t['displaytext'], t['name'], t['id'] ]:
|
||||||
|
self.template = t
|
||||||
|
return self._get_by_key(key, self.template)
|
||||||
|
self.module.fail_json(msg="Template '%s' not found" % template)
|
||||||
|
|
||||||
|
elif iso:
|
||||||
|
if self.iso:
|
||||||
|
return self._get_by_key(key, self.iso)
|
||||||
|
args['isofilter'] = 'executable'
|
||||||
|
isos = self.cs.listIsos(**args)
|
||||||
|
if isos:
|
||||||
|
for i in isos['iso']:
|
||||||
|
if iso in [ i['displaytext'], i['name'], i['id'] ]:
|
||||||
|
self.iso = i
|
||||||
|
return self._get_by_key(key, self.iso)
|
||||||
|
self.module.fail_json(msg="ISO '%s' not found" % iso)
|
||||||
|
|
||||||
|
|
||||||
|
def get_disk_offering_id(self):
|
||||||
|
disk_offering = self.module.params.get('disk_offering')
|
||||||
|
|
||||||
|
if not disk_offering:
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
|
||||||
|
disk_offerings = self.cs.listDiskOfferings(**args)
|
||||||
|
if disk_offerings:
|
||||||
|
for d in disk_offerings['diskoffering']:
|
||||||
|
if disk_offering in [ d['displaytext'], d['name'], d['id'] ]:
|
||||||
|
return d['id']
|
||||||
|
self.module.fail_json(msg="Disk offering '%s' not found" % disk_offering)
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance(self):
|
||||||
|
instance = self.instance
|
||||||
|
if not instance:
|
||||||
|
instance_name = self.module.params.get('name')
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
|
||||||
|
instances = self.cs.listVirtualMachines(**args)
|
||||||
|
if instances:
|
||||||
|
for v in instances['virtualmachine']:
|
||||||
|
if instance_name in [ v['name'], v['displayname'], v['id'] ]:
|
||||||
|
self.instance = v
|
||||||
|
break
|
||||||
|
return self.instance
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_ids(self):
|
||||||
|
network_names = self.module.params.get('networks')
|
||||||
|
if not network_names:
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
|
||||||
|
networks = self.cs.listNetworks(**args)
|
||||||
|
if not networks:
|
||||||
|
self.module.fail_json(msg="No networks available")
|
||||||
|
|
||||||
|
network_ids = []
|
||||||
|
network_displaytexts = []
|
||||||
|
for network_name in network_names:
|
||||||
|
for n in networks['network']:
|
||||||
|
if network_name in [ n['displaytext'], n['name'], n['id'] ]:
|
||||||
|
network_ids.append(n['id'])
|
||||||
|
network_displaytexts.append(n['name'])
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(network_ids) != len(network_names):
|
||||||
|
self.module.fail_json(msg="Could not find all networks, networks list found: %s" % network_displaytexts)
|
||||||
|
|
||||||
|
return ','.join(network_ids)
|
||||||
|
|
||||||
|
|
||||||
|
def present_instance(self):
|
||||||
|
instance = self.get_instance()
|
||||||
|
if not instance:
|
||||||
|
instance = self.deploy_instance()
|
||||||
|
else:
|
||||||
|
instance = self.update_instance(instance)
|
||||||
|
|
||||||
|
instance = self.ensure_tags(resource=instance, resource_type='UserVm')
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_data(self):
|
||||||
|
user_data = self.module.params.get('user_data')
|
||||||
|
if user_data:
|
||||||
|
user_data = base64.b64encode(user_data)
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
|
||||||
|
def get_display_name(self):
|
||||||
|
display_name = self.module.params.get('display_name')
|
||||||
|
if not display_name:
|
||||||
|
display_name = self.module.params.get('name')
|
||||||
|
return display_name
|
||||||
|
|
||||||
|
|
||||||
|
def deploy_instance(self):
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['templateid'] = self.get_template_or_iso(key='id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
args['serviceofferingid'] = self.get_service_offering_id()
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['diskofferingid'] = self.get_disk_offering_id()
|
||||||
|
args['networkids'] = self.get_network_ids()
|
||||||
|
args['userdata'] = self.get_user_data()
|
||||||
|
args['keyboard'] = self.module.params.get('keyboard')
|
||||||
|
args['ipaddress'] = self.module.params.get('ip_address')
|
||||||
|
args['ip6address'] = self.module.params.get('ip6_address')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['group'] = self.module.params.get('group')
|
||||||
|
args['keypair'] = self.module.params.get('ssh_key')
|
||||||
|
args['size'] = self.module.params.get('disk_size')
|
||||||
|
args['securitygroupnames'] = ','.join(self.module.params.get('security_groups'))
|
||||||
|
args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups'))
|
||||||
|
|
||||||
|
template_iso = self.get_template_or_iso()
|
||||||
|
if 'hypervisor' not in template_iso:
|
||||||
|
args['hypervisor'] = self.get_hypervisor()
|
||||||
|
|
||||||
|
instance = None
|
||||||
|
if not self.module.check_mode:
|
||||||
|
instance = self.cs.deployVirtualMachine(**args)
|
||||||
|
|
||||||
|
if 'errortext' in instance:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % instance['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
instance = self._poll_job(instance, 'virtualmachine')
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def update_instance(self, instance):
|
||||||
|
args_service_offering = {}
|
||||||
|
args_service_offering['id'] = instance['id']
|
||||||
|
args_service_offering['serviceofferingid'] = self.get_service_offering_id()
|
||||||
|
|
||||||
|
args_instance_update = {}
|
||||||
|
args_instance_update['id'] = instance['id']
|
||||||
|
args_instance_update['group'] = self.module.params.get('group')
|
||||||
|
args_instance_update['displayname'] = self.get_display_name()
|
||||||
|
args_instance_update['userdata'] = self.get_user_data()
|
||||||
|
args_instance_update['ostypeid'] = self.get_os_type('id')
|
||||||
|
|
||||||
|
args_ssh_key = {}
|
||||||
|
args_ssh_key['id'] = instance['id']
|
||||||
|
args_ssh_key['keypair'] = self.module.params.get('ssh_key')
|
||||||
|
args_ssh_key['projectid'] = self.get_project('id')
|
||||||
|
|
||||||
|
if self._has_changed(args_service_offering, instance) or \
|
||||||
|
self._has_changed(args_instance_update, instance) or \
|
||||||
|
self._has_changed(args_ssh_key, instance):
|
||||||
|
|
||||||
|
force = self.module.params.get('force')
|
||||||
|
instance_state = instance['state'].lower()
|
||||||
|
|
||||||
|
if instance_state == 'stopped' or force:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
|
||||||
|
# Ensure VM has stopped
|
||||||
|
instance = self.stop_instance()
|
||||||
|
instance = self._poll_job(instance, 'virtualmachine')
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
# Change service offering
|
||||||
|
if self._has_changed(args_service_offering, instance):
|
||||||
|
res = self.cs.changeServiceForVirtualMachine(**args_service_offering)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
instance = res['virtualmachine']
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
# Update VM
|
||||||
|
if self._has_changed(args_instance_update, instance):
|
||||||
|
res = self.cs.updateVirtualMachine(**args_instance_update)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
instance = res['virtualmachine']
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
# Reset SSH key
|
||||||
|
if self._has_changed(args_ssh_key, instance):
|
||||||
|
instance = self.cs.resetSSHKeyForVirtualMachine(**args_ssh_key)
|
||||||
|
if 'errortext' in instance:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % instance['errortext'])
|
||||||
|
|
||||||
|
instance = self._poll_job(instance, 'virtualmachine')
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
|
# Start VM again if it was running before
|
||||||
|
if instance_state == 'running':
|
||||||
|
instance = self.start_instance()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def absent_instance(self):
|
||||||
|
instance = self.get_instance()
|
||||||
|
if instance:
|
||||||
|
if instance['state'].lower() not in ['expunging', 'destroying', 'destroyed']:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.destroyVirtualMachine(id=instance['id'])
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
instance = self._poll_job(res, 'virtualmachine')
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def expunge_instance(self):
|
||||||
|
instance = self.get_instance()
|
||||||
|
if instance:
|
||||||
|
res = {}
|
||||||
|
if instance['state'].lower() in [ 'destroying', 'destroyed' ]:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.expungeVirtualMachine(id=instance['id'])
|
||||||
|
|
||||||
|
elif instance['state'].lower() not in [ 'expunging' ]:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.destroyVirtualMachine(id=instance['id'], expunge=True)
|
||||||
|
|
||||||
|
if res and 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
instance = self._poll_job(res, 'virtualmachine')
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def stop_instance(self):
|
||||||
|
instance = self.get_instance()
|
||||||
|
if not instance:
|
||||||
|
self.module.fail_json(msg="Instance named '%s' not found" % self.module.params.get('name'))
|
||||||
|
|
||||||
|
if instance['state'].lower() in ['stopping', 'stopped']:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
if instance['state'].lower() in ['starting', 'running']:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
instance = self.cs.stopVirtualMachine(id=instance['id'])
|
||||||
|
|
||||||
|
if 'errortext' in instance:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % instance['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
instance = self._poll_job(instance, 'virtualmachine')
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def start_instance(self):
|
||||||
|
instance = self.get_instance()
|
||||||
|
if not instance:
|
||||||
|
self.module.fail_json(msg="Instance named '%s' not found" % module.params.get('name'))
|
||||||
|
|
||||||
|
if instance['state'].lower() in ['starting', 'running']:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
if instance['state'].lower() in ['stopped', 'stopping']:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
instance = self.cs.startVirtualMachine(id=instance['id'])
|
||||||
|
|
||||||
|
if 'errortext' in instance:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % instance['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
instance = self._poll_job(instance, 'virtualmachine')
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def restart_instance(self):
|
||||||
|
instance = self.get_instance()
|
||||||
|
if not instance:
|
||||||
|
module.fail_json(msg="Instance named '%s' not found" % self.module.params.get('name'))
|
||||||
|
|
||||||
|
if instance['state'].lower() in [ 'running', 'starting' ]:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
instance = self.cs.rebootVirtualMachine(id=instance['id'])
|
||||||
|
|
||||||
|
if 'errortext' in instance:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % instance['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
instance = self._poll_job(instance, 'virtualmachine')
|
||||||
|
|
||||||
|
elif instance['state'].lower() in [ 'stopping', 'stopped' ]:
|
||||||
|
instance = self.start_instance()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, instance):
|
||||||
|
if instance:
|
||||||
|
if 'id' in instance:
|
||||||
|
self.result['id'] = instance['id']
|
||||||
|
if 'name' in instance:
|
||||||
|
self.result['name'] = instance['name']
|
||||||
|
if 'displayname' in instance:
|
||||||
|
self.result['display_name'] = instance['displayname']
|
||||||
|
if 'group' in instance:
|
||||||
|
self.result['group'] = instance['group']
|
||||||
|
if 'domain' in instance:
|
||||||
|
self.result['domain'] = instance['domain']
|
||||||
|
if 'account' in instance:
|
||||||
|
self.result['account'] = instance['account']
|
||||||
|
if 'project' in instance:
|
||||||
|
self.result['project'] = instance['project']
|
||||||
|
if 'hypervisor' in instance:
|
||||||
|
self.result['hypervisor'] = instance['hypervisor']
|
||||||
|
if 'instancename' in instance:
|
||||||
|
self.result['instance_name'] = instance['instancename']
|
||||||
|
if 'publicip' in instance:
|
||||||
|
self.result['public_ip'] = instance['public_ip']
|
||||||
|
if 'passwordenabled' in instance:
|
||||||
|
self.result['password_enabled'] = instance['passwordenabled']
|
||||||
|
if 'password' in instance:
|
||||||
|
self.result['password'] = instance['password']
|
||||||
|
if 'serviceofferingname' in instance:
|
||||||
|
self.result['service_offering'] = instance['serviceofferingname']
|
||||||
|
if 'zonename' in instance:
|
||||||
|
self.result['zone'] = instance['zonename']
|
||||||
|
if 'templatename' in instance:
|
||||||
|
self.result['template'] = instance['templatename']
|
||||||
|
if 'isoname' in instance:
|
||||||
|
self.result['iso'] = instance['isoname']
|
||||||
|
if 'keypair' in instance:
|
||||||
|
self.result['ssh_key'] = instance['keypair']
|
||||||
|
if 'created' in instance:
|
||||||
|
self.result['created'] = instance['created']
|
||||||
|
if 'state' in instance:
|
||||||
|
self.result['state'] = instance['state']
|
||||||
|
if 'tags' in instance:
|
||||||
|
self.result['tags'] = []
|
||||||
|
for tag in instance['tags']:
|
||||||
|
result_tag = {}
|
||||||
|
result_tag['key'] = tag['key']
|
||||||
|
result_tag['value'] = tag['value']
|
||||||
|
self.result['tags'].append(result_tag)
|
||||||
|
if 'securitygroup' in instance:
|
||||||
|
security_groups = []
|
||||||
|
for securitygroup in instance['securitygroup']:
|
||||||
|
security_groups.append(securitygroup['name'])
|
||||||
|
self.result['security_groups'] = security_groups
|
||||||
|
if 'affinitygroup' in instance:
|
||||||
|
affinity_groups = []
|
||||||
|
for affinitygroup in instance['affinitygroup']:
|
||||||
|
affinity_groups.append(affinitygroup['name'])
|
||||||
|
self.result['affinity_groups'] = affinity_groups
|
||||||
|
if 'nic' in instance:
|
||||||
|
for nic in instance['nic']:
|
||||||
|
if nic['isdefault']:
|
||||||
|
self.result['default_ip'] = nic['ipaddress']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
display_name = dict(default=None),
|
||||||
|
group = dict(default=None),
|
||||||
|
state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'),
|
||||||
|
service_offering = dict(default=None),
|
||||||
|
template = dict(default=None),
|
||||||
|
iso = dict(default=None),
|
||||||
|
networks = dict(type='list', aliases=[ 'network' ], default=None),
|
||||||
|
ip_address = dict(defaul=None),
|
||||||
|
ip6_address = dict(defaul=None),
|
||||||
|
disk_offering = dict(default=None),
|
||||||
|
disk_size = dict(type='int', default=None),
|
||||||
|
keyboard = dict(choices=['de', 'de-ch', 'es', 'fi', 'fr', 'fr-be', 'fr-ch', 'is', 'it', 'jp', 'nl-be', 'no', 'pt', 'uk', 'us'], default=None),
|
||||||
|
hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None),
|
||||||
|
security_groups = dict(type='list', aliases=[ 'security_group' ], default=[]),
|
||||||
|
affinity_groups = dict(type='list', aliases=[ 'affinity_group' ], default=[]),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
user_data = dict(default=None),
|
||||||
|
zone = dict(default=None),
|
||||||
|
ssh_key = dict(default=None),
|
||||||
|
force = dict(choices=BOOLEANS, default=False),
|
||||||
|
tags = dict(type='list', aliases=[ 'tag' ], default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_instance = AnsibleCloudStackInstance(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
if state in ['absent', 'destroyed']:
|
||||||
|
instance = acs_instance.absent_instance()
|
||||||
|
|
||||||
|
elif state in ['expunged']:
|
||||||
|
instance = acs_instance.expunge_instance()
|
||||||
|
|
||||||
|
elif state in ['present', 'deployed']:
|
||||||
|
instance = acs_instance.present_instance()
|
||||||
|
|
||||||
|
elif state in ['stopped']:
|
||||||
|
instance = acs_instance.stop_instance()
|
||||||
|
|
||||||
|
elif state in ['started']:
|
||||||
|
instance = acs_instance.start_instance()
|
||||||
|
|
||||||
|
elif state in ['restarted']:
|
||||||
|
instance = acs_instance.restart_instance()
|
||||||
|
|
||||||
|
if instance and 'state' in instance and instance['state'].lower() == 'error':
|
||||||
|
module.fail_json(msg="Instance named '%s' in error state." % module.params.get('name'))
|
||||||
|
|
||||||
|
result = acs_instance.get_result(instance)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,233 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_instancegroup
|
||||||
|
short_description: Manages instance groups on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create and remove instance groups.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the instance group.
|
||||||
|
required: true
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the instance group is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the instance group is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Project the instance group is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the instance group.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create an instance group
|
||||||
|
- local_action:
|
||||||
|
module: cs_instancegroup
|
||||||
|
name: loadbalancers
|
||||||
|
|
||||||
|
# Remove an instance group
|
||||||
|
- local_action:
|
||||||
|
module: cs_instancegroup
|
||||||
|
name: loadbalancers
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
id:
|
||||||
|
description: ID of the instance group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
|
||||||
|
name:
|
||||||
|
description: Name of the instance group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: webservers
|
||||||
|
created:
|
||||||
|
description: Date when the instance group was created.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2015-05-03T15:05:51+0200
|
||||||
|
domain:
|
||||||
|
description: Domain the instance group is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example domain
|
||||||
|
account:
|
||||||
|
description: Account the instance group is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
project:
|
||||||
|
description: Project the instance group is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example project
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackInstanceGroup(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.instance_group = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_instance_group(self):
|
||||||
|
if self.instance_group:
|
||||||
|
return self.instance_group
|
||||||
|
|
||||||
|
name = self.module.params.get('name')
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
|
||||||
|
instance_groups = self.cs.listInstanceGroups(**args)
|
||||||
|
if instance_groups:
|
||||||
|
for g in instance_groups['instancegroup']:
|
||||||
|
if name in [ g['name'], g['id'] ]:
|
||||||
|
self.instance_group = g
|
||||||
|
break
|
||||||
|
return self.instance_group
|
||||||
|
|
||||||
|
|
||||||
|
def present_instance_group(self):
|
||||||
|
instance_group = self.get_instance_group()
|
||||||
|
if not instance_group:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createInstanceGroup(**args)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
instance_group = res['instancegroup']
|
||||||
|
return instance_group
|
||||||
|
|
||||||
|
|
||||||
|
def absent_instance_group(self):
|
||||||
|
instance_group = self.get_instance_group()
|
||||||
|
if instance_group:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteInstanceGroup(id=instance_group['id'])
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
return instance_group
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, instance_group):
|
||||||
|
if instance_group:
|
||||||
|
if 'id' in instance_group:
|
||||||
|
self.result['id'] = instance_group['id']
|
||||||
|
if 'created' in instance_group:
|
||||||
|
self.result['created'] = instance_group['created']
|
||||||
|
if 'name' in instance_group:
|
||||||
|
self.result['name'] = instance_group['name']
|
||||||
|
if 'project' in instance_group:
|
||||||
|
self.result['project'] = instance_group['project']
|
||||||
|
if 'domain' in instance_group:
|
||||||
|
self.result['domain'] = instance_group['domain']
|
||||||
|
if 'account' in instance_group:
|
||||||
|
self.result['account'] = instance_group['account']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
state = dict(default='present', choices=['present', 'absent']),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_ig = AnsibleCloudStackInstanceGroup(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
instance_group = acs_ig.absent_instance_group()
|
||||||
|
else:
|
||||||
|
instance_group = acs_ig.present_instance_group()
|
||||||
|
|
||||||
|
result = acs_ig.get_result(instance_group)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,364 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_iso
|
||||||
|
short_description: Manages ISOs images on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Register and remove ISO images.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the ISO.
|
||||||
|
required: true
|
||||||
|
url:
|
||||||
|
description:
|
||||||
|
- URL where the ISO can be downloaded from. Required if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
os_type:
|
||||||
|
description:
|
||||||
|
- Name of the OS that best represents the OS of this ISO. If the iso is bootable this parameter needs to be passed. Required if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
is_ready:
|
||||||
|
description:
|
||||||
|
- This flag is used for searching existing ISOs. If set to C(true), it will only list ISO ready for deployment e.g. successfully downloaded and installed. Recommended to set it to C(false).
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
aliases: []
|
||||||
|
is_public:
|
||||||
|
description:
|
||||||
|
- Register the ISO to be publicly available to all users. Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_featured:
|
||||||
|
description:
|
||||||
|
- Register the ISO to be featured. Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_dynamically_scalable:
|
||||||
|
description:
|
||||||
|
- Register the ISO having XS/VMWare tools installed inorder to support dynamic scaling of VM cpu/memory. Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
aliases: []
|
||||||
|
checksum:
|
||||||
|
description:
|
||||||
|
- The MD5 checksum value of this ISO. If set, we search by checksum instead of name.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
bootable:
|
||||||
|
description:
|
||||||
|
- Register the ISO to be bootable. Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the ISO is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the ISO is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the ISO to be registered in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
zone:
|
||||||
|
description:
|
||||||
|
- Name of the zone you wish the ISO to be registered or deleted from. If not specified, first zone found will be used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
iso_filter:
|
||||||
|
description:
|
||||||
|
- Name of the filter used to search for the ISO.
|
||||||
|
required: false
|
||||||
|
default: 'self'
|
||||||
|
choices: [ 'featured', 'self', 'selfexecutable','sharedexecutable','executable', 'community' ]
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the ISO.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Register an ISO if ISO name does not already exist.
|
||||||
|
- local_action:
|
||||||
|
module: cs_iso
|
||||||
|
name: Debian 7 64-bit
|
||||||
|
url: http://mirror.switch.ch/ftp/mirror/debian-cd/current/amd64/iso-cd/debian-7.7.0-amd64-netinst.iso
|
||||||
|
os_type: Debian GNU/Linux 7(64-bit)
|
||||||
|
|
||||||
|
# Register an ISO with given name if ISO md5 checksum does not already exist.
|
||||||
|
- local_action:
|
||||||
|
module: cs_iso
|
||||||
|
name: Debian 7 64-bit
|
||||||
|
url: http://mirror.switch.ch/ftp/mirror/debian-cd/current/amd64/iso-cd/debian-7.7.0-amd64-netinst.iso
|
||||||
|
os_type: Debian GNU/Linux 7(64-bit)
|
||||||
|
checksum: 0b31bccccb048d20b551f70830bb7ad0
|
||||||
|
|
||||||
|
# Remove an ISO by name
|
||||||
|
- local_action:
|
||||||
|
module: cs_iso
|
||||||
|
name: Debian 7 64-bit
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
# Remove an ISO by checksum
|
||||||
|
- local_action:
|
||||||
|
module: cs_iso
|
||||||
|
name: Debian 7 64-bit
|
||||||
|
checksum: 0b31bccccb048d20b551f70830bb7ad0
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of the ISO.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Debian 7 64-bit
|
||||||
|
displaytext:
|
||||||
|
description: Text to be displayed of the ISO.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Debian 7.7 64-bit minimal 2015-03-19
|
||||||
|
zone:
|
||||||
|
description: Name of zone the ISO is registered in.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: zuerich
|
||||||
|
status:
|
||||||
|
description: Status of the ISO.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Successfully Installed
|
||||||
|
is_ready:
|
||||||
|
description: True if the ISO is ready to be deployed from.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
checksum:
|
||||||
|
description: MD5 checksum of the ISO.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 0b31bccccb048d20b551f70830bb7ad0
|
||||||
|
created:
|
||||||
|
description: Date of registering.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2015-03-29T14:57:06+0200
|
||||||
|
domain:
|
||||||
|
description: Domain the ISO is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example domain
|
||||||
|
account:
|
||||||
|
description: Account the ISO is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
project:
|
||||||
|
description: Project the ISO is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example project
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackIso(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.iso = None
|
||||||
|
|
||||||
|
def register_iso(self):
|
||||||
|
iso = self.get_iso()
|
||||||
|
if not iso:
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['bootable'] = self.module.params.get('bootable')
|
||||||
|
args['ostypeid'] = self.get_os_type('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['displaytext'] = self.module.params.get('name')
|
||||||
|
args['checksum'] = self.module.params.get('checksum')
|
||||||
|
args['isdynamicallyscalable'] = self.module.params.get('is_dynamically_scalable')
|
||||||
|
args['isfeatured'] = self.module.params.get('is_featured')
|
||||||
|
args['ispublic'] = self.module.params.get('is_public')
|
||||||
|
|
||||||
|
if args['bootable'] and not args['ostypeid']:
|
||||||
|
self.module.fail_json(msg="OS type 'os_type' is requried if 'bootable=true'.")
|
||||||
|
|
||||||
|
args['url'] = self.module.params.get('url')
|
||||||
|
if not args['url']:
|
||||||
|
self.module.fail_json(msg="URL is requried.")
|
||||||
|
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.registerIso(**args)
|
||||||
|
iso = res['iso'][0]
|
||||||
|
return iso
|
||||||
|
|
||||||
|
|
||||||
|
def get_iso(self):
|
||||||
|
if not self.iso:
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['isready'] = self.module.params.get('is_ready')
|
||||||
|
args['isofilter'] = self.module.params.get('iso_filter')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
|
||||||
|
# if checksum is set, we only look on that.
|
||||||
|
checksum = self.module.params.get('checksum')
|
||||||
|
if not checksum:
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
|
||||||
|
isos = self.cs.listIsos(**args)
|
||||||
|
if isos:
|
||||||
|
if not checksum:
|
||||||
|
self.iso = isos['iso'][0]
|
||||||
|
else:
|
||||||
|
for i in isos['iso']:
|
||||||
|
if i['checksum'] == checksum:
|
||||||
|
self.iso = i
|
||||||
|
break
|
||||||
|
return self.iso
|
||||||
|
|
||||||
|
|
||||||
|
def remove_iso(self):
|
||||||
|
iso = self.get_iso()
|
||||||
|
if iso:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = iso['id']
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['zoneid'] = self.get_zone('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteIso(**args)
|
||||||
|
return iso
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, iso):
|
||||||
|
if iso:
|
||||||
|
if 'displaytext' in iso:
|
||||||
|
self.result['displaytext'] = iso['displaytext']
|
||||||
|
if 'name' in iso:
|
||||||
|
self.result['name'] = iso['name']
|
||||||
|
if 'zonename' in iso:
|
||||||
|
self.result['zone'] = iso['zonename']
|
||||||
|
if 'checksum' in iso:
|
||||||
|
self.result['checksum'] = iso['checksum']
|
||||||
|
if 'status' in iso:
|
||||||
|
self.result['status'] = iso['status']
|
||||||
|
if 'isready' in iso:
|
||||||
|
self.result['is_ready'] = iso['isready']
|
||||||
|
if 'created' in iso:
|
||||||
|
self.result['created'] = iso['created']
|
||||||
|
if 'project' in iso:
|
||||||
|
self.result['project'] = iso['project']
|
||||||
|
if 'domain' in iso:
|
||||||
|
self.result['domain'] = iso['domain']
|
||||||
|
if 'account' in iso:
|
||||||
|
self.result['account'] = iso['account']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
url = dict(default=None),
|
||||||
|
os_type = dict(default=None),
|
||||||
|
zone = dict(default=None),
|
||||||
|
iso_filter = dict(default='self', choices=[ 'featured', 'self', 'selfexecutable','sharedexecutable','executable', 'community' ]),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
checksum = dict(default=None),
|
||||||
|
is_ready = dict(choices=BOOLEANS, default=False),
|
||||||
|
bootable = dict(choices=BOOLEANS, default=True),
|
||||||
|
is_featured = dict(choices=BOOLEANS, default=False),
|
||||||
|
is_dynamically_scalable = dict(choices=BOOLEANS, default=False),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_iso = AnsibleCloudStackIso(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
iso = acs_iso.remove_iso()
|
||||||
|
else:
|
||||||
|
iso = acs_iso.register_iso()
|
||||||
|
|
||||||
|
result = acs_iso.get_result(iso)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,637 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_network
|
||||||
|
short_description: Manages networks on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create, update, restart and delete networks.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name (case sensitive) of the network.
|
||||||
|
required: true
|
||||||
|
displaytext:
|
||||||
|
description:
|
||||||
|
- Displaytext of the network.
|
||||||
|
- If not specified, C(name) will be used as displaytext.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
network_offering:
|
||||||
|
description:
|
||||||
|
- Name of the offering for the network.
|
||||||
|
- Required if C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
start_ip:
|
||||||
|
description:
|
||||||
|
- The beginning IPv4 address of the network belongs to.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
end_ip:
|
||||||
|
description:
|
||||||
|
- The ending IPv4 address of the network belongs to.
|
||||||
|
- If not specified, value of C(start_ip) is used.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
gateway:
|
||||||
|
description:
|
||||||
|
- The gateway of the network.
|
||||||
|
- Required for shared networks and isolated networks when it belongs to VPC.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
netmask:
|
||||||
|
description:
|
||||||
|
- The netmask of the network.
|
||||||
|
- Required for shared networks and isolated networks when it belongs to VPC.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
start_ipv6:
|
||||||
|
description:
|
||||||
|
- The beginning IPv6 address of the network belongs to.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
end_ipv6:
|
||||||
|
description:
|
||||||
|
- The ending IPv6 address of the network belongs to.
|
||||||
|
- If not specified, value of C(start_ipv6) is used.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
cidr_ipv6:
|
||||||
|
description:
|
||||||
|
- CIDR of IPv6 network, must be at least /64.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
gateway_ipv6:
|
||||||
|
description:
|
||||||
|
- The gateway of the IPv6 network.
|
||||||
|
- Required for shared networks.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
vlan:
|
||||||
|
description:
|
||||||
|
- The ID or VID of the network.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
vpc:
|
||||||
|
description:
|
||||||
|
- The ID or VID of the network.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
isolated_pvlan:
|
||||||
|
description:
|
||||||
|
- The isolated private vlan for this network.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
clean_up:
|
||||||
|
description:
|
||||||
|
- Cleanup old network elements.
|
||||||
|
- Only considered on C(state=restarted).
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
acl_type:
|
||||||
|
description:
|
||||||
|
- Access control type.
|
||||||
|
- Only considered on create.
|
||||||
|
required: false
|
||||||
|
default: account
|
||||||
|
choices: [ 'account', 'domain' ]
|
||||||
|
network_domain:
|
||||||
|
description:
|
||||||
|
- The network domain.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the network.
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
choices: [ 'present', 'absent', 'restarted' ]
|
||||||
|
zone:
|
||||||
|
description:
|
||||||
|
- Name of the zone in which the network should be deployed.
|
||||||
|
- If not set, default zone is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the network to be deployed in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the network is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the network is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# create a network
|
||||||
|
- local_action:
|
||||||
|
module: cs_network
|
||||||
|
name: my network
|
||||||
|
zone: gva-01
|
||||||
|
network_offering: DefaultIsolatedNetworkOfferingWithSourceNatService
|
||||||
|
network_domain: example.com
|
||||||
|
|
||||||
|
# update a network
|
||||||
|
- local_action:
|
||||||
|
module: cs_network
|
||||||
|
name: my network
|
||||||
|
displaytext: network of domain example.local
|
||||||
|
network_domain: example.local
|
||||||
|
|
||||||
|
# restart a network with clean up
|
||||||
|
- local_action:
|
||||||
|
module: cs_network
|
||||||
|
name: my network
|
||||||
|
clean_up: yes
|
||||||
|
state: restared
|
||||||
|
|
||||||
|
# remove a network
|
||||||
|
- local_action:
|
||||||
|
module: cs_network
|
||||||
|
name: my network
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
id:
|
||||||
|
description: ID of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
|
||||||
|
name:
|
||||||
|
description: Name of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web project
|
||||||
|
displaytext:
|
||||||
|
description: Display text of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web project
|
||||||
|
dns1:
|
||||||
|
description: IP address of the 1st nameserver.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 1.2.3.4
|
||||||
|
dns2:
|
||||||
|
description: IP address of the 2nd nameserver.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 1.2.3.4
|
||||||
|
cidr:
|
||||||
|
description: IPv4 network CIDR.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 10.101.64.0/24
|
||||||
|
gateway:
|
||||||
|
description: IPv4 gateway.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 10.101.64.1
|
||||||
|
netmask:
|
||||||
|
description: IPv4 netmask.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 255.255.255.0
|
||||||
|
cidr_ipv6:
|
||||||
|
description: IPv6 network CIDR.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2001:db8::/64
|
||||||
|
gateway_ipv6:
|
||||||
|
description: IPv6 gateway.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2001:db8::1
|
||||||
|
state:
|
||||||
|
description: State of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Implemented
|
||||||
|
zone:
|
||||||
|
description: Name of zone.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ch-gva-2
|
||||||
|
domain:
|
||||||
|
description: Domain the network is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ROOT
|
||||||
|
account:
|
||||||
|
description: Account the network is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
project:
|
||||||
|
description: Name of project.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Production
|
||||||
|
tags:
|
||||||
|
description: List of resource tags associated with the network.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: '[ { "key": "foo", "value": "bar" } ]'
|
||||||
|
acl_type:
|
||||||
|
description: Access type of the network (Domain, Account).
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Account
|
||||||
|
broadcast_domaintype:
|
||||||
|
description: Broadcast domain type of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Vlan
|
||||||
|
type:
|
||||||
|
description: Type of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Isolated
|
||||||
|
traffic_type:
|
||||||
|
description: Traffic type of the network.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Guest
|
||||||
|
state:
|
||||||
|
description: State of the network (Allocated, Implemented, Setup).
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Allocated
|
||||||
|
is_persistent:
|
||||||
|
description: Whether the network is persistent or not.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: false
|
||||||
|
network_domain:
|
||||||
|
description: The network domain
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example.local
|
||||||
|
network_offering:
|
||||||
|
description: The network offering name.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: DefaultIsolatedNetworkOfferingWithSourceNatService
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackNetwork(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.network = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_fallback(self, key=None, fallback_key=None):
|
||||||
|
value = self.module.params.get(key)
|
||||||
|
if not value:
|
||||||
|
value = self.module.params.get(fallback_key)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def get_vpc(self, key=None):
|
||||||
|
vpc = self.module.params.get('vpc')
|
||||||
|
if not vpc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
|
||||||
|
vpcs = self.cs.listVPCs(**args)
|
||||||
|
if vpcs:
|
||||||
|
for v in vpcs['vpc']:
|
||||||
|
if vpc in [ v['name'], v['displaytext'], v['id'] ]:
|
||||||
|
return self._get_by_key(key, v)
|
||||||
|
self.module.fail_json(msg="VPC '%s' not found" % vpc)
|
||||||
|
|
||||||
|
|
||||||
|
def get_network_offering(self, key=None):
|
||||||
|
network_offering = self.module.params.get('network_offering')
|
||||||
|
if not network_offering:
|
||||||
|
self.module.fail_json(msg="missing required arguments: network_offering")
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
|
||||||
|
network_offerings = self.cs.listNetworkOfferings(**args)
|
||||||
|
if network_offerings:
|
||||||
|
for no in network_offerings['networkoffering']:
|
||||||
|
if network_offering in [ no['name'], no['displaytext'], no['id'] ]:
|
||||||
|
return self._get_by_key(key, no)
|
||||||
|
self.module.fail_json(msg="Network offering '%s' not found" % network_offering)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_args(self):
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['displaytext'] = self.get_or_fallback('displaytext','name')
|
||||||
|
args['networkdomain'] = self.module.params.get('network_domain')
|
||||||
|
args['networkofferingid'] = self.get_network_offering(key='id')
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def get_network(self):
|
||||||
|
if not self.network:
|
||||||
|
network = self.module.params.get('name')
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
|
||||||
|
networks = self.cs.listNetworks(**args)
|
||||||
|
if networks:
|
||||||
|
for n in networks['network']:
|
||||||
|
if network in [ n['name'], n['displaytext'], n['id']]:
|
||||||
|
self.network = n
|
||||||
|
break
|
||||||
|
return self.network
|
||||||
|
|
||||||
|
|
||||||
|
def present_network(self):
|
||||||
|
network = self.get_network()
|
||||||
|
if not network:
|
||||||
|
network = self.create_network(network)
|
||||||
|
else:
|
||||||
|
network = self.update_network(network)
|
||||||
|
return network
|
||||||
|
|
||||||
|
|
||||||
|
def update_network(self, network):
|
||||||
|
args = self._get_args()
|
||||||
|
args['id'] = network['id']
|
||||||
|
|
||||||
|
if self._has_changed(args, network):
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
network = self.cs.updateNetwork(**args)
|
||||||
|
|
||||||
|
if 'errortext' in network:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % network['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if network and poll_async:
|
||||||
|
network = self._poll_job(network, 'network')
|
||||||
|
return network
|
||||||
|
|
||||||
|
|
||||||
|
def create_network(self, network):
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = self._get_args()
|
||||||
|
args['acltype'] = self.module.params.get('acl_type')
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
args['startip'] = self.module.params.get('start_ip')
|
||||||
|
args['endip'] = self.get_or_fallback('end_ip', 'start_ip')
|
||||||
|
args['netmask'] = self.module.params.get('netmask')
|
||||||
|
args['gateway'] = self.module.params.get('gateway')
|
||||||
|
args['startipv6'] = self.module.params.get('start_ipv6')
|
||||||
|
args['endipv6'] = self.get_or_fallback('end_ipv6', 'start_ipv6')
|
||||||
|
args['ip6cidr'] = self.module.params.get('cidr_ipv6')
|
||||||
|
args['ip6gateway'] = self.module.params.get('gateway_ipv6')
|
||||||
|
args['vlan'] = self.module.params.get('vlan')
|
||||||
|
args['isolatedpvlan'] = self.module.params.get('isolated_pvlan')
|
||||||
|
args['subdomainaccess'] = self.module.params.get('subdomain_access')
|
||||||
|
args['vpcid'] = self.get_vpc(key='id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createNetwork(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
network = res['network']
|
||||||
|
return network
|
||||||
|
|
||||||
|
|
||||||
|
def restart_network(self):
|
||||||
|
network = self.get_network()
|
||||||
|
|
||||||
|
if not network:
|
||||||
|
self.module.fail_json(msg="No network named '%s' found." % self.module.params('name'))
|
||||||
|
|
||||||
|
# Restarting only available for these states
|
||||||
|
if network['state'].lower() in [ 'implemented', 'setup' ]:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = network['id']
|
||||||
|
args['cleanup'] = self.module.params.get('clean_up')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
network = self.cs.restartNetwork(**args)
|
||||||
|
|
||||||
|
if 'errortext' in network:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % network['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if network and poll_async:
|
||||||
|
network = self._poll_job(network, 'network')
|
||||||
|
return network
|
||||||
|
|
||||||
|
|
||||||
|
def absent_network(self):
|
||||||
|
network = self.get_network()
|
||||||
|
if network:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = network['id']
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteNetwork(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
res = self._poll_job(res, 'network')
|
||||||
|
return network
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, network):
|
||||||
|
if network:
|
||||||
|
if 'id' in network:
|
||||||
|
self.result['id'] = network['id']
|
||||||
|
if 'name' in network:
|
||||||
|
self.result['name'] = network['name']
|
||||||
|
if 'displaytext' in network:
|
||||||
|
self.result['displaytext'] = network['displaytext']
|
||||||
|
if 'dns1' in network:
|
||||||
|
self.result['dns1'] = network['dns1']
|
||||||
|
if 'dns2' in network:
|
||||||
|
self.result['dns2'] = network['dns2']
|
||||||
|
if 'cidr' in network:
|
||||||
|
self.result['cidr'] = network['cidr']
|
||||||
|
if 'broadcastdomaintype' in network:
|
||||||
|
self.result['broadcast_domaintype'] = network['broadcastdomaintype']
|
||||||
|
if 'netmask' in network:
|
||||||
|
self.result['netmask'] = network['netmask']
|
||||||
|
if 'gateway' in network:
|
||||||
|
self.result['gateway'] = network['gateway']
|
||||||
|
if 'ip6cidr' in network:
|
||||||
|
self.result['cidr_ipv6'] = network['ip6cidr']
|
||||||
|
if 'ip6gateway' in network:
|
||||||
|
self.result['gateway_ipv6'] = network['ip6gateway']
|
||||||
|
if 'state' in network:
|
||||||
|
self.result['state'] = network['state']
|
||||||
|
if 'type' in network:
|
||||||
|
self.result['type'] = network['type']
|
||||||
|
if 'traffictype' in network:
|
||||||
|
self.result['traffic_type'] = network['traffictype']
|
||||||
|
if 'zone' in network:
|
||||||
|
self.result['zone'] = network['zonename']
|
||||||
|
if 'domain' in network:
|
||||||
|
self.result['domain'] = network['domain']
|
||||||
|
if 'account' in network:
|
||||||
|
self.result['account'] = network['account']
|
||||||
|
if 'project' in network:
|
||||||
|
self.result['project'] = network['project']
|
||||||
|
if 'acltype' in network:
|
||||||
|
self.result['acl_type'] = network['acltype']
|
||||||
|
if 'networkdomain' in network:
|
||||||
|
self.result['network_domain'] = network['networkdomain']
|
||||||
|
if 'networkofferingname' in network:
|
||||||
|
self.result['network_offering'] = network['networkofferingname']
|
||||||
|
if 'ispersistent' in network:
|
||||||
|
self.result['is_persistent'] = network['ispersistent']
|
||||||
|
if 'tags' in network:
|
||||||
|
self.result['tags'] = []
|
||||||
|
for tag in network['tags']:
|
||||||
|
result_tag = {}
|
||||||
|
result_tag['key'] = tag['key']
|
||||||
|
result_tag['value'] = tag['value']
|
||||||
|
self.result['tags'].append(result_tag)
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
displaytext = dict(default=None),
|
||||||
|
network_offering = dict(default=None),
|
||||||
|
zone = dict(default=None),
|
||||||
|
start_ip = dict(default=None),
|
||||||
|
end_ip = dict(default=None),
|
||||||
|
gateway = dict(default=None),
|
||||||
|
netmask = dict(default=None),
|
||||||
|
start_ipv6 = dict(default=None),
|
||||||
|
end_ipv6 = dict(default=None),
|
||||||
|
cidr_ipv6 = dict(default=None),
|
||||||
|
gateway_ipv6 = dict(default=None),
|
||||||
|
vlan = dict(default=None),
|
||||||
|
vpc = dict(default=None),
|
||||||
|
isolated_pvlan = dict(default=None),
|
||||||
|
clean_up = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
network_domain = dict(default=None),
|
||||||
|
state = dict(choices=['present', 'absent', 'restarted' ], default='present'),
|
||||||
|
acl_type = dict(choices=['account', 'domain'], default='account'),
|
||||||
|
project = dict(default=None),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
poll_async = dict(type='bool', choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
['start_ip', 'netmask', 'gateway'],
|
||||||
|
['start_ipv6', 'cidr_ipv6', 'gateway_ipv6'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_network = AnsibleCloudStackNetwork(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
network = acs_network.absent_network()
|
||||||
|
|
||||||
|
elif state in ['restarted']:
|
||||||
|
network = acs_network.restart_network()
|
||||||
|
|
||||||
|
else:
|
||||||
|
network = acs_network.present_network()
|
||||||
|
|
||||||
|
result = acs_network.get_result(network)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,437 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_portforward
|
||||||
|
short_description: Manages port forwarding rules on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create, update and remove port forwarding rules.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
ip_address:
|
||||||
|
description:
|
||||||
|
- Public IP address the rule is assigned to.
|
||||||
|
required: true
|
||||||
|
vm:
|
||||||
|
description:
|
||||||
|
- Name of virtual machine which we make the port forwarding rule for.
|
||||||
|
- Required if C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the port forwarding rule.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
protocol:
|
||||||
|
description:
|
||||||
|
- Protocol of the port forwarding rule.
|
||||||
|
required: false
|
||||||
|
default: 'tcp'
|
||||||
|
choices: [ 'tcp', 'udp' ]
|
||||||
|
public_port:
|
||||||
|
description:
|
||||||
|
- Start public port for this rule.
|
||||||
|
required: true
|
||||||
|
public_end_port:
|
||||||
|
description:
|
||||||
|
- End public port for this rule.
|
||||||
|
- If not specified equal C(public_port).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
private_port:
|
||||||
|
description:
|
||||||
|
- Start private port for this rule.
|
||||||
|
required: true
|
||||||
|
private_end_port:
|
||||||
|
description:
|
||||||
|
- End private port for this rule.
|
||||||
|
- If not specified equal C(private_port).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
open_firewall:
|
||||||
|
description:
|
||||||
|
- Whether the firewall rule for public port should be created, while creating the new rule.
|
||||||
|
- Use M(cs_firewall) for managing firewall rules.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
vm_guest_ip:
|
||||||
|
description:
|
||||||
|
- VM guest NIC secondary IP address for the port forwarding rule.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the C(vm) is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the C(vm) is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the C(vm) is located in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
zone:
|
||||||
|
description:
|
||||||
|
- Name of the zone in which the virtual machine is in.
|
||||||
|
- If not set, default zone is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# 1.2.3.4:80 -> web01:8080
|
||||||
|
- local_action:
|
||||||
|
module: cs_portforward
|
||||||
|
ip_address: 1.2.3.4
|
||||||
|
vm: web01
|
||||||
|
public_port: 80
|
||||||
|
private_port: 8080
|
||||||
|
|
||||||
|
# forward SSH and open firewall
|
||||||
|
- local_action:
|
||||||
|
module: cs_portforward
|
||||||
|
ip_address: '{{ public_ip }}'
|
||||||
|
vm: '{{ inventory_hostname }}'
|
||||||
|
public_port: '{{ ansible_ssh_port }}'
|
||||||
|
private_port: 22
|
||||||
|
open_firewall: true
|
||||||
|
|
||||||
|
# forward DNS traffic, but do not open firewall
|
||||||
|
- local_action:
|
||||||
|
module: cs_portforward
|
||||||
|
ip_address: 1.2.3.4
|
||||||
|
vm: '{{ inventory_hostname }}'
|
||||||
|
public_port: 53
|
||||||
|
private_port: 53
|
||||||
|
protocol: udp
|
||||||
|
open_firewall: true
|
||||||
|
|
||||||
|
# remove ssh port forwarding
|
||||||
|
- local_action:
|
||||||
|
module: cs_portforward
|
||||||
|
ip_address: 1.2.3.4
|
||||||
|
public_port: 22
|
||||||
|
private_port: 22
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
ip_address:
|
||||||
|
description: Public IP address.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 1.2.3.4
|
||||||
|
protocol:
|
||||||
|
description: Protocol.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: tcp
|
||||||
|
private_port:
|
||||||
|
description: Start port on the virtual machine's IP address.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
private_end_port:
|
||||||
|
description: End port on the virtual machine's IP address.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
public_port:
|
||||||
|
description: Start port on the public IP address.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
public_end_port:
|
||||||
|
description: End port on the public IP address.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
tags:
|
||||||
|
description: Tags related to the port forwarding.
|
||||||
|
returned: success
|
||||||
|
type: list
|
||||||
|
sample: []
|
||||||
|
vm_name:
|
||||||
|
description: Name of the virtual machine.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web-01
|
||||||
|
vm_display_name:
|
||||||
|
description: Display name of the virtual machine.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web-01
|
||||||
|
vm_guest_ip:
|
||||||
|
description: IP of the virtual machine.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 10.101.65.152
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackPortforwarding(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.portforwarding_rule = None
|
||||||
|
self.vm_default_nic = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_end_port(self):
|
||||||
|
if not self.module.params.get('public_end_port'):
|
||||||
|
return self.module.params.get('public_port')
|
||||||
|
return self.module.params.get('public_end_port')
|
||||||
|
|
||||||
|
|
||||||
|
def get_private_end_port(self):
|
||||||
|
if not self.module.params.get('private_end_port'):
|
||||||
|
return self.module.params.get('private_port')
|
||||||
|
return self.module.params.get('private_end_port')
|
||||||
|
|
||||||
|
|
||||||
|
def get_vm_guest_ip(self):
|
||||||
|
vm_guest_ip = self.module.params.get('vm_guest_ip')
|
||||||
|
default_nic = self.get_vm_default_nic()
|
||||||
|
|
||||||
|
if not vm_guest_ip:
|
||||||
|
return default_nic['ipaddress']
|
||||||
|
|
||||||
|
for secondary_ip in default_nic['secondaryip']:
|
||||||
|
if vm_guest_ip == secondary_ip['ipaddress']:
|
||||||
|
return vm_guest_ip
|
||||||
|
self.module.fail_json(msg="Secondary IP '%s' not assigned to VM" % vm_guest_ip)
|
||||||
|
|
||||||
|
|
||||||
|
def get_vm_default_nic(self):
|
||||||
|
if self.vm_default_nic:
|
||||||
|
return self.vm_default_nic
|
||||||
|
|
||||||
|
nics = self.cs.listNics(virtualmachineid=self.get_vm(key='id'))
|
||||||
|
if nics:
|
||||||
|
for n in nics['nic']:
|
||||||
|
if n['isdefault']:
|
||||||
|
self.vm_default_nic = n
|
||||||
|
return self.vm_default_nic
|
||||||
|
self.module.fail_json(msg="No default IP address of VM '%s' found" % self.module.params.get('vm'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_portforwarding_rule(self):
|
||||||
|
if not self.portforwarding_rule:
|
||||||
|
protocol = self.module.params.get('protocol')
|
||||||
|
public_port = self.module.params.get('public_port')
|
||||||
|
public_end_port = self.get_public_end_port()
|
||||||
|
private_port = self.module.params.get('private_port')
|
||||||
|
private_end_port = self.get_public_end_port()
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['ipaddressid'] = self.get_ip_address(key='id')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
portforwarding_rules = self.cs.listPortForwardingRules(**args)
|
||||||
|
|
||||||
|
if portforwarding_rules and 'portforwardingrule' in portforwarding_rules:
|
||||||
|
for rule in portforwarding_rules['portforwardingrule']:
|
||||||
|
if protocol == rule['protocol'] \
|
||||||
|
and public_port == int(rule['publicport']):
|
||||||
|
self.portforwarding_rule = rule
|
||||||
|
break
|
||||||
|
return self.portforwarding_rule
|
||||||
|
|
||||||
|
|
||||||
|
def present_portforwarding_rule(self):
|
||||||
|
portforwarding_rule = self.get_portforwarding_rule()
|
||||||
|
if portforwarding_rule:
|
||||||
|
portforwarding_rule = self.update_portforwarding_rule(portforwarding_rule)
|
||||||
|
else:
|
||||||
|
portforwarding_rule = self.create_portforwarding_rule()
|
||||||
|
return portforwarding_rule
|
||||||
|
|
||||||
|
|
||||||
|
def create_portforwarding_rule(self):
|
||||||
|
args = {}
|
||||||
|
args['protocol'] = self.module.params.get('protocol')
|
||||||
|
args['publicport'] = self.module.params.get('public_port')
|
||||||
|
args['publicendport'] = self.get_public_end_port()
|
||||||
|
args['privateport'] = self.module.params.get('private_port')
|
||||||
|
args['privateendport'] = self.get_private_end_port()
|
||||||
|
args['openfirewall'] = self.module.params.get('open_firewall')
|
||||||
|
args['vmguestip'] = self.get_vm_guest_ip()
|
||||||
|
args['ipaddressid'] = self.get_ip_address(key='id')
|
||||||
|
args['virtualmachineid'] = self.get_vm(key='id')
|
||||||
|
|
||||||
|
portforwarding_rule = None
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
portforwarding_rule = self.cs.createPortForwardingRule(**args)
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
portforwarding_rule = self._poll_job(portforwarding_rule, 'portforwardingrule')
|
||||||
|
return portforwarding_rule
|
||||||
|
|
||||||
|
|
||||||
|
def update_portforwarding_rule(self, portforwarding_rule):
|
||||||
|
args = {}
|
||||||
|
args['protocol'] = self.module.params.get('protocol')
|
||||||
|
args['publicport'] = self.module.params.get('public_port')
|
||||||
|
args['publicendport'] = self.get_public_end_port()
|
||||||
|
args['privateport'] = self.module.params.get('private_port')
|
||||||
|
args['privateendport'] = self.get_private_end_port()
|
||||||
|
args['openfirewall'] = self.module.params.get('open_firewall')
|
||||||
|
args['vmguestip'] = self.get_vm_guest_ip()
|
||||||
|
args['ipaddressid'] = self.get_ip_address(key='id')
|
||||||
|
args['virtualmachineid'] = self.get_vm(key='id')
|
||||||
|
|
||||||
|
if self._has_changed(args, portforwarding_rule):
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
# API broken in 4.2.1?, workaround using remove/create instead of update
|
||||||
|
# portforwarding_rule = self.cs.updatePortForwardingRule(**args)
|
||||||
|
self.absent_portforwarding_rule()
|
||||||
|
portforwarding_rule = self.cs.createPortForwardingRule(**args)
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
portforwarding_rule = self._poll_job(portforwarding_rule, 'portforwardingrule')
|
||||||
|
return portforwarding_rule
|
||||||
|
|
||||||
|
|
||||||
|
def absent_portforwarding_rule(self):
|
||||||
|
portforwarding_rule = self.get_portforwarding_rule()
|
||||||
|
|
||||||
|
if portforwarding_rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
args = {}
|
||||||
|
args['id'] = portforwarding_rule['id']
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deletePortForwardingRule(**args)
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
self._poll_job(res, 'portforwardingrule')
|
||||||
|
return portforwarding_rule
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, portforwarding_rule):
|
||||||
|
if portforwarding_rule:
|
||||||
|
if 'id' in portforwarding_rule:
|
||||||
|
self.result['id'] = portforwarding_rule['id']
|
||||||
|
if 'virtualmachinedisplayname' in portforwarding_rule:
|
||||||
|
self.result['vm_display_name'] = portforwarding_rule['virtualmachinedisplayname']
|
||||||
|
if 'virtualmachinename' in portforwarding_rule:
|
||||||
|
self.result['vm_name'] = portforwarding_rule['virtualmachinename']
|
||||||
|
if 'ipaddress' in portforwarding_rule:
|
||||||
|
self.result['ip_address'] = portforwarding_rule['ipaddress']
|
||||||
|
if 'vmguestip' in portforwarding_rule:
|
||||||
|
self.result['vm_guest_ip'] = portforwarding_rule['vmguestip']
|
||||||
|
if 'publicport' in portforwarding_rule:
|
||||||
|
self.result['public_port'] = int(portforwarding_rule['publicport'])
|
||||||
|
if 'publicendport' in portforwarding_rule:
|
||||||
|
self.result['public_end_port'] = int(portforwarding_rule['publicendport'])
|
||||||
|
if 'privateport' in portforwarding_rule:
|
||||||
|
self.result['private_port'] = int(portforwarding_rule['privateport'])
|
||||||
|
if 'privateendport' in portforwarding_rule:
|
||||||
|
self.result['private_end_port'] = int(portforwarding_rule['privateendport'])
|
||||||
|
if 'protocol' in portforwarding_rule:
|
||||||
|
self.result['protocol'] = portforwarding_rule['protocol']
|
||||||
|
if 'tags' in portforwarding_rule:
|
||||||
|
self.result['tags'] = []
|
||||||
|
for tag in portforwarding_rule['tags']:
|
||||||
|
result_tag = {}
|
||||||
|
result_tag['key'] = tag['key']
|
||||||
|
result_tag['value'] = tag['value']
|
||||||
|
self.result['tags'].append(result_tag)
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
ip_address = dict(required=True),
|
||||||
|
protocol= dict(choices=['tcp', 'udp'], default='tcp'),
|
||||||
|
public_port = dict(type='int', required=True),
|
||||||
|
public_end_port = dict(type='int', default=None),
|
||||||
|
private_port = dict(type='int', required=True),
|
||||||
|
private_end_port = dict(type='int', default=None),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
open_firewall = dict(choices=BOOLEANS, default=False),
|
||||||
|
vm_guest_ip = dict(default=None),
|
||||||
|
vm = dict(default=None),
|
||||||
|
zone = dict(default=None),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_pf = AnsibleCloudStackPortforwarding(module)
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
pf_rule = acs_pf.absent_portforwarding_rule()
|
||||||
|
else:
|
||||||
|
pf_rule = acs_pf.present_portforwarding_rule()
|
||||||
|
|
||||||
|
result = acs_pf.get_result(pf_rule)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,342 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_project
|
||||||
|
short_description: Manages projects on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create, update, suspend, activate and remove projects.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the project.
|
||||||
|
required: true
|
||||||
|
displaytext:
|
||||||
|
description:
|
||||||
|
- Displaytext of the project.
|
||||||
|
- If not specified, C(name) will be used as displaytext.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the project.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent', 'active', 'suspended' ]
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the project is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the project is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a project
|
||||||
|
- local_action:
|
||||||
|
module: cs_project
|
||||||
|
name: web
|
||||||
|
|
||||||
|
# Rename a project
|
||||||
|
- local_action:
|
||||||
|
module: cs_project
|
||||||
|
name: web
|
||||||
|
displaytext: my web project
|
||||||
|
|
||||||
|
# Suspend an existing project
|
||||||
|
- local_action:
|
||||||
|
module: cs_project
|
||||||
|
name: web
|
||||||
|
state: suspended
|
||||||
|
|
||||||
|
# Activate an existing project
|
||||||
|
- local_action:
|
||||||
|
module: cs_project
|
||||||
|
name: web
|
||||||
|
state: active
|
||||||
|
|
||||||
|
# Remove a project
|
||||||
|
- local_action:
|
||||||
|
module: cs_project
|
||||||
|
name: web
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
id:
|
||||||
|
description: ID of the project.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 04589590-ac63-4ffc-93f5-b698b8ac38b6
|
||||||
|
name:
|
||||||
|
description: Name of the project.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web project
|
||||||
|
displaytext:
|
||||||
|
description: Display text of the project.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: web project
|
||||||
|
state:
|
||||||
|
description: State of the project.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Active
|
||||||
|
domain:
|
||||||
|
description: Domain the project is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example domain
|
||||||
|
account:
|
||||||
|
description: Account the project is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
tags:
|
||||||
|
description: List of resource tags associated with the project.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: '[ { "key": "foo", "value": "bar" } ]'
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackProject(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.project = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_displaytext(self):
|
||||||
|
displaytext = self.module.params.get('displaytext')
|
||||||
|
if not displaytext:
|
||||||
|
displaytext = self.module.params.get('name')
|
||||||
|
return displaytext
|
||||||
|
|
||||||
|
|
||||||
|
def get_project(self):
|
||||||
|
if not self.project:
|
||||||
|
project = self.module.params.get('name')
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
|
||||||
|
projects = self.cs.listProjects(**args)
|
||||||
|
if projects:
|
||||||
|
for p in projects['project']:
|
||||||
|
if project.lower() in [ p['name'].lower(), p['id']]:
|
||||||
|
self.project = p
|
||||||
|
break
|
||||||
|
return self.project
|
||||||
|
|
||||||
|
|
||||||
|
def present_project(self):
|
||||||
|
project = self.get_project()
|
||||||
|
if not project:
|
||||||
|
project = self.create_project(project)
|
||||||
|
else:
|
||||||
|
project = self.update_project(project)
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def update_project(self, project):
|
||||||
|
args = {}
|
||||||
|
args['id'] = project['id']
|
||||||
|
args['displaytext'] = self.get_displaytext()
|
||||||
|
|
||||||
|
if self._has_changed(args, project):
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
project = self.cs.updateProject(**args)
|
||||||
|
|
||||||
|
if 'errortext' in project:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % project['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if project and poll_async:
|
||||||
|
project = self._poll_job(project, 'project')
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def create_project(self, project):
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['displaytext'] = self.get_displaytext()
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
project = self.cs.createProject(**args)
|
||||||
|
|
||||||
|
if 'errortext' in project:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % project['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if project and poll_async:
|
||||||
|
project = self._poll_job(project, 'project')
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def state_project(self, state=None):
|
||||||
|
project = self.get_project()
|
||||||
|
|
||||||
|
if not project:
|
||||||
|
self.module.fail_json(msg="No project named '%s' found." % self.module.params('name'))
|
||||||
|
|
||||||
|
if project['state'].lower() != state:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = project['id']
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
if state == 'suspended':
|
||||||
|
project = self.cs.suspendProject(**args)
|
||||||
|
else:
|
||||||
|
project = self.cs.activateProject(**args)
|
||||||
|
|
||||||
|
if 'errortext' in project:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % project['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if project and poll_async:
|
||||||
|
project = self._poll_job(project, 'project')
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def absent_project(self):
|
||||||
|
project = self.get_project()
|
||||||
|
if project:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = project['id']
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteProject(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
res = self._poll_job(res, 'project')
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, project):
|
||||||
|
if project:
|
||||||
|
if 'name' in project:
|
||||||
|
self.result['name'] = project['name']
|
||||||
|
if 'displaytext' in project:
|
||||||
|
self.result['displaytext'] = project['displaytext']
|
||||||
|
if 'account' in project:
|
||||||
|
self.result['account'] = project['account']
|
||||||
|
if 'domain' in project:
|
||||||
|
self.result['domain'] = project['domain']
|
||||||
|
if 'state' in project:
|
||||||
|
self.result['state'] = project['state']
|
||||||
|
if 'tags' in project:
|
||||||
|
self.result['tags'] = []
|
||||||
|
for tag in project['tags']:
|
||||||
|
result_tag = {}
|
||||||
|
result_tag['key'] = tag['key']
|
||||||
|
result_tag['value'] = tag['value']
|
||||||
|
self.result['tags'].append(result_tag)
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
displaytext = dict(default=None),
|
||||||
|
state = dict(choices=['present', 'absent', 'active', 'suspended' ], default='present'),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
poll_async = dict(type='bool', choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_project = AnsibleCloudStackProject(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
project = acs_project.absent_project()
|
||||||
|
|
||||||
|
elif state in ['active', 'suspended']:
|
||||||
|
project = acs_project.state_project(state=state)
|
||||||
|
|
||||||
|
else:
|
||||||
|
project = acs_project.present_project()
|
||||||
|
|
||||||
|
result = acs_project.get_result(project)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,200 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_securitygroup
|
||||||
|
short_description: Manages security groups on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create and remove security groups.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the security group.
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Description of the security group.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the security group.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the security group to be created in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a security group
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup
|
||||||
|
name: default
|
||||||
|
description: default security group
|
||||||
|
|
||||||
|
# Remove a security group
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup
|
||||||
|
name: default
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of security group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: app
|
||||||
|
description:
|
||||||
|
description: Description of security group.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: application security group
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackSecurityGroup(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.security_group = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_security_group(self):
|
||||||
|
if not self.security_group:
|
||||||
|
sg_name = self.module.params.get('name')
|
||||||
|
args = {}
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
sgs = self.cs.listSecurityGroups(**args)
|
||||||
|
if sgs:
|
||||||
|
for s in sgs['securitygroup']:
|
||||||
|
if s['name'] == sg_name:
|
||||||
|
self.security_group = s
|
||||||
|
break
|
||||||
|
return self.security_group
|
||||||
|
|
||||||
|
|
||||||
|
def create_security_group(self):
|
||||||
|
security_group = self.get_security_group()
|
||||||
|
if not security_group:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['description'] = self.module.params.get('description')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createSecurityGroup(**args)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
security_group = res['securitygroup']
|
||||||
|
|
||||||
|
return security_group
|
||||||
|
|
||||||
|
|
||||||
|
def remove_security_group(self):
|
||||||
|
security_group = self.get_security_group()
|
||||||
|
if security_group:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteSecurityGroup(**args)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
return security_group
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, security_group):
|
||||||
|
if security_group:
|
||||||
|
if 'name' in security_group:
|
||||||
|
self.result['name'] = security_group['name']
|
||||||
|
if 'description' in security_group:
|
||||||
|
self.result['description'] = security_group['description']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
description = dict(default=None),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
project = dict(default=None),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_sg = AnsibleCloudStackSecurityGroup(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
sg = acs_sg.remove_security_group()
|
||||||
|
else:
|
||||||
|
sg = acs_sg.create_security_group()
|
||||||
|
|
||||||
|
result = acs_sg.get_result(sg)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,439 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_securitygroup_rule
|
||||||
|
short_description: Manages security group rules on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Add and remove security group rules.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
security_group:
|
||||||
|
description:
|
||||||
|
- Name of the security group the rule is related to. The security group must be existing.
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the security group rule.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
protocol:
|
||||||
|
description:
|
||||||
|
- Protocol of the security group rule.
|
||||||
|
required: false
|
||||||
|
default: 'tcp'
|
||||||
|
choices: [ 'tcp', 'udp', 'icmp', 'ah', 'esp', 'gre' ]
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- Ingress or egress security group rule.
|
||||||
|
required: false
|
||||||
|
default: 'ingress'
|
||||||
|
choices: [ 'ingress', 'egress' ]
|
||||||
|
cidr:
|
||||||
|
description:
|
||||||
|
- CIDR (full notation) to be used for security group rule.
|
||||||
|
required: false
|
||||||
|
default: '0.0.0.0/0'
|
||||||
|
user_security_group:
|
||||||
|
description:
|
||||||
|
- Security group this rule is based of.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
start_port:
|
||||||
|
description:
|
||||||
|
- Start port for this rule. Required if C(protocol=tcp) or C(protocol=udp).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aliases: [ 'port' ]
|
||||||
|
end_port:
|
||||||
|
description:
|
||||||
|
- End port for this rule. Required if C(protocol=tcp) or C(protocol=udp), but C(start_port) will be used if not set.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
icmp_type:
|
||||||
|
description:
|
||||||
|
- Type of the icmp message being sent. Required if C(protocol=icmp).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
icmp_code:
|
||||||
|
description:
|
||||||
|
- Error code for this icmp message. Required if C(protocol=icmp).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the security group to be created in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
---
|
||||||
|
# Allow inbound port 80/tcp from 1.2.3.4 added to security group 'default'
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup_rule
|
||||||
|
security_group: default
|
||||||
|
port: 80
|
||||||
|
cidr: 1.2.3.4/32
|
||||||
|
|
||||||
|
# Allow tcp/udp outbound added to security group 'default'
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup_rule
|
||||||
|
security_group: default
|
||||||
|
type: egress
|
||||||
|
start_port: 1
|
||||||
|
end_port: 65535
|
||||||
|
protocol: '{{ item }}'
|
||||||
|
with_items:
|
||||||
|
- tcp
|
||||||
|
- udp
|
||||||
|
|
||||||
|
# Allow inbound icmp from 0.0.0.0/0 added to security group 'default'
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup_rule
|
||||||
|
security_group: default
|
||||||
|
protocol: icmp
|
||||||
|
icmp_code: -1
|
||||||
|
icmp_type: -1
|
||||||
|
|
||||||
|
# Remove rule inbound port 80/tcp from 0.0.0.0/0 from security group 'default'
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup_rule
|
||||||
|
security_group: default
|
||||||
|
port: 80
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
# Allow inbound port 80/tcp from security group web added to security group 'default'
|
||||||
|
- local_action:
|
||||||
|
module: cs_securitygroup_rule
|
||||||
|
security_group: default
|
||||||
|
port: 80
|
||||||
|
user_security_group: web
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
security_group:
|
||||||
|
description: security group of the rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: default
|
||||||
|
type:
|
||||||
|
description: type of the rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: ingress
|
||||||
|
cidr:
|
||||||
|
description: CIDR of the rule.
|
||||||
|
returned: success and cidr is defined
|
||||||
|
type: string
|
||||||
|
sample: 0.0.0.0/0
|
||||||
|
user_security_group:
|
||||||
|
description: user security group of the rule.
|
||||||
|
returned: success and user_security_group is defined
|
||||||
|
type: string
|
||||||
|
sample: default
|
||||||
|
protocol:
|
||||||
|
description: protocol of the rule.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: tcp
|
||||||
|
start_port:
|
||||||
|
description: start port of the rule.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
end_port:
|
||||||
|
description: end port of the rule.
|
||||||
|
returned: success
|
||||||
|
type: int
|
||||||
|
sample: 80
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackSecurityGroupRule(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
|
||||||
|
|
||||||
|
def _tcp_udp_match(self, rule, protocol, start_port, end_port):
|
||||||
|
return protocol in ['tcp', 'udp'] \
|
||||||
|
and protocol == rule['protocol'] \
|
||||||
|
and start_port == int(rule['startport']) \
|
||||||
|
and end_port == int(rule['endport'])
|
||||||
|
|
||||||
|
|
||||||
|
def _icmp_match(self, rule, protocol, icmp_code, icmp_type):
|
||||||
|
return protocol == 'icmp' \
|
||||||
|
and protocol == rule['protocol'] \
|
||||||
|
and icmp_code == int(rule['icmpcode']) \
|
||||||
|
and icmp_type == int(rule['icmptype'])
|
||||||
|
|
||||||
|
|
||||||
|
def _ah_esp_gre_match(self, rule, protocol):
|
||||||
|
return protocol in ['ah', 'esp', 'gre'] \
|
||||||
|
and protocol == rule['protocol']
|
||||||
|
|
||||||
|
|
||||||
|
def _type_security_group_match(self, rule, security_group_name):
|
||||||
|
return security_group_name \
|
||||||
|
and 'securitygroupname' in rule \
|
||||||
|
and security_group_name == rule['securitygroupname']
|
||||||
|
|
||||||
|
|
||||||
|
def _type_cidr_match(self, rule, cidr):
|
||||||
|
return 'cidr' in rule \
|
||||||
|
and cidr == rule['cidr']
|
||||||
|
|
||||||
|
|
||||||
|
def get_end_port(self):
|
||||||
|
if self.module.params.get('end_port'):
|
||||||
|
return self.module.params.get('end_port')
|
||||||
|
return self.module.params.get('start_port')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_rule(self, rules):
|
||||||
|
user_security_group_name = self.module.params.get('user_security_group')
|
||||||
|
cidr = self.module.params.get('cidr')
|
||||||
|
protocol = self.module.params.get('protocol')
|
||||||
|
start_port = self.module.params.get('start_port')
|
||||||
|
end_port = self.get_end_port()
|
||||||
|
icmp_code = self.module.params.get('icmp_code')
|
||||||
|
icmp_type = self.module.params.get('icmp_type')
|
||||||
|
|
||||||
|
if protocol in ['tcp', 'udp'] and not (start_port and end_port):
|
||||||
|
self.module.fail_json(msg="no start_port or end_port set for protocol '%s'" % protocol)
|
||||||
|
|
||||||
|
if protocol == 'icmp' and not (icmp_type and icmp_code):
|
||||||
|
self.module.fail_json(msg="no icmp_type or icmp_code set for protocol '%s'" % protocol)
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
if user_security_group_name:
|
||||||
|
type_match = self._type_security_group_match(rule, user_security_group_name)
|
||||||
|
else:
|
||||||
|
type_match = self._type_cidr_match(rule, cidr)
|
||||||
|
|
||||||
|
protocol_match = ( self._tcp_udp_match(rule, protocol, start_port, end_port) \
|
||||||
|
or self._icmp_match(rule, protocol, icmp_code, icmp_type) \
|
||||||
|
or self._ah_esp_gre_match(rule, protocol)
|
||||||
|
)
|
||||||
|
|
||||||
|
if type_match and protocol_match:
|
||||||
|
return rule
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_security_group(self, security_group_name=None):
|
||||||
|
if not security_group_name:
|
||||||
|
security_group_name = self.module.params.get('security_group')
|
||||||
|
args = {}
|
||||||
|
args['securitygroupname'] = security_group_name
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
sgs = self.cs.listSecurityGroups(**args)
|
||||||
|
if not sgs or 'securitygroup' not in sgs:
|
||||||
|
self.module.fail_json(msg="security group '%s' not found" % security_group_name)
|
||||||
|
return sgs['securitygroup'][0]
|
||||||
|
|
||||||
|
|
||||||
|
def add_rule(self):
|
||||||
|
security_group = self.get_security_group()
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
user_security_group_name = self.module.params.get('user_security_group')
|
||||||
|
|
||||||
|
# the user_security_group and cidr are mutually_exclusive, but cidr is defaulted to 0.0.0.0/0.
|
||||||
|
# that is why we ignore if we have a user_security_group.
|
||||||
|
if user_security_group_name:
|
||||||
|
args['usersecuritygrouplist'] = []
|
||||||
|
user_security_group = self.get_security_group(user_security_group_name)
|
||||||
|
args['usersecuritygrouplist'].append({
|
||||||
|
'group': user_security_group['name'],
|
||||||
|
'account': user_security_group['account'],
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
args['cidrlist'] = self.module.params.get('cidr')
|
||||||
|
|
||||||
|
args['protocol'] = self.module.params.get('protocol')
|
||||||
|
args['startport'] = self.module.params.get('start_port')
|
||||||
|
args['endport'] = self.get_end_port()
|
||||||
|
args['icmptype'] = self.module.params.get('icmp_type')
|
||||||
|
args['icmpcode'] = self.module.params.get('icmp_code')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['securitygroupid'] = security_group['id']
|
||||||
|
|
||||||
|
rule = None
|
||||||
|
res = None
|
||||||
|
sg_type = self.module.params.get('type')
|
||||||
|
if sg_type == 'ingress':
|
||||||
|
rule = self._get_rule(security_group['ingressrule'])
|
||||||
|
if not rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.authorizeSecurityGroupIngress(**args)
|
||||||
|
|
||||||
|
elif sg_type == 'egress':
|
||||||
|
rule = self._get_rule(security_group['egressrule'])
|
||||||
|
if not rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.authorizeSecurityGroupEgress(**args)
|
||||||
|
|
||||||
|
if res and 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
security_group = self._poll_job(res, 'securitygroup')
|
||||||
|
key = sg_type + "rule" # ingressrule / egressrule
|
||||||
|
if key in security_group:
|
||||||
|
rule = security_group[key][0]
|
||||||
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def remove_rule(self):
|
||||||
|
security_group = self.get_security_group()
|
||||||
|
rule = None
|
||||||
|
res = None
|
||||||
|
sg_type = self.module.params.get('type')
|
||||||
|
if sg_type == 'ingress':
|
||||||
|
rule = self._get_rule(security_group['ingressrule'])
|
||||||
|
if rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.revokeSecurityGroupIngress(id=rule['ruleid'])
|
||||||
|
|
||||||
|
elif sg_type == 'egress':
|
||||||
|
rule = self._get_rule(security_group['egressrule'])
|
||||||
|
if rule:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.revokeSecurityGroupEgress(id=rule['ruleid'])
|
||||||
|
|
||||||
|
if res and 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
res = self._poll_job(res, 'securitygroup')
|
||||||
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, security_group_rule):
|
||||||
|
|
||||||
|
self.result['type'] = self.module.params.get('type')
|
||||||
|
self.result['security_group'] = self.module.params.get('security_group')
|
||||||
|
|
||||||
|
if security_group_rule:
|
||||||
|
rule = security_group_rule
|
||||||
|
if 'securitygroupname' in rule:
|
||||||
|
self.result['user_security_group'] = rule['securitygroupname']
|
||||||
|
if 'cidr' in rule:
|
||||||
|
self.result['cidr'] = rule['cidr']
|
||||||
|
if 'protocol' in rule:
|
||||||
|
self.result['protocol'] = rule['protocol']
|
||||||
|
if 'startport' in rule:
|
||||||
|
self.result['start_port'] = rule['startport']
|
||||||
|
if 'endport' in rule:
|
||||||
|
self.result['end_port'] = rule['endport']
|
||||||
|
if 'icmpcode' in rule:
|
||||||
|
self.result['icmp_code'] = rule['icmpcode']
|
||||||
|
if 'icmptype' in rule:
|
||||||
|
self.result['icmp_type'] = rule['icmptype']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
security_group = dict(required=True),
|
||||||
|
type = dict(choices=['ingress', 'egress'], default='ingress'),
|
||||||
|
cidr = dict(default='0.0.0.0/0'),
|
||||||
|
user_security_group = dict(default=None),
|
||||||
|
protocol = dict(choices=['tcp', 'udp', 'icmp', 'ah', 'esp', 'gre'], default='tcp'),
|
||||||
|
icmp_type = dict(type='int', default=None),
|
||||||
|
icmp_code = dict(type='int', default=None),
|
||||||
|
start_port = dict(type='int', default=None, aliases=['port']),
|
||||||
|
end_port = dict(type='int', default=None),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
project = dict(default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['icmp_type', 'icmp_code'],
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
mutually_exclusive = (
|
||||||
|
['icmp_type', 'start_port'],
|
||||||
|
['icmp_type', 'end_port'],
|
||||||
|
['icmp_code', 'start_port'],
|
||||||
|
['icmp_code', 'end_port'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_sg_rule = AnsibleCloudStackSecurityGroupRule(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
sg_rule = acs_sg_rule.remove_rule()
|
||||||
|
else:
|
||||||
|
sg_rule = acs_sg_rule.add_rule()
|
||||||
|
|
||||||
|
result = acs_sg_rule.get_result(sg_rule)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_sshkeypair
|
||||||
|
short_description: Manages SSH keys on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create, register and remove SSH keys.
|
||||||
|
- If no key was found and no public key was provided and a new SSH
|
||||||
|
private/public key pair will be created and the private key will be returned.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of public key.
|
||||||
|
required: true
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the public key is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the public key is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the public key to be registered in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the public key.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
public_key:
|
||||||
|
description:
|
||||||
|
- String of the public key.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# create a new private / public key pair:
|
||||||
|
- local_action: cs_sshkeypair name=linus@example.com
|
||||||
|
register: key
|
||||||
|
- debug: msg='private key is {{ key.private_key }}'
|
||||||
|
|
||||||
|
# remove a public key by its name:
|
||||||
|
- local_action: cs_sshkeypair name=linus@example.com state=absent
|
||||||
|
|
||||||
|
# register your existing local public key:
|
||||||
|
- local_action: cs_sshkeypair name=linus@example.com public_key='{{ lookup('file', '~/.ssh/id_rsa.pub') }}'
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of the SSH public key.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: linus@example.com
|
||||||
|
fingerprint:
|
||||||
|
description: Fingerprint of the SSH public key.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: "86:5e:a3:e8:bd:95:7b:07:7c:c2:5c:f7:ad:8b:09:28"
|
||||||
|
private_key:
|
||||||
|
description: Private key of generated SSH keypair.
|
||||||
|
returned: changed
|
||||||
|
type: string
|
||||||
|
sample: "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQCkeFYjI+4k8bWfIRMzp4pCzhlopNydbbwRu824P5ilD4ATWMUG\nvEtuCQ2Mp5k5Bma30CdYHgh2/SbxC5RxXSUKTUJtTKpoJUy8PAhb1nn9dnfkC2oU\naRVi9NRUgypTIZxMpgooHOxvAzWxbZCyh1W+91Ld3FNaGxTLqTgeevY84wIDAQAB\nAoGAcwQwgLyUwsNB1vmjWwE0QEmvHS4FlhZyahhi4hGfZvbzAxSWHIK7YUT1c8KU\n9XsThEIN8aJ3GvcoL3OAqNKRnoNb14neejVHkYRadhxqc0GVN6AUIyCqoEMpvhFI\nQrinM572ORzv5ffRjCTbvZcYlW+sqFKNo5e8pYIB8TigpFECQQDu7bg9vkvg8xPs\nkP1K+EH0vsR6vUfy+m3euXjnbJtiP7RoTkZk0JQMOmexgy1qQhISWT0e451wd62v\nJ7M0trl5AkEAsDivJnMIlCCCypwPN4tdNUYpe9dtidR1zLmb3SA7wXk5xMUgLZI9\ncWPjBCMt0KKShdDhQ+hjXAyKQLF7iAPuOwJABjdHCMwvmy2XwhrPjCjDRoPEBtFv\n0sFzJE08+QBZVogDwIbwy+SlRWArnHGmN9J6N+H8dhZD3U4vxZPJ1MBAOQJBAJxO\nCv1dt1Q76gbwmYa49LnWO+F+2cgRTVODpr5iYt5fOmBQQRRqzFkRMkFvOqn+KVzM\nQ6LKM6dn8BEl295vLhUCQQCVDWzoSk3GjL3sOjfAUTyAj8VAXM69llaptxWWySPM\nE9pA+8rYmHfohYFx7FD5/KWCO+sfmxTNB48X0uwyE8tO\n-----END RSA PRIVATE KEY-----\n"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import sshpubkeys
|
||||||
|
has_lib_sshpubkeys = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_sshpubkeys = False
|
||||||
|
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
class AnsibleCloudStackSshKey(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
self.ssh_key = None
|
||||||
|
|
||||||
|
|
||||||
|
def register_ssh_key(self, public_key):
|
||||||
|
ssh_key = self.get_ssh_key()
|
||||||
|
args = {}
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
|
||||||
|
res = None
|
||||||
|
if not ssh_key:
|
||||||
|
self.result['changed'] = True
|
||||||
|
args['publickey'] = public_key
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.registerSSHKeyPair(**args)
|
||||||
|
|
||||||
|
else:
|
||||||
|
fingerprint = self._get_ssh_fingerprint(public_key)
|
||||||
|
if ssh_key['fingerprint'] != fingerprint:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
self.cs.deleteSSHKeyPair(**args)
|
||||||
|
args['publickey'] = public_key
|
||||||
|
res = self.cs.registerSSHKeyPair(**args)
|
||||||
|
|
||||||
|
if res and 'keypair' in res:
|
||||||
|
ssh_key = res['keypair']
|
||||||
|
|
||||||
|
return ssh_key
|
||||||
|
|
||||||
|
|
||||||
|
def create_ssh_key(self):
|
||||||
|
ssh_key = self.get_ssh_key()
|
||||||
|
if not ssh_key:
|
||||||
|
self.result['changed'] = True
|
||||||
|
args = {}
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createSSHKeyPair(**args)
|
||||||
|
ssh_key = res['keypair']
|
||||||
|
return ssh_key
|
||||||
|
|
||||||
|
|
||||||
|
def remove_ssh_key(self):
|
||||||
|
ssh_key = self.get_ssh_key()
|
||||||
|
if ssh_key:
|
||||||
|
self.result['changed'] = True
|
||||||
|
args = {}
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteSSHKeyPair(**args)
|
||||||
|
return ssh_key
|
||||||
|
|
||||||
|
|
||||||
|
def get_ssh_key(self):
|
||||||
|
if not self.ssh_key:
|
||||||
|
args = {}
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
|
||||||
|
ssh_keys = self.cs.listSSHKeyPairs(**args)
|
||||||
|
if ssh_keys and 'sshkeypair' in ssh_keys:
|
||||||
|
self.ssh_key = ssh_keys['sshkeypair'][0]
|
||||||
|
return self.ssh_key
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, ssh_key):
|
||||||
|
if ssh_key:
|
||||||
|
if 'fingerprint' in ssh_key:
|
||||||
|
self.result['fingerprint'] = ssh_key['fingerprint']
|
||||||
|
if 'name' in ssh_key:
|
||||||
|
self.result['name'] = ssh_key['name']
|
||||||
|
if 'privatekey' in ssh_key:
|
||||||
|
self.result['private_key'] = ssh_key['privatekey']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def _get_ssh_fingerprint(self, public_key):
|
||||||
|
key = sshpubkeys.SSHKey(public_key)
|
||||||
|
return key.hash()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
public_key = dict(default=None),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
if not has_lib_sshpubkeys:
|
||||||
|
module.fail_json(msg="python library sshpubkeys required: pip install sshpubkeys")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_sshkey = AnsibleCloudStackSshKey(module)
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
ssh_key = acs_sshkey.remove_ssh_key()
|
||||||
|
else:
|
||||||
|
public_key = module.params.get('public_key')
|
||||||
|
if public_key:
|
||||||
|
ssh_key = acs_sshkey.register_ssh_key(public_key)
|
||||||
|
else:
|
||||||
|
ssh_key = acs_sshkey.create_ssh_key()
|
||||||
|
|
||||||
|
result = acs_sshkey.get_result(ssh_key)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,633 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_template
|
||||||
|
short_description: Manages templates on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Register a template from URL, create a template from a ROOT volume of a stopped VM or its snapshot and delete templates.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the template.
|
||||||
|
required: true
|
||||||
|
url:
|
||||||
|
description:
|
||||||
|
- URL of where the template is hosted.
|
||||||
|
- Mutually exclusive with C(vm).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
vm:
|
||||||
|
description:
|
||||||
|
- VM name the template will be created from its volume or alternatively from a snapshot.
|
||||||
|
- VM must be in stopped state if created from its volume.
|
||||||
|
- Mutually exclusive with C(url).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
snapshot:
|
||||||
|
description:
|
||||||
|
- Name of the snapshot, created from the VM ROOT volume, the template will be created from.
|
||||||
|
- C(vm) is required together with this argument.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
os_type:
|
||||||
|
description:
|
||||||
|
- OS type that best represents the OS of this template.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
checksum:
|
||||||
|
description:
|
||||||
|
- The MD5 checksum value of this template.
|
||||||
|
- If set, we search by checksum instead of name.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_ready:
|
||||||
|
description:
|
||||||
|
- This flag is used for searching existing templates.
|
||||||
|
- If set to C(true), it will only list template ready for deployment e.g. successfully downloaded and installed.
|
||||||
|
- Recommended to set it to C(false).
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_public:
|
||||||
|
description:
|
||||||
|
- Register the template to be publicly available to all users.
|
||||||
|
- Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_featured:
|
||||||
|
description:
|
||||||
|
- Register the template to be featured.
|
||||||
|
- Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_dynamically_scalable:
|
||||||
|
description:
|
||||||
|
- Register the template having XS/VMWare tools installed in order to support dynamic scaling of VM CPU/memory.
|
||||||
|
- Only used if C(state) is present.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the template to be registered in.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
zone:
|
||||||
|
description:
|
||||||
|
- Name of the zone you wish the template to be registered or deleted from.
|
||||||
|
- If not specified, first found zone will be used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
template_filter:
|
||||||
|
description:
|
||||||
|
- Name of the filter used to search for the template.
|
||||||
|
required: false
|
||||||
|
default: 'self'
|
||||||
|
choices: [ 'featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community' ]
|
||||||
|
hypervisor:
|
||||||
|
description:
|
||||||
|
- Name the hypervisor to be used for creating the new template.
|
||||||
|
- Relevant when using C(state=present).
|
||||||
|
required: false
|
||||||
|
default: none
|
||||||
|
choices: [ 'KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM' ]
|
||||||
|
requires_hvm:
|
||||||
|
description:
|
||||||
|
- true if this template requires HVM.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
password_enabled:
|
||||||
|
description:
|
||||||
|
- True if the template supports the password reset feature.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
template_tag:
|
||||||
|
description:
|
||||||
|
- the tag for this template.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
sshkey_enabled:
|
||||||
|
description:
|
||||||
|
- True if the template supports the sshkey upload feature.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
is_routing:
|
||||||
|
description:
|
||||||
|
- True if the template type is routing i.e., if template is used to deploy router.
|
||||||
|
- Only considered if C(url) is used.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
format:
|
||||||
|
description:
|
||||||
|
- The format for the template.
|
||||||
|
- Relevant when using C(state=present).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
choices: [ 'QCOW2', 'RAW', 'VHD', 'OVA' ]
|
||||||
|
is_extractable:
|
||||||
|
description:
|
||||||
|
- True if the template or its derivatives are extractable.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
details:
|
||||||
|
description:
|
||||||
|
- Template details in key/value pairs.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
bits:
|
||||||
|
description:
|
||||||
|
- 32 or 64 bits support.
|
||||||
|
required: false
|
||||||
|
default: '64'
|
||||||
|
displaytext:
|
||||||
|
description:
|
||||||
|
- the display text of the template.
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the template.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Register a systemvm template
|
||||||
|
- local_action:
|
||||||
|
module: cs_template
|
||||||
|
name: systemvm-4.5
|
||||||
|
url: "http://packages.shapeblue.com/systemvmtemplate/4.5/systemvm64template-4.5-vmware.ova"
|
||||||
|
hypervisor: VMware
|
||||||
|
format: OVA
|
||||||
|
zone: tokio-ix
|
||||||
|
os_type: Debian GNU/Linux 7(64-bit)
|
||||||
|
is_routing: yes
|
||||||
|
|
||||||
|
# Create a template from a stopped virtual machine's volume
|
||||||
|
- local_action:
|
||||||
|
module: cs_template
|
||||||
|
name: debian-base-template
|
||||||
|
vm: debian-base-vm
|
||||||
|
os_type: Debian GNU/Linux 7(64-bit)
|
||||||
|
zone: tokio-ix
|
||||||
|
password_enabled: yes
|
||||||
|
is_public: yes
|
||||||
|
|
||||||
|
# Create a template from a virtual machine's root volume snapshot
|
||||||
|
- local_action:
|
||||||
|
module: cs_template
|
||||||
|
name: debian-base-template
|
||||||
|
vm: debian-base-vm
|
||||||
|
snapshot: ROOT-233_2015061509114
|
||||||
|
os_type: Debian GNU/Linux 7(64-bit)
|
||||||
|
zone: tokio-ix
|
||||||
|
password_enabled: yes
|
||||||
|
is_public: yes
|
||||||
|
|
||||||
|
# Remove a template
|
||||||
|
- local_action:
|
||||||
|
module: cs_template
|
||||||
|
name: systemvm-4.2
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of the template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Debian 7 64-bit
|
||||||
|
displaytext:
|
||||||
|
description: Displaytext of the template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Debian 7.7 64-bit minimal 2015-03-19
|
||||||
|
checksum:
|
||||||
|
description: MD5 checksum of the template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 0b31bccccb048d20b551f70830bb7ad0
|
||||||
|
status:
|
||||||
|
description: Status of the template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Download Complete
|
||||||
|
is_ready:
|
||||||
|
description: True if the template is ready to be deployed from.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
is_public:
|
||||||
|
description: True if the template is public.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
is_featured:
|
||||||
|
description: True if the template is featured.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
is_extractable:
|
||||||
|
description: True if the template is extractable.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: true
|
||||||
|
format:
|
||||||
|
description: Format of the template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: OVA
|
||||||
|
os_type:
|
||||||
|
description: Typo of the OS.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: CentOS 6.5 (64-bit)
|
||||||
|
password_enabled:
|
||||||
|
description: True if the reset password feature is enabled, false otherwise.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: false
|
||||||
|
sshkey_enabled:
|
||||||
|
description: true if template is sshkey enabled, false otherwise.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: false
|
||||||
|
cross_zones:
|
||||||
|
description: true if the template is managed across all zones, false otherwise.
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: false
|
||||||
|
template_type:
|
||||||
|
description: Type of the template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: USER
|
||||||
|
created:
|
||||||
|
description: Date of registering.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2015-03-29T14:57:06+0200
|
||||||
|
template_tag:
|
||||||
|
description: Template tag related to this template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: special
|
||||||
|
hypervisor:
|
||||||
|
description: Hypervisor related to this template.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: VMware
|
||||||
|
tags:
|
||||||
|
description: List of resource tags associated with the template.
|
||||||
|
returned: success
|
||||||
|
type: dict
|
||||||
|
sample: '[ { "key": "foo", "value": "bar" } ]'
|
||||||
|
zone:
|
||||||
|
description: Name of zone the template is registered in.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: zuerich
|
||||||
|
domain:
|
||||||
|
description: Domain the template is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example domain
|
||||||
|
account:
|
||||||
|
description: Account the template is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
project:
|
||||||
|
description: Name of project the template is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Production
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackTemplate(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_args(self):
|
||||||
|
args = {}
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['displaytext'] = self.module.params.get('displaytext')
|
||||||
|
args['bits'] = self.module.params.get('bits')
|
||||||
|
args['isdynamicallyscalable'] = self.module.params.get('is_dynamically_scalable')
|
||||||
|
args['isextractable'] = self.module.params.get('is_extractable')
|
||||||
|
args['isfeatured'] = self.module.params.get('is_featured')
|
||||||
|
args['ispublic'] = self.module.params.get('is_public')
|
||||||
|
args['passwordenabled'] = self.module.params.get('password_enabled')
|
||||||
|
args['requireshvm'] = self.module.params.get('requires_hvm')
|
||||||
|
args['templatetag'] = self.module.params.get('template_tag')
|
||||||
|
args['ostypeid'] = self.get_os_type(key='id')
|
||||||
|
|
||||||
|
if not args['ostypeid']:
|
||||||
|
self.module.fail_json(msg="Missing required arguments: os_type")
|
||||||
|
|
||||||
|
if not args['displaytext']:
|
||||||
|
args['displaytext'] = self.module.params.get('name')
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def get_root_volume(self, key=None):
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
args['virtualmachineid'] = self.get_vm(key='id')
|
||||||
|
args['type'] = "ROOT"
|
||||||
|
|
||||||
|
volumes = self.cs.listVolumes(**args)
|
||||||
|
if volumes:
|
||||||
|
return self._get_by_key(key, volumes['volume'][0])
|
||||||
|
self.module.fail_json(msg="Root volume for '%s' not found" % self.get_vm('name'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_snapshot(self, key=None):
|
||||||
|
snapshot = self.module.params.get('snapshot')
|
||||||
|
if not snapshot:
|
||||||
|
return None
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
args['volumeid'] = self.get_root_volume('id')
|
||||||
|
snapshots = self.cs.listSnapshots(**args)
|
||||||
|
if snapshots:
|
||||||
|
for s in snapshots['snapshot']:
|
||||||
|
if snapshot in [ s['name'], s['id'] ]:
|
||||||
|
return self._get_by_key(key, s)
|
||||||
|
self.module.fail_json(msg="Snapshot '%s' not found" % snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
def create_template(self):
|
||||||
|
template = self.get_template()
|
||||||
|
if not template:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = self._get_args()
|
||||||
|
snapshot_id = self.get_snapshot(key='id')
|
||||||
|
if snapshot_id:
|
||||||
|
args['snapshotid'] = snapshot_id
|
||||||
|
else:
|
||||||
|
args['volumeid'] = self.get_root_volume('id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
template = self.cs.createTemplate(**args)
|
||||||
|
|
||||||
|
if 'errortext' in template:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % template['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
template = self._poll_job(template, 'template')
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def register_template(self):
|
||||||
|
template = self.get_template()
|
||||||
|
if not template:
|
||||||
|
self.result['changed'] = True
|
||||||
|
args = self._get_args()
|
||||||
|
args['url'] = self.module.params.get('url')
|
||||||
|
args['format'] = self.module.params.get('format')
|
||||||
|
args['checksum'] = self.module.params.get('checksum')
|
||||||
|
args['isextractable'] = self.module.params.get('is_extractable')
|
||||||
|
args['isrouting'] = self.module.params.get('is_routing')
|
||||||
|
args['sshkeyenabled'] = self.module.params.get('sshkey_enabled')
|
||||||
|
args['hypervisor'] = self.get_hypervisor()
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.registerTemplate(**args)
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
template = res['template']
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def get_template(self):
|
||||||
|
args = {}
|
||||||
|
args['isready'] = self.module.params.get('is_ready')
|
||||||
|
args['templatefilter'] = self.module.params.get('template_filter')
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
args['domainid'] = self.get_domain(key='id')
|
||||||
|
args['account'] = self.get_account(key='name')
|
||||||
|
args['projectid'] = self.get_project(key='id')
|
||||||
|
|
||||||
|
# if checksum is set, we only look on that.
|
||||||
|
checksum = self.module.params.get('checksum')
|
||||||
|
if not checksum:
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
|
||||||
|
templates = self.cs.listTemplates(**args)
|
||||||
|
if templates:
|
||||||
|
# if checksum is set, we only look on that.
|
||||||
|
if not checksum:
|
||||||
|
return templates['template'][0]
|
||||||
|
else:
|
||||||
|
for i in templates['template']:
|
||||||
|
if i['checksum'] == checksum:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def remove_template(self):
|
||||||
|
template = self.get_template()
|
||||||
|
if template:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['id'] = template['id']
|
||||||
|
args['zoneid'] = self.get_zone(key='id')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteTemplate(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if poll_async:
|
||||||
|
res = self._poll_job(res, 'template')
|
||||||
|
return template
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, template):
|
||||||
|
if template:
|
||||||
|
if 'displaytext' in template:
|
||||||
|
self.result['displaytext'] = template['displaytext']
|
||||||
|
if 'name' in template:
|
||||||
|
self.result['name'] = template['name']
|
||||||
|
if 'hypervisor' in template:
|
||||||
|
self.result['hypervisor'] = template['hypervisor']
|
||||||
|
if 'zonename' in template:
|
||||||
|
self.result['zone'] = template['zonename']
|
||||||
|
if 'checksum' in template:
|
||||||
|
self.result['checksum'] = template['checksum']
|
||||||
|
if 'format' in template:
|
||||||
|
self.result['format'] = template['format']
|
||||||
|
if 'isready' in template:
|
||||||
|
self.result['is_ready'] = template['isready']
|
||||||
|
if 'ispublic' in template:
|
||||||
|
self.result['is_public'] = template['ispublic']
|
||||||
|
if 'isfeatured' in template:
|
||||||
|
self.result['is_featured'] = template['isfeatured']
|
||||||
|
if 'isextractable' in template:
|
||||||
|
self.result['is_extractable'] = template['isextractable']
|
||||||
|
# and yes! it is really camelCase!
|
||||||
|
if 'crossZones' in template:
|
||||||
|
self.result['cross_zones'] = template['crossZones']
|
||||||
|
if 'ostypename' in template:
|
||||||
|
self.result['os_type'] = template['ostypename']
|
||||||
|
if 'templatetype' in template:
|
||||||
|
self.result['template_type'] = template['templatetype']
|
||||||
|
if 'passwordenabled' in template:
|
||||||
|
self.result['password_enabled'] = template['passwordenabled']
|
||||||
|
if 'sshkeyenabled' in template:
|
||||||
|
self.result['sshkey_enabled'] = template['sshkeyenabled']
|
||||||
|
if 'status' in template:
|
||||||
|
self.result['status'] = template['status']
|
||||||
|
if 'created' in template:
|
||||||
|
self.result['created'] = template['created']
|
||||||
|
if 'templatetag' in template:
|
||||||
|
self.result['template_tag'] = template['templatetag']
|
||||||
|
if 'tags' in template:
|
||||||
|
self.result['tags'] = []
|
||||||
|
for tag in template['tags']:
|
||||||
|
result_tag = {}
|
||||||
|
result_tag['key'] = tag['key']
|
||||||
|
result_tag['value'] = tag['value']
|
||||||
|
self.result['tags'].append(result_tag)
|
||||||
|
if 'domain' in template:
|
||||||
|
self.result['domain'] = template['domain']
|
||||||
|
if 'account' in template:
|
||||||
|
self.result['account'] = template['account']
|
||||||
|
if 'project' in template:
|
||||||
|
self.result['project'] = template['project']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
displaytext = dict(default=None),
|
||||||
|
url = dict(default=None),
|
||||||
|
vm = dict(default=None),
|
||||||
|
snapshot = dict(default=None),
|
||||||
|
os_type = dict(default=None),
|
||||||
|
is_ready = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
is_public = dict(type='bool', choices=BOOLEANS, default=True),
|
||||||
|
is_featured = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
is_dynamically_scalable = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
is_extractable = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
is_routing = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
checksum = dict(default=None),
|
||||||
|
template_filter = dict(default='self', choices=['featured', 'self', 'selfexecutable', 'sharedexecutable', 'executable', 'community']),
|
||||||
|
hypervisor = dict(choices=['KVM', 'VMware', 'BareMetal', 'XenServer', 'LXC', 'HyperV', 'UCS', 'OVM'], default=None),
|
||||||
|
requires_hvm = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
password_enabled = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
template_tag = dict(default=None),
|
||||||
|
sshkey_enabled = dict(type='bool', choices=BOOLEANS, default=False),
|
||||||
|
format = dict(choices=['QCOW2', 'RAW', 'VHD', 'OVA'], default=None),
|
||||||
|
details = dict(default=None),
|
||||||
|
bits = dict(type='int', choices=[ 32, 64 ], default=64),
|
||||||
|
state = dict(choices=['present', 'absent'], default='present'),
|
||||||
|
zone = dict(default=None),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
poll_async = dict(type='bool', choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
mutually_exclusive = (
|
||||||
|
['url', 'vm'],
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
['format', 'url', 'hypervisor'],
|
||||||
|
),
|
||||||
|
required_one_of = (
|
||||||
|
['url', 'vm'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_tpl = AnsibleCloudStackTemplate(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['absent']:
|
||||||
|
tpl = acs_tpl.remove_template()
|
||||||
|
else:
|
||||||
|
url = module.params.get('url')
|
||||||
|
if url:
|
||||||
|
tpl = acs_tpl.register_template()
|
||||||
|
else:
|
||||||
|
tpl = acs_tpl.create_template()
|
||||||
|
|
||||||
|
result = acs_tpl.get_result(tpl)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,327 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# (c) 2015, René Moser <mail@renemoser.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: cs_vmsnapshot
|
||||||
|
short_description: Manages VM snapshots on Apache CloudStack based clouds.
|
||||||
|
description:
|
||||||
|
- Create, remove and revert VM from snapshots.
|
||||||
|
version_added: '2.0'
|
||||||
|
author: "René Moser (@resmo)"
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Unique Name of the snapshot. In CloudStack terms C(displayname).
|
||||||
|
required: true
|
||||||
|
aliases: ['displayname']
|
||||||
|
vm:
|
||||||
|
description:
|
||||||
|
- Name of the virtual machine.
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Description of the snapshot.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
snapshot_memory:
|
||||||
|
description:
|
||||||
|
- Snapshot memory if set to true.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
zone:
|
||||||
|
description:
|
||||||
|
- Name of the zone in which the VM is in. If not set, default zone is used.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
project:
|
||||||
|
description:
|
||||||
|
- Name of the project the VM is assigned to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the snapshot.
|
||||||
|
required: false
|
||||||
|
default: 'present'
|
||||||
|
choices: [ 'present', 'absent', 'revert' ]
|
||||||
|
domain:
|
||||||
|
description:
|
||||||
|
- Domain the VM snapshot is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
account:
|
||||||
|
description:
|
||||||
|
- Account the VM snapshot is related to.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
poll_async:
|
||||||
|
description:
|
||||||
|
- Poll async jobs until job has finished.
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
extends_documentation_fragment: cloudstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a VM snapshot of disk and memory before an upgrade
|
||||||
|
- local_action:
|
||||||
|
module: cs_vmsnapshot
|
||||||
|
name: Snapshot before upgrade
|
||||||
|
vm: web-01
|
||||||
|
snapshot_memory: yes
|
||||||
|
|
||||||
|
# Revert a VM to a snapshot after a failed upgrade
|
||||||
|
- local_action:
|
||||||
|
module: cs_vmsnapshot
|
||||||
|
name: Snapshot before upgrade
|
||||||
|
vm: web-01
|
||||||
|
state: revert
|
||||||
|
|
||||||
|
# Remove a VM snapshot after successful upgrade
|
||||||
|
- local_action:
|
||||||
|
module: cs_vmsnapshot
|
||||||
|
name: Snapshot before upgrade
|
||||||
|
vm: web-01
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = '''
|
||||||
|
---
|
||||||
|
name:
|
||||||
|
description: Name of the snapshot.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: snapshot before update
|
||||||
|
displayname:
|
||||||
|
description: displayname of the snapshot.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: snapshot before update
|
||||||
|
created:
|
||||||
|
description: date of the snapshot.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: 2015-03-29T14:57:06+0200
|
||||||
|
current:
|
||||||
|
description: true if snapshot is current
|
||||||
|
returned: success
|
||||||
|
type: boolean
|
||||||
|
sample: True
|
||||||
|
state:
|
||||||
|
description: state of the vm snapshot
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Allocated
|
||||||
|
type:
|
||||||
|
description: type of vm snapshot
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: DiskAndMemory
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
description: description of vm snapshot
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: snapshot brought to you by Ansible
|
||||||
|
domain:
|
||||||
|
description: Domain the the vm snapshot is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example domain
|
||||||
|
account:
|
||||||
|
description: Account the vm snapshot is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: example account
|
||||||
|
project:
|
||||||
|
description: Name of project the vm snapshot is related to.
|
||||||
|
returned: success
|
||||||
|
type: string
|
||||||
|
sample: Production
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cs import CloudStack, CloudStackException, read_config
|
||||||
|
has_lib_cs = True
|
||||||
|
except ImportError:
|
||||||
|
has_lib_cs = False
|
||||||
|
|
||||||
|
# import cloudstack common
|
||||||
|
from ansible.module_utils.cloudstack import *
|
||||||
|
|
||||||
|
|
||||||
|
class AnsibleCloudStackVmSnapshot(AnsibleCloudStack):
|
||||||
|
|
||||||
|
def __init__(self, module):
|
||||||
|
AnsibleCloudStack.__init__(self, module)
|
||||||
|
|
||||||
|
|
||||||
|
def get_snapshot(self):
|
||||||
|
args = {}
|
||||||
|
args['virtualmachineid'] = self.get_vm('id')
|
||||||
|
args['account'] = self.get_account('name')
|
||||||
|
args['domainid'] = self.get_domain('id')
|
||||||
|
args['projectid'] = self.get_project('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
|
||||||
|
snapshots = self.cs.listVMSnapshot(**args)
|
||||||
|
if snapshots:
|
||||||
|
return snapshots['vmSnapshot'][0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def create_snapshot(self):
|
||||||
|
snapshot = self.get_snapshot()
|
||||||
|
if not snapshot:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
args = {}
|
||||||
|
args['virtualmachineid'] = self.get_vm('id')
|
||||||
|
args['name'] = self.module.params.get('name')
|
||||||
|
args['description'] = self.module.params.get('description')
|
||||||
|
args['snapshotmemory'] = self.module.params.get('snapshot_memory')
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.createVMSnapshot(**args)
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
snapshot = self._poll_job(res, 'vmsnapshot')
|
||||||
|
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def remove_snapshot(self):
|
||||||
|
snapshot = self.get_snapshot()
|
||||||
|
if snapshot:
|
||||||
|
self.result['changed'] = True
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.deleteVMSnapshot(vmsnapshotid=snapshot['id'])
|
||||||
|
|
||||||
|
if 'errortext' in res:
|
||||||
|
self.module.fail_json(msg="Failed: '%s'" % res['errortext'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
res = self._poll_job(res, 'vmsnapshot')
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def revert_vm_to_snapshot(self):
|
||||||
|
snapshot = self.get_snapshot()
|
||||||
|
if snapshot:
|
||||||
|
self.result['changed'] = True
|
||||||
|
|
||||||
|
if snapshot['state'] != "Ready":
|
||||||
|
self.module.fail_json(msg="snapshot state is '%s', not ready, could not revert VM" % snapshot['state'])
|
||||||
|
|
||||||
|
if not self.module.check_mode:
|
||||||
|
res = self.cs.revertToVMSnapshot(vmsnapshotid=snapshot['id'])
|
||||||
|
|
||||||
|
poll_async = self.module.params.get('poll_async')
|
||||||
|
if res and poll_async:
|
||||||
|
res = self._poll_job(res, 'vmsnapshot')
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
self.module.fail_json(msg="snapshot not found, could not revert VM")
|
||||||
|
|
||||||
|
|
||||||
|
def get_result(self, snapshot):
|
||||||
|
if snapshot:
|
||||||
|
if 'displayname' in snapshot:
|
||||||
|
self.result['displayname'] = snapshot['displayname']
|
||||||
|
if 'created' in snapshot:
|
||||||
|
self.result['created'] = snapshot['created']
|
||||||
|
if 'current' in snapshot:
|
||||||
|
self.result['current'] = snapshot['current']
|
||||||
|
if 'state' in snapshot:
|
||||||
|
self.result['state'] = snapshot['state']
|
||||||
|
if 'type' in snapshot:
|
||||||
|
self.result['type'] = snapshot['type']
|
||||||
|
if 'name' in snapshot:
|
||||||
|
self.result['name'] = snapshot['name']
|
||||||
|
if 'description' in snapshot:
|
||||||
|
self.result['description'] = snapshot['description']
|
||||||
|
if 'domain' in snapshot:
|
||||||
|
self.result['domain'] = snapshot['domain']
|
||||||
|
if 'account' in snapshot:
|
||||||
|
self.result['account'] = snapshot['account']
|
||||||
|
if 'project' in snapshot:
|
||||||
|
self.result['project'] = snapshot['project']
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True, aliases=['displayname']),
|
||||||
|
vm = dict(required=True),
|
||||||
|
description = dict(default=None),
|
||||||
|
zone = dict(default=None),
|
||||||
|
snapshot_memory = dict(choices=BOOLEANS, default=False),
|
||||||
|
state = dict(choices=['present', 'absent', 'revert'], default='present'),
|
||||||
|
domain = dict(default=None),
|
||||||
|
account = dict(default=None),
|
||||||
|
project = dict(default=None),
|
||||||
|
poll_async = dict(choices=BOOLEANS, default=True),
|
||||||
|
api_key = dict(default=None),
|
||||||
|
api_secret = dict(default=None, no_log=True),
|
||||||
|
api_url = dict(default=None),
|
||||||
|
api_http_method = dict(choices=['get', 'post'], default='get'),
|
||||||
|
api_timeout = dict(type='int', default=10),
|
||||||
|
),
|
||||||
|
required_together = (
|
||||||
|
['icmp_type', 'icmp_code'],
|
||||||
|
['api_key', 'api_secret', 'api_url'],
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not has_lib_cs:
|
||||||
|
module.fail_json(msg="python library cs required: pip install cs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
acs_vmsnapshot = AnsibleCloudStackVmSnapshot(module)
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
if state in ['revert']:
|
||||||
|
snapshot = acs_vmsnapshot.revert_vm_to_snapshot()
|
||||||
|
elif state in ['absent']:
|
||||||
|
snapshot = acs_vmsnapshot.remove_snapshot()
|
||||||
|
else:
|
||||||
|
snapshot = acs_vmsnapshot.create_snapshot()
|
||||||
|
|
||||||
|
result = acs_vmsnapshot.get_result(snapshot)
|
||||||
|
|
||||||
|
except CloudStackException, e:
|
||||||
|
module.fail_json(msg='CloudStackException: %s' % str(e))
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='Exception: %s' % str(e))
|
||||||
|
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,433 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: proxmox
|
||||||
|
short_description: management of instances in Proxmox VE cluster
|
||||||
|
description:
|
||||||
|
- allows you to create/delete/stop instances in Proxmox VE cluster
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
api_host:
|
||||||
|
description:
|
||||||
|
- the host of the Proxmox VE cluster
|
||||||
|
required: true
|
||||||
|
api_user:
|
||||||
|
description:
|
||||||
|
- the user to authenticate with
|
||||||
|
required: true
|
||||||
|
api_password:
|
||||||
|
description:
|
||||||
|
- the password to authenticate with
|
||||||
|
- you can use PROXMOX_PASSWORD environment variable
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
vmid:
|
||||||
|
description:
|
||||||
|
- the instance id
|
||||||
|
default: null
|
||||||
|
required: true
|
||||||
|
validate_certs:
|
||||||
|
description:
|
||||||
|
- enable / disable https certificate verification
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
node:
|
||||||
|
description:
|
||||||
|
- Proxmox VE node, when new VM will be created
|
||||||
|
- required only for C(state=present)
|
||||||
|
- for another states will be autodiscovered
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- the instance root password
|
||||||
|
- required only for C(state=present)
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
hostname:
|
||||||
|
description:
|
||||||
|
- the instance hostname
|
||||||
|
- required only for C(state=present)
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
ostemplate:
|
||||||
|
description:
|
||||||
|
- the template for VM creating
|
||||||
|
- required only for C(state=present)
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
disk:
|
||||||
|
description:
|
||||||
|
- hard disk size in GB for instance
|
||||||
|
default: 3
|
||||||
|
required: false
|
||||||
|
cpus:
|
||||||
|
description:
|
||||||
|
- numbers of allocated cpus for instance
|
||||||
|
default: 1
|
||||||
|
required: false
|
||||||
|
memory:
|
||||||
|
description:
|
||||||
|
- memory size in MB for instance
|
||||||
|
default: 512
|
||||||
|
required: false
|
||||||
|
swap:
|
||||||
|
description:
|
||||||
|
- swap memory size in MB for instance
|
||||||
|
default: 0
|
||||||
|
required: false
|
||||||
|
netif:
|
||||||
|
description:
|
||||||
|
- specifies network interfaces for the container
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
ip_address:
|
||||||
|
description:
|
||||||
|
- specifies the address the container will be assigned
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
onboot:
|
||||||
|
description:
|
||||||
|
- specifies whether a VM will be started during system bootup
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
storage:
|
||||||
|
description:
|
||||||
|
- target storage
|
||||||
|
default: 'local'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
cpuunits:
|
||||||
|
description:
|
||||||
|
- CPU weight for a VM
|
||||||
|
default: 1000
|
||||||
|
required: false
|
||||||
|
type: integer
|
||||||
|
nameserver:
|
||||||
|
description:
|
||||||
|
- sets DNS server IP address for a container
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
searchdomain:
|
||||||
|
description:
|
||||||
|
- sets DNS search domain for a container
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- timeout for operations
|
||||||
|
default: 30
|
||||||
|
required: false
|
||||||
|
type: integer
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- forcing operations
|
||||||
|
- can be used only with states C(present), C(stopped), C(restarted)
|
||||||
|
- with C(state=present) force option allow to overwrite existing container
|
||||||
|
- with states C(stopped) , C(restarted) allow to force stop instance
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Indicate desired state of the instance
|
||||||
|
choices: ['present', 'started', 'absent', 'stopped', 'restarted']
|
||||||
|
default: present
|
||||||
|
notes:
|
||||||
|
- Requires proxmoxer and requests modules on host. This modules can be installed with pip.
|
||||||
|
requirements: [ "proxmoxer", "requests" ]
|
||||||
|
author: "Sergei Antipov @UnderGreen"
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create new container with minimal options
|
||||||
|
- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz'
|
||||||
|
|
||||||
|
# Create new container with minimal options with force(it will rewrite existing container)
|
||||||
|
- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz' force=yes
|
||||||
|
|
||||||
|
# Create new container with minimal options use environment PROXMOX_PASSWORD variable(you should export it before)
|
||||||
|
- proxmox: vmid=100 node='uk-mc02' api_user='root@pam' api_host='node1' password='123456' hostname='example.org' ostemplate='local:vztmpl/ubuntu-14.04-x86_64.tar.gz'
|
||||||
|
|
||||||
|
# Start container
|
||||||
|
- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=started
|
||||||
|
|
||||||
|
# Stop container
|
||||||
|
- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=stopped
|
||||||
|
|
||||||
|
# Stop container with force
|
||||||
|
- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' force=yes state=stopped
|
||||||
|
|
||||||
|
# Restart container(stopped or mounted container you can't restart)
|
||||||
|
- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=stopped
|
||||||
|
|
||||||
|
# Remove container
|
||||||
|
- proxmox: vmid=100 api_user='root@pam' api_password='1q2w3e' api_host='node1' state=absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
from proxmoxer import ProxmoxAPI
|
||||||
|
HAS_PROXMOXER = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PROXMOXER = False
|
||||||
|
|
||||||
|
def get_instance(proxmox, vmid):
|
||||||
|
return [ vm for vm in proxmox.cluster.resources.get(type='vm') if vm['vmid'] == int(vmid) ]
|
||||||
|
|
||||||
|
def content_check(proxmox, node, ostemplate, storage):
|
||||||
|
return [ True for cnt in proxmox.nodes(node).storage(storage).content.get() if cnt['volid'] == ostemplate ]
|
||||||
|
|
||||||
|
def node_check(proxmox, node):
|
||||||
|
return [ True for nd in proxmox.nodes.get() if nd['node'] == node ]
|
||||||
|
|
||||||
|
def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, **kwargs):
|
||||||
|
proxmox_node = proxmox.nodes(node)
|
||||||
|
taskid = proxmox_node.openvz.create(vmid=vmid, storage=storage, memory=memory, swap=swap,
|
||||||
|
cpus=cpus, disk=disk, **kwargs)
|
||||||
|
|
||||||
|
while timeout:
|
||||||
|
if ( proxmox_node.tasks(taskid).status.get()['status'] == 'stopped'
|
||||||
|
and proxmox_node.tasks(taskid).status.get()['exitstatus'] == 'OK' ):
|
||||||
|
return True
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for creating VM. Last line in task before timeout: %s'
|
||||||
|
% proxmox_node.tasks(taskid).log.get()[:1])
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_instance(module, proxmox, vm, vmid, timeout):
|
||||||
|
taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.start.post()
|
||||||
|
while timeout:
|
||||||
|
if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
|
||||||
|
and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
|
||||||
|
return True
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for starting VM. Last line in task before timeout: %s'
|
||||||
|
% proxmox.nodes(vm[0]['node']).tasks(taskid).log.get()[:1])
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_instance(module, proxmox, vm, vmid, timeout, force):
|
||||||
|
if force:
|
||||||
|
taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.shutdown.post(forceStop=1)
|
||||||
|
else:
|
||||||
|
taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.shutdown.post()
|
||||||
|
while timeout:
|
||||||
|
if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
|
||||||
|
and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
|
||||||
|
return True
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for stopping VM. Last line in task before timeout: %s'
|
||||||
|
% proxmox_node.tasks(taskid).log.get()[:1])
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def umount_instance(module, proxmox, vm, vmid, timeout):
|
||||||
|
taskid = proxmox.nodes(vm[0]['node']).openvz(vmid).status.umount.post()
|
||||||
|
while timeout:
|
||||||
|
if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
|
||||||
|
and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
|
||||||
|
return True
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for unmounting VM. Last line in task before timeout: %s'
|
||||||
|
% proxmox_node.tasks(taskid).log.get()[:1])
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
api_host = dict(required=True),
|
||||||
|
api_user = dict(required=True),
|
||||||
|
api_password = dict(no_log=True),
|
||||||
|
vmid = dict(required=True),
|
||||||
|
validate_certs = dict(type='bool', choices=BOOLEANS, default='no'),
|
||||||
|
node = dict(),
|
||||||
|
password = dict(no_log=True),
|
||||||
|
hostname = dict(),
|
||||||
|
ostemplate = dict(),
|
||||||
|
disk = dict(type='int', default=3),
|
||||||
|
cpus = dict(type='int', default=1),
|
||||||
|
memory = dict(type='int', default=512),
|
||||||
|
swap = dict(type='int', default=0),
|
||||||
|
netif = dict(),
|
||||||
|
ip_address = dict(),
|
||||||
|
onboot = dict(type='bool', choices=BOOLEANS, default='no'),
|
||||||
|
storage = dict(default='local'),
|
||||||
|
cpuunits = dict(type='int', default=1000),
|
||||||
|
nameserver = dict(),
|
||||||
|
searchdomain = dict(),
|
||||||
|
timeout = dict(type='int', default=30),
|
||||||
|
force = dict(type='bool', choices=BOOLEANS, default='no'),
|
||||||
|
state = dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PROXMOXER:
|
||||||
|
module.fail_json(msg='proxmoxer required for this module')
|
||||||
|
|
||||||
|
state = module.params['state']
|
||||||
|
api_user = module.params['api_user']
|
||||||
|
api_host = module.params['api_host']
|
||||||
|
api_password = module.params['api_password']
|
||||||
|
vmid = module.params['vmid']
|
||||||
|
validate_certs = module.params['validate_certs']
|
||||||
|
node = module.params['node']
|
||||||
|
disk = module.params['disk']
|
||||||
|
cpus = module.params['cpus']
|
||||||
|
memory = module.params['memory']
|
||||||
|
swap = module.params['swap']
|
||||||
|
storage = module.params['storage']
|
||||||
|
timeout = module.params['timeout']
|
||||||
|
|
||||||
|
# If password not set get it from PROXMOX_PASSWORD env
|
||||||
|
if not api_password:
|
||||||
|
try:
|
||||||
|
api_password = os.environ['PROXMOX_PASSWORD']
|
||||||
|
except KeyError, e:
|
||||||
|
module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
try:
|
||||||
|
if get_instance(proxmox, vmid) and not module.params['force']:
|
||||||
|
module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid)
|
||||||
|
elif not (node, module.params['hostname'] and module.params['password'] and module.params['ostemplate']):
|
||||||
|
module.fail_json(msg='node, hostname, password and ostemplate are mandatory for creating vm')
|
||||||
|
elif not node_check(proxmox, node):
|
||||||
|
module.fail_json(msg="node '%s' not exists in cluster" % node)
|
||||||
|
elif not content_check(proxmox, node, module.params['ostemplate'], storage):
|
||||||
|
module.fail_json(msg="ostemplate '%s' not exists on node %s and storage %s"
|
||||||
|
% (module.params['ostemplate'], node, storage))
|
||||||
|
|
||||||
|
create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout,
|
||||||
|
password = module.params['password'],
|
||||||
|
hostname = module.params['hostname'],
|
||||||
|
ostemplate = module.params['ostemplate'],
|
||||||
|
netif = module.params['netif'],
|
||||||
|
ip_address = module.params['ip_address'],
|
||||||
|
onboot = int(module.params['onboot']),
|
||||||
|
cpuunits = module.params['cpuunits'],
|
||||||
|
nameserver = module.params['nameserver'],
|
||||||
|
searchdomain = module.params['searchdomain'],
|
||||||
|
force = int(module.params['force']))
|
||||||
|
|
||||||
|
module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmid, module.params['ostemplate']))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="creation of VM %s failed with exception: %s" % ( vmid, e ))
|
||||||
|
|
||||||
|
elif state == 'started':
|
||||||
|
try:
|
||||||
|
vm = get_instance(proxmox, vmid)
|
||||||
|
if not vm:
|
||||||
|
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||||
|
if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'running':
|
||||||
|
module.exit_json(changed=False, msg="VM %s is already running" % vmid)
|
||||||
|
|
||||||
|
if start_instance(module, proxmox, vm, vmid, timeout):
|
||||||
|
module.exit_json(changed=True, msg="VM %s started" % vmid)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="starting of VM %s failed with exception: %s" % ( vmid, e ))
|
||||||
|
|
||||||
|
elif state == 'stopped':
|
||||||
|
try:
|
||||||
|
vm = get_instance(proxmox, vmid)
|
||||||
|
if not vm:
|
||||||
|
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||||
|
|
||||||
|
if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'mounted':
|
||||||
|
if module.params['force']:
|
||||||
|
if umount_instance(module, proxmox, vm, vmid, timeout):
|
||||||
|
module.exit_json(changed=True, msg="VM %s is shutting down" % vmid)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=False, msg=("VM %s is already shutdown, but mounted. "
|
||||||
|
"You can use force option to umount it.") % vmid)
|
||||||
|
|
||||||
|
if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'stopped':
|
||||||
|
module.exit_json(changed=False, msg="VM %s is already shutdown" % vmid)
|
||||||
|
|
||||||
|
if stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']):
|
||||||
|
module.exit_json(changed=True, msg="VM %s is shutting down" % vmid)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="stopping of VM %s failed with exception: %s" % ( vmid, e ))
|
||||||
|
|
||||||
|
elif state == 'restarted':
|
||||||
|
try:
|
||||||
|
vm = get_instance(proxmox, vmid)
|
||||||
|
if not vm:
|
||||||
|
module.fail_json(msg='VM with vmid = %s not exists in cluster' % vmid)
|
||||||
|
if ( proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'stopped'
|
||||||
|
or proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'mounted' ):
|
||||||
|
module.exit_json(changed=False, msg="VM %s is not running" % vmid)
|
||||||
|
|
||||||
|
if ( stop_instance(module, proxmox, vm, vmid, timeout, force = module.params['force']) and
|
||||||
|
start_instance(module, proxmox, vm, vmid, timeout) ):
|
||||||
|
module.exit_json(changed=True, msg="VM %s is restarted" % vmid)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="restarting of VM %s failed with exception: %s" % ( vmid, e ))
|
||||||
|
|
||||||
|
elif state == 'absent':
|
||||||
|
try:
|
||||||
|
vm = get_instance(proxmox, vmid)
|
||||||
|
if not vm:
|
||||||
|
module.exit_json(changed=False, msg="VM %s does not exist" % vmid)
|
||||||
|
|
||||||
|
if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'running':
|
||||||
|
module.exit_json(changed=False, msg="VM %s is running. Stop it before deletion." % vmid)
|
||||||
|
|
||||||
|
if proxmox.nodes(vm[0]['node']).openvz(vmid).status.current.get()['status'] == 'mounted':
|
||||||
|
module.exit_json(changed=False, msg="VM %s is mounted. Stop it with force option before deletion." % vmid)
|
||||||
|
|
||||||
|
taskid = proxmox.nodes(vm[0]['node']).openvz.delete(vmid)
|
||||||
|
while timeout:
|
||||||
|
if ( proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped'
|
||||||
|
and proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK' ):
|
||||||
|
module.exit_json(changed=True, msg="VM %s removed" % vmid)
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for removing VM. Last line in task before timeout: %s'
|
||||||
|
% proxmox_node.tasks(taskid).log.get()[:1])
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="deletion of VM %s failed with exception: %s" % ( vmid, e ))
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: proxmox_template
|
||||||
|
short_description: management of OS templates in Proxmox VE cluster
|
||||||
|
description:
|
||||||
|
- allows you to upload/delete templates in Proxmox VE cluster
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
api_host:
|
||||||
|
description:
|
||||||
|
- the host of the Proxmox VE cluster
|
||||||
|
required: true
|
||||||
|
api_user:
|
||||||
|
description:
|
||||||
|
- the user to authenticate with
|
||||||
|
required: true
|
||||||
|
api_password:
|
||||||
|
description:
|
||||||
|
- the password to authenticate with
|
||||||
|
- you can use PROXMOX_PASSWORD environment variable
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
validate_certs:
|
||||||
|
description:
|
||||||
|
- enable / disable https certificate verification
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
node:
|
||||||
|
description:
|
||||||
|
- Proxmox VE node, when you will operate with template
|
||||||
|
default: null
|
||||||
|
required: true
|
||||||
|
src:
|
||||||
|
description:
|
||||||
|
- path to uploaded file
|
||||||
|
- required only for C(state=present)
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
aliases: ['path']
|
||||||
|
template:
|
||||||
|
description:
|
||||||
|
- the template name
|
||||||
|
- required only for states C(absent), C(info)
|
||||||
|
default: null
|
||||||
|
required: false
|
||||||
|
content_type:
|
||||||
|
description:
|
||||||
|
- content type
|
||||||
|
- required only for C(state=present)
|
||||||
|
default: 'vztmpl'
|
||||||
|
required: false
|
||||||
|
choices: ['vztmpl', 'iso']
|
||||||
|
storage:
|
||||||
|
description:
|
||||||
|
- target storage
|
||||||
|
default: 'local'
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- timeout for operations
|
||||||
|
default: 30
|
||||||
|
required: false
|
||||||
|
type: integer
|
||||||
|
force:
|
||||||
|
description:
|
||||||
|
- can be used only with C(state=present), exists template will be overwritten
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Indicate desired state of the template
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: present
|
||||||
|
notes:
|
||||||
|
- Requires proxmoxer and requests modules on host. This modules can be installed with pip.
|
||||||
|
requirements: [ "proxmoxer", "requests" ]
|
||||||
|
author: "Sergei Antipov @UnderGreen"
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Upload new openvz template with minimal options
|
||||||
|
- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' src='~/ubuntu-14.04-x86_64.tar.gz'
|
||||||
|
|
||||||
|
# Upload new openvz template with minimal options use environment PROXMOX_PASSWORD variable(you should export it before)
|
||||||
|
- proxmox_template: node='uk-mc02' api_user='root@pam' api_host='node1' src='~/ubuntu-14.04-x86_64.tar.gz'
|
||||||
|
|
||||||
|
# Upload new openvz template with all options and force overwrite
|
||||||
|
- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' storage='local' content_type='vztmpl' src='~/ubuntu-14.04-x86_64.tar.gz' force=yes
|
||||||
|
|
||||||
|
# Delete template with minimal options
|
||||||
|
- proxmox_template: node='uk-mc02' api_user='root@pam' api_password='1q2w3e' api_host='node1' template='ubuntu-14.04-x86_64.tar.gz' state=absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
try:
|
||||||
|
from proxmoxer import ProxmoxAPI
|
||||||
|
HAS_PROXMOXER = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PROXMOXER = False
|
||||||
|
|
||||||
|
def get_template(proxmox, node, storage, content_type, template):
|
||||||
|
return [ True for tmpl in proxmox.nodes(node).storage(storage).content.get()
|
||||||
|
if tmpl['volid'] == '%s:%s/%s' % (storage, content_type, template) ]
|
||||||
|
|
||||||
|
def upload_template(module, proxmox, api_host, node, storage, content_type, realpath, timeout):
|
||||||
|
taskid = proxmox.nodes(node).storage(storage).upload.post(content=content_type, filename=open(realpath))
|
||||||
|
while timeout:
|
||||||
|
task_status = proxmox.nodes(api_host.split('.')[0]).tasks(taskid).status.get()
|
||||||
|
if task_status['status'] == 'stopped' and task_status['exitstatus'] == 'OK':
|
||||||
|
return True
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for uploading template. Last line in task before timeout: %s'
|
||||||
|
% proxmox.node(node).tasks(taskid).log.get()[:1])
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def delete_template(module, proxmox, node, storage, content_type, template, timeout):
|
||||||
|
volid = '%s:%s/%s' % (storage, content_type, template)
|
||||||
|
proxmox.nodes(node).storage(storage).content.delete(volid)
|
||||||
|
while timeout:
|
||||||
|
if not get_template(proxmox, node, storage, content_type, template):
|
||||||
|
return True
|
||||||
|
timeout = timeout - 1
|
||||||
|
if timeout == 0:
|
||||||
|
module.fail_json(msg='Reached timeout while waiting for deleting template.')
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
api_host = dict(required=True),
|
||||||
|
api_user = dict(required=True),
|
||||||
|
api_password = dict(no_log=True),
|
||||||
|
validate_certs = dict(type='bool', choices=BOOLEANS, default='no'),
|
||||||
|
node = dict(),
|
||||||
|
src = dict(),
|
||||||
|
template = dict(),
|
||||||
|
content_type = dict(default='vztmpl', choices=['vztmpl','iso']),
|
||||||
|
storage = dict(default='local'),
|
||||||
|
timeout = dict(type='int', default=30),
|
||||||
|
force = dict(type='bool', choices=BOOLEANS, default='no'),
|
||||||
|
state = dict(default='present', choices=['present', 'absent']),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PROXMOXER:
|
||||||
|
module.fail_json(msg='proxmoxer required for this module')
|
||||||
|
|
||||||
|
state = module.params['state']
|
||||||
|
api_user = module.params['api_user']
|
||||||
|
api_host = module.params['api_host']
|
||||||
|
api_password = module.params['api_password']
|
||||||
|
validate_certs = module.params['validate_certs']
|
||||||
|
node = module.params['node']
|
||||||
|
storage = module.params['storage']
|
||||||
|
timeout = module.params['timeout']
|
||||||
|
|
||||||
|
# If password not set get it from PROXMOX_PASSWORD env
|
||||||
|
if not api_password:
|
||||||
|
try:
|
||||||
|
api_password = os.environ['PROXMOX_PASSWORD']
|
||||||
|
except KeyError, e:
|
||||||
|
module.fail_json(msg='You should set api_password param or use PROXMOX_PASSWORD environment variable')
|
||||||
|
|
||||||
|
try:
|
||||||
|
proxmox = ProxmoxAPI(api_host, user=api_user, password=api_password, verify_ssl=validate_certs)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg='authorization on proxmox cluster failed with exception: %s' % e)
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
try:
|
||||||
|
content_type = module.params['content_type']
|
||||||
|
src = module.params['src']
|
||||||
|
|
||||||
|
from ansible import utils
|
||||||
|
realpath = utils.path_dwim(None, src)
|
||||||
|
template = os.path.basename(realpath)
|
||||||
|
if get_template(proxmox, node, storage, content_type, template) and not module.params['force']:
|
||||||
|
module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already exists' % (storage, content_type, template))
|
||||||
|
elif not src:
|
||||||
|
module.fail_json(msg='src param to uploading template file is mandatory')
|
||||||
|
elif not (os.path.exists(realpath) and os.path.isfile(realpath)):
|
||||||
|
module.fail_json(msg='template file on path %s not exists' % realpath)
|
||||||
|
|
||||||
|
if upload_template(module, proxmox, api_host, node, storage, content_type, realpath, timeout):
|
||||||
|
module.exit_json(changed=True, msg='template with volid=%s:%s/%s uploaded' % (storage, content_type, template))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="uploading of template %s failed with exception: %s" % ( template, e ))
|
||||||
|
|
||||||
|
elif state == 'absent':
|
||||||
|
try:
|
||||||
|
content_type = module.params['content_type']
|
||||||
|
template = module.params['template']
|
||||||
|
|
||||||
|
if not template:
|
||||||
|
module.fail_json(msg='template param is mandatory')
|
||||||
|
elif not get_template(proxmox, node, storage, content_type, template):
|
||||||
|
module.exit_json(changed=False, msg='template with volid=%s:%s/%s is already deleted' % (storage, content_type, template))
|
||||||
|
|
||||||
|
if delete_template(module, proxmox, node, storage, content_type, template, timeout):
|
||||||
|
module.exit_json(changed=True, msg='template with volid=%s:%s/%s deleted' % (storage, content_type, template))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="deleting of template %s failed with exception: %s" % ( template, e ))
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,227 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||||
|
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rax_mon_alarm
|
||||||
|
short_description: Create or delete a Rackspace Cloud Monitoring alarm.
|
||||||
|
description:
|
||||||
|
- Create or delete a Rackspace Cloud Monitoring alarm that associates an
|
||||||
|
existing rax_mon_entity, rax_mon_check, and rax_mon_notification_plan with
|
||||||
|
criteria that specify what conditions will trigger which levels of
|
||||||
|
notifications. Rackspace monitoring module flow | rax_mon_entity ->
|
||||||
|
rax_mon_check -> rax_mon_notification -> rax_mon_notification_plan ->
|
||||||
|
*rax_mon_alarm*
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Ensure that the alarm with this C(label) exists or does not exist.
|
||||||
|
choices: [ "present", "absent" ]
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
label:
|
||||||
|
description:
|
||||||
|
- Friendly name for this alarm, used to achieve idempotence. Must be a String
|
||||||
|
between 1 and 255 characters long.
|
||||||
|
required: true
|
||||||
|
entity_id:
|
||||||
|
description:
|
||||||
|
- ID of the entity this alarm is attached to. May be acquired by registering
|
||||||
|
the value of a rax_mon_entity task.
|
||||||
|
required: true
|
||||||
|
check_id:
|
||||||
|
description:
|
||||||
|
- ID of the check that should be alerted on. May be acquired by registering
|
||||||
|
the value of a rax_mon_check task.
|
||||||
|
required: true
|
||||||
|
notification_plan_id:
|
||||||
|
description:
|
||||||
|
- ID of the notification plan to trigger if this alarm fires. May be acquired
|
||||||
|
by registering the value of a rax_mon_notification_plan task.
|
||||||
|
required: true
|
||||||
|
criteria:
|
||||||
|
description:
|
||||||
|
- Alarm DSL that describes alerting conditions and their output states. Must
|
||||||
|
be between 1 and 16384 characters long. See
|
||||||
|
http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/alerts-language.html
|
||||||
|
for a reference on the alerting language.
|
||||||
|
disabled:
|
||||||
|
description:
|
||||||
|
- If yes, create this alarm, but leave it in an inactive state. Defaults to
|
||||||
|
no.
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
metadata:
|
||||||
|
description:
|
||||||
|
- Arbitrary key/value pairs to accompany the alarm. Must be a hash of String
|
||||||
|
keys and values between 1 and 255 characters long.
|
||||||
|
author: Ash Wilson
|
||||||
|
extends_documentation_fragment: rackspace.openstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Alarm example
|
||||||
|
gather_facts: False
|
||||||
|
hosts: local
|
||||||
|
connection: local
|
||||||
|
tasks:
|
||||||
|
- name: Ensure that a specific alarm exists.
|
||||||
|
rax_mon_alarm:
|
||||||
|
credentials: ~/.rax_pub
|
||||||
|
state: present
|
||||||
|
label: uhoh
|
||||||
|
entity_id: "{{ the_entity['entity']['id'] }}"
|
||||||
|
check_id: "{{ the_check['check']['id'] }}"
|
||||||
|
notification_plan_id: "{{ defcon1['notification_plan']['id'] }}"
|
||||||
|
criteria: >
|
||||||
|
if (rate(metric['average']) > 10) {
|
||||||
|
return new AlarmStatus(WARNING);
|
||||||
|
}
|
||||||
|
return new AlarmStatus(OK);
|
||||||
|
register: the_alarm
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyrax
|
||||||
|
HAS_PYRAX = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYRAX = False
|
||||||
|
|
||||||
|
def alarm(module, state, label, entity_id, check_id, notification_plan_id, criteria,
|
||||||
|
disabled, metadata):
|
||||||
|
|
||||||
|
if len(label) < 1 or len(label) > 255:
|
||||||
|
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||||
|
|
||||||
|
if criteria and len(criteria) < 1 or len(criteria) > 16384:
|
||||||
|
module.fail_json(msg='criteria must be between 1 and 16384 characters long')
|
||||||
|
|
||||||
|
# Coerce attributes.
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
alarm = None
|
||||||
|
|
||||||
|
cm = pyrax.cloud_monitoring
|
||||||
|
if not cm:
|
||||||
|
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||||
|
'indicates an invalid region or an incorrectly '
|
||||||
|
'capitalized region name.')
|
||||||
|
|
||||||
|
existing = [a for a in cm.list_alarms(entity_id) if a.label == label]
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
alarm = existing[0]
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
should_create = False
|
||||||
|
should_update = False
|
||||||
|
should_delete = False
|
||||||
|
|
||||||
|
if len(existing) > 1:
|
||||||
|
module.fail_json(msg='%s existing alarms have the label %s.' %
|
||||||
|
(len(existing), label))
|
||||||
|
|
||||||
|
if alarm:
|
||||||
|
if check_id != alarm.check_id or notification_plan_id != alarm.notification_plan_id:
|
||||||
|
should_delete = should_create = True
|
||||||
|
|
||||||
|
should_update = (disabled and disabled != alarm.disabled) or \
|
||||||
|
(metadata and metadata != alarm.metadata) or \
|
||||||
|
(criteria and criteria != alarm.criteria)
|
||||||
|
|
||||||
|
if should_update and not should_delete:
|
||||||
|
cm.update_alarm(entity=entity_id, alarm=alarm,
|
||||||
|
criteria=criteria, disabled=disabled,
|
||||||
|
label=label, metadata=metadata)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if should_delete:
|
||||||
|
alarm.delete()
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
should_create = True
|
||||||
|
|
||||||
|
if should_create:
|
||||||
|
alarm = cm.create_alarm(entity=entity_id, check=check_id,
|
||||||
|
notification_plan=notification_plan_id,
|
||||||
|
criteria=criteria, disabled=disabled, label=label,
|
||||||
|
metadata=metadata)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
for a in existing:
|
||||||
|
a.delete()
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if alarm:
|
||||||
|
alarm_dict = {
|
||||||
|
"id": alarm.id,
|
||||||
|
"label": alarm.label,
|
||||||
|
"check_id": alarm.check_id,
|
||||||
|
"notification_plan_id": alarm.notification_plan_id,
|
||||||
|
"criteria": alarm.criteria,
|
||||||
|
"disabled": alarm.disabled,
|
||||||
|
"metadata": alarm.metadata
|
||||||
|
}
|
||||||
|
module.exit_json(changed=changed, alarm=alarm_dict)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = rax_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
label=dict(required=True),
|
||||||
|
entity_id=dict(required=True),
|
||||||
|
check_id=dict(required=True),
|
||||||
|
notification_plan_id=dict(required=True),
|
||||||
|
criteria=dict(),
|
||||||
|
disabled=dict(type='bool', default=False),
|
||||||
|
metadata=dict(type='dict')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
required_together=rax_required_together()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PYRAX:
|
||||||
|
module.fail_json(msg='pyrax is required for this module')
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
label = module.params.get('label')
|
||||||
|
entity_id = module.params.get('entity_id')
|
||||||
|
check_id = module.params.get('check_id')
|
||||||
|
notification_plan_id = module.params.get('notification_plan_id')
|
||||||
|
criteria = module.params.get('criteria')
|
||||||
|
disabled = module.boolean(module.params.get('disabled'))
|
||||||
|
metadata = module.params.get('metadata')
|
||||||
|
|
||||||
|
setup_rax_module(module, pyrax)
|
||||||
|
|
||||||
|
alarm(module, state, label, entity_id, check_id, notification_plan_id,
|
||||||
|
criteria, disabled, metadata)
|
||||||
|
|
||||||
|
|
||||||
|
# Import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.rax import *
|
||||||
|
|
||||||
|
# Invoke the module.
|
||||||
|
main()
|
@ -0,0 +1,313 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||||
|
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rax_mon_check
|
||||||
|
short_description: Create or delete a Rackspace Cloud Monitoring check for an
|
||||||
|
existing entity.
|
||||||
|
description:
|
||||||
|
- Create or delete a Rackspace Cloud Monitoring check associated with an
|
||||||
|
existing rax_mon_entity. A check is a specific test or measurement that is
|
||||||
|
performed, possibly from different monitoring zones, on the systems you
|
||||||
|
monitor. Rackspace monitoring module flow | rax_mon_entity ->
|
||||||
|
*rax_mon_check* -> rax_mon_notification -> rax_mon_notification_plan ->
|
||||||
|
rax_mon_alarm
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Ensure that a check with this C(label) exists or does not exist.
|
||||||
|
choices: ["present", "absent"]
|
||||||
|
entity_id:
|
||||||
|
description:
|
||||||
|
- ID of the rax_mon_entity to target with this check.
|
||||||
|
required: true
|
||||||
|
label:
|
||||||
|
description:
|
||||||
|
- Defines a label for this check, between 1 and 64 characters long.
|
||||||
|
required: true
|
||||||
|
check_type:
|
||||||
|
description:
|
||||||
|
- The type of check to create. C(remote.) checks may be created on any
|
||||||
|
rax_mon_entity. C(agent.) checks may only be created on rax_mon_entities
|
||||||
|
that have a non-null C(agent_id).
|
||||||
|
choices:
|
||||||
|
- remote.dns
|
||||||
|
- remote.ftp-banner
|
||||||
|
- remote.http
|
||||||
|
- remote.imap-banner
|
||||||
|
- remote.mssql-banner
|
||||||
|
- remote.mysql-banner
|
||||||
|
- remote.ping
|
||||||
|
- remote.pop3-banner
|
||||||
|
- remote.postgresql-banner
|
||||||
|
- remote.smtp-banner
|
||||||
|
- remote.smtp
|
||||||
|
- remote.ssh
|
||||||
|
- remote.tcp
|
||||||
|
- remote.telnet-banner
|
||||||
|
- agent.filesystem
|
||||||
|
- agent.memory
|
||||||
|
- agent.load_average
|
||||||
|
- agent.cpu
|
||||||
|
- agent.disk
|
||||||
|
- agent.network
|
||||||
|
- agent.plugin
|
||||||
|
required: true
|
||||||
|
monitoring_zones_poll:
|
||||||
|
description:
|
||||||
|
- Comma-separated list of the names of the monitoring zones the check should
|
||||||
|
run from. Available monitoring zones include mzdfw, mzhkg, mziad, mzlon,
|
||||||
|
mzord and mzsyd. Required for remote.* checks; prohibited for agent.* checks.
|
||||||
|
target_hostname:
|
||||||
|
description:
|
||||||
|
- One of `target_hostname` and `target_alias` is required for remote.* checks,
|
||||||
|
but prohibited for agent.* checks. The hostname this check should target.
|
||||||
|
Must be a valid IPv4, IPv6, or FQDN.
|
||||||
|
target_alias:
|
||||||
|
description:
|
||||||
|
- One of `target_alias` and `target_hostname` is required for remote.* checks,
|
||||||
|
but prohibited for agent.* checks. Use the corresponding key in the entity's
|
||||||
|
`ip_addresses` hash to resolve an IP address to target.
|
||||||
|
details:
|
||||||
|
description:
|
||||||
|
- Additional details specific to the check type. Must be a hash of strings
|
||||||
|
between 1 and 255 characters long, or an array or object containing 0 to
|
||||||
|
256 items.
|
||||||
|
disabled:
|
||||||
|
description:
|
||||||
|
- If "yes", ensure the check is created, but don't actually use it yet.
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
metadata:
|
||||||
|
description:
|
||||||
|
- Hash of arbitrary key-value pairs to accompany this check if it fires.
|
||||||
|
Keys and values must be strings between 1 and 255 characters long.
|
||||||
|
period:
|
||||||
|
description:
|
||||||
|
- The number of seconds between each time the check is performed. Must be
|
||||||
|
greater than the minimum period set on your account.
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- The number of seconds this check will wait when attempting to collect
|
||||||
|
results. Must be less than the period.
|
||||||
|
author: Ash Wilson
|
||||||
|
extends_documentation_fragment: rackspace.openstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create a monitoring check
|
||||||
|
gather_facts: False
|
||||||
|
hosts: local
|
||||||
|
connection: local
|
||||||
|
tasks:
|
||||||
|
- name: Associate a check with an existing entity.
|
||||||
|
rax_mon_check:
|
||||||
|
credentials: ~/.rax_pub
|
||||||
|
state: present
|
||||||
|
entity_id: "{{ the_entity['entity']['id'] }}"
|
||||||
|
label: the_check
|
||||||
|
check_type: remote.ping
|
||||||
|
monitoring_zones_poll: mziad,mzord,mzdfw
|
||||||
|
details:
|
||||||
|
count: 10
|
||||||
|
meta:
|
||||||
|
hurf: durf
|
||||||
|
register: the_check
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyrax
|
||||||
|
HAS_PYRAX = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYRAX = False
|
||||||
|
|
||||||
|
def cloud_check(module, state, entity_id, label, check_type,
|
||||||
|
monitoring_zones_poll, target_hostname, target_alias, details,
|
||||||
|
disabled, metadata, period, timeout):
|
||||||
|
|
||||||
|
# Coerce attributes.
|
||||||
|
|
||||||
|
if monitoring_zones_poll and not isinstance(monitoring_zones_poll, list):
|
||||||
|
monitoring_zones_poll = [monitoring_zones_poll]
|
||||||
|
|
||||||
|
if period:
|
||||||
|
period = int(period)
|
||||||
|
|
||||||
|
if timeout:
|
||||||
|
timeout = int(timeout)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
check = None
|
||||||
|
|
||||||
|
cm = pyrax.cloud_monitoring
|
||||||
|
if not cm:
|
||||||
|
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||||
|
'indicates an invalid region or an incorrectly '
|
||||||
|
'capitalized region name.')
|
||||||
|
|
||||||
|
entity = cm.get_entity(entity_id)
|
||||||
|
if not entity:
|
||||||
|
module.fail_json(msg='Failed to instantiate entity. "%s" may not be'
|
||||||
|
' a valid entity id.' % entity_id)
|
||||||
|
|
||||||
|
existing = [e for e in entity.list_checks() if e.label == label]
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
check = existing[0]
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
if len(existing) > 1:
|
||||||
|
module.fail_json(msg='%s existing checks have a label of %s.' %
|
||||||
|
(len(existing), label))
|
||||||
|
|
||||||
|
should_delete = False
|
||||||
|
should_create = False
|
||||||
|
should_update = False
|
||||||
|
|
||||||
|
if check:
|
||||||
|
# Details may include keys set to default values that are not
|
||||||
|
# included in the initial creation.
|
||||||
|
#
|
||||||
|
# Only force a recreation of the check if one of the *specified*
|
||||||
|
# keys is missing or has a different value.
|
||||||
|
if details:
|
||||||
|
for (key, value) in details.iteritems():
|
||||||
|
if key not in check.details:
|
||||||
|
should_delete = should_create = True
|
||||||
|
elif value != check.details[key]:
|
||||||
|
should_delete = should_create = True
|
||||||
|
|
||||||
|
should_update = label != check.label or \
|
||||||
|
(target_hostname and target_hostname != check.target_hostname) or \
|
||||||
|
(target_alias and target_alias != check.target_alias) or \
|
||||||
|
(disabled != check.disabled) or \
|
||||||
|
(metadata and metadata != check.metadata) or \
|
||||||
|
(period and period != check.period) or \
|
||||||
|
(timeout and timeout != check.timeout) or \
|
||||||
|
(monitoring_zones_poll and monitoring_zones_poll != check.monitoring_zones_poll)
|
||||||
|
|
||||||
|
if should_update and not should_delete:
|
||||||
|
check.update(label=label,
|
||||||
|
disabled=disabled,
|
||||||
|
metadata=metadata,
|
||||||
|
monitoring_zones_poll=monitoring_zones_poll,
|
||||||
|
timeout=timeout,
|
||||||
|
period=period,
|
||||||
|
target_alias=target_alias,
|
||||||
|
target_hostname=target_hostname)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
# The check doesn't exist yet.
|
||||||
|
should_create = True
|
||||||
|
|
||||||
|
if should_delete:
|
||||||
|
check.delete()
|
||||||
|
|
||||||
|
if should_create:
|
||||||
|
check = cm.create_check(entity,
|
||||||
|
label=label,
|
||||||
|
check_type=check_type,
|
||||||
|
target_hostname=target_hostname,
|
||||||
|
target_alias=target_alias,
|
||||||
|
monitoring_zones_poll=monitoring_zones_poll,
|
||||||
|
details=details,
|
||||||
|
disabled=disabled,
|
||||||
|
metadata=metadata,
|
||||||
|
period=period,
|
||||||
|
timeout=timeout)
|
||||||
|
changed = True
|
||||||
|
elif state == 'absent':
|
||||||
|
if check:
|
||||||
|
check.delete()
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='state must be either present or absent.')
|
||||||
|
|
||||||
|
if check:
|
||||||
|
check_dict = {
|
||||||
|
"id": check.id,
|
||||||
|
"label": check.label,
|
||||||
|
"type": check.type,
|
||||||
|
"target_hostname": check.target_hostname,
|
||||||
|
"target_alias": check.target_alias,
|
||||||
|
"monitoring_zones_poll": check.monitoring_zones_poll,
|
||||||
|
"details": check.details,
|
||||||
|
"disabled": check.disabled,
|
||||||
|
"metadata": check.metadata,
|
||||||
|
"period": check.period,
|
||||||
|
"timeout": check.timeout
|
||||||
|
}
|
||||||
|
module.exit_json(changed=changed, check=check_dict)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = rax_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
entity_id=dict(required=True),
|
||||||
|
label=dict(required=True),
|
||||||
|
check_type=dict(required=True),
|
||||||
|
monitoring_zones_poll=dict(),
|
||||||
|
target_hostname=dict(),
|
||||||
|
target_alias=dict(),
|
||||||
|
details=dict(type='dict', default={}),
|
||||||
|
disabled=dict(type='bool', default=False),
|
||||||
|
metadata=dict(type='dict', default={}),
|
||||||
|
period=dict(type='int'),
|
||||||
|
timeout=dict(type='int'),
|
||||||
|
state=dict(default='present', choices=['present', 'absent'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
required_together=rax_required_together()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PYRAX:
|
||||||
|
module.fail_json(msg='pyrax is required for this module')
|
||||||
|
|
||||||
|
entity_id = module.params.get('entity_id')
|
||||||
|
label = module.params.get('label')
|
||||||
|
check_type = module.params.get('check_type')
|
||||||
|
monitoring_zones_poll = module.params.get('monitoring_zones_poll')
|
||||||
|
target_hostname = module.params.get('target_hostname')
|
||||||
|
target_alias = module.params.get('target_alias')
|
||||||
|
details = module.params.get('details')
|
||||||
|
disabled = module.boolean(module.params.get('disabled'))
|
||||||
|
metadata = module.params.get('metadata')
|
||||||
|
period = module.params.get('period')
|
||||||
|
timeout = module.params.get('timeout')
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
setup_rax_module(module, pyrax)
|
||||||
|
|
||||||
|
cloud_check(module, state, entity_id, label, check_type,
|
||||||
|
monitoring_zones_poll, target_hostname, target_alias, details,
|
||||||
|
disabled, metadata, period, timeout)
|
||||||
|
|
||||||
|
|
||||||
|
# Import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.rax import *
|
||||||
|
|
||||||
|
# Invoke the module.
|
||||||
|
main()
|
@ -0,0 +1,192 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||||
|
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rax_mon_entity
|
||||||
|
short_description: Create or delete a Rackspace Cloud Monitoring entity
|
||||||
|
description:
|
||||||
|
- Create or delete a Rackspace Cloud Monitoring entity, which represents a device
|
||||||
|
to monitor. Entities associate checks and alarms with a target system and
|
||||||
|
provide a convenient, centralized place to store IP addresses. Rackspace
|
||||||
|
monitoring module flow | *rax_mon_entity* -> rax_mon_check ->
|
||||||
|
rax_mon_notification -> rax_mon_notification_plan -> rax_mon_alarm
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
label:
|
||||||
|
description:
|
||||||
|
- Defines a name for this entity. Must be a non-empty string between 1 and
|
||||||
|
255 characters long.
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Ensure that an entity with this C(name) exists or does not exist.
|
||||||
|
choices: ["present", "absent"]
|
||||||
|
agent_id:
|
||||||
|
description:
|
||||||
|
- Rackspace monitoring agent on the target device to which this entity is
|
||||||
|
bound. Necessary to collect C(agent.) rax_mon_checks against this entity.
|
||||||
|
named_ip_addresses:
|
||||||
|
description:
|
||||||
|
- Hash of IP addresses that may be referenced by name by rax_mon_checks
|
||||||
|
added to this entity. Must be a dictionary of with keys that are names
|
||||||
|
between 1 and 64 characters long, and values that are valid IPv4 or IPv6
|
||||||
|
addresses.
|
||||||
|
metadata:
|
||||||
|
description:
|
||||||
|
- Hash of arbitrary C(name), C(value) pairs that are passed to associated
|
||||||
|
rax_mon_alarms. Names and values must all be between 1 and 255 characters
|
||||||
|
long.
|
||||||
|
author: Ash Wilson
|
||||||
|
extends_documentation_fragment: rackspace.openstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Entity example
|
||||||
|
gather_facts: False
|
||||||
|
hosts: local
|
||||||
|
connection: local
|
||||||
|
tasks:
|
||||||
|
- name: Ensure an entity exists
|
||||||
|
rax_mon_entity:
|
||||||
|
credentials: ~/.rax_pub
|
||||||
|
state: present
|
||||||
|
label: my_entity
|
||||||
|
named_ip_addresses:
|
||||||
|
web_box: 192.168.0.10
|
||||||
|
db_box: 192.168.0.11
|
||||||
|
meta:
|
||||||
|
hurf: durf
|
||||||
|
register: the_entity
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyrax
|
||||||
|
HAS_PYRAX = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYRAX = False
|
||||||
|
|
||||||
|
def cloud_monitoring(module, state, label, agent_id, named_ip_addresses,
|
||||||
|
metadata):
|
||||||
|
|
||||||
|
if len(label) < 1 or len(label) > 255:
|
||||||
|
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
cm = pyrax.cloud_monitoring
|
||||||
|
if not cm:
|
||||||
|
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||||
|
'indicates an invalid region or an incorrectly '
|
||||||
|
'capitalized region name.')
|
||||||
|
|
||||||
|
existing = []
|
||||||
|
for entity in cm.list_entities():
|
||||||
|
if label == entity.label:
|
||||||
|
existing.append(entity)
|
||||||
|
|
||||||
|
entity = None
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
entity = existing[0]
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
should_update = False
|
||||||
|
should_delete = False
|
||||||
|
should_create = False
|
||||||
|
|
||||||
|
if len(existing) > 1:
|
||||||
|
module.fail_json(msg='%s existing entities have the label %s.' %
|
||||||
|
(len(existing), label))
|
||||||
|
|
||||||
|
if entity:
|
||||||
|
if named_ip_addresses and named_ip_addresses != entity.ip_addresses:
|
||||||
|
should_delete = should_create = True
|
||||||
|
|
||||||
|
# Change an existing Entity, unless there's nothing to do.
|
||||||
|
should_update = agent_id and agent_id != entity.agent_id or \
|
||||||
|
(metadata and metadata != entity.metadata)
|
||||||
|
|
||||||
|
if should_update and not should_delete:
|
||||||
|
entity.update(agent_id, metadata)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if should_delete:
|
||||||
|
entity.delete()
|
||||||
|
else:
|
||||||
|
should_create = True
|
||||||
|
|
||||||
|
if should_create:
|
||||||
|
# Create a new Entity.
|
||||||
|
entity = cm.create_entity(label=label, agent=agent_id,
|
||||||
|
ip_addresses=named_ip_addresses,
|
||||||
|
metadata=metadata)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
# Delete the existing Entities.
|
||||||
|
for e in existing:
|
||||||
|
e.delete()
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if entity:
|
||||||
|
entity_dict = {
|
||||||
|
"id": entity.id,
|
||||||
|
"name": entity.name,
|
||||||
|
"agent_id": entity.agent_id,
|
||||||
|
}
|
||||||
|
module.exit_json(changed=changed, entity=entity_dict)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = rax_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
label=dict(required=True),
|
||||||
|
agent_id=dict(),
|
||||||
|
named_ip_addresses=dict(type='dict', default={}),
|
||||||
|
metadata=dict(type='dict', default={})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
required_together=rax_required_together()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PYRAX:
|
||||||
|
module.fail_json(msg='pyrax is required for this module')
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
label = module.params.get('label')
|
||||||
|
agent_id = module.params.get('agent_id')
|
||||||
|
named_ip_addresses = module.params.get('named_ip_addresses')
|
||||||
|
metadata = module.params.get('metadata')
|
||||||
|
|
||||||
|
setup_rax_module(module, pyrax)
|
||||||
|
|
||||||
|
cloud_monitoring(module, state, label, agent_id, named_ip_addresses, metadata)
|
||||||
|
|
||||||
|
# Import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.rax import *
|
||||||
|
|
||||||
|
# Invoke the module.
|
||||||
|
main()
|
@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||||
|
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rax_mon_notification
|
||||||
|
short_description: Create or delete a Rackspace Cloud Monitoring notification.
|
||||||
|
description:
|
||||||
|
- Create or delete a Rackspace Cloud Monitoring notification that specifies a
|
||||||
|
channel that can be used to communicate alarms, such as email, webhooks, or
|
||||||
|
PagerDuty. Rackspace monitoring module flow | rax_mon_entity -> rax_mon_check ->
|
||||||
|
*rax_mon_notification* -> rax_mon_notification_plan -> rax_mon_alarm
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Ensure that the notification with this C(label) exists or does not exist.
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
label:
|
||||||
|
description:
|
||||||
|
- Defines a friendly name for this notification. String between 1 and 255
|
||||||
|
characters long.
|
||||||
|
required: true
|
||||||
|
notification_type:
|
||||||
|
description:
|
||||||
|
- A supported notification type.
|
||||||
|
choices: ["webhook", "email", "pagerduty"]
|
||||||
|
required: true
|
||||||
|
details:
|
||||||
|
description:
|
||||||
|
- Dictionary of key-value pairs used to initialize the notification.
|
||||||
|
Required keys and meanings vary with notification type. See
|
||||||
|
http://docs.rackspace.com/cm/api/v1.0/cm-devguide/content/
|
||||||
|
service-notification-types-crud.html for details.
|
||||||
|
required: true
|
||||||
|
author: Ash Wilson
|
||||||
|
extends_documentation_fragment: rackspace.openstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Monitoring notification example
|
||||||
|
gather_facts: False
|
||||||
|
hosts: local
|
||||||
|
connection: local
|
||||||
|
tasks:
|
||||||
|
- name: Email me when something goes wrong.
|
||||||
|
rax_mon_entity:
|
||||||
|
credentials: ~/.rax_pub
|
||||||
|
label: omg
|
||||||
|
type: email
|
||||||
|
details:
|
||||||
|
address: me@mailhost.com
|
||||||
|
register: the_notification
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyrax
|
||||||
|
HAS_PYRAX = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYRAX = False
|
||||||
|
|
||||||
|
def notification(module, state, label, notification_type, details):
|
||||||
|
|
||||||
|
if len(label) < 1 or len(label) > 255:
|
||||||
|
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
notification = None
|
||||||
|
|
||||||
|
cm = pyrax.cloud_monitoring
|
||||||
|
if not cm:
|
||||||
|
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||||
|
'indicates an invalid region or an incorrectly '
|
||||||
|
'capitalized region name.')
|
||||||
|
|
||||||
|
existing = []
|
||||||
|
for n in cm.list_notifications():
|
||||||
|
if n.label == label:
|
||||||
|
existing.append(n)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
notification = existing[0]
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
should_update = False
|
||||||
|
should_delete = False
|
||||||
|
should_create = False
|
||||||
|
|
||||||
|
if len(existing) > 1:
|
||||||
|
module.fail_json(msg='%s existing notifications are labelled %s.' %
|
||||||
|
(len(existing), label))
|
||||||
|
|
||||||
|
if notification:
|
||||||
|
should_delete = (notification_type != notification.type)
|
||||||
|
|
||||||
|
should_update = (details != notification.details)
|
||||||
|
|
||||||
|
if should_update and not should_delete:
|
||||||
|
notification.update(details=notification.details)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if should_delete:
|
||||||
|
notification.delete()
|
||||||
|
else:
|
||||||
|
should_create = True
|
||||||
|
|
||||||
|
if should_create:
|
||||||
|
notification = cm.create_notification(notification_type,
|
||||||
|
label=label, details=details)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
for n in existing:
|
||||||
|
n.delete()
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if notification:
|
||||||
|
notification_dict = {
|
||||||
|
"id": notification.id,
|
||||||
|
"type": notification.type,
|
||||||
|
"label": notification.label,
|
||||||
|
"details": notification.details
|
||||||
|
}
|
||||||
|
module.exit_json(changed=changed, notification=notification_dict)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = rax_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
label=dict(required=True),
|
||||||
|
notification_type=dict(required=True, choices=['webhook', 'email', 'pagerduty']),
|
||||||
|
details=dict(required=True, type='dict')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
required_together=rax_required_together()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PYRAX:
|
||||||
|
module.fail_json(msg='pyrax is required for this module')
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
label = module.params.get('label')
|
||||||
|
notification_type = module.params.get('notification_type')
|
||||||
|
details = module.params.get('details')
|
||||||
|
|
||||||
|
setup_rax_module(module, pyrax)
|
||||||
|
|
||||||
|
notification(module, state, label, notification_type, details)
|
||||||
|
|
||||||
|
# Import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.rax import *
|
||||||
|
|
||||||
|
# Invoke the module.
|
||||||
|
main()
|
@ -0,0 +1,181 @@
|
|||||||
|
#!/usr/bin/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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# This is a DOCUMENTATION stub specific to this module, it extends
|
||||||
|
# a documentation fragment located in ansible.utils.module_docs_fragments
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rax_mon_notification_plan
|
||||||
|
short_description: Create or delete a Rackspace Cloud Monitoring notification
|
||||||
|
plan.
|
||||||
|
description:
|
||||||
|
- Create or delete a Rackspace Cloud Monitoring notification plan by
|
||||||
|
associating existing rax_mon_notifications with severity levels. Rackspace
|
||||||
|
monitoring module flow | rax_mon_entity -> rax_mon_check ->
|
||||||
|
rax_mon_notification -> *rax_mon_notification_plan* -> rax_mon_alarm
|
||||||
|
version_added: "2.0"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Ensure that the notification plan with this C(label) exists or does not
|
||||||
|
exist.
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
label:
|
||||||
|
description:
|
||||||
|
- Defines a friendly name for this notification plan. String between 1 and
|
||||||
|
255 characters long.
|
||||||
|
required: true
|
||||||
|
critical_state:
|
||||||
|
description:
|
||||||
|
- Notification list to use when the alarm state is CRITICAL. Must be an
|
||||||
|
array of valid rax_mon_notification ids.
|
||||||
|
warning_state:
|
||||||
|
description:
|
||||||
|
- Notification list to use when the alarm state is WARNING. Must be an array
|
||||||
|
of valid rax_mon_notification ids.
|
||||||
|
ok_state:
|
||||||
|
description:
|
||||||
|
- Notification list to use when the alarm state is OK. Must be an array of
|
||||||
|
valid rax_mon_notification ids.
|
||||||
|
author: Ash Wilson
|
||||||
|
extends_documentation_fragment: rackspace.openstack
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Example notification plan
|
||||||
|
gather_facts: False
|
||||||
|
hosts: local
|
||||||
|
connection: local
|
||||||
|
tasks:
|
||||||
|
- name: Establish who gets called when.
|
||||||
|
rax_mon_notification_plan:
|
||||||
|
credentials: ~/.rax_pub
|
||||||
|
state: present
|
||||||
|
label: defcon1
|
||||||
|
critical_state:
|
||||||
|
- "{{ everyone['notification']['id'] }}"
|
||||||
|
warning_state:
|
||||||
|
- "{{ opsfloor['notification']['id'] }}"
|
||||||
|
register: defcon1
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyrax
|
||||||
|
HAS_PYRAX = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYRAX = False
|
||||||
|
|
||||||
|
def notification_plan(module, state, label, critical_state, warning_state, ok_state):
|
||||||
|
|
||||||
|
if len(label) < 1 or len(label) > 255:
|
||||||
|
module.fail_json(msg='label must be between 1 and 255 characters long')
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
notification_plan = None
|
||||||
|
|
||||||
|
cm = pyrax.cloud_monitoring
|
||||||
|
if not cm:
|
||||||
|
module.fail_json(msg='Failed to instantiate client. This typically '
|
||||||
|
'indicates an invalid region or an incorrectly '
|
||||||
|
'capitalized region name.')
|
||||||
|
|
||||||
|
existing = []
|
||||||
|
for n in cm.list_notification_plans():
|
||||||
|
if n.label == label:
|
||||||
|
existing.append(n)
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
notification_plan = existing[0]
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
should_create = False
|
||||||
|
should_delete = False
|
||||||
|
|
||||||
|
if len(existing) > 1:
|
||||||
|
module.fail_json(msg='%s notification plans are labelled %s.' %
|
||||||
|
(len(existing), label))
|
||||||
|
|
||||||
|
if notification_plan:
|
||||||
|
should_delete = (critical_state and critical_state != notification_plan.critical_state) or \
|
||||||
|
(warning_state and warning_state != notification_plan.warning_state) or \
|
||||||
|
(ok_state and ok_state != notification_plan.ok_state)
|
||||||
|
|
||||||
|
if should_delete:
|
||||||
|
notification_plan.delete()
|
||||||
|
should_create = True
|
||||||
|
else:
|
||||||
|
should_create = True
|
||||||
|
|
||||||
|
if should_create:
|
||||||
|
notification_plan = cm.create_notification_plan(label=label,
|
||||||
|
critical_state=critical_state,
|
||||||
|
warning_state=warning_state,
|
||||||
|
ok_state=ok_state)
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
for np in existing:
|
||||||
|
np.delete()
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if notification_plan:
|
||||||
|
notification_plan_dict = {
|
||||||
|
"id": notification_plan.id,
|
||||||
|
"critical_state": notification_plan.critical_state,
|
||||||
|
"warning_state": notification_plan.warning_state,
|
||||||
|
"ok_state": notification_plan.ok_state,
|
||||||
|
"metadata": notification_plan.metadata
|
||||||
|
}
|
||||||
|
module.exit_json(changed=changed, notification_plan=notification_plan_dict)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=changed)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = rax_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
label=dict(required=True),
|
||||||
|
critical_state=dict(type='list'),
|
||||||
|
warning_state=dict(type='list'),
|
||||||
|
ok_state=dict(type='list')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=argument_spec,
|
||||||
|
required_together=rax_required_together()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PYRAX:
|
||||||
|
module.fail_json(msg='pyrax is required for this module')
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
label = module.params.get('label')
|
||||||
|
critical_state = module.params.get('critical_state')
|
||||||
|
warning_state = module.params.get('warning_state')
|
||||||
|
ok_state = module.params.get('ok_state')
|
||||||
|
|
||||||
|
setup_rax_module(module, pyrax)
|
||||||
|
|
||||||
|
notification_plan(module, state, label, critical_state, warning_state, ok_state)
|
||||||
|
|
||||||
|
# Import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.rax import *
|
||||||
|
|
||||||
|
# Invoke the module.
|
||||||
|
main()
|
@ -0,0 +1,176 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2015, Joseph Callen <jcallen () csc.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: vmware_datacenter
|
||||||
|
short_description: Manage VMware vSphere Datacenters
|
||||||
|
description:
|
||||||
|
- Manage VMware vSphere Datacenters
|
||||||
|
version_added: 2.0
|
||||||
|
author: "Joseph Callen (@jcpowermac)"
|
||||||
|
notes:
|
||||||
|
- Tested on vSphere 5.5
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- PyVmomi
|
||||||
|
options:
|
||||||
|
hostname:
|
||||||
|
description:
|
||||||
|
- The hostname or IP address of the vSphere vCenter API server
|
||||||
|
required: True
|
||||||
|
username:
|
||||||
|
description:
|
||||||
|
- The username of the vSphere vCenter
|
||||||
|
required: True
|
||||||
|
aliases: ['user', 'admin']
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- The password of the vSphere vCenter
|
||||||
|
required: True
|
||||||
|
aliases: ['pass', 'pwd']
|
||||||
|
datacenter_name:
|
||||||
|
description:
|
||||||
|
- The name of the datacenter the cluster will be created in.
|
||||||
|
required: True
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- If the datacenter should be present or absent
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
required: True
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Example vmware_datacenter command from Ansible Playbooks
|
||||||
|
- name: Create Datacenter
|
||||||
|
local_action: >
|
||||||
|
vmware_datacenter
|
||||||
|
hostname="{{ ansible_ssh_host }}" username=root password=vmware
|
||||||
|
datacenter_name="datacenter"
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from pyVmomi import vim, vmodl
|
||||||
|
HAS_PYVMOMI = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PYVMOMI = False
|
||||||
|
|
||||||
|
|
||||||
|
def state_create_datacenter(module):
|
||||||
|
datacenter_name = module.params['datacenter_name']
|
||||||
|
content = module.params['content']
|
||||||
|
changed = True
|
||||||
|
datacenter = None
|
||||||
|
|
||||||
|
folder = content.rootFolder
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not module.check_mode:
|
||||||
|
datacenter = folder.CreateDatacenter(name=datacenter_name)
|
||||||
|
module.exit_json(changed=changed, result=str(datacenter))
|
||||||
|
except vim.fault.DuplicateName:
|
||||||
|
module.fail_json(msg="A datacenter with the name %s already exists" % datacenter_name)
|
||||||
|
except vim.fault.InvalidName:
|
||||||
|
module.fail_json(msg="%s is an invalid name for a cluster" % datacenter_name)
|
||||||
|
except vmodl.fault.NotSupported:
|
||||||
|
# This should never happen
|
||||||
|
module.fail_json(msg="Trying to create a datacenter on an incorrect folder object")
|
||||||
|
except vmodl.RuntimeFault as runtime_fault:
|
||||||
|
module.fail_json(msg=runtime_fault.msg)
|
||||||
|
except vmodl.MethodFault as method_fault:
|
||||||
|
module.fail_json(msg=method_fault.msg)
|
||||||
|
|
||||||
|
|
||||||
|
def check_datacenter_state(module):
|
||||||
|
datacenter_name = module.params['datacenter_name']
|
||||||
|
|
||||||
|
try:
|
||||||
|
content = connect_to_api(module)
|
||||||
|
datacenter = find_datacenter_by_name(content, datacenter_name)
|
||||||
|
module.params['content'] = content
|
||||||
|
|
||||||
|
if datacenter is None:
|
||||||
|
return 'absent'
|
||||||
|
else:
|
||||||
|
module.params['datacenter'] = datacenter
|
||||||
|
return 'present'
|
||||||
|
except vmodl.RuntimeFault as runtime_fault:
|
||||||
|
module.fail_json(msg=runtime_fault.msg)
|
||||||
|
except vmodl.MethodFault as method_fault:
|
||||||
|
module.fail_json(msg=method_fault.msg)
|
||||||
|
|
||||||
|
|
||||||
|
def state_destroy_datacenter(module):
|
||||||
|
datacenter = module.params['datacenter']
|
||||||
|
changed = True
|
||||||
|
result = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not module.check_mode:
|
||||||
|
task = datacenter.Destroy_Task()
|
||||||
|
changed, result = wait_for_task(task)
|
||||||
|
module.exit_json(changed=changed, result=result)
|
||||||
|
except vim.fault.VimFault as vim_fault:
|
||||||
|
module.fail_json(msg=vim_fault.msg)
|
||||||
|
except vmodl.RuntimeFault as runtime_fault:
|
||||||
|
module.fail_json(msg=runtime_fault.msg)
|
||||||
|
except vmodl.MethodFault as method_fault:
|
||||||
|
module.fail_json(msg=method_fault.msg)
|
||||||
|
|
||||||
|
|
||||||
|
def state_exit_unchanged(module):
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
argument_spec = vmware_argument_spec()
|
||||||
|
argument_spec.update(
|
||||||
|
dict(
|
||||||
|
datacenter_name=dict(required=True, type='str'),
|
||||||
|
state=dict(required=True, choices=['present', 'absent'], type='str'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
|
||||||
|
|
||||||
|
if not HAS_PYVMOMI:
|
||||||
|
module.fail_json(msg='pyvmomi is required for this module')
|
||||||
|
|
||||||
|
datacenter_states = {
|
||||||
|
'absent': {
|
||||||
|
'present': state_destroy_datacenter,
|
||||||
|
'absent': state_exit_unchanged,
|
||||||
|
},
|
||||||
|
'present': {
|
||||||
|
'present': state_exit_unchanged,
|
||||||
|
'absent': state_create_datacenter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
desired_state = module.params['state']
|
||||||
|
current_state = check_datacenter_state(module)
|
||||||
|
|
||||||
|
datacenter_states[desired_state][current_state](module)
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.vmware import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2015 Dag Wieers <dag@wieers.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: vsphere_copy
|
||||||
|
short_description: Copy a file to a vCenter datastore
|
||||||
|
description: Upload files to a vCenter datastore
|
||||||
|
version_added: 2.0
|
||||||
|
author: Dag Wieers (@dagwieers) <dag@wieers.com>
|
||||||
|
options:
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- The vCenter server on which the datastore is available.
|
||||||
|
required: true
|
||||||
|
login:
|
||||||
|
description:
|
||||||
|
- The login name to authenticate on the vCenter server.
|
||||||
|
required: true
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- The password to authenticate on the vCenter server.
|
||||||
|
required: true
|
||||||
|
src:
|
||||||
|
description:
|
||||||
|
- The file to push to vCenter
|
||||||
|
required: true
|
||||||
|
datacenter:
|
||||||
|
description:
|
||||||
|
- The datacenter on the vCenter server that holds the datastore.
|
||||||
|
required: true
|
||||||
|
datastore:
|
||||||
|
description:
|
||||||
|
- The datastore on the vCenter server to push files to.
|
||||||
|
required: true
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- The file to push to the datastore on the vCenter server.
|
||||||
|
required: true
|
||||||
|
notes:
|
||||||
|
- "This module ought to be run from a system that can access vCenter directly and has the file to transfer.
|
||||||
|
It can be the normal remote target or you can change it either by using C(transport: local) or using C(delegate_to)."
|
||||||
|
- Tested on vSphere 5.5
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- vsphere_copy: host=vhost login=vuser password=vpass src=/some/local/file datacenter='DC1 Someplace' datastore=datastore1 path=some/remote/file
|
||||||
|
transport: local
|
||||||
|
- vsphere_copy: host=vhost login=vuser password=vpass src=/other/local/file datacenter='DC2 Someplace' datastore=datastore2 path=other/remote/file
|
||||||
|
delegate_to: other_system
|
||||||
|
'''
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import base64
|
||||||
|
import httplib
|
||||||
|
import urllib
|
||||||
|
import mmap
|
||||||
|
import errno
|
||||||
|
import socket
|
||||||
|
|
||||||
|
def vmware_path(datastore, datacenter, path):
|
||||||
|
''' Constructs a URL path that VSphere accepts reliably '''
|
||||||
|
path = "/folder/%s" % path.lstrip("/")
|
||||||
|
if not path.startswith("/"):
|
||||||
|
path = "/" + path
|
||||||
|
params = dict( dsName = datastore )
|
||||||
|
if datacenter:
|
||||||
|
params["dcPath"] = datacenter
|
||||||
|
params = urllib.urlencode(params)
|
||||||
|
return "%s?%s" % (path, params)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
host = dict(required=True, aliases=[ 'hostname' ]),
|
||||||
|
login = dict(required=True, aliases=[ 'username' ]),
|
||||||
|
password = dict(required=True),
|
||||||
|
src = dict(required=True, aliases=[ 'name' ]),
|
||||||
|
datacenter = dict(required=True),
|
||||||
|
datastore = dict(required=True),
|
||||||
|
dest = dict(required=True, aliases=[ 'path' ]),
|
||||||
|
),
|
||||||
|
# Implementing check-mode using HEAD is impossible, since size/date is not 100% reliable
|
||||||
|
supports_check_mode = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
host = module.params.get('host')
|
||||||
|
login = module.params.get('login')
|
||||||
|
password = module.params.get('password')
|
||||||
|
src = module.params.get('src')
|
||||||
|
datacenter = module.params.get('datacenter')
|
||||||
|
datastore = module.params.get('datastore')
|
||||||
|
dest = module.params.get('dest')
|
||||||
|
|
||||||
|
fd = open(src, "rb")
|
||||||
|
atexit.register(fd.close)
|
||||||
|
|
||||||
|
data = mmap.mmap(fd.fileno(), 0, access=mmap.ACCESS_READ)
|
||||||
|
atexit.register(data.close)
|
||||||
|
|
||||||
|
conn = httplib.HTTPSConnection(host)
|
||||||
|
atexit.register(conn.close)
|
||||||
|
|
||||||
|
remote_path = vmware_path(datastore, datacenter, dest)
|
||||||
|
auth = base64.encodestring('%s:%s' % (login, password)).rstrip()
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/octet-stream",
|
||||||
|
"Content-Length": str(len(data)),
|
||||||
|
"Authorization": "Basic %s" % auth,
|
||||||
|
}
|
||||||
|
|
||||||
|
# URL is only used in JSON output (helps troubleshooting)
|
||||||
|
url = 'https://%s%s' % (host, remote_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.request("PUT", remote_path, body=data, headers=headers)
|
||||||
|
except socket.error, e:
|
||||||
|
if isinstance(e.args, tuple) and e[0] == errno.ECONNRESET:
|
||||||
|
# VSphere resets connection if the file is in use and cannot be replaced
|
||||||
|
module.fail_json(msg='Failed to upload, image probably in use', status=e[0], reason=str(e), url=url)
|
||||||
|
else:
|
||||||
|
module.fail_json(msg=str(e), status=e[0], reason=str(e), url=url)
|
||||||
|
|
||||||
|
resp = conn.getresponse()
|
||||||
|
|
||||||
|
if resp.status in range(200, 300):
|
||||||
|
module.exit_json(changed=True, status=resp.status, reason=resp.reason, url=url)
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='Failed to upload', status=resp.status, reason=resp.reason, length=resp.length, version=resp.version, headers=resp.getheaders(), chunked=resp.chunked, url=url)
|
||||||
|
|
||||||
|
# this is magic, see lib/ansible/module_common.py
|
||||||
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||||||
|
main()
|
@ -0,0 +1,180 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
#
|
||||||
|
# Create a Webfaction application using Ansible and the Webfaction API
|
||||||
|
#
|
||||||
|
# Valid application types can be found by looking here:
|
||||||
|
# http://docs.webfaction.com/xmlrpc-api/apps.html#application-types
|
||||||
|
#
|
||||||
|
# ------------------------------------------
|
||||||
|
#
|
||||||
|
# (c) Quentin Stafford-Fraser 2015
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: webfaction_app
|
||||||
|
short_description: Add or remove applications on a Webfaction host
|
||||||
|
description:
|
||||||
|
- Add or remove applications on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction.
|
||||||
|
author: Quentin Stafford-Fraser (@quentinsf)
|
||||||
|
version_added: "2.0"
|
||||||
|
notes:
|
||||||
|
- "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays."
|
||||||
|
- See `the webfaction API <http://docs.webfaction.com/xmlrpc-api/>`_ for more info.
|
||||||
|
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of the application
|
||||||
|
required: true
|
||||||
|
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the application should exist
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- The type of application to create. See the Webfaction docs at http://docs.webfaction.com/xmlrpc-api/apps.html for a list.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
autostart:
|
||||||
|
description:
|
||||||
|
- Whether the app should restart with an autostart.cgi script
|
||||||
|
required: false
|
||||||
|
default: "no"
|
||||||
|
|
||||||
|
extra_info:
|
||||||
|
description:
|
||||||
|
- Any extra parameters required by the app
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
|
||||||
|
open_port:
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
|
||||||
|
login_name:
|
||||||
|
description:
|
||||||
|
- The webfaction account to use
|
||||||
|
required: true
|
||||||
|
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The webfaction password to use
|
||||||
|
required: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create a test app
|
||||||
|
webfaction_app:
|
||||||
|
name="my_wsgi_app1"
|
||||||
|
state=present
|
||||||
|
type=mod_wsgi35-python27
|
||||||
|
login_name={{webfaction_user}}
|
||||||
|
login_password={{webfaction_passwd}}
|
||||||
|
'''
|
||||||
|
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
state = dict(required=False, choices=['present', 'absent'], default='present'),
|
||||||
|
type = dict(required=True),
|
||||||
|
autostart = dict(required=False, choices=BOOLEANS, default=False),
|
||||||
|
extra_info = dict(required=False, default=""),
|
||||||
|
port_open = dict(required=False, choices=BOOLEANS, default=False),
|
||||||
|
login_name = dict(required=True),
|
||||||
|
login_password = dict(required=True),
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
app_name = module.params['name']
|
||||||
|
app_type = module.params['type']
|
||||||
|
app_state = module.params['state']
|
||||||
|
|
||||||
|
session_id, account = webfaction.login(
|
||||||
|
module.params['login_name'],
|
||||||
|
module.params['login_password']
|
||||||
|
)
|
||||||
|
|
||||||
|
app_list = webfaction.list_apps(session_id)
|
||||||
|
app_map = dict([(i['name'], i) for i in app_list])
|
||||||
|
existing_app = app_map.get(app_name)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Here's where the real stuff happens
|
||||||
|
|
||||||
|
if app_state == 'present':
|
||||||
|
|
||||||
|
# Does an app with this name already exist?
|
||||||
|
if existing_app:
|
||||||
|
if existing_app['type'] != app_type:
|
||||||
|
module.fail_json(msg="App already exists with different type. Please fix by hand.")
|
||||||
|
|
||||||
|
# If it exists with the right type, we don't change it
|
||||||
|
# Should check other parameters.
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, create the app
|
||||||
|
result.update(
|
||||||
|
webfaction.create_app(
|
||||||
|
session_id, app_name, app_type,
|
||||||
|
module.boolean(module.params['autostart']),
|
||||||
|
module.params['extra_info'],
|
||||||
|
module.boolean(module.params['port_open'])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif app_state == 'absent':
|
||||||
|
|
||||||
|
# If the app's already not there, nothing changed.
|
||||||
|
if not existing_app:
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, delete the app
|
||||||
|
result.update(
|
||||||
|
webfaction.delete_app(session_id, app_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Unknown state specified: {}".format(app_state))
|
||||||
|
|
||||||
|
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
result = result
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,184 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
#
|
||||||
|
# Create a webfaction database using Ansible and the Webfaction API
|
||||||
|
#
|
||||||
|
# ------------------------------------------
|
||||||
|
#
|
||||||
|
# (c) Quentin Stafford-Fraser and Andy Baker 2015
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: webfaction_db
|
||||||
|
short_description: Add or remove a database on Webfaction
|
||||||
|
description:
|
||||||
|
- Add or remove a database on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction.
|
||||||
|
author: Quentin Stafford-Fraser (@quentinsf)
|
||||||
|
version_added: "2.0"
|
||||||
|
notes:
|
||||||
|
- "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays."
|
||||||
|
- See `the webfaction API <http://docs.webfaction.com/xmlrpc-api/>`_ for more info.
|
||||||
|
options:
|
||||||
|
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of the database
|
||||||
|
required: true
|
||||||
|
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the database should exist
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- The type of database to create.
|
||||||
|
required: true
|
||||||
|
choices: ['mysql', 'postgresql']
|
||||||
|
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- The password for the new database user.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
|
||||||
|
login_name:
|
||||||
|
description:
|
||||||
|
- The webfaction account to use
|
||||||
|
required: true
|
||||||
|
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The webfaction password to use
|
||||||
|
required: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# This will also create a default DB user with the same
|
||||||
|
# name as the database, and the specified password.
|
||||||
|
|
||||||
|
- name: Create a database
|
||||||
|
webfaction_db:
|
||||||
|
name: "{{webfaction_user}}_db1"
|
||||||
|
password: mytestsql
|
||||||
|
type: mysql
|
||||||
|
login_name: "{{webfaction_user}}"
|
||||||
|
login_password: "{{webfaction_passwd}}"
|
||||||
|
|
||||||
|
# Note that, for symmetry's sake, deleting a database using
|
||||||
|
# 'state: absent' will also delete the matching user.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
state = dict(required=False, choices=['present', 'absent'], default='present'),
|
||||||
|
# You can specify an IP address or hostname.
|
||||||
|
type = dict(required=True),
|
||||||
|
password = dict(required=False, default=None),
|
||||||
|
login_name = dict(required=True),
|
||||||
|
login_password = dict(required=True),
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
db_name = module.params['name']
|
||||||
|
db_state = module.params['state']
|
||||||
|
db_type = module.params['type']
|
||||||
|
db_passwd = module.params['password']
|
||||||
|
|
||||||
|
session_id, account = webfaction.login(
|
||||||
|
module.params['login_name'],
|
||||||
|
module.params['login_password']
|
||||||
|
)
|
||||||
|
|
||||||
|
db_list = webfaction.list_dbs(session_id)
|
||||||
|
db_map = dict([(i['name'], i) for i in db_list])
|
||||||
|
existing_db = db_map.get(db_name)
|
||||||
|
|
||||||
|
user_list = webfaction.list_db_users(session_id)
|
||||||
|
user_map = dict([(i['username'], i) for i in user_list])
|
||||||
|
existing_user = user_map.get(db_name)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Here's where the real stuff happens
|
||||||
|
|
||||||
|
if db_state == 'present':
|
||||||
|
|
||||||
|
# Does an database with this name already exist?
|
||||||
|
if existing_db:
|
||||||
|
# Yes, but of a different type - fail
|
||||||
|
if existing_db['db_type'] != db_type:
|
||||||
|
module.fail_json(msg="Database already exists but is a different type. Please fix by hand.")
|
||||||
|
|
||||||
|
# If it exists with the right type, we don't change anything.
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, create the db
|
||||||
|
# and default user.
|
||||||
|
result.update(
|
||||||
|
webfaction.create_db(
|
||||||
|
session_id, db_name, db_type, db_passwd
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif db_state == 'absent':
|
||||||
|
|
||||||
|
# If this isn't a dry run...
|
||||||
|
if not module.check_mode:
|
||||||
|
|
||||||
|
if not (existing_db or existing_user):
|
||||||
|
module.exit_json(changed = False,)
|
||||||
|
|
||||||
|
if existing_db:
|
||||||
|
# Delete the db if it exists
|
||||||
|
result.update(
|
||||||
|
webfaction.delete_db(session_id, db_name, db_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
# Delete the default db user if it exists
|
||||||
|
result.update(
|
||||||
|
webfaction.delete_db_user(session_id, db_name, db_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Unknown state specified: {}".format(db_state))
|
||||||
|
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
result = result
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,171 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
#
|
||||||
|
# Create Webfaction domains and subdomains using Ansible and the Webfaction API
|
||||||
|
#
|
||||||
|
# ------------------------------------------
|
||||||
|
#
|
||||||
|
# (c) Quentin Stafford-Fraser 2015
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: webfaction_domain
|
||||||
|
short_description: Add or remove domains and subdomains on Webfaction
|
||||||
|
description:
|
||||||
|
- Add or remove domains or subdomains on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction.
|
||||||
|
author: Quentin Stafford-Fraser (@quentinsf)
|
||||||
|
version_added: "2.0"
|
||||||
|
notes:
|
||||||
|
- If you are I(deleting) domains by using C(state=absent), then note that if you specify subdomains, just those particular subdomains will be deleted. If you don't specify subdomains, the domain will be deleted.
|
||||||
|
- "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays."
|
||||||
|
- See `the webfaction API <http://docs.webfaction.com/xmlrpc-api/>`_ for more info.
|
||||||
|
|
||||||
|
options:
|
||||||
|
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of the domain
|
||||||
|
required: true
|
||||||
|
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the domain should exist
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
|
||||||
|
subdomains:
|
||||||
|
description:
|
||||||
|
- Any subdomains to create.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
|
||||||
|
login_name:
|
||||||
|
description:
|
||||||
|
- The webfaction account to use
|
||||||
|
required: true
|
||||||
|
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The webfaction password to use
|
||||||
|
required: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create a test domain
|
||||||
|
webfaction_domain:
|
||||||
|
name: mydomain.com
|
||||||
|
state: present
|
||||||
|
subdomains:
|
||||||
|
- www
|
||||||
|
- blog
|
||||||
|
login_name: "{{webfaction_user}}"
|
||||||
|
login_password: "{{webfaction_passwd}}"
|
||||||
|
|
||||||
|
- name: Delete test domain and any subdomains
|
||||||
|
webfaction_domain:
|
||||||
|
name: mydomain.com
|
||||||
|
state: absent
|
||||||
|
login_name: "{{webfaction_user}}"
|
||||||
|
login_password: "{{webfaction_passwd}}"
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
state = dict(required=False, choices=['present', 'absent'], default='present'),
|
||||||
|
subdomains = dict(required=False, default=[]),
|
||||||
|
login_name = dict(required=True),
|
||||||
|
login_password = dict(required=True),
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
domain_name = module.params['name']
|
||||||
|
domain_state = module.params['state']
|
||||||
|
domain_subdomains = module.params['subdomains']
|
||||||
|
|
||||||
|
session_id, account = webfaction.login(
|
||||||
|
module.params['login_name'],
|
||||||
|
module.params['login_password']
|
||||||
|
)
|
||||||
|
|
||||||
|
domain_list = webfaction.list_domains(session_id)
|
||||||
|
domain_map = dict([(i['domain'], i) for i in domain_list])
|
||||||
|
existing_domain = domain_map.get(domain_name)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Here's where the real stuff happens
|
||||||
|
|
||||||
|
if domain_state == 'present':
|
||||||
|
|
||||||
|
# Does an app with this name already exist?
|
||||||
|
if existing_domain:
|
||||||
|
|
||||||
|
if set(existing_domain['subdomains']) >= set(domain_subdomains):
|
||||||
|
# If it exists with the right subdomains, we don't change anything.
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
positional_args = [session_id, domain_name] + domain_subdomains
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, create the app
|
||||||
|
# print positional_args
|
||||||
|
result.update(
|
||||||
|
webfaction.create_domain(
|
||||||
|
*positional_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif domain_state == 'absent':
|
||||||
|
|
||||||
|
# If the app's already not there, nothing changed.
|
||||||
|
if not existing_domain:
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
positional_args = [session_id, domain_name] + domain_subdomains
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, delete the app
|
||||||
|
result.update(
|
||||||
|
webfaction.delete_domain(*positional_args)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Unknown state specified: {}".format(domain_state))
|
||||||
|
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
result = result
|
||||||
|
)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,139 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
#
|
||||||
|
# Create webfaction mailbox using Ansible and the Webfaction API
|
||||||
|
#
|
||||||
|
# ------------------------------------------
|
||||||
|
# (c) Quentin Stafford-Fraser and Andy Baker 2015
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: webfaction_mailbox
|
||||||
|
short_description: Add or remove mailboxes on Webfaction
|
||||||
|
description:
|
||||||
|
- Add or remove mailboxes on a Webfaction account. Further documentation at http://github.com/quentinsf/ansible-webfaction.
|
||||||
|
author: Quentin Stafford-Fraser (@quentinsf)
|
||||||
|
version_added: "2.0"
|
||||||
|
notes:
|
||||||
|
- "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays."
|
||||||
|
- See `the webfaction API <http://docs.webfaction.com/xmlrpc-api/>`_ for more info.
|
||||||
|
options:
|
||||||
|
|
||||||
|
mailbox_name:
|
||||||
|
description:
|
||||||
|
- The name of the mailbox
|
||||||
|
required: true
|
||||||
|
|
||||||
|
mailbox_password:
|
||||||
|
description:
|
||||||
|
- The password for the mailbox
|
||||||
|
required: true
|
||||||
|
default: null
|
||||||
|
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the mailbox should exist
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
|
||||||
|
login_name:
|
||||||
|
description:
|
||||||
|
- The webfaction account to use
|
||||||
|
required: true
|
||||||
|
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The webfaction password to use
|
||||||
|
required: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create a mailbox
|
||||||
|
webfaction_mailbox:
|
||||||
|
mailbox_name="mybox"
|
||||||
|
mailbox_password="myboxpw"
|
||||||
|
state=present
|
||||||
|
login_name={{webfaction_user}}
|
||||||
|
login_password={{webfaction_passwd}}
|
||||||
|
'''
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
mailbox_name=dict(required=True),
|
||||||
|
mailbox_password=dict(required=True),
|
||||||
|
state=dict(required=False, choices=['present', 'absent'], default='present'),
|
||||||
|
login_name=dict(required=True),
|
||||||
|
login_password=dict(required=True),
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
mailbox_name = module.params['mailbox_name']
|
||||||
|
site_state = module.params['state']
|
||||||
|
|
||||||
|
session_id, account = webfaction.login(
|
||||||
|
module.params['login_name'],
|
||||||
|
module.params['login_password']
|
||||||
|
)
|
||||||
|
|
||||||
|
mailbox_list = webfaction.list_mailboxes(session_id)
|
||||||
|
existing_mailbox = mailbox_name in mailbox_list
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Here's where the real stuff happens
|
||||||
|
|
||||||
|
if site_state == 'present':
|
||||||
|
|
||||||
|
# Does a mailbox with this name already exist?
|
||||||
|
if existing_mailbox:
|
||||||
|
module.exit_json(changed=False,)
|
||||||
|
|
||||||
|
positional_args = [session_id, mailbox_name]
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, create the mailbox
|
||||||
|
result.update(webfaction.create_mailbox(*positional_args))
|
||||||
|
|
||||||
|
elif site_state == 'absent':
|
||||||
|
|
||||||
|
# If the mailbox is already not there, nothing changed.
|
||||||
|
if not existing_mailbox:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, delete the mailbox
|
||||||
|
result.update(webfaction.delete_mailbox(session_id, mailbox_name))
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Unknown state specified: {}".format(site_state))
|
||||||
|
|
||||||
|
module.exit_json(changed=True, result=result)
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,208 @@
|
|||||||
|
#! /usr/bin/python
|
||||||
|
#
|
||||||
|
# Create Webfaction website using Ansible and the Webfaction API
|
||||||
|
#
|
||||||
|
# ------------------------------------------
|
||||||
|
#
|
||||||
|
# (c) Quentin Stafford-Fraser 2015
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: webfaction_site
|
||||||
|
short_description: Add or remove a website on a Webfaction host
|
||||||
|
description:
|
||||||
|
- Add or remove a website on a Webfaction host. Further documentation at http://github.com/quentinsf/ansible-webfaction.
|
||||||
|
author: Quentin Stafford-Fraser (@quentinsf)
|
||||||
|
version_added: "2.0"
|
||||||
|
notes:
|
||||||
|
- Sadly, you I(do) need to know your webfaction hostname for the C(host) parameter. But at least, unlike the API, you don't need to know the IP address - you can use a DNS name.
|
||||||
|
- If a site of the same name exists in the account but on a different host, the operation will exit.
|
||||||
|
- "You can run playbooks that use this on a local machine, or on a Webfaction host, or elsewhere, since the scripts use the remote webfaction API - the location is not important. However, running them on multiple hosts I(simultaneously) is best avoided. If you don't specify I(localhost) as your host, you may want to add C(serial: 1) to the plays."
|
||||||
|
- See `the webfaction API <http://docs.webfaction.com/xmlrpc-api/>`_ for more info.
|
||||||
|
|
||||||
|
options:
|
||||||
|
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of the website
|
||||||
|
required: true
|
||||||
|
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the website should exist
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- The webfaction host on which the site should be created.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
https:
|
||||||
|
description:
|
||||||
|
- Whether or not to use HTTPS
|
||||||
|
required: false
|
||||||
|
choices: BOOLEANS
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
site_apps:
|
||||||
|
description:
|
||||||
|
- A mapping of URLs to apps
|
||||||
|
required: false
|
||||||
|
|
||||||
|
subdomains:
|
||||||
|
description:
|
||||||
|
- A list of subdomains associated with this site.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
|
||||||
|
login_name:
|
||||||
|
description:
|
||||||
|
- The webfaction account to use
|
||||||
|
required: true
|
||||||
|
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The webfaction password to use
|
||||||
|
required: true
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: create website
|
||||||
|
webfaction_site:
|
||||||
|
name: testsite1
|
||||||
|
state: present
|
||||||
|
host: myhost.webfaction.com
|
||||||
|
subdomains:
|
||||||
|
- 'testsite1.my_domain.org'
|
||||||
|
site_apps:
|
||||||
|
- ['testapp1', '/']
|
||||||
|
https: no
|
||||||
|
login_name: "{{webfaction_user}}"
|
||||||
|
login_password: "{{webfaction_passwd}}"
|
||||||
|
'''
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import xmlrpclib
|
||||||
|
|
||||||
|
webfaction = xmlrpclib.ServerProxy('https://api.webfaction.com/')
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
name = dict(required=True),
|
||||||
|
state = dict(required=False, choices=['present', 'absent'], default='present'),
|
||||||
|
# You can specify an IP address or hostname.
|
||||||
|
host = dict(required=True),
|
||||||
|
https = dict(required=False, choices=BOOLEANS, default=False),
|
||||||
|
subdomains = dict(required=False, default=[]),
|
||||||
|
site_apps = dict(required=False, default=[]),
|
||||||
|
login_name = dict(required=True),
|
||||||
|
login_password = dict(required=True),
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
site_name = module.params['name']
|
||||||
|
site_state = module.params['state']
|
||||||
|
site_host = module.params['host']
|
||||||
|
site_ip = socket.gethostbyname(site_host)
|
||||||
|
|
||||||
|
session_id, account = webfaction.login(
|
||||||
|
module.params['login_name'],
|
||||||
|
module.params['login_password']
|
||||||
|
)
|
||||||
|
|
||||||
|
site_list = webfaction.list_websites(session_id)
|
||||||
|
site_map = dict([(i['name'], i) for i in site_list])
|
||||||
|
existing_site = site_map.get(site_name)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
# Here's where the real stuff happens
|
||||||
|
|
||||||
|
if site_state == 'present':
|
||||||
|
|
||||||
|
# Does a site with this name already exist?
|
||||||
|
if existing_site:
|
||||||
|
|
||||||
|
# If yes, but it's on a different IP address, then fail.
|
||||||
|
# If we wanted to allow relocation, we could add a 'relocate=true' option
|
||||||
|
# which would get the existing IP address, delete the site there, and create it
|
||||||
|
# at the new address. A bit dangerous, perhaps, so for now we'll require manual
|
||||||
|
# deletion if it's on another host.
|
||||||
|
|
||||||
|
if existing_site['ip'] != site_ip:
|
||||||
|
module.fail_json(msg="Website already exists with a different IP address. Please fix by hand.")
|
||||||
|
|
||||||
|
# If it's on this host and the key parameters are the same, nothing needs to be done.
|
||||||
|
|
||||||
|
if (existing_site['https'] == module.boolean(module.params['https'])) and \
|
||||||
|
(set(existing_site['subdomains']) == set(module.params['subdomains'])) and \
|
||||||
|
(dict(existing_site['website_apps']) == dict(module.params['site_apps'])):
|
||||||
|
module.exit_json(
|
||||||
|
changed = False
|
||||||
|
)
|
||||||
|
|
||||||
|
positional_args = [
|
||||||
|
session_id, site_name, site_ip,
|
||||||
|
module.boolean(module.params['https']),
|
||||||
|
module.params['subdomains'],
|
||||||
|
]
|
||||||
|
for a in module.params['site_apps']:
|
||||||
|
positional_args.append( (a[0], a[1]) )
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, create or modify the site
|
||||||
|
result.update(
|
||||||
|
webfaction.create_website(
|
||||||
|
*positional_args
|
||||||
|
) if not existing_site else webfaction.update_website (
|
||||||
|
*positional_args
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif site_state == 'absent':
|
||||||
|
|
||||||
|
# If the site's already not there, nothing changed.
|
||||||
|
if not existing_site:
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
# If this isn't a dry run, delete the site
|
||||||
|
result.update(
|
||||||
|
webfaction.delete_website(session_id, site_name, site_ip)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.fail_json(msg="Unknown state specified: {}".format(site_state))
|
||||||
|
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
result = result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,507 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
module: consul
|
||||||
|
short_description: "Add, modify & delete services within a consul cluster.
|
||||||
|
See http://consul.io for more details."
|
||||||
|
description:
|
||||||
|
- registers services and checks for an agent with a consul cluster. A service
|
||||||
|
is some process running on the agent node that should be advertised by
|
||||||
|
consul's discovery mechanism. It may optionally supply a check definition,
|
||||||
|
a periodic service test to notify the consul cluster of service's health.
|
||||||
|
Checks may also be registered per node e.g. disk usage, or cpu usage and
|
||||||
|
notify the health of the entire node to the cluster.
|
||||||
|
Service level checks do not require a check name or id as these are derived
|
||||||
|
by Consul from the Service name and id respectively by appending 'service:'.
|
||||||
|
Node level checks require a check_name and optionally a check_id.
|
||||||
|
Currently, there is no complete way to retrieve the script, interval or ttl
|
||||||
|
metadata for a registered check. Without this metadata it is not possible to
|
||||||
|
tell if the data supplied with ansible represents a change to a check. As a
|
||||||
|
result this does not attempt to determine changes and will always report a
|
||||||
|
changed occurred. An api method is planned to supply this metadata so at that
|
||||||
|
stage change management will be added.
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- python-consul
|
||||||
|
- requests
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Steve Gargan (@sgargan)"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- register or deregister the consul service, defaults to present
|
||||||
|
required: true
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
service_name:
|
||||||
|
desciption:
|
||||||
|
- Unique name for the service on a node, must be unique per node,
|
||||||
|
required if registering a service. May be ommitted if registering
|
||||||
|
a node level check
|
||||||
|
required: false
|
||||||
|
service_id:
|
||||||
|
description:
|
||||||
|
- the ID for the service, must be unique per node, defaults to the
|
||||||
|
service name if the service name is supplied
|
||||||
|
required: false
|
||||||
|
default: service_name if supplied
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- host of the consul agent defaults to localhost
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- the port on which the consul agent is running
|
||||||
|
required: false
|
||||||
|
default: 8500
|
||||||
|
notes:
|
||||||
|
description:
|
||||||
|
- Notes to attach to check when registering it.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
service_port:
|
||||||
|
description:
|
||||||
|
- the port on which the service is listening required for
|
||||||
|
registration of a service, i.e. if service_name or service_id is set
|
||||||
|
required: false
|
||||||
|
tags:
|
||||||
|
description:
|
||||||
|
- a list of tags that will be attached to the service registration.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
script:
|
||||||
|
description:
|
||||||
|
- the script/command that will be run periodically to check the health
|
||||||
|
of the service. Scripts require an interval and vise versa
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
interval:
|
||||||
|
description:
|
||||||
|
- the interval at which the service check will be run. This is a number
|
||||||
|
with a s or m suffix to signify the units of seconds or minutes e.g
|
||||||
|
15s or 1m. If no suffix is supplied, m will be used by default e.g.
|
||||||
|
1 will be 1m. Required if the script param is specified.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
check_id:
|
||||||
|
description:
|
||||||
|
- an ID for the service check, defaults to the check name, ignored if
|
||||||
|
part of a service definition.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
check_name:
|
||||||
|
description:
|
||||||
|
- a name for the service check, defaults to the check id. required if
|
||||||
|
standalone, ignored if part of service definition.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
ttl:
|
||||||
|
description:
|
||||||
|
- checks can be registered with a ttl instead of a script and interval
|
||||||
|
this means that the service will check in with the agent before the
|
||||||
|
ttl expires. If it doesn't the check will be considered failed.
|
||||||
|
Required if registering a check and the script an interval are missing
|
||||||
|
Similar to the interval this is a number with a s or m suffix to
|
||||||
|
signify the units of seconds or minutes e.g 15s or 1m. If no suffix
|
||||||
|
is supplied, m will be used by default e.g. 1 will be 1m
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
token:
|
||||||
|
description:
|
||||||
|
- the token key indentifying an ACL rule set. May be required to
|
||||||
|
register services.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: register nginx service with the local consul agent
|
||||||
|
consul:
|
||||||
|
name: nginx
|
||||||
|
service_port: 80
|
||||||
|
|
||||||
|
- name: register nginx service with curl check
|
||||||
|
consul:
|
||||||
|
name: nginx
|
||||||
|
service_port: 80
|
||||||
|
script: "curl http://localhost"
|
||||||
|
interval: 60s
|
||||||
|
|
||||||
|
- name: register nginx with some service tags
|
||||||
|
consul:
|
||||||
|
name: nginx
|
||||||
|
service_port: 80
|
||||||
|
tags:
|
||||||
|
- prod
|
||||||
|
- webservers
|
||||||
|
|
||||||
|
- name: remove nginx service
|
||||||
|
consul:
|
||||||
|
name: nginx
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: create a node level check to test disk usage
|
||||||
|
consul:
|
||||||
|
check_name: Disk usage
|
||||||
|
check_id: disk_usage
|
||||||
|
script: "/opt/disk_usage.py"
|
||||||
|
interval: 5m
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
try:
|
||||||
|
import consul
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
python_consul_installed = True
|
||||||
|
except ImportError, e:
|
||||||
|
python_consul_installed = False
|
||||||
|
|
||||||
|
def register_with_consul(module):
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
add(module)
|
||||||
|
else:
|
||||||
|
remove(module)
|
||||||
|
|
||||||
|
|
||||||
|
def add(module):
|
||||||
|
''' adds a service or a check depending on supplied configuration'''
|
||||||
|
check = parse_check(module)
|
||||||
|
service = parse_service(module)
|
||||||
|
|
||||||
|
if not service and not check:
|
||||||
|
module.fail_json(msg='a name and port are required to register a service')
|
||||||
|
|
||||||
|
if service:
|
||||||
|
if check:
|
||||||
|
service.add_check(check)
|
||||||
|
add_service(module, service)
|
||||||
|
elif check:
|
||||||
|
add_check(module, check)
|
||||||
|
|
||||||
|
|
||||||
|
def remove(module):
|
||||||
|
''' removes a service or a check '''
|
||||||
|
service_id = module.params.get('service_id') or module.params.get('service_name')
|
||||||
|
check_id = module.params.get('check_id') or module.params.get('check_name')
|
||||||
|
if not (service_id or check_id):
|
||||||
|
module.fail_json(msg='services and checks are removed by id or name.'\
|
||||||
|
' please supply a service id/name or a check id/name')
|
||||||
|
if service_id:
|
||||||
|
remove_service(module, service_id)
|
||||||
|
else:
|
||||||
|
remove_check(module, check_id)
|
||||||
|
|
||||||
|
|
||||||
|
def add_check(module, check):
|
||||||
|
''' registers a check with the given agent. currently there is no way
|
||||||
|
retrieve the full metadata of an existing check through the consul api.
|
||||||
|
Without this we can't compare to the supplied check and so we must assume
|
||||||
|
a change. '''
|
||||||
|
if not check.name:
|
||||||
|
module.fail_json(msg='a check name is required for a node level check,'\
|
||||||
|
' one not attached to a service')
|
||||||
|
|
||||||
|
consul_api = get_consul_api(module)
|
||||||
|
check.register(consul_api)
|
||||||
|
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
check_id=check.check_id,
|
||||||
|
check_name=check.name,
|
||||||
|
script=check.script,
|
||||||
|
interval=check.interval,
|
||||||
|
ttl=check.ttl)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_check(module, check_id):
|
||||||
|
''' removes a check using its id '''
|
||||||
|
consul_api = get_consul_api(module)
|
||||||
|
|
||||||
|
if check_id in consul_api.agent.checks():
|
||||||
|
consul_api.agent.check.deregister(check_id)
|
||||||
|
module.exit_json(changed=True, id=check_id)
|
||||||
|
|
||||||
|
module.exit_json(changed=False, id=check_id)
|
||||||
|
|
||||||
|
|
||||||
|
def add_service(module, service):
|
||||||
|
''' registers a service with the the current agent '''
|
||||||
|
result = service
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
consul_api = get_consul_api(module)
|
||||||
|
existing = get_service_by_id(consul_api, service.id)
|
||||||
|
|
||||||
|
# there is no way to retreive the details of checks so if a check is present
|
||||||
|
# in the service it must be reregistered
|
||||||
|
if service.has_checks() or not existing or not existing == service:
|
||||||
|
|
||||||
|
service.register(consul_api)
|
||||||
|
# check that it registered correctly
|
||||||
|
registered = get_service_by_id(consul_api, service.id)
|
||||||
|
if registered:
|
||||||
|
result = registered
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
module.exit_json(changed=changed,
|
||||||
|
service_id=result.id,
|
||||||
|
service_name=result.name,
|
||||||
|
service_port=result.port,
|
||||||
|
checks=map(lambda x: x.to_dict(), service.checks),
|
||||||
|
tags=result.tags)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_service(module, service_id):
|
||||||
|
''' deregister a service from the given agent using its service id '''
|
||||||
|
consul_api = get_consul_api(module)
|
||||||
|
service = get_service_by_id(consul_api, service_id)
|
||||||
|
if service:
|
||||||
|
consul_api.agent.service.deregister(service_id)
|
||||||
|
module.exit_json(changed=True, id=service_id)
|
||||||
|
|
||||||
|
module.exit_json(changed=False, id=service_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_consul_api(module, token=None):
|
||||||
|
return consul.Consul(host=module.params.get('host'),
|
||||||
|
port=module.params.get('port'),
|
||||||
|
token=module.params.get('token'))
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_by_id(consul_api, service_id):
|
||||||
|
''' iterate the registered services and find one with the given id '''
|
||||||
|
for name, service in consul_api.agent.services().iteritems():
|
||||||
|
if service['ID'] == service_id:
|
||||||
|
return ConsulService(loaded=service)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_check(module):
|
||||||
|
|
||||||
|
if module.params.get('script') and module.params.get('ttl'):
|
||||||
|
module.fail_json(
|
||||||
|
msg='check are either script or ttl driven, supplying both does'\
|
||||||
|
' not make sense')
|
||||||
|
|
||||||
|
if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl'):
|
||||||
|
|
||||||
|
return ConsulCheck(
|
||||||
|
module.params.get('check_id'),
|
||||||
|
module.params.get('check_name'),
|
||||||
|
module.params.get('check_node'),
|
||||||
|
module.params.get('check_host'),
|
||||||
|
module.params.get('script'),
|
||||||
|
module.params.get('interval'),
|
||||||
|
module.params.get('ttl'),
|
||||||
|
module.params.get('notes')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_service(module):
|
||||||
|
|
||||||
|
if module.params.get('service_name') and module.params.get('service_port'):
|
||||||
|
return ConsulService(
|
||||||
|
module.params.get('service_id'),
|
||||||
|
module.params.get('service_name'),
|
||||||
|
module.params.get('service_port'),
|
||||||
|
module.params.get('tags'),
|
||||||
|
)
|
||||||
|
elif module.params.get('service_name') and not module.params.get('service_port'):
|
||||||
|
|
||||||
|
module.fail_json(
|
||||||
|
msg="service_name supplied but no service_port, a port is required"\
|
||||||
|
" to configure a service. Did you configure the 'port' "\
|
||||||
|
"argument meaning 'service_port'?")
|
||||||
|
|
||||||
|
|
||||||
|
class ConsulService():
|
||||||
|
|
||||||
|
def __init__(self, service_id=None, name=None, port=-1,
|
||||||
|
tags=None, loaded=None):
|
||||||
|
self.id = self.name = name
|
||||||
|
if service_id:
|
||||||
|
self.id = service_id
|
||||||
|
self.port = port
|
||||||
|
self.tags = tags
|
||||||
|
self.checks = []
|
||||||
|
if loaded:
|
||||||
|
self.id = loaded['ID']
|
||||||
|
self.name = loaded['Service']
|
||||||
|
self.port = loaded['Port']
|
||||||
|
self.tags = loaded['Tags']
|
||||||
|
|
||||||
|
def register(self, consul_api):
|
||||||
|
if len(self.checks) > 0:
|
||||||
|
check = self.checks[0]
|
||||||
|
consul_api.agent.service.register(
|
||||||
|
self.name,
|
||||||
|
service_id=self.id,
|
||||||
|
port=self.port,
|
||||||
|
tags=self.tags,
|
||||||
|
script=check.script,
|
||||||
|
interval=check.interval,
|
||||||
|
ttl=check.ttl)
|
||||||
|
else:
|
||||||
|
consul_api.agent.service.register(
|
||||||
|
self.name,
|
||||||
|
service_id=self.id,
|
||||||
|
port=self.port,
|
||||||
|
tags=self.tags)
|
||||||
|
|
||||||
|
def add_check(self, check):
|
||||||
|
self.checks.append(check)
|
||||||
|
|
||||||
|
def checks(self):
|
||||||
|
return self.checks
|
||||||
|
|
||||||
|
def has_checks(self):
|
||||||
|
return len(self.checks) > 0
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, self.__class__)
|
||||||
|
and self.id == other.id
|
||||||
|
and self.name == other.name
|
||||||
|
and self.port == other.port
|
||||||
|
and self.tags == other.tags)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
data = {'id': self.id, "name": self.name}
|
||||||
|
if self.port:
|
||||||
|
data['port'] = self.port
|
||||||
|
if self.tags and len(self.tags) > 0:
|
||||||
|
data['tags'] = self.tags
|
||||||
|
if len(self.checks) > 0:
|
||||||
|
data['check'] = self.checks[0].to_dict()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class ConsulCheck():
|
||||||
|
|
||||||
|
def __init__(self, check_id, name, node=None, host='localhost',
|
||||||
|
script=None, interval=None, ttl=None, notes=None):
|
||||||
|
self.check_id = self.name = name
|
||||||
|
if check_id:
|
||||||
|
self.check_id = check_id
|
||||||
|
self.script = script
|
||||||
|
self.interval = self.validate_duration('interval', interval)
|
||||||
|
self.ttl = self.validate_duration('ttl', ttl)
|
||||||
|
self.notes = notes
|
||||||
|
self.node = node
|
||||||
|
self.host = host
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def validate_duration(self, name, duration):
|
||||||
|
if duration:
|
||||||
|
duration_units = ['ns', 'us', 'ms', 's', 'm', 'h']
|
||||||
|
if not any((duration.endswith(suffix) for suffix in duration_units)):
|
||||||
|
raise Exception('Invalid %s %s you must specify units (%s)' %
|
||||||
|
(name, duration, ', '.join(duration_units)))
|
||||||
|
return duration
|
||||||
|
|
||||||
|
def register(self, consul_api):
|
||||||
|
consul_api.agent.check.register(self.name, check_id=self.check_id,
|
||||||
|
script=self.script,
|
||||||
|
interval=self.interval,
|
||||||
|
ttl=self.ttl, notes=self.notes)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, self.__class__)
|
||||||
|
and self.check_id == other.check_id
|
||||||
|
and self.name == other.name
|
||||||
|
and self.script == script
|
||||||
|
and self.interval == interval)
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
data = {}
|
||||||
|
self._add(data, 'id', attr='check_id')
|
||||||
|
self._add(data, 'name', attr='check_name')
|
||||||
|
self._add(data, 'script')
|
||||||
|
self._add(data, 'node')
|
||||||
|
self._add(data, 'notes')
|
||||||
|
self._add(data, 'host')
|
||||||
|
self._add(data, 'interval')
|
||||||
|
self._add(data, 'ttl')
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _add(self, data, key, attr=None):
|
||||||
|
try:
|
||||||
|
if attr == None:
|
||||||
|
attr = key
|
||||||
|
data[key] = getattr(self, attr)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_dependencies(module):
|
||||||
|
if not python_consul_installed:
|
||||||
|
module.fail_json(msg="python-consul required for this module. "\
|
||||||
|
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
host=dict(default='localhost'),
|
||||||
|
port=dict(default=8500, type='int'),
|
||||||
|
check_id=dict(required=False),
|
||||||
|
check_name=dict(required=False),
|
||||||
|
check_node=dict(required=False),
|
||||||
|
check_host=dict(required=False),
|
||||||
|
notes=dict(required=False),
|
||||||
|
script=dict(required=False),
|
||||||
|
service_id=dict(required=False),
|
||||||
|
service_name=dict(required=False),
|
||||||
|
service_port=dict(required=False, type='int'),
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
interval=dict(required=False, type='str'),
|
||||||
|
ttl=dict(required=False, type='str'),
|
||||||
|
tags=dict(required=False, type='list'),
|
||||||
|
token=dict(required=False)
|
||||||
|
),
|
||||||
|
supports_check_mode=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_dependencies(module)
|
||||||
|
|
||||||
|
try:
|
||||||
|
register_with_consul(module)
|
||||||
|
except ConnectionError, e:
|
||||||
|
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||||
|
module.params.get('host'), module.params.get('port'), str(e)))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,321 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
module: consul_acl
|
||||||
|
short_description: "manipulate consul acl keys and rules"
|
||||||
|
description:
|
||||||
|
- allows the addition, modification and deletion of ACL keys and associated
|
||||||
|
rules in a consul cluster via the agent. For more details on using and
|
||||||
|
configuring ACLs, see https://www.consul.io/docs/internals/acl.html.
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- python-consul
|
||||||
|
- pyhcl
|
||||||
|
- requests
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Steve Gargan (@sgargan)"
|
||||||
|
options:
|
||||||
|
mgmt_token:
|
||||||
|
description:
|
||||||
|
- a management token is required to manipulate the acl lists
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- whether the ACL pair should be present or absent, defaults to present
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
type:
|
||||||
|
description:
|
||||||
|
- the type of token that should be created, either management or
|
||||||
|
client, defaults to client
|
||||||
|
choices: ['client', 'management']
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- the name that should be associated with the acl key, this is opaque
|
||||||
|
to Consul
|
||||||
|
required: false
|
||||||
|
token:
|
||||||
|
description:
|
||||||
|
- the token key indentifying an ACL rule set. If generated by consul
|
||||||
|
this will be a UUID.
|
||||||
|
required: false
|
||||||
|
rules:
|
||||||
|
description:
|
||||||
|
- an list of the rules that should be associated with a given key/token.
|
||||||
|
required: false
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- host of the consul agent defaults to localhost
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- the port on which the consul agent is running
|
||||||
|
required: false
|
||||||
|
default: 8500
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: create an acl token with rules
|
||||||
|
consul_acl:
|
||||||
|
mgmt_token: 'some_management_acl'
|
||||||
|
host: 'consul1.mycluster.io'
|
||||||
|
name: 'Foo access'
|
||||||
|
rules:
|
||||||
|
- key: 'foo'
|
||||||
|
policy: read
|
||||||
|
- key: 'private/foo'
|
||||||
|
policy: deny
|
||||||
|
|
||||||
|
- name: remove a token
|
||||||
|
consul_acl:
|
||||||
|
mgmt_token: 'some_management_acl'
|
||||||
|
host: 'consul1.mycluster.io'
|
||||||
|
token: '172bd5c8-9fe9-11e4-b1b0-3c15c2c9fd5e'
|
||||||
|
state: absent
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
try:
|
||||||
|
import consul
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
python_consul_installed = True
|
||||||
|
except ImportError, e:
|
||||||
|
python_consul_installed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import hcl
|
||||||
|
pyhcl_installed = True
|
||||||
|
except ImportError:
|
||||||
|
pyhcl_installed = False
|
||||||
|
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
def execute(module):
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
update_acl(module)
|
||||||
|
else:
|
||||||
|
remove_acl(module)
|
||||||
|
|
||||||
|
|
||||||
|
def update_acl(module):
|
||||||
|
|
||||||
|
rules = module.params.get('rules')
|
||||||
|
state = module.params.get('state')
|
||||||
|
token = module.params.get('token')
|
||||||
|
token_type = module.params.get('token_type')
|
||||||
|
mgmt = module.params.get('mgmt_token')
|
||||||
|
name = module.params.get('name')
|
||||||
|
consul = get_consul_api(module, mgmt)
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
if token:
|
||||||
|
existing_rules = load_rules_for_token(module, consul, token)
|
||||||
|
supplied_rules = yml_to_rules(module, rules)
|
||||||
|
print existing_rules
|
||||||
|
print supplied_rules
|
||||||
|
changed = not existing_rules == supplied_rules
|
||||||
|
if changed:
|
||||||
|
y = supplied_rules.to_hcl()
|
||||||
|
token = consul.acl.update(
|
||||||
|
token,
|
||||||
|
name=name,
|
||||||
|
type=token_type,
|
||||||
|
rules=supplied_rules.to_hcl())
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
rules = yml_to_rules(module, rules)
|
||||||
|
if rules.are_rules():
|
||||||
|
rules = rules.to_json()
|
||||||
|
else:
|
||||||
|
rules = None
|
||||||
|
|
||||||
|
token = consul.acl.create(
|
||||||
|
name=name, type=token_type, rules=rules)
|
||||||
|
changed = True
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(
|
||||||
|
msg="No token returned, check your managment key and that \
|
||||||
|
the host is in the acl datacenter %s" % e)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Could not create/update acl %s" % e)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed,
|
||||||
|
token=token,
|
||||||
|
rules=rules,
|
||||||
|
name=name,
|
||||||
|
type=token_type)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_acl(module):
|
||||||
|
state = module.params.get('state')
|
||||||
|
token = module.params.get('token')
|
||||||
|
mgmt = module.params.get('mgmt_token')
|
||||||
|
|
||||||
|
consul = get_consul_api(module, token=mgmt)
|
||||||
|
changed = token and consul.acl.info(token)
|
||||||
|
if changed:
|
||||||
|
token = consul.acl.destroy(token)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, token=token)
|
||||||
|
|
||||||
|
|
||||||
|
def load_rules_for_token(module, consul_api, token):
|
||||||
|
try:
|
||||||
|
rules = Rules()
|
||||||
|
info = consul_api.acl.info(token)
|
||||||
|
if info and info['Rules']:
|
||||||
|
rule_set = to_ascii(info['Rules'])
|
||||||
|
for rule in hcl.loads(rule_set).values():
|
||||||
|
for key, policy in rule.iteritems():
|
||||||
|
rules.add_rule(Rule(key, policy['policy']))
|
||||||
|
return rules
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(
|
||||||
|
msg="Could not load rule list from retrieved rule data %s, %s" % (
|
||||||
|
token, e))
|
||||||
|
|
||||||
|
return json_to_rules(module, loaded)
|
||||||
|
|
||||||
|
def to_ascii(unicode_string):
|
||||||
|
if isinstance(unicode_string, unicode):
|
||||||
|
return unicode_string.encode('ascii', 'ignore')
|
||||||
|
return unicode_string
|
||||||
|
|
||||||
|
def yml_to_rules(module, yml_rules):
|
||||||
|
rules = Rules()
|
||||||
|
if yml_rules:
|
||||||
|
for rule in yml_rules:
|
||||||
|
if not('key' in rule or 'policy' in rule):
|
||||||
|
module.fail_json(msg="a rule requires a key and a policy.")
|
||||||
|
rules.add_rule(Rule(rule['key'], rule['policy']))
|
||||||
|
return rules
|
||||||
|
|
||||||
|
template = '''key "%s" {
|
||||||
|
policy = "%s"
|
||||||
|
}'''
|
||||||
|
|
||||||
|
class Rules:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.rules = {}
|
||||||
|
|
||||||
|
def add_rule(self, rule):
|
||||||
|
self.rules[rule.key] = rule
|
||||||
|
|
||||||
|
def are_rules(self):
|
||||||
|
return len(self.rules) > 0
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
rules = {}
|
||||||
|
for key, rule in self.rules.iteritems():
|
||||||
|
rules[key] = {'policy': rule.policy}
|
||||||
|
return json.dumps({'keys': rules})
|
||||||
|
|
||||||
|
def to_hcl(self):
|
||||||
|
|
||||||
|
rules = ""
|
||||||
|
for key, rule in self.rules.iteritems():
|
||||||
|
rules += template % (key, rule.policy)
|
||||||
|
|
||||||
|
return to_ascii(rules)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not (other or isinstance(other, self.__class__)
|
||||||
|
or len(other.rules) == len(self.rules)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for name, other_rule in other.rules.iteritems():
|
||||||
|
if not name in self.rules:
|
||||||
|
return False
|
||||||
|
rule = self.rules[name]
|
||||||
|
|
||||||
|
if not (rule and rule == other_rule):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.to_hcl()
|
||||||
|
|
||||||
|
class Rule:
|
||||||
|
|
||||||
|
def __init__(self, key, policy):
|
||||||
|
self.key = key
|
||||||
|
self.policy = policy
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return (isinstance(other, self.__class__)
|
||||||
|
and self.key == other.key
|
||||||
|
and self.policy == other.policy)
|
||||||
|
def __hash__(self):
|
||||||
|
return hash(self.key) ^ hash(self.policy)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '%s %s' % (self.key, self.policy)
|
||||||
|
|
||||||
|
def get_consul_api(module, token=None):
|
||||||
|
if not token:
|
||||||
|
token = token = module.params.get('token')
|
||||||
|
return consul.Consul(host=module.params.get('host'),
|
||||||
|
port=module.params.get('port'),
|
||||||
|
token=token)
|
||||||
|
|
||||||
|
def test_dependencies(module):
|
||||||
|
if not python_consul_installed:
|
||||||
|
module.fail_json(msg="python-consul required for this module. "\
|
||||||
|
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||||
|
|
||||||
|
if not pyhcl_installed:
|
||||||
|
module.fail_json( msg="pyhcl required for this module."\
|
||||||
|
" see https://pypi.python.org/pypi/pyhcl")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = dict(
|
||||||
|
mgmt_token=dict(required=True),
|
||||||
|
host=dict(default='localhost'),
|
||||||
|
name=dict(required=False),
|
||||||
|
port=dict(default=8500, type='int'),
|
||||||
|
rules=dict(default=None, required=False, type='list'),
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
token=dict(required=False),
|
||||||
|
token_type=dict(
|
||||||
|
required=False, choices=['client', 'management'], default='client')
|
||||||
|
)
|
||||||
|
module = AnsibleModule(argument_spec, supports_check_mode=False)
|
||||||
|
|
||||||
|
test_dependencies(module)
|
||||||
|
|
||||||
|
try:
|
||||||
|
execute(module)
|
||||||
|
except ConnectionError, e:
|
||||||
|
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||||
|
module.params.get('host'), module.params.get('port'), str(e)))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,264 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
module: consul_kv
|
||||||
|
short_description: "manipulate entries in the key/value store of a consul
|
||||||
|
cluster. See http://www.consul.io/docs/agent/http.html#kv for more details."
|
||||||
|
description:
|
||||||
|
- allows the addition, modification and deletion of key/value entries in a
|
||||||
|
consul cluster via the agent. The entire contents of the record, including
|
||||||
|
the indices, flags and session are returned as 'value'. If the key
|
||||||
|
represents a prefix then Note that when a value is removed, the existing
|
||||||
|
value if any is returned as part of the results.
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- python-consul
|
||||||
|
- requests
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Steve Gargan (@sgargan)"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- the action to take with the supplied key and value. If the state is
|
||||||
|
'present', the key contents will be set to the value supplied,
|
||||||
|
'changed' will be set to true only if the value was different to the
|
||||||
|
current contents. The state 'absent' will remove the key/value pair,
|
||||||
|
again 'changed' will be set to true only if the key actually existed
|
||||||
|
prior to the removal. An attempt can be made to obtain or free the
|
||||||
|
lock associated with a key/value pair with the states 'acquire' or
|
||||||
|
'release' respectively. a valid session must be supplied to make the
|
||||||
|
attempt changed will be true if the attempt is successful, false
|
||||||
|
otherwise.
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent', 'acquire', 'release']
|
||||||
|
default: present
|
||||||
|
key:
|
||||||
|
description:
|
||||||
|
- the key at which the value should be stored.
|
||||||
|
required: true
|
||||||
|
value:
|
||||||
|
description:
|
||||||
|
- the value should be associated with the given key, required if state
|
||||||
|
is present
|
||||||
|
required: true
|
||||||
|
recurse:
|
||||||
|
description:
|
||||||
|
- if the key represents a prefix, each entry with the prefix can be
|
||||||
|
retrieved by setting this to true.
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
session:
|
||||||
|
description:
|
||||||
|
- the session that should be used to acquire or release a lock
|
||||||
|
associated with a key/value pair
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
token:
|
||||||
|
description:
|
||||||
|
- the token key indentifying an ACL rule set that controls access to
|
||||||
|
the key value pair
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
cas:
|
||||||
|
description:
|
||||||
|
- used when acquiring a lock with a session. If the cas is 0, then
|
||||||
|
Consul will only put the key if it does not already exist. If the
|
||||||
|
cas value is non-zero, then the key is only set if the index matches
|
||||||
|
the ModifyIndex of that key.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
flags:
|
||||||
|
description:
|
||||||
|
- opaque integer value that can be passed when setting a value.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- host of the consul agent defaults to localhost
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- the port on which the consul agent is running
|
||||||
|
required: false
|
||||||
|
default: 8500
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
|
||||||
|
- name: add or update the value associated with a key in the key/value store
|
||||||
|
consul_kv:
|
||||||
|
key: somekey
|
||||||
|
value: somevalue
|
||||||
|
|
||||||
|
- name: remove a key from the store
|
||||||
|
consul_kv:
|
||||||
|
key: somekey
|
||||||
|
state: absent
|
||||||
|
|
||||||
|
- name: add a node to an arbitrary group via consul inventory (see consul.ini)
|
||||||
|
consul_kv:
|
||||||
|
key: ansible/groups/dc1/somenode
|
||||||
|
value: 'top_secret'
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
try:
|
||||||
|
import consul
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
python_consul_installed = True
|
||||||
|
except ImportError, e:
|
||||||
|
python_consul_installed = False
|
||||||
|
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
|
||||||
|
def execute(module):
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
if state == 'acquire' or state == 'release':
|
||||||
|
lock(module, state)
|
||||||
|
if state == 'present':
|
||||||
|
add_value(module)
|
||||||
|
else:
|
||||||
|
remove_value(module)
|
||||||
|
|
||||||
|
|
||||||
|
def lock(module, state):
|
||||||
|
|
||||||
|
session = module.params.get('session')
|
||||||
|
key = module.params.get('key')
|
||||||
|
value = module.params.get('value')
|
||||||
|
|
||||||
|
if not session:
|
||||||
|
module.fail(
|
||||||
|
msg='%s of lock for %s requested but no session supplied' %
|
||||||
|
(state, key))
|
||||||
|
|
||||||
|
if state == 'acquire':
|
||||||
|
successful = consul_api.kv.put(key, value,
|
||||||
|
cas=module.params.get('cas'),
|
||||||
|
acquire=session,
|
||||||
|
flags=module.params.get('flags'))
|
||||||
|
else:
|
||||||
|
successful = consul_api.kv.put(key, value,
|
||||||
|
cas=module.params.get('cas'),
|
||||||
|
release=session,
|
||||||
|
flags=module.params.get('flags'))
|
||||||
|
|
||||||
|
module.exit_json(changed=successful,
|
||||||
|
index=index,
|
||||||
|
key=key)
|
||||||
|
|
||||||
|
|
||||||
|
def add_value(module):
|
||||||
|
|
||||||
|
consul_api = get_consul_api(module)
|
||||||
|
|
||||||
|
key = module.params.get('key')
|
||||||
|
value = module.params.get('value')
|
||||||
|
|
||||||
|
index, existing = consul_api.kv.get(key)
|
||||||
|
|
||||||
|
changed = not existing or (existing and existing['Value'] != value)
|
||||||
|
if changed and not module.check_mode:
|
||||||
|
changed = consul_api.kv.put(key, value,
|
||||||
|
cas=module.params.get('cas'),
|
||||||
|
flags=module.params.get('flags'))
|
||||||
|
|
||||||
|
if module.params.get('retrieve'):
|
||||||
|
index, stored = consul_api.kv.get(key)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed,
|
||||||
|
index=index,
|
||||||
|
key=key,
|
||||||
|
data=stored)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_value(module):
|
||||||
|
''' remove the value associated with the given key. if the recurse parameter
|
||||||
|
is set then any key prefixed with the given key will be removed. '''
|
||||||
|
consul_api = get_consul_api(module)
|
||||||
|
|
||||||
|
key = module.params.get('key')
|
||||||
|
value = module.params.get('value')
|
||||||
|
|
||||||
|
index, existing = consul_api.kv.get(
|
||||||
|
key, recurse=module.params.get('recurse'))
|
||||||
|
|
||||||
|
changed = existing != None
|
||||||
|
if changed and not module.check_mode:
|
||||||
|
consul_api.kv.delete(key, module.params.get('recurse'))
|
||||||
|
|
||||||
|
module.exit_json(changed=changed,
|
||||||
|
index=index,
|
||||||
|
key=key,
|
||||||
|
data=existing)
|
||||||
|
|
||||||
|
|
||||||
|
def get_consul_api(module, token=None):
|
||||||
|
return consul.Consul(host=module.params.get('host'),
|
||||||
|
port=module.params.get('port'),
|
||||||
|
token=module.params.get('token'))
|
||||||
|
|
||||||
|
def test_dependencies(module):
|
||||||
|
if not python_consul_installed:
|
||||||
|
module.fail_json(msg="python-consul required for this module. "\
|
||||||
|
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
argument_spec = dict(
|
||||||
|
cas=dict(required=False),
|
||||||
|
flags=dict(required=False),
|
||||||
|
key=dict(required=True),
|
||||||
|
host=dict(default='localhost'),
|
||||||
|
port=dict(default=8500, type='int'),
|
||||||
|
recurse=dict(required=False, type='bool'),
|
||||||
|
retrieve=dict(required=False, default=True),
|
||||||
|
state=dict(default='present', choices=['present', 'absent']),
|
||||||
|
token=dict(required=False, default='anonymous'),
|
||||||
|
value=dict(required=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec, supports_check_mode=False)
|
||||||
|
|
||||||
|
test_dependencies(module)
|
||||||
|
|
||||||
|
try:
|
||||||
|
execute(module)
|
||||||
|
except ConnectionError, e:
|
||||||
|
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||||
|
module.params.get('host'), module.params.get('port'), str(e)))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,269 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
#
|
||||||
|
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
module: consul_session
|
||||||
|
short_description: "manipulate consul sessions"
|
||||||
|
description:
|
||||||
|
- allows the addition, modification and deletion of sessions in a consul
|
||||||
|
cluster. These sessions can then be used in conjunction with key value pairs
|
||||||
|
to implement distributed locks. In depth documentation for working with
|
||||||
|
sessions can be found here http://www.consul.io/docs/internals/sessions.html
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- python-consul
|
||||||
|
- requests
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Steve Gargan (@sgargan)"
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- whether the session should be present i.e. created if it doesn't
|
||||||
|
exist, or absent, removed if present. If created, the ID for the
|
||||||
|
session is returned in the output. If absent, the name or ID is
|
||||||
|
required to remove the session. Info for a single session, all the
|
||||||
|
sessions for a node or all available sessions can be retrieved by
|
||||||
|
specifying info, node or list for the state; for node or info, the
|
||||||
|
node name or session id is required as parameter.
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent', 'info', 'node', 'list']
|
||||||
|
default: present
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- the name that should be associated with the session. This is opaque
|
||||||
|
to Consul and not required.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
delay:
|
||||||
|
description:
|
||||||
|
- the optional lock delay that can be attached to the session when it
|
||||||
|
is created. Locks for invalidated sessions ar blocked from being
|
||||||
|
acquired until this delay has expired. Valid units for delays
|
||||||
|
include 'ns', 'us', 'ms', 's', 'm', 'h'
|
||||||
|
default: 15s
|
||||||
|
required: false
|
||||||
|
node:
|
||||||
|
description:
|
||||||
|
- the name of the node that with which the session will be associated.
|
||||||
|
by default this is the name of the agent.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
datacenter:
|
||||||
|
description:
|
||||||
|
- name of the datacenter in which the session exists or should be
|
||||||
|
created.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
checks:
|
||||||
|
description:
|
||||||
|
- a list of checks that will be used to verify the session health. If
|
||||||
|
all the checks fail, the session will be invalidated and any locks
|
||||||
|
associated with the session will be release and can be acquired once
|
||||||
|
the associated lock delay has expired.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
host:
|
||||||
|
description:
|
||||||
|
- host of the consul agent defaults to localhost
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- the port on which the consul agent is running
|
||||||
|
required: false
|
||||||
|
default: 8500
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: register basic session with consul
|
||||||
|
consul_session:
|
||||||
|
name: session1
|
||||||
|
|
||||||
|
- name: register a session with an existing check
|
||||||
|
consul_session:
|
||||||
|
name: session_with_check
|
||||||
|
checks:
|
||||||
|
- existing_check_name
|
||||||
|
|
||||||
|
- name: register a session with lock_delay
|
||||||
|
consul_session:
|
||||||
|
name: session_with_delay
|
||||||
|
delay: 20s
|
||||||
|
|
||||||
|
- name: retrieve info about session by id
|
||||||
|
consul_session: id=session_id state=info
|
||||||
|
|
||||||
|
- name: retrieve active sessions
|
||||||
|
consul_session: state=list
|
||||||
|
'''
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
try:
|
||||||
|
import consul
|
||||||
|
from requests.exceptions import ConnectionError
|
||||||
|
python_consul_installed = True
|
||||||
|
except ImportError, e:
|
||||||
|
python_consul_installed = False
|
||||||
|
|
||||||
|
def execute(module):
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
|
||||||
|
if state in ['info', 'list', 'node']:
|
||||||
|
lookup_sessions(module)
|
||||||
|
elif state == 'present':
|
||||||
|
update_session(module)
|
||||||
|
else:
|
||||||
|
remove_session(module)
|
||||||
|
|
||||||
|
def lookup_sessions(module):
|
||||||
|
|
||||||
|
datacenter = module.params.get('datacenter')
|
||||||
|
|
||||||
|
state = module.params.get('state')
|
||||||
|
consul = get_consul_api(module)
|
||||||
|
try:
|
||||||
|
if state == 'list':
|
||||||
|
sessions_list = consul.session.list(dc=datacenter)
|
||||||
|
#ditch the index, this can be grabbed from the results
|
||||||
|
if sessions_list and sessions_list[1]:
|
||||||
|
sessions_list = sessions_list[1]
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
sessions=sessions_list)
|
||||||
|
elif state == 'node':
|
||||||
|
node = module.params.get('node')
|
||||||
|
if not node:
|
||||||
|
module.fail_json(
|
||||||
|
msg="node name is required to retrieve sessions for node")
|
||||||
|
sessions = consul.session.node(node, dc=datacenter)
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
node=node,
|
||||||
|
sessions=sessions)
|
||||||
|
elif state == 'info':
|
||||||
|
session_id = module.params.get('id')
|
||||||
|
if not session_id:
|
||||||
|
module.fail_json(
|
||||||
|
msg="session_id is required to retrieve indvidual session info")
|
||||||
|
|
||||||
|
session_by_id = consul.session.info(session_id, dc=datacenter)
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
session_id=session_id,
|
||||||
|
sessions=session_by_id)
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Could not retrieve session info %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def update_session(module):
|
||||||
|
|
||||||
|
name = module.params.get('name')
|
||||||
|
session_id = module.params.get('id')
|
||||||
|
delay = module.params.get('delay')
|
||||||
|
checks = module.params.get('checks')
|
||||||
|
datacenter = module.params.get('datacenter')
|
||||||
|
node = module.params.get('node')
|
||||||
|
|
||||||
|
consul = get_consul_api(module)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
session = consul.session.create(
|
||||||
|
name=name,
|
||||||
|
node=node,
|
||||||
|
lock_delay=validate_duration('delay', delay),
|
||||||
|
dc=datacenter,
|
||||||
|
checks=checks
|
||||||
|
)
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
session_id=session,
|
||||||
|
name=name,
|
||||||
|
delay=delay,
|
||||||
|
checks=checks,
|
||||||
|
node=node)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Could not create/update session %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_session(module):
|
||||||
|
session_id = module.params.get('id')
|
||||||
|
|
||||||
|
if not session_id:
|
||||||
|
module.fail_json(msg="""A session id must be supplied in order to
|
||||||
|
remove a session.""")
|
||||||
|
|
||||||
|
consul = get_consul_api(module)
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
session = consul.session.destroy(session_id)
|
||||||
|
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
session_id=session_id)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Could not remove session with id '%s' %s" % (
|
||||||
|
session_id, e))
|
||||||
|
|
||||||
|
def validate_duration(name, duration):
|
||||||
|
if duration:
|
||||||
|
duration_units = ['ns', 'us', 'ms', 's', 'm', 'h']
|
||||||
|
if not any((duration.endswith(suffix) for suffix in duration_units)):
|
||||||
|
raise Exception('Invalid %s %s you must specify units (%s)' %
|
||||||
|
(name, duration, ', '.join(duration_units)))
|
||||||
|
return duration
|
||||||
|
|
||||||
|
def get_consul_api(module):
|
||||||
|
return consul.Consul(host=module.params.get('host'),
|
||||||
|
port=module.params.get('port'))
|
||||||
|
|
||||||
|
def test_dependencies(module):
|
||||||
|
if not python_consul_installed:
|
||||||
|
module.fail_json(msg="python-consul required for this module. "\
|
||||||
|
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
argument_spec = dict(
|
||||||
|
checks=dict(default=None, required=False, type='list'),
|
||||||
|
delay=dict(required=False,type='str', default='15s'),
|
||||||
|
host=dict(default='localhost'),
|
||||||
|
port=dict(default=8500, type='int'),
|
||||||
|
id=dict(required=False),
|
||||||
|
name=dict(required=False),
|
||||||
|
node=dict(required=False),
|
||||||
|
state=dict(default='present',
|
||||||
|
choices=['present', 'absent', 'info', 'node', 'list'])
|
||||||
|
)
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec, supports_check_mode=False)
|
||||||
|
|
||||||
|
test_dependencies(module)
|
||||||
|
|
||||||
|
try:
|
||||||
|
execute(module)
|
||||||
|
except ConnectionError, e:
|
||||||
|
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||||
|
module.params.get('host'), module.params.get('port'), str(e)))
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2015, Matt Martz <matt@sivel.net>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pexpect
|
||||||
|
HAS_PEXPECT = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_PEXPECT = False
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: expect
|
||||||
|
version_added: 2.0
|
||||||
|
short_description: Executes a command and responds to prompts
|
||||||
|
description:
|
||||||
|
- The M(expect) module executes a command and responds to prompts
|
||||||
|
- The given command will be executed on all selected nodes. It will not be
|
||||||
|
processed through the shell, so variables like C($HOME) and operations
|
||||||
|
like C("<"), C(">"), C("|"), and C("&") will not work
|
||||||
|
options:
|
||||||
|
command:
|
||||||
|
description:
|
||||||
|
- the command module takes command to run.
|
||||||
|
required: true
|
||||||
|
creates:
|
||||||
|
description:
|
||||||
|
- a filename, when it already exists, this step will B(not) be run.
|
||||||
|
required: false
|
||||||
|
removes:
|
||||||
|
description:
|
||||||
|
- a filename, when it does not exist, this step will B(not) be run.
|
||||||
|
required: false
|
||||||
|
chdir:
|
||||||
|
description:
|
||||||
|
- cd into this directory before running the command
|
||||||
|
required: false
|
||||||
|
responses:
|
||||||
|
description:
|
||||||
|
- Mapping of expected string and string to respond with
|
||||||
|
required: true
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- Amount of time in seconds to wait for the expected strings
|
||||||
|
default: 30
|
||||||
|
echo:
|
||||||
|
description:
|
||||||
|
- Whether or not to echo out your response strings
|
||||||
|
default: false
|
||||||
|
requirements:
|
||||||
|
- python >= 2.6
|
||||||
|
- pexpect >= 3.3
|
||||||
|
notes:
|
||||||
|
- If you want to run a command through the shell (say you are using C(<),
|
||||||
|
C(>), C(|), etc), you must specify a shell in the command such as
|
||||||
|
C(/bin/bash -c "/path/to/something | grep else")
|
||||||
|
author: "Matt Martz (@sivel)"
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- expect:
|
||||||
|
command: passwd username
|
||||||
|
responses:
|
||||||
|
(?i)password: "MySekretPa$$word"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
command=dict(required=True),
|
||||||
|
chdir=dict(),
|
||||||
|
creates=dict(),
|
||||||
|
removes=dict(),
|
||||||
|
responses=dict(type='dict', required=True),
|
||||||
|
timeout=dict(type='int', default=30),
|
||||||
|
echo=dict(type='bool', default=False),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_PEXPECT:
|
||||||
|
module.fail_json(msg='The pexpect python module is required')
|
||||||
|
|
||||||
|
chdir = module.params['chdir']
|
||||||
|
args = module.params['command']
|
||||||
|
creates = module.params['creates']
|
||||||
|
removes = module.params['removes']
|
||||||
|
responses = module.params['responses']
|
||||||
|
timeout = module.params['timeout']
|
||||||
|
echo = module.params['echo']
|
||||||
|
|
||||||
|
events = dict()
|
||||||
|
for key, value in responses.iteritems():
|
||||||
|
events[key.decode()] = u'%s\n' % value.rstrip('\n').decode()
|
||||||
|
|
||||||
|
if args.strip() == '':
|
||||||
|
module.fail_json(rc=256, msg="no command given")
|
||||||
|
|
||||||
|
if chdir:
|
||||||
|
chdir = os.path.abspath(os.path.expanduser(chdir))
|
||||||
|
os.chdir(chdir)
|
||||||
|
|
||||||
|
if creates:
|
||||||
|
# do not run the command if the line contains creates=filename
|
||||||
|
# and the filename already exists. This allows idempotence
|
||||||
|
# of command executions.
|
||||||
|
v = os.path.expanduser(creates)
|
||||||
|
if os.path.exists(v):
|
||||||
|
module.exit_json(
|
||||||
|
cmd=args,
|
||||||
|
stdout="skipped, since %s exists" % v,
|
||||||
|
changed=False,
|
||||||
|
stderr=False,
|
||||||
|
rc=0
|
||||||
|
)
|
||||||
|
|
||||||
|
if removes:
|
||||||
|
# do not run the command if the line contains removes=filename
|
||||||
|
# and the filename does not exist. This allows idempotence
|
||||||
|
# of command executions.
|
||||||
|
v = os.path.expanduser(removes)
|
||||||
|
if not os.path.exists(v):
|
||||||
|
module.exit_json(
|
||||||
|
cmd=args,
|
||||||
|
stdout="skipped, since %s does not exist" % v,
|
||||||
|
changed=False,
|
||||||
|
stderr=False,
|
||||||
|
rc=0
|
||||||
|
)
|
||||||
|
|
||||||
|
startd = datetime.datetime.now()
|
||||||
|
|
||||||
|
try:
|
||||||
|
out, rc = pexpect.runu(args, timeout=timeout, withexitstatus=True,
|
||||||
|
events=events, cwd=chdir, echo=echo)
|
||||||
|
except pexpect.ExceptionPexpect, e:
|
||||||
|
module.fail_json(msg='%s' % e)
|
||||||
|
|
||||||
|
endd = datetime.datetime.now()
|
||||||
|
delta = endd - startd
|
||||||
|
|
||||||
|
if out is None:
|
||||||
|
out = ''
|
||||||
|
|
||||||
|
module.exit_json(
|
||||||
|
cmd=args,
|
||||||
|
stdout=out.rstrip('\r\n'),
|
||||||
|
rc=rc,
|
||||||
|
start=str(startd),
|
||||||
|
end=str(endd),
|
||||||
|
delta=str(delta),
|
||||||
|
changed=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
|
||||||
|
main()
|
@ -0,0 +1,194 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: vertica_configuration
|
||||||
|
version_added: '2.0'
|
||||||
|
short_description: Updates Vertica configuration parameters.
|
||||||
|
description:
|
||||||
|
- Updates Vertica configuration parameters.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the parameter to update.
|
||||||
|
required: true
|
||||||
|
value:
|
||||||
|
description:
|
||||||
|
- Value of the parameter to be set.
|
||||||
|
required: true
|
||||||
|
db:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica database.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
cluster:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica cluster.
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- Vertica cluster port to connect to.
|
||||||
|
required: false
|
||||||
|
default: 5433
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- The username used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: dbadmin
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The password used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
notes:
|
||||||
|
- The default authentication assumes that you are either logging in as or sudo'ing
|
||||||
|
to the C(dbadmin) account on the host.
|
||||||
|
- This module uses C(pyodbc), a Python ODBC database adapter. You must ensure
|
||||||
|
that C(unixODBC) and C(pyodbc) is installed on the host and properly configured.
|
||||||
|
- Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so)
|
||||||
|
to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini)
|
||||||
|
and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16)
|
||||||
|
to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini).
|
||||||
|
requirements: [ 'unixODBC', 'pyodbc' ]
|
||||||
|
author: "Dariusz Owczarek (@dareko)"
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: updating load_balance_policy
|
||||||
|
vertica_configuration: name=failovertostandbyafter value='8 hours'
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
except ImportError:
|
||||||
|
pyodbc_found = False
|
||||||
|
else:
|
||||||
|
pyodbc_found = True
|
||||||
|
|
||||||
|
class NotSupportedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CannotDropError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# module specific functions
|
||||||
|
|
||||||
|
def get_configuration_facts(cursor, parameter_name=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select c.parameter_name, c.current_value, c.default_value
|
||||||
|
from configuration_parameters c
|
||||||
|
where c.node_name = 'ALL'
|
||||||
|
and (? = '' or c.parameter_name ilike ?)
|
||||||
|
""", parameter_name, parameter_name)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
facts[row.parameter_name.lower()] = {
|
||||||
|
'parameter_name': row.parameter_name,
|
||||||
|
'current_value': row.current_value,
|
||||||
|
'default_value': row.default_value}
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def check(configuration_facts, parameter_name, current_value):
|
||||||
|
parameter_key = parameter_name.lower()
|
||||||
|
if current_value and current_value.lower() != configuration_facts[parameter_key]['current_value'].lower():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def present(configuration_facts, cursor, parameter_name, current_value):
|
||||||
|
parameter_key = parameter_name.lower()
|
||||||
|
changed = False
|
||||||
|
if current_value and current_value.lower() != configuration_facts[parameter_key]['current_value'].lower():
|
||||||
|
cursor.execute("select set_config_parameter('{0}', '{1}')".format(parameter_name, current_value))
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
configuration_facts.update(get_configuration_facts(cursor, parameter_name))
|
||||||
|
return changed
|
||||||
|
|
||||||
|
# module logic
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
parameter=dict(required=True, aliases=['name']),
|
||||||
|
value=dict(default=None),
|
||||||
|
db=dict(default=None),
|
||||||
|
cluster=dict(default='localhost'),
|
||||||
|
port=dict(default='5433'),
|
||||||
|
login_user=dict(default='dbadmin'),
|
||||||
|
login_password=dict(default=None),
|
||||||
|
), supports_check_mode = True)
|
||||||
|
|
||||||
|
if not pyodbc_found:
|
||||||
|
module.fail_json(msg="The python pyodbc module is required.")
|
||||||
|
|
||||||
|
parameter_name = module.params['parameter']
|
||||||
|
current_value = module.params['value']
|
||||||
|
db = ''
|
||||||
|
if module.params['db']:
|
||||||
|
db = module.params['db']
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dsn = (
|
||||||
|
"Driver=Vertica;"
|
||||||
|
"Server={0};"
|
||||||
|
"Port={1};"
|
||||||
|
"Database={2};"
|
||||||
|
"User={3};"
|
||||||
|
"Password={4};"
|
||||||
|
"ConnectionLoadBalance={5}"
|
||||||
|
).format(module.params['cluster'], module.params['port'], db,
|
||||||
|
module.params['login_user'], module.params['login_password'], 'true')
|
||||||
|
db_conn = pyodbc.connect(dsn, autocommit=True)
|
||||||
|
cursor = db_conn.cursor()
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Unable to connect to database: {0}.".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
configuration_facts = get_configuration_facts(cursor)
|
||||||
|
if module.check_mode:
|
||||||
|
changed = not check(configuration_facts, parameter_name, current_value)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
changed = present(configuration_facts, cursor, parameter_name, current_value)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
except NotSupportedError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_configuration': configuration_facts})
|
||||||
|
except CannotDropError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_configuration': configuration_facts})
|
||||||
|
except SystemExit:
|
||||||
|
# avoid catching this on python 2.4
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, parameter=parameter_name, ansible_facts={'vertica_configuration': configuration_facts})
|
||||||
|
|
||||||
|
# import ansible utilities
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,276 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: vertica_facts
|
||||||
|
version_added: '2.0'
|
||||||
|
short_description: Gathers Vertica database facts.
|
||||||
|
description:
|
||||||
|
- Gathers Vertica database facts.
|
||||||
|
options:
|
||||||
|
cluster:
|
||||||
|
description:
|
||||||
|
- Name of the cluster running the schema.
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
Database port to connect to.
|
||||||
|
required: false
|
||||||
|
default: 5433
|
||||||
|
db:
|
||||||
|
description:
|
||||||
|
- Name of the database running the schema.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- The username used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: dbadmin
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The password used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
notes:
|
||||||
|
- The default authentication assumes that you are either logging in as or sudo'ing
|
||||||
|
to the C(dbadmin) account on the host.
|
||||||
|
- This module uses C(pyodbc), a Python ODBC database adapter. You must ensure
|
||||||
|
that C(unixODBC) and C(pyodbc) is installed on the host and properly configured.
|
||||||
|
- Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so)
|
||||||
|
to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini)
|
||||||
|
and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16)
|
||||||
|
to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini).
|
||||||
|
requirements: [ 'unixODBC', 'pyodbc' ]
|
||||||
|
author: "Dariusz Owczarek (@dareko)"
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: gathering vertica facts
|
||||||
|
vertica_facts: db=db_name
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
except ImportError:
|
||||||
|
pyodbc_found = False
|
||||||
|
else:
|
||||||
|
pyodbc_found = True
|
||||||
|
|
||||||
|
class NotSupportedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# module specific functions
|
||||||
|
|
||||||
|
def get_schema_facts(cursor, schema=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select schema_name, schema_owner, create_time
|
||||||
|
from schemata
|
||||||
|
where not is_system_schema and schema_name not in ('public')
|
||||||
|
and (? = '' or schema_name ilike ?)
|
||||||
|
""", schema, schema)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
facts[row.schema_name.lower()] = {
|
||||||
|
'name': row.schema_name,
|
||||||
|
'owner': row.schema_owner,
|
||||||
|
'create_time': str(row.create_time),
|
||||||
|
'usage_roles': [],
|
||||||
|
'create_roles': []}
|
||||||
|
cursor.execute("""
|
||||||
|
select g.object_name as schema_name, r.name as role_name,
|
||||||
|
lower(g.privileges_description) privileges_description
|
||||||
|
from roles r join grants g
|
||||||
|
on g.grantee = r.name and g.object_type='SCHEMA'
|
||||||
|
and g.privileges_description like '%USAGE%'
|
||||||
|
and g.grantee not in ('public', 'dbadmin')
|
||||||
|
and (? = '' or g.object_name ilike ?)
|
||||||
|
""", schema, schema)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
schema_key = row.schema_name.lower()
|
||||||
|
if 'create' in row.privileges_description:
|
||||||
|
facts[schema_key]['create_roles'].append(row.role_name)
|
||||||
|
else:
|
||||||
|
facts[schema_key]['usage_roles'].append(row.role_name)
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def get_user_facts(cursor, user=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select u.user_name, u.is_locked, u.lock_time,
|
||||||
|
p.password, p.acctexpired as is_expired,
|
||||||
|
u.profile_name, u.resource_pool,
|
||||||
|
u.all_roles, u.default_roles
|
||||||
|
from users u join password_auditor p on p.user_id = u.user_id
|
||||||
|
where not u.is_super_user
|
||||||
|
and (? = '' or u.user_name ilike ?)
|
||||||
|
""", user, user)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
user_key = row.user_name.lower()
|
||||||
|
facts[user_key] = {
|
||||||
|
'name': row.user_name,
|
||||||
|
'locked': str(row.is_locked),
|
||||||
|
'password': row.password,
|
||||||
|
'expired': str(row.is_expired),
|
||||||
|
'profile': row.profile_name,
|
||||||
|
'resource_pool': row.resource_pool,
|
||||||
|
'roles': [],
|
||||||
|
'default_roles': []}
|
||||||
|
if row.is_locked:
|
||||||
|
facts[user_key]['locked_time'] = str(row.lock_time)
|
||||||
|
if row.all_roles:
|
||||||
|
facts[user_key]['roles'] = row.all_roles.replace(' ', '').split(',')
|
||||||
|
if row.default_roles:
|
||||||
|
facts[user_key]['default_roles'] = row.default_roles.replace(' ', '').split(',')
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def get_role_facts(cursor, role=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select r.name, r.assigned_roles
|
||||||
|
from roles r
|
||||||
|
where (? = '' or r.name ilike ?)
|
||||||
|
""", role, role)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
role_key = row.name.lower()
|
||||||
|
facts[role_key] = {
|
||||||
|
'name': row.name,
|
||||||
|
'assigned_roles': []}
|
||||||
|
if row.assigned_roles:
|
||||||
|
facts[role_key]['assigned_roles'] = row.assigned_roles.replace(' ', '').split(',')
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def get_configuration_facts(cursor, parameter=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select c.parameter_name, c.current_value, c.default_value
|
||||||
|
from configuration_parameters c
|
||||||
|
where c.node_name = 'ALL'
|
||||||
|
and (? = '' or c.parameter_name ilike ?)
|
||||||
|
""", parameter, parameter)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
facts[row.parameter_name.lower()] = {
|
||||||
|
'parameter_name': row.parameter_name,
|
||||||
|
'current_value': row.current_value,
|
||||||
|
'default_value': row.default_value}
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def get_node_facts(cursor, schema=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select node_name, node_address, export_address, node_state, node_type,
|
||||||
|
catalog_path
|
||||||
|
from nodes
|
||||||
|
""")
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
facts[row.node_address] = {
|
||||||
|
'node_name': row.node_name,
|
||||||
|
'export_address': row.export_address,
|
||||||
|
'node_state': row.node_state,
|
||||||
|
'node_type': row.node_type,
|
||||||
|
'catalog_path': row.catalog_path}
|
||||||
|
return facts
|
||||||
|
|
||||||
|
# module logic
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
cluster=dict(default='localhost'),
|
||||||
|
port=dict(default='5433'),
|
||||||
|
db=dict(default=None),
|
||||||
|
login_user=dict(default='dbadmin'),
|
||||||
|
login_password=dict(default=None),
|
||||||
|
), supports_check_mode = True)
|
||||||
|
|
||||||
|
if not pyodbc_found:
|
||||||
|
module.fail_json(msg="The python pyodbc module is required.")
|
||||||
|
|
||||||
|
db = ''
|
||||||
|
if module.params['db']:
|
||||||
|
db = module.params['db']
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dsn = (
|
||||||
|
"Driver=Vertica;"
|
||||||
|
"Server={0};"
|
||||||
|
"Port={1};"
|
||||||
|
"Database={2};"
|
||||||
|
"User={3};"
|
||||||
|
"Password={4};"
|
||||||
|
"ConnectionLoadBalance={5}"
|
||||||
|
).format(module.params['cluster'], module.params['port'], db,
|
||||||
|
module.params['login_user'], module.params['login_password'], 'true')
|
||||||
|
db_conn = pyodbc.connect(dsn, autocommit=True)
|
||||||
|
cursor = db_conn.cursor()
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Unable to connect to database: {0}.".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
schema_facts = get_schema_facts(cursor)
|
||||||
|
user_facts = get_user_facts(cursor)
|
||||||
|
role_facts = get_role_facts(cursor)
|
||||||
|
configuration_facts = get_configuration_facts(cursor)
|
||||||
|
node_facts = get_node_facts(cursor)
|
||||||
|
module.exit_json(changed=False,
|
||||||
|
ansible_facts={'vertica_schemas': schema_facts,
|
||||||
|
'vertica_users': user_facts,
|
||||||
|
'vertica_roles': role_facts,
|
||||||
|
'vertica_configuration': configuration_facts,
|
||||||
|
'vertica_nodes': node_facts})
|
||||||
|
except NotSupportedError, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
except SystemExit:
|
||||||
|
# avoid catching this on python 2.4
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e)
|
||||||
|
|
||||||
|
# import ansible utilities
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,243 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: vertica_role
|
||||||
|
version_added: '2.0'
|
||||||
|
short_description: Adds or removes Vertica database roles and assigns roles to them.
|
||||||
|
description:
|
||||||
|
- Adds or removes Vertica database role and, optionally, assign other roles.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the role to add or remove.
|
||||||
|
required: true
|
||||||
|
assigned_roles:
|
||||||
|
description:
|
||||||
|
- Comma separated list of roles to assign to the role.
|
||||||
|
aliases: ['assigned_role']
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether to create C(present), drop C(absent) or lock C(locked) a role.
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: present
|
||||||
|
db:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica database.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
cluster:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica cluster.
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- Vertica cluster port to connect to.
|
||||||
|
required: false
|
||||||
|
default: 5433
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- The username used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: dbadmin
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The password used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
notes:
|
||||||
|
- The default authentication assumes that you are either logging in as or sudo'ing
|
||||||
|
to the C(dbadmin) account on the host.
|
||||||
|
- This module uses C(pyodbc), a Python ODBC database adapter. You must ensure
|
||||||
|
that C(unixODBC) and C(pyodbc) is installed on the host and properly configured.
|
||||||
|
- Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so)
|
||||||
|
to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini)
|
||||||
|
and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16)
|
||||||
|
to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini).
|
||||||
|
requirements: [ 'unixODBC', 'pyodbc' ]
|
||||||
|
author: "Dariusz Owczarek (@dareko)"
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: creating a new vertica role
|
||||||
|
vertica_role: name=role_name db=db_name state=present
|
||||||
|
|
||||||
|
- name: creating a new vertica role with other role assigned
|
||||||
|
vertica_role: name=role_name assigned_role=other_role_name state=present
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
except ImportError:
|
||||||
|
pyodbc_found = False
|
||||||
|
else:
|
||||||
|
pyodbc_found = True
|
||||||
|
|
||||||
|
class NotSupportedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CannotDropError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# module specific functions
|
||||||
|
|
||||||
|
def get_role_facts(cursor, role=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select r.name, r.assigned_roles
|
||||||
|
from roles r
|
||||||
|
where (? = '' or r.name ilike ?)
|
||||||
|
""", role, role)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
role_key = row.name.lower()
|
||||||
|
facts[role_key] = {
|
||||||
|
'name': row.name,
|
||||||
|
'assigned_roles': []}
|
||||||
|
if row.assigned_roles:
|
||||||
|
facts[role_key]['assigned_roles'] = row.assigned_roles.replace(' ', '').split(',')
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def update_roles(role_facts, cursor, role,
|
||||||
|
existing, required):
|
||||||
|
for assigned_role in set(existing) - set(required):
|
||||||
|
cursor.execute("revoke {0} from {1}".format(assigned_role, role))
|
||||||
|
for assigned_role in set(required) - set(existing):
|
||||||
|
cursor.execute("grant {0} to {1}".format(assigned_role, role))
|
||||||
|
|
||||||
|
def check(role_facts, role, assigned_roles):
|
||||||
|
role_key = role.lower()
|
||||||
|
if role_key not in role_facts:
|
||||||
|
return False
|
||||||
|
if assigned_roles and cmp(sorted(assigned_roles), sorted(role_facts[role_key]['assigned_roles'])) != 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def present(role_facts, cursor, role, assigned_roles):
|
||||||
|
role_key = role.lower()
|
||||||
|
if role_key not in role_facts:
|
||||||
|
cursor.execute("create role {0}".format(role))
|
||||||
|
update_roles(role_facts, cursor, role, [], assigned_roles)
|
||||||
|
role_facts.update(get_role_facts(cursor, role))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
changed = False
|
||||||
|
if assigned_roles and cmp(sorted(assigned_roles), sorted(role_facts[role_key]['assigned_roles'])) != 0:
|
||||||
|
update_roles(role_facts, cursor, role,
|
||||||
|
role_facts[role_key]['assigned_roles'], assigned_roles)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
role_facts.update(get_role_facts(cursor, role))
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def absent(role_facts, cursor, role, assigned_roles):
|
||||||
|
role_key = role.lower()
|
||||||
|
if role_key in role_facts:
|
||||||
|
update_roles(role_facts, cursor, role,
|
||||||
|
role_facts[role_key]['assigned_roles'], [])
|
||||||
|
cursor.execute("drop role {0} cascade".format(role_facts[role_key]['name']))
|
||||||
|
del role_facts[role_key]
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# module logic
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
role=dict(required=True, aliases=['name']),
|
||||||
|
assigned_roles=dict(default=None, aliases=['assigned_role']),
|
||||||
|
state=dict(default='present', choices=['absent', 'present']),
|
||||||
|
db=dict(default=None),
|
||||||
|
cluster=dict(default='localhost'),
|
||||||
|
port=dict(default='5433'),
|
||||||
|
login_user=dict(default='dbadmin'),
|
||||||
|
login_password=dict(default=None),
|
||||||
|
), supports_check_mode = True)
|
||||||
|
|
||||||
|
if not pyodbc_found:
|
||||||
|
module.fail_json(msg="The python pyodbc module is required.")
|
||||||
|
|
||||||
|
role = module.params['role']
|
||||||
|
assigned_roles = []
|
||||||
|
if module.params['assigned_roles']:
|
||||||
|
assigned_roles = module.params['assigned_roles'].split(',')
|
||||||
|
assigned_roles = filter(None, assigned_roles)
|
||||||
|
state = module.params['state']
|
||||||
|
db = ''
|
||||||
|
if module.params['db']:
|
||||||
|
db = module.params['db']
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dsn = (
|
||||||
|
"Driver=Vertica;"
|
||||||
|
"Server={0};"
|
||||||
|
"Port={1};"
|
||||||
|
"Database={2};"
|
||||||
|
"User={3};"
|
||||||
|
"Password={4};"
|
||||||
|
"ConnectionLoadBalance={5}"
|
||||||
|
).format(module.params['cluster'], module.params['port'], db,
|
||||||
|
module.params['login_user'], module.params['login_password'], 'true')
|
||||||
|
db_conn = pyodbc.connect(dsn, autocommit=True)
|
||||||
|
cursor = db_conn.cursor()
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Unable to connect to database: {0}.".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
role_facts = get_role_facts(cursor)
|
||||||
|
if module.check_mode:
|
||||||
|
changed = not check(role_facts, role, assigned_roles)
|
||||||
|
elif state == 'absent':
|
||||||
|
try:
|
||||||
|
changed = absent(role_facts, cursor, role, assigned_roles)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
elif state == 'present':
|
||||||
|
try:
|
||||||
|
changed = present(role_facts, cursor, role, assigned_roles)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
except NotSupportedError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_roles': role_facts})
|
||||||
|
except CannotDropError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_roles': role_facts})
|
||||||
|
except SystemExit:
|
||||||
|
# avoid catching this on python 2.4
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, role=role, ansible_facts={'vertica_roles': role_facts})
|
||||||
|
|
||||||
|
# import ansible utilities
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: vertica_schema
|
||||||
|
version_added: '2.0'
|
||||||
|
short_description: Adds or removes Vertica database schema and roles.
|
||||||
|
description:
|
||||||
|
- Adds or removes Vertica database schema and, optionally, roles
|
||||||
|
with schema access privileges.
|
||||||
|
- A schema will not be removed until all the objects have been dropped.
|
||||||
|
- In such a situation, if the module tries to remove the schema it
|
||||||
|
will fail and only remove roles created for the schema if they have
|
||||||
|
no dependencies.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the schema to add or remove.
|
||||||
|
required: true
|
||||||
|
usage_roles:
|
||||||
|
description:
|
||||||
|
- Comma separated list of roles to create and grant usage access to the schema.
|
||||||
|
aliases: ['usage_role']
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
create_roles:
|
||||||
|
description:
|
||||||
|
- Comma separated list of roles to create and grant usage and create access to the schema.
|
||||||
|
aliases: ['create_role']
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
owner:
|
||||||
|
description:
|
||||||
|
- Name of the user to set as owner of the schema.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether to create C(present), or drop C(absent) a schema.
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
db:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica database.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
cluster:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica cluster.
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- Vertica cluster port to connect to.
|
||||||
|
required: false
|
||||||
|
default: 5433
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- The username used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: dbadmin
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The password used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
notes:
|
||||||
|
- The default authentication assumes that you are either logging in as or sudo'ing
|
||||||
|
to the C(dbadmin) account on the host.
|
||||||
|
- This module uses C(pyodbc), a Python ODBC database adapter. You must ensure
|
||||||
|
that C(unixODBC) and C(pyodbc) is installed on the host and properly configured.
|
||||||
|
- Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so)
|
||||||
|
to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini)
|
||||||
|
and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16)
|
||||||
|
to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini).
|
||||||
|
requirements: [ 'unixODBC', 'pyodbc' ]
|
||||||
|
author: "Dariusz Owczarek (@dareko)"
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: creating a new vertica schema
|
||||||
|
vertica_schema: name=schema_name db=db_name state=present
|
||||||
|
|
||||||
|
- name: creating a new schema with specific schema owner
|
||||||
|
vertica_schema: name=schema_name owner=dbowner db=db_name state=present
|
||||||
|
|
||||||
|
- name: creating a new schema with roles
|
||||||
|
vertica_schema:
|
||||||
|
name=schema_name
|
||||||
|
create_roles=schema_name_all
|
||||||
|
usage_roles=schema_name_ro,schema_name_rw
|
||||||
|
db=db_name
|
||||||
|
state=present
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
except ImportError:
|
||||||
|
pyodbc_found = False
|
||||||
|
else:
|
||||||
|
pyodbc_found = True
|
||||||
|
|
||||||
|
class NotSupportedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CannotDropError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# module specific functions
|
||||||
|
|
||||||
|
def get_schema_facts(cursor, schema=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select schema_name, schema_owner, create_time
|
||||||
|
from schemata
|
||||||
|
where not is_system_schema and schema_name not in ('public', 'TxtIndex')
|
||||||
|
and (? = '' or schema_name ilike ?)
|
||||||
|
""", schema, schema)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
facts[row.schema_name.lower()] = {
|
||||||
|
'name': row.schema_name,
|
||||||
|
'owner': row.schema_owner,
|
||||||
|
'create_time': str(row.create_time),
|
||||||
|
'usage_roles': [],
|
||||||
|
'create_roles': []}
|
||||||
|
cursor.execute("""
|
||||||
|
select g.object_name as schema_name, r.name as role_name,
|
||||||
|
lower(g.privileges_description) privileges_description
|
||||||
|
from roles r join grants g
|
||||||
|
on g.grantee_id = r.role_id and g.object_type='SCHEMA'
|
||||||
|
and g.privileges_description like '%USAGE%'
|
||||||
|
and g.grantee not in ('public', 'dbadmin')
|
||||||
|
and (? = '' or g.object_name ilike ?)
|
||||||
|
""", schema, schema)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
schema_key = row.schema_name.lower()
|
||||||
|
if 'create' in row.privileges_description:
|
||||||
|
facts[schema_key]['create_roles'].append(row.role_name)
|
||||||
|
else:
|
||||||
|
facts[schema_key]['usage_roles'].append(row.role_name)
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def update_roles(schema_facts, cursor, schema,
|
||||||
|
existing, required,
|
||||||
|
create_existing, create_required):
|
||||||
|
for role in set(existing + create_existing) - set(required + create_required):
|
||||||
|
cursor.execute("drop role {0} cascade".format(role))
|
||||||
|
for role in set(create_existing) - set(create_required):
|
||||||
|
cursor.execute("revoke create on schema {0} from {1}".format(schema, role))
|
||||||
|
for role in set(required + create_required) - set(existing + create_existing):
|
||||||
|
cursor.execute("create role {0}".format(role))
|
||||||
|
cursor.execute("grant usage on schema {0} to {1}".format(schema, role))
|
||||||
|
for role in set(create_required) - set(create_existing):
|
||||||
|
cursor.execute("grant create on schema {0} to {1}".format(schema, role))
|
||||||
|
|
||||||
|
def check(schema_facts, schema, usage_roles, create_roles, owner):
|
||||||
|
schema_key = schema.lower()
|
||||||
|
if schema_key not in schema_facts:
|
||||||
|
return False
|
||||||
|
if owner and owner.lower() == schema_facts[schema_key]['owner'].lower():
|
||||||
|
return False
|
||||||
|
if cmp(sorted(usage_roles), sorted(schema_facts[schema_key]['usage_roles'])) != 0:
|
||||||
|
return False
|
||||||
|
if cmp(sorted(create_roles), sorted(schema_facts[schema_key]['create_roles'])) != 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def present(schema_facts, cursor, schema, usage_roles, create_roles, owner):
|
||||||
|
schema_key = schema.lower()
|
||||||
|
if schema_key not in schema_facts:
|
||||||
|
query_fragments = ["create schema {0}".format(schema)]
|
||||||
|
if owner:
|
||||||
|
query_fragments.append("authorization {0}".format(owner))
|
||||||
|
cursor.execute(' '.join(query_fragments))
|
||||||
|
update_roles(schema_facts, cursor, schema, [], usage_roles, [], create_roles)
|
||||||
|
schema_facts.update(get_schema_facts(cursor, schema))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
changed = False
|
||||||
|
if owner and owner.lower() != schema_facts[schema_key]['owner'].lower():
|
||||||
|
raise NotSupportedError((
|
||||||
|
"Changing schema owner is not supported. "
|
||||||
|
"Current owner: {0}."
|
||||||
|
).format(schema_facts[schema_key]['owner']))
|
||||||
|
if cmp(sorted(usage_roles), sorted(schema_facts[schema_key]['usage_roles'])) != 0 or \
|
||||||
|
cmp(sorted(create_roles), sorted(schema_facts[schema_key]['create_roles'])) != 0:
|
||||||
|
update_roles(schema_facts, cursor, schema,
|
||||||
|
schema_facts[schema_key]['usage_roles'], usage_roles,
|
||||||
|
schema_facts[schema_key]['create_roles'], create_roles)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
schema_facts.update(get_schema_facts(cursor, schema))
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def absent(schema_facts, cursor, schema, usage_roles, create_roles):
|
||||||
|
schema_key = schema.lower()
|
||||||
|
if schema_key in schema_facts:
|
||||||
|
update_roles(schema_facts, cursor, schema,
|
||||||
|
schema_facts[schema_key]['usage_roles'], [], schema_facts[schema_key]['create_roles'], [])
|
||||||
|
try:
|
||||||
|
cursor.execute("drop schema {0} restrict".format(schema_facts[schema_key]['name']))
|
||||||
|
except pyodbc.Error:
|
||||||
|
raise CannotDropError("Dropping schema failed due to dependencies.")
|
||||||
|
del schema_facts[schema_key]
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# module logic
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
schema=dict(required=True, aliases=['name']),
|
||||||
|
usage_roles=dict(default=None, aliases=['usage_role']),
|
||||||
|
create_roles=dict(default=None, aliases=['create_role']),
|
||||||
|
owner=dict(default=None),
|
||||||
|
state=dict(default='present', choices=['absent', 'present']),
|
||||||
|
db=dict(default=None),
|
||||||
|
cluster=dict(default='localhost'),
|
||||||
|
port=dict(default='5433'),
|
||||||
|
login_user=dict(default='dbadmin'),
|
||||||
|
login_password=dict(default=None),
|
||||||
|
), supports_check_mode = True)
|
||||||
|
|
||||||
|
if not pyodbc_found:
|
||||||
|
module.fail_json(msg="The python pyodbc module is required.")
|
||||||
|
|
||||||
|
schema = module.params['schema']
|
||||||
|
usage_roles = []
|
||||||
|
if module.params['usage_roles']:
|
||||||
|
usage_roles = module.params['usage_roles'].split(',')
|
||||||
|
usage_roles = filter(None, usage_roles)
|
||||||
|
create_roles = []
|
||||||
|
if module.params['create_roles']:
|
||||||
|
create_roles = module.params['create_roles'].split(',')
|
||||||
|
create_roles = filter(None, create_roles)
|
||||||
|
owner = module.params['owner']
|
||||||
|
state = module.params['state']
|
||||||
|
db = ''
|
||||||
|
if module.params['db']:
|
||||||
|
db = module.params['db']
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dsn = (
|
||||||
|
"Driver=Vertica;"
|
||||||
|
"Server={0};"
|
||||||
|
"Port={1};"
|
||||||
|
"Database={2};"
|
||||||
|
"User={3};"
|
||||||
|
"Password={4};"
|
||||||
|
"ConnectionLoadBalance={5}"
|
||||||
|
).format(module.params['cluster'], module.params['port'], db,
|
||||||
|
module.params['login_user'], module.params['login_password'], 'true')
|
||||||
|
db_conn = pyodbc.connect(dsn, autocommit=True)
|
||||||
|
cursor = db_conn.cursor()
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Unable to connect to database: {0}.".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
schema_facts = get_schema_facts(cursor)
|
||||||
|
if module.check_mode:
|
||||||
|
changed = not check(schema_facts, schema, usage_roles, create_roles, owner)
|
||||||
|
elif state == 'absent':
|
||||||
|
try:
|
||||||
|
changed = absent(schema_facts, cursor, schema, usage_roles, create_roles)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
elif state == 'present':
|
||||||
|
try:
|
||||||
|
changed = present(schema_facts, cursor, schema, usage_roles, create_roles, owner)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
except NotSupportedError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_schemas': schema_facts})
|
||||||
|
except CannotDropError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_schemas': schema_facts})
|
||||||
|
except SystemExit:
|
||||||
|
# avoid catching this on python 2.4
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, schema=schema, ansible_facts={'vertica_schemas': schema_facts})
|
||||||
|
|
||||||
|
# import ansible utilities
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,388 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
DOCUMENTATION = """
|
||||||
|
---
|
||||||
|
module: vertica_user
|
||||||
|
version_added: '2.0'
|
||||||
|
short_description: Adds or removes Vertica database users and assigns roles.
|
||||||
|
description:
|
||||||
|
- Adds or removes Vertica database user and, optionally, assigns roles.
|
||||||
|
- A user will not be removed until all the dependencies have been dropped.
|
||||||
|
- In such a situation, if the module tries to remove the user it
|
||||||
|
will fail and only remove roles granted to the user.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the user to add or remove.
|
||||||
|
required: true
|
||||||
|
profile:
|
||||||
|
description:
|
||||||
|
- Sets the user's profile.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
resource_pool:
|
||||||
|
description:
|
||||||
|
- Sets the user's resource pool.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
password:
|
||||||
|
description:
|
||||||
|
- The user's password encrypted by the MD5 algorithm.
|
||||||
|
- The password must be generated with the format C("md5" + md5[password + username]),
|
||||||
|
resulting in a total of 35 characters. An easy way to do this is by querying
|
||||||
|
the Vertica database with select 'md5'||md5('<user_password><user_name>').
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
expired:
|
||||||
|
description:
|
||||||
|
- Sets the user's password expiration.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
ldap:
|
||||||
|
description:
|
||||||
|
- Set to true if users are authenticated via LDAP.
|
||||||
|
- The user will be created with password expired and set to I($ldap$).
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
roles:
|
||||||
|
description:
|
||||||
|
- Comma separated list of roles to assign to the user.
|
||||||
|
aliases: ['role']
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether to create C(present), drop C(absent) or lock C(locked) a user.
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent', 'locked']
|
||||||
|
default: present
|
||||||
|
db:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica database.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
cluster:
|
||||||
|
description:
|
||||||
|
- Name of the Vertica cluster.
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
port:
|
||||||
|
description:
|
||||||
|
- Vertica cluster port to connect to.
|
||||||
|
required: false
|
||||||
|
default: 5433
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- The username used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: dbadmin
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- The password used to authenticate with.
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
notes:
|
||||||
|
- The default authentication assumes that you are either logging in as or sudo'ing
|
||||||
|
to the C(dbadmin) account on the host.
|
||||||
|
- This module uses C(pyodbc), a Python ODBC database adapter. You must ensure
|
||||||
|
that C(unixODBC) and C(pyodbc) is installed on the host and properly configured.
|
||||||
|
- Configuring C(unixODBC) for Vertica requires C(Driver = /opt/vertica/lib64/libverticaodbc.so)
|
||||||
|
to be added to the C(Vertica) section of either C(/etc/odbcinst.ini) or C($HOME/.odbcinst.ini)
|
||||||
|
and both C(ErrorMessagesPath = /opt/vertica/lib64) and C(DriverManagerEncoding = UTF-16)
|
||||||
|
to be added to the C(Driver) section of either C(/etc/vertica.ini) or C($HOME/.vertica.ini).
|
||||||
|
requirements: [ 'unixODBC', 'pyodbc' ]
|
||||||
|
author: "Dariusz Owczarek (@dareko)"
|
||||||
|
"""
|
||||||
|
|
||||||
|
EXAMPLES = """
|
||||||
|
- name: creating a new vertica user with password
|
||||||
|
vertica_user: name=user_name password=md5<encrypted_password> db=db_name state=present
|
||||||
|
|
||||||
|
- name: creating a new vertica user authenticated via ldap with roles assigned
|
||||||
|
vertica_user:
|
||||||
|
name=user_name
|
||||||
|
ldap=true
|
||||||
|
db=db_name
|
||||||
|
roles=schema_name_ro
|
||||||
|
state=present
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyodbc
|
||||||
|
except ImportError:
|
||||||
|
pyodbc_found = False
|
||||||
|
else:
|
||||||
|
pyodbc_found = True
|
||||||
|
|
||||||
|
class NotSupportedError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class CannotDropError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# module specific functions
|
||||||
|
|
||||||
|
def get_user_facts(cursor, user=''):
|
||||||
|
facts = {}
|
||||||
|
cursor.execute("""
|
||||||
|
select u.user_name, u.is_locked, u.lock_time,
|
||||||
|
p.password, p.acctexpired as is_expired,
|
||||||
|
u.profile_name, u.resource_pool,
|
||||||
|
u.all_roles, u.default_roles
|
||||||
|
from users u join password_auditor p on p.user_id = u.user_id
|
||||||
|
where not u.is_super_user
|
||||||
|
and (? = '' or u.user_name ilike ?)
|
||||||
|
""", user, user)
|
||||||
|
while True:
|
||||||
|
rows = cursor.fetchmany(100)
|
||||||
|
if not rows:
|
||||||
|
break
|
||||||
|
for row in rows:
|
||||||
|
user_key = row.user_name.lower()
|
||||||
|
facts[user_key] = {
|
||||||
|
'name': row.user_name,
|
||||||
|
'locked': str(row.is_locked),
|
||||||
|
'password': row.password,
|
||||||
|
'expired': str(row.is_expired),
|
||||||
|
'profile': row.profile_name,
|
||||||
|
'resource_pool': row.resource_pool,
|
||||||
|
'roles': [],
|
||||||
|
'default_roles': []}
|
||||||
|
if row.is_locked:
|
||||||
|
facts[user_key]['locked_time'] = str(row.lock_time)
|
||||||
|
if row.all_roles:
|
||||||
|
facts[user_key]['roles'] = row.all_roles.replace(' ', '').split(',')
|
||||||
|
if row.default_roles:
|
||||||
|
facts[user_key]['default_roles'] = row.default_roles.replace(' ', '').split(',')
|
||||||
|
return facts
|
||||||
|
|
||||||
|
def update_roles(user_facts, cursor, user,
|
||||||
|
existing_all, existing_default, required):
|
||||||
|
del_roles = list(set(existing_all) - set(required))
|
||||||
|
if del_roles:
|
||||||
|
cursor.execute("revoke {0} from {1}".format(','.join(del_roles), user))
|
||||||
|
new_roles = list(set(required) - set(existing_all))
|
||||||
|
if new_roles:
|
||||||
|
cursor.execute("grant {0} to {1}".format(','.join(new_roles), user))
|
||||||
|
if required:
|
||||||
|
cursor.execute("alter user {0} default role {1}".format(user, ','.join(required)))
|
||||||
|
|
||||||
|
def check(user_facts, user, profile, resource_pool,
|
||||||
|
locked, password, expired, ldap, roles):
|
||||||
|
user_key = user.lower()
|
||||||
|
if user_key not in user_facts:
|
||||||
|
return False
|
||||||
|
if profile and profile != user_facts[user_key]['profile']:
|
||||||
|
return False
|
||||||
|
if resource_pool and resource_pool != user_facts[user_key]['resource_pool']:
|
||||||
|
return False
|
||||||
|
if locked != (user_facts[user_key]['locked'] == 'True'):
|
||||||
|
return False
|
||||||
|
if password and password != user_facts[user_key]['password']:
|
||||||
|
return False
|
||||||
|
if expired is not None and expired != (user_facts[user_key]['expired'] == 'True') or \
|
||||||
|
ldap is not None and ldap != (user_facts[user_key]['expired'] == 'True'):
|
||||||
|
return False
|
||||||
|
if roles and (cmp(sorted(roles), sorted(user_facts[user_key]['roles'])) != 0 or \
|
||||||
|
cmp(sorted(roles), sorted(user_facts[user_key]['default_roles'])) != 0):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def present(user_facts, cursor, user, profile, resource_pool,
|
||||||
|
locked, password, expired, ldap, roles):
|
||||||
|
user_key = user.lower()
|
||||||
|
if user_key not in user_facts:
|
||||||
|
query_fragments = ["create user {0}".format(user)]
|
||||||
|
if locked:
|
||||||
|
query_fragments.append("account lock")
|
||||||
|
if password or ldap:
|
||||||
|
if password:
|
||||||
|
query_fragments.append("identified by '{0}'".format(password))
|
||||||
|
else:
|
||||||
|
query_fragments.append("identified by '$ldap$'")
|
||||||
|
if expired or ldap:
|
||||||
|
query_fragments.append("password expire")
|
||||||
|
if profile:
|
||||||
|
query_fragments.append("profile {0}".format(profile))
|
||||||
|
if resource_pool:
|
||||||
|
query_fragments.append("resource pool {0}".format(resource_pool))
|
||||||
|
cursor.execute(' '.join(query_fragments))
|
||||||
|
if resource_pool and resource_pool != 'general':
|
||||||
|
cursor.execute("grant usage on resource pool {0} to {1}".format(
|
||||||
|
resource_pool, user))
|
||||||
|
update_roles(user_facts, cursor, user, [], [], roles)
|
||||||
|
user_facts.update(get_user_facts(cursor, user))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
changed = False
|
||||||
|
query_fragments = ["alter user {0}".format(user)]
|
||||||
|
if locked is not None and locked != (user_facts[user_key]['locked'] == 'True'):
|
||||||
|
if locked:
|
||||||
|
state = 'lock'
|
||||||
|
else:
|
||||||
|
state = 'unlock'
|
||||||
|
query_fragments.append("account {0}".format(state))
|
||||||
|
changed = True
|
||||||
|
if password and password != user_facts[user_key]['password']:
|
||||||
|
query_fragments.append("identified by '{0}'".format(password))
|
||||||
|
changed = True
|
||||||
|
if ldap:
|
||||||
|
if ldap != (user_facts[user_key]['expired'] == 'True'):
|
||||||
|
query_fragments.append("password expire")
|
||||||
|
changed = True
|
||||||
|
elif expired is not None and expired != (user_facts[user_key]['expired'] == 'True'):
|
||||||
|
if expired:
|
||||||
|
query_fragments.append("password expire")
|
||||||
|
changed = True
|
||||||
|
else:
|
||||||
|
raise NotSupportedError("Unexpiring user password is not supported.")
|
||||||
|
if profile and profile != user_facts[user_key]['profile']:
|
||||||
|
query_fragments.append("profile {0}".format(profile))
|
||||||
|
changed = True
|
||||||
|
if resource_pool and resource_pool != user_facts[user_key]['resource_pool']:
|
||||||
|
query_fragments.append("resource pool {0}".format(resource_pool))
|
||||||
|
if user_facts[user_key]['resource_pool'] != 'general':
|
||||||
|
cursor.execute("revoke usage on resource pool {0} from {1}".format(
|
||||||
|
user_facts[user_key]['resource_pool'], user))
|
||||||
|
if resource_pool != 'general':
|
||||||
|
cursor.execute("grant usage on resource pool {0} to {1}".format(
|
||||||
|
resource_pool, user))
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
cursor.execute(' '.join(query_fragments))
|
||||||
|
if roles and (cmp(sorted(roles), sorted(user_facts[user_key]['roles'])) != 0 or \
|
||||||
|
cmp(sorted(roles), sorted(user_facts[user_key]['default_roles'])) != 0):
|
||||||
|
update_roles(user_facts, cursor, user,
|
||||||
|
user_facts[user_key]['roles'], user_facts[user_key]['default_roles'], roles)
|
||||||
|
changed = True
|
||||||
|
if changed:
|
||||||
|
user_facts.update(get_user_facts(cursor, user))
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def absent(user_facts, cursor, user, roles):
|
||||||
|
user_key = user.lower()
|
||||||
|
if user_key in user_facts:
|
||||||
|
update_roles(user_facts, cursor, user,
|
||||||
|
user_facts[user_key]['roles'], user_facts[user_key]['default_roles'], [])
|
||||||
|
try:
|
||||||
|
cursor.execute("drop user {0}".format(user_facts[user_key]['name']))
|
||||||
|
except pyodbc.Error:
|
||||||
|
raise CannotDropError("Dropping user failed due to dependencies.")
|
||||||
|
del user_facts[user_key]
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# module logic
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
user=dict(required=True, aliases=['name']),
|
||||||
|
profile=dict(default=None),
|
||||||
|
resource_pool=dict(default=None),
|
||||||
|
password=dict(default=None),
|
||||||
|
expired=dict(type='bool', default=None),
|
||||||
|
ldap=dict(type='bool', default=None),
|
||||||
|
roles=dict(default=None, aliases=['role']),
|
||||||
|
state=dict(default='present', choices=['absent', 'present', 'locked']),
|
||||||
|
db=dict(default=None),
|
||||||
|
cluster=dict(default='localhost'),
|
||||||
|
port=dict(default='5433'),
|
||||||
|
login_user=dict(default='dbadmin'),
|
||||||
|
login_password=dict(default=None),
|
||||||
|
), supports_check_mode = True)
|
||||||
|
|
||||||
|
if not pyodbc_found:
|
||||||
|
module.fail_json(msg="The python pyodbc module is required.")
|
||||||
|
|
||||||
|
user = module.params['user']
|
||||||
|
profile = module.params['profile']
|
||||||
|
if profile:
|
||||||
|
profile = profile.lower()
|
||||||
|
resource_pool = module.params['resource_pool']
|
||||||
|
if resource_pool:
|
||||||
|
resource_pool = resource_pool.lower()
|
||||||
|
password = module.params['password']
|
||||||
|
expired = module.params['expired']
|
||||||
|
ldap = module.params['ldap']
|
||||||
|
roles = []
|
||||||
|
if module.params['roles']:
|
||||||
|
roles = module.params['roles'].split(',')
|
||||||
|
roles = filter(None, roles)
|
||||||
|
state = module.params['state']
|
||||||
|
if state == 'locked':
|
||||||
|
locked = True
|
||||||
|
else:
|
||||||
|
locked = False
|
||||||
|
db = ''
|
||||||
|
if module.params['db']:
|
||||||
|
db = module.params['db']
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
dsn = (
|
||||||
|
"Driver=Vertica;"
|
||||||
|
"Server={0};"
|
||||||
|
"Port={1};"
|
||||||
|
"Database={2};"
|
||||||
|
"User={3};"
|
||||||
|
"Password={4};"
|
||||||
|
"ConnectionLoadBalance={5}"
|
||||||
|
).format(module.params['cluster'], module.params['port'], db,
|
||||||
|
module.params['login_user'], module.params['login_password'], 'true')
|
||||||
|
db_conn = pyodbc.connect(dsn, autocommit=True)
|
||||||
|
cursor = db_conn.cursor()
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Unable to connect to database: {0}.".format(e))
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_facts = get_user_facts(cursor)
|
||||||
|
if module.check_mode:
|
||||||
|
changed = not check(user_facts, user, profile, resource_pool,
|
||||||
|
locked, password, expired, ldap, roles)
|
||||||
|
elif state == 'absent':
|
||||||
|
try:
|
||||||
|
changed = absent(user_facts, cursor, user, roles)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
elif state in ['present', 'locked']:
|
||||||
|
try:
|
||||||
|
changed = present(user_facts, cursor, user, profile, resource_pool,
|
||||||
|
locked, password, expired, ldap, roles)
|
||||||
|
except pyodbc.Error, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
except NotSupportedError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_users': user_facts})
|
||||||
|
except CannotDropError, e:
|
||||||
|
module.fail_json(msg=str(e), ansible_facts={'vertica_users': user_facts})
|
||||||
|
except SystemExit:
|
||||||
|
# avoid catching this on python 2.4
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=e)
|
||||||
|
|
||||||
|
module.exit_json(changed=changed, user=user, ansible_facts={'vertica_users': user_facts})
|
||||||
|
|
||||||
|
# import ansible utilities
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2015, Manuel Sousa <manuel.sousa@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rabbitmq_binding
|
||||||
|
author: "Manuel Sousa (@manuel-sousa)"
|
||||||
|
version_added: "2.0"
|
||||||
|
|
||||||
|
short_description: This module manages rabbitMQ bindings
|
||||||
|
description:
|
||||||
|
- This module uses rabbitMQ Rest API to create/delete bindings
|
||||||
|
requirements: [ python requests ]
|
||||||
|
options:
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the exchange should be present or absent
|
||||||
|
- Only present implemented atm
|
||||||
|
choices: [ "present", "absent" ]
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- source exchange to create binding on
|
||||||
|
required: true
|
||||||
|
aliases: [ "src", "source" ]
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- rabbitMQ user for connection
|
||||||
|
required: false
|
||||||
|
default: guest
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- rabbitMQ password for connection
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
login_host:
|
||||||
|
description:
|
||||||
|
- rabbitMQ host for connection
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
login_port:
|
||||||
|
description:
|
||||||
|
- rabbitMQ management api port
|
||||||
|
required: false
|
||||||
|
default: 15672
|
||||||
|
vhost:
|
||||||
|
description:
|
||||||
|
- rabbitMQ virtual host
|
||||||
|
- default vhost is /
|
||||||
|
required: false
|
||||||
|
default: "/"
|
||||||
|
destination:
|
||||||
|
description:
|
||||||
|
- destination exchange or queue for the binding
|
||||||
|
required: true
|
||||||
|
aliases: [ "dst", "dest" ]
|
||||||
|
destination_type:
|
||||||
|
description:
|
||||||
|
- Either queue or exchange
|
||||||
|
required: true
|
||||||
|
choices: [ "queue", "exchange" ]
|
||||||
|
aliases: [ "type", "dest_type" ]
|
||||||
|
routing_key:
|
||||||
|
description:
|
||||||
|
- routing key for the binding
|
||||||
|
- default is #
|
||||||
|
required: false
|
||||||
|
default: "#"
|
||||||
|
arguments:
|
||||||
|
description:
|
||||||
|
- extra arguments for exchange. If defined this argument is a key/value dictionary
|
||||||
|
required: false
|
||||||
|
default: {}
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Bind myQueue to directExchange with routing key info
|
||||||
|
- rabbitmq_binding: name=directExchange destination=myQueue type=queue routing_key=info
|
||||||
|
|
||||||
|
# Bind directExchange to topicExchange with routing key *.info
|
||||||
|
- rabbitmq_binding: name=topicExchange destination=topicExchange type=exchange routing_key="*.info"
|
||||||
|
'''
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib
|
||||||
|
import json
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
state = dict(default='present', choices=['present', 'absent'], type='str'),
|
||||||
|
name = dict(required=True, aliases=[ "src", "source" ], type='str'),
|
||||||
|
login_user = dict(default='guest', type='str'),
|
||||||
|
login_password = dict(default='guest', type='str', no_log=True),
|
||||||
|
login_host = dict(default='localhost', type='str'),
|
||||||
|
login_port = dict(default='15672', type='str'),
|
||||||
|
vhost = dict(default='/', type='str'),
|
||||||
|
destination = dict(required=True, aliases=[ "dst", "dest"], type='str'),
|
||||||
|
destination_type = dict(required=True, aliases=[ "type", "dest_type"], choices=[ "queue", "exchange" ],type='str'),
|
||||||
|
routing_key = dict(default='#', type='str'),
|
||||||
|
arguments = dict(default=dict(), type='dict')
|
||||||
|
),
|
||||||
|
supports_check_mode = True
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['destination_type'] == "queue":
|
||||||
|
dest_type="q"
|
||||||
|
else:
|
||||||
|
dest_type="e"
|
||||||
|
|
||||||
|
url = "http://%s:%s/api/bindings/%s/e/%s/%s/%s/%s" % (
|
||||||
|
module.params['login_host'],
|
||||||
|
module.params['login_port'],
|
||||||
|
urllib.quote(module.params['vhost'],''),
|
||||||
|
module.params['name'],
|
||||||
|
dest_type,
|
||||||
|
module.params['destination'],
|
||||||
|
urllib.quote(module.params['routing_key'],'')
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if exchange already exists
|
||||||
|
r = requests.get( url, auth=(module.params['login_user'],module.params['login_password']))
|
||||||
|
|
||||||
|
if r.status_code==200:
|
||||||
|
binding_exists = True
|
||||||
|
response = r.json()
|
||||||
|
elif r.status_code==404:
|
||||||
|
binding_exists = False
|
||||||
|
response = r.text
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg = "Invalid response from RESTAPI when trying to check if exchange exists",
|
||||||
|
details = r.text
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['state']=='present':
|
||||||
|
change_required = not binding_exists
|
||||||
|
else:
|
||||||
|
change_required = binding_exists
|
||||||
|
|
||||||
|
# Exit if check_mode
|
||||||
|
if module.check_mode:
|
||||||
|
module.exit_json(
|
||||||
|
changed= change_required,
|
||||||
|
name = module.params['name'],
|
||||||
|
details = response,
|
||||||
|
arguments = module.params['arguments']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Do changes
|
||||||
|
if change_required:
|
||||||
|
if module.params['state'] == 'present':
|
||||||
|
url = "http://%s:%s/api/bindings/%s/e/%s/%s/%s" % (
|
||||||
|
module.params['login_host'],
|
||||||
|
module.params['login_port'],
|
||||||
|
urllib.quote(module.params['vhost'],''),
|
||||||
|
module.params['name'],
|
||||||
|
dest_type,
|
||||||
|
module.params['destination']
|
||||||
|
)
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
auth = (module.params['login_user'],module.params['login_password']),
|
||||||
|
headers = { "content-type": "application/json"},
|
||||||
|
data = json.dumps({
|
||||||
|
"routing_key": module.params['routing_key'],
|
||||||
|
"arguments": module.params['arguments']
|
||||||
|
})
|
||||||
|
)
|
||||||
|
elif module.params['state'] == 'absent':
|
||||||
|
r = requests.delete( url, auth = (module.params['login_user'],module.params['login_password']))
|
||||||
|
|
||||||
|
if r.status_code == 204 or r.status_code == 201:
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
name = module.params['name'],
|
||||||
|
destination = module.params['destination']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg = "Error creating exchange",
|
||||||
|
status = r.status_code,
|
||||||
|
details = r.text
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
name = module.params['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2015, Manuel Sousa <manuel.sousa@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rabbitmq_exchange
|
||||||
|
author: "Manuel Sousa (@manuel-sousa)"
|
||||||
|
version_added: "2.0"
|
||||||
|
|
||||||
|
short_description: This module manages rabbitMQ exchanges
|
||||||
|
description:
|
||||||
|
- This module uses rabbitMQ Rest API to create/delete exchanges
|
||||||
|
requirements: [ python requests ]
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the exchange to create
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the exchange should be present or absent
|
||||||
|
- Only present implemented atm
|
||||||
|
choices: [ "present", "absent" ]
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- rabbitMQ user for connection
|
||||||
|
required: false
|
||||||
|
default: guest
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- rabbitMQ password for connection
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
login_host:
|
||||||
|
description:
|
||||||
|
- rabbitMQ host for connection
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
login_port:
|
||||||
|
description:
|
||||||
|
- rabbitMQ management api port
|
||||||
|
required: false
|
||||||
|
default: 15672
|
||||||
|
vhost:
|
||||||
|
description:
|
||||||
|
- rabbitMQ virtual host
|
||||||
|
required: false
|
||||||
|
default: "/"
|
||||||
|
durable:
|
||||||
|
description:
|
||||||
|
- whether exchange is durable or not
|
||||||
|
required: false
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
default: yes
|
||||||
|
exchange_type:
|
||||||
|
description:
|
||||||
|
- type for the exchange
|
||||||
|
required: false
|
||||||
|
choices: [ "fanout", "direct", "headers", "topic" ]
|
||||||
|
aliases: [ "type" ]
|
||||||
|
default: direct
|
||||||
|
auto_delete:
|
||||||
|
description:
|
||||||
|
- if the exchange should delete itself after all queues/exchanges unbound from it
|
||||||
|
required: false
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
default: no
|
||||||
|
internal:
|
||||||
|
description:
|
||||||
|
- exchange is available only for other exchanges
|
||||||
|
required: false
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
default: no
|
||||||
|
arguments:
|
||||||
|
description:
|
||||||
|
- extra arguments for exchange. If defined this argument is a key/value dictionary
|
||||||
|
required: false
|
||||||
|
default: {}
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create direct exchange
|
||||||
|
- rabbitmq_exchange: name=directExchange
|
||||||
|
|
||||||
|
# Create topic exchange on vhost
|
||||||
|
- rabbitmq_exchange: name=topicExchange type=topic vhost=myVhost
|
||||||
|
'''
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib
|
||||||
|
import json
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
state = dict(default='present', choices=['present', 'absent'], type='str'),
|
||||||
|
name = dict(required=True, type='str'),
|
||||||
|
login_user = dict(default='guest', type='str'),
|
||||||
|
login_password = dict(default='guest', type='str', no_log=True),
|
||||||
|
login_host = dict(default='localhost', type='str'),
|
||||||
|
login_port = dict(default='15672', type='str'),
|
||||||
|
vhost = dict(default='/', type='str'),
|
||||||
|
durable = dict(default=True, choices=BOOLEANS, type='bool'),
|
||||||
|
auto_delete = dict(default=False, choices=BOOLEANS, type='bool'),
|
||||||
|
internal = dict(default=False, choices=BOOLEANS, type='bool'),
|
||||||
|
exchange_type = dict(default='direct', aliases=['type'], type='str'),
|
||||||
|
arguments = dict(default=dict(), type='dict')
|
||||||
|
),
|
||||||
|
supports_check_mode = True
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "http://%s:%s/api/exchanges/%s/%s" % (
|
||||||
|
module.params['login_host'],
|
||||||
|
module.params['login_port'],
|
||||||
|
urllib.quote(module.params['vhost'],''),
|
||||||
|
module.params['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if exchange already exists
|
||||||
|
r = requests.get( url, auth=(module.params['login_user'],module.params['login_password']))
|
||||||
|
|
||||||
|
if r.status_code==200:
|
||||||
|
exchange_exists = True
|
||||||
|
response = r.json()
|
||||||
|
elif r.status_code==404:
|
||||||
|
exchange_exists = False
|
||||||
|
response = r.text
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg = "Invalid response from RESTAPI when trying to check if exchange exists",
|
||||||
|
details = r.text
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['state']=='present':
|
||||||
|
change_required = not exchange_exists
|
||||||
|
else:
|
||||||
|
change_required = exchange_exists
|
||||||
|
|
||||||
|
# Check if attributes change on existing exchange
|
||||||
|
if not change_required and r.status_code==200 and module.params['state'] == 'present':
|
||||||
|
if not (
|
||||||
|
response['durable'] == module.params['durable'] and
|
||||||
|
response['auto_delete'] == module.params['auto_delete'] and
|
||||||
|
response['internal'] == module.params['internal'] and
|
||||||
|
response['type'] == module.params['exchange_type']
|
||||||
|
):
|
||||||
|
module.fail_json(
|
||||||
|
msg = "RabbitMQ RESTAPI doesn't support attribute changes for existing exchanges"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Exit if check_mode
|
||||||
|
if module.check_mode:
|
||||||
|
module.exit_json(
|
||||||
|
changed= change_required,
|
||||||
|
name = module.params['name'],
|
||||||
|
details = response,
|
||||||
|
arguments = module.params['arguments']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Do changes
|
||||||
|
if change_required:
|
||||||
|
if module.params['state'] == 'present':
|
||||||
|
r = requests.put(
|
||||||
|
url,
|
||||||
|
auth = (module.params['login_user'],module.params['login_password']),
|
||||||
|
headers = { "content-type": "application/json"},
|
||||||
|
data = json.dumps({
|
||||||
|
"durable": module.params['durable'],
|
||||||
|
"auto_delete": module.params['auto_delete'],
|
||||||
|
"internal": module.params['internal'],
|
||||||
|
"type": module.params['exchange_type'],
|
||||||
|
"arguments": module.params['arguments']
|
||||||
|
})
|
||||||
|
)
|
||||||
|
elif module.params['state'] == 'absent':
|
||||||
|
r = requests.delete( url, auth = (module.params['login_user'],module.params['login_password']))
|
||||||
|
|
||||||
|
if r.status_code == 204:
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
name = module.params['name']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg = "Error creating exchange",
|
||||||
|
status = r.status_code,
|
||||||
|
details = r.text
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
name = module.params['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2015, Manuel Sousa <manuel.sousa@gmail.com>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: rabbitmq_queue
|
||||||
|
author: "Manuel Sousa (@manuel-sousa)"
|
||||||
|
version_added: "2.0"
|
||||||
|
|
||||||
|
short_description: This module manages rabbitMQ queues
|
||||||
|
description:
|
||||||
|
- This module uses rabbitMQ Rest API to create/delete queues
|
||||||
|
requirements: [ python requests ]
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- Name of the queue to create
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- Whether the queue should be present or absent
|
||||||
|
- Only present implemented atm
|
||||||
|
choices: [ "present", "absent" ]
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- rabbitMQ user for connection
|
||||||
|
required: false
|
||||||
|
default: guest
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- rabbitMQ password for connection
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
login_host:
|
||||||
|
description:
|
||||||
|
- rabbitMQ host for connection
|
||||||
|
required: false
|
||||||
|
default: localhost
|
||||||
|
login_port:
|
||||||
|
description:
|
||||||
|
- rabbitMQ management api port
|
||||||
|
required: false
|
||||||
|
default: 15672
|
||||||
|
vhost:
|
||||||
|
description:
|
||||||
|
- rabbitMQ virtual host
|
||||||
|
required: false
|
||||||
|
default: "/"
|
||||||
|
durable:
|
||||||
|
description:
|
||||||
|
- whether queue is durable or not
|
||||||
|
required: false
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
default: yes
|
||||||
|
auto_delete:
|
||||||
|
description:
|
||||||
|
- if the queue should delete itself after all queues/queues unbound from it
|
||||||
|
required: false
|
||||||
|
choices: [ "yes", "no" ]
|
||||||
|
default: no
|
||||||
|
message_ttl:
|
||||||
|
description:
|
||||||
|
- How long a message can live in queue before it is discarded (milliseconds)
|
||||||
|
required: False
|
||||||
|
default: forever
|
||||||
|
auto_expires:
|
||||||
|
description:
|
||||||
|
- How long a queue can be unused before it is automatically deleted (milliseconds)
|
||||||
|
required: false
|
||||||
|
default: forever
|
||||||
|
max_length:
|
||||||
|
description:
|
||||||
|
- How many messages can the queue contain before it starts rejecting
|
||||||
|
required: false
|
||||||
|
default: no limit
|
||||||
|
dead_letter_exchange:
|
||||||
|
description:
|
||||||
|
- Optional name of an exchange to which messages will be republished if they
|
||||||
|
- are rejected or expire
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
dead_letter_routing_key:
|
||||||
|
description:
|
||||||
|
- Optional replacement routing key to use when a message is dead-lettered.
|
||||||
|
- Original routing key will be used if unset
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
arguments:
|
||||||
|
description:
|
||||||
|
- extra arguments for queue. If defined this argument is a key/value dictionary
|
||||||
|
required: false
|
||||||
|
default: {}
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a queue
|
||||||
|
- rabbitmq_queue: name=myQueue
|
||||||
|
|
||||||
|
# Create a queue on remote host
|
||||||
|
- rabbitmq_queue: name=myRemoteQueue login_user=user login_password=secret login_host=remote.example.org
|
||||||
|
'''
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import urllib
|
||||||
|
import json
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec = dict(
|
||||||
|
state = dict(default='present', choices=['present', 'absent'], type='str'),
|
||||||
|
name = dict(required=True, type='str'),
|
||||||
|
login_user = dict(default='guest', type='str'),
|
||||||
|
login_password = dict(default='guest', type='str', no_log=True),
|
||||||
|
login_host = dict(default='localhost', type='str'),
|
||||||
|
login_port = dict(default='15672', type='str'),
|
||||||
|
vhost = dict(default='/', type='str'),
|
||||||
|
durable = dict(default=True, choices=BOOLEANS, type='bool'),
|
||||||
|
auto_delete = dict(default=False, choices=BOOLEANS, type='bool'),
|
||||||
|
message_ttl = dict(default=None, type='int'),
|
||||||
|
auto_expires = dict(default=None, type='int'),
|
||||||
|
max_length = dict(default=None, type='int'),
|
||||||
|
dead_letter_exchange = dict(default=None, type='str'),
|
||||||
|
dead_letter_routing_key = dict(default=None, type='str'),
|
||||||
|
arguments = dict(default=dict(), type='dict')
|
||||||
|
),
|
||||||
|
supports_check_mode = True
|
||||||
|
)
|
||||||
|
|
||||||
|
url = "http://%s:%s/api/queues/%s/%s" % (
|
||||||
|
module.params['login_host'],
|
||||||
|
module.params['login_port'],
|
||||||
|
urllib.quote(module.params['vhost'],''),
|
||||||
|
module.params['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if queue already exists
|
||||||
|
r = requests.get( url, auth=(module.params['login_user'],module.params['login_password']))
|
||||||
|
|
||||||
|
if r.status_code==200:
|
||||||
|
queue_exists = True
|
||||||
|
response = r.json()
|
||||||
|
elif r.status_code==404:
|
||||||
|
queue_exists = False
|
||||||
|
response = r.text
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg = "Invalid response from RESTAPI when trying to check if queue exists",
|
||||||
|
details = r.text
|
||||||
|
)
|
||||||
|
|
||||||
|
if module.params['state']=='present':
|
||||||
|
change_required = not queue_exists
|
||||||
|
else:
|
||||||
|
change_required = queue_exists
|
||||||
|
|
||||||
|
# Check if attributes change on existing queue
|
||||||
|
if not change_required and r.status_code==200 and module.params['state'] == 'present':
|
||||||
|
if not (
|
||||||
|
response['durable'] == module.params['durable'] and
|
||||||
|
response['auto_delete'] == module.params['auto_delete'] and
|
||||||
|
(
|
||||||
|
( 'x-message-ttl' in response['arguments'] and response['arguments']['x-message-ttl'] == module.params['message_ttl'] ) or
|
||||||
|
( 'x-message-ttl' not in response['arguments'] and module.params['message_ttl'] is None )
|
||||||
|
) and
|
||||||
|
(
|
||||||
|
( 'x-expires' in response['arguments'] and response['arguments']['x-expires'] == module.params['auto_expires'] ) or
|
||||||
|
( 'x-expires' not in response['arguments'] and module.params['auto_expires'] is None )
|
||||||
|
) and
|
||||||
|
(
|
||||||
|
( 'x-max-length' in response['arguments'] and response['arguments']['x-max-length'] == module.params['max_length'] ) or
|
||||||
|
( 'x-max-length' not in response['arguments'] and module.params['max_length'] is None )
|
||||||
|
) and
|
||||||
|
(
|
||||||
|
( 'x-dead-letter-exchange' in response['arguments'] and response['arguments']['x-dead-letter-exchange'] == module.params['dead_letter_exchange'] ) or
|
||||||
|
( 'x-dead-letter-exchange' not in response['arguments'] and module.params['dead_letter_exchange'] is None )
|
||||||
|
) and
|
||||||
|
(
|
||||||
|
( 'x-dead-letter-routing-key' in response['arguments'] and response['arguments']['x-dead-letter-routing-key'] == module.params['dead_letter_routing_key'] ) or
|
||||||
|
( 'x-dead-letter-routing-key' not in response['arguments'] and module.params['dead_letter_routing_key'] is None )
|
||||||
|
)
|
||||||
|
):
|
||||||
|
module.fail_json(
|
||||||
|
msg = "RabbitMQ RESTAPI doesn't support attribute changes for existing queues",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Copy parameters to arguments as used by RabbitMQ
|
||||||
|
for k,v in {
|
||||||
|
'message_ttl': 'x-message-ttl',
|
||||||
|
'auto_expires': 'x-expires',
|
||||||
|
'max_length': 'x-max-length',
|
||||||
|
'dead_letter_exchange': 'x-dead-letter-exchange',
|
||||||
|
'dead_letter_routing_key': 'x-dead-letter-routing-key'
|
||||||
|
}.items():
|
||||||
|
if module.params[k]:
|
||||||
|
module.params['arguments'][v] = module.params[k]
|
||||||
|
|
||||||
|
# Exit if check_mode
|
||||||
|
if module.check_mode:
|
||||||
|
module.exit_json(
|
||||||
|
changed= change_required,
|
||||||
|
name = module.params['name'],
|
||||||
|
details = response,
|
||||||
|
arguments = module.params['arguments']
|
||||||
|
)
|
||||||
|
|
||||||
|
# Do changes
|
||||||
|
if change_required:
|
||||||
|
if module.params['state'] == 'present':
|
||||||
|
r = requests.put(
|
||||||
|
url,
|
||||||
|
auth = (module.params['login_user'],module.params['login_password']),
|
||||||
|
headers = { "content-type": "application/json"},
|
||||||
|
data = json.dumps({
|
||||||
|
"durable": module.params['durable'],
|
||||||
|
"auto_delete": module.params['auto_delete'],
|
||||||
|
"arguments": module.params['arguments']
|
||||||
|
})
|
||||||
|
)
|
||||||
|
elif module.params['state'] == 'absent':
|
||||||
|
r = requests.delete( url, auth = (module.params['login_user'],module.params['login_password']))
|
||||||
|
|
||||||
|
if r.status_code == 204:
|
||||||
|
module.exit_json(
|
||||||
|
changed = True,
|
||||||
|
name = module.params['name']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
module.fail_json(
|
||||||
|
msg = "Error creating queue",
|
||||||
|
status = r.status_code,
|
||||||
|
details = r.text
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
module.exit_json(
|
||||||
|
changed = False,
|
||||||
|
name = module.params['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# import module snippets
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
# (c) 2014-2015, Epic Games, Inc.
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: circonus_annotation
|
||||||
|
short_description: create an annotation in circonus
|
||||||
|
description:
|
||||||
|
- Create an annotation event with a given category, title and description. Optionally start, end or durations can be provided
|
||||||
|
author: "Nick Harring (@NickatEpic)"
|
||||||
|
version_added: 2.0
|
||||||
|
requirements:
|
||||||
|
- urllib3
|
||||||
|
- requests
|
||||||
|
- time
|
||||||
|
options:
|
||||||
|
api_key:
|
||||||
|
description:
|
||||||
|
- Circonus API key
|
||||||
|
required: true
|
||||||
|
category:
|
||||||
|
description:
|
||||||
|
- Annotation Category
|
||||||
|
required: true
|
||||||
|
description:
|
||||||
|
description:
|
||||||
|
- Description of annotation
|
||||||
|
required: true
|
||||||
|
title:
|
||||||
|
description:
|
||||||
|
- Title of annotation
|
||||||
|
required: true
|
||||||
|
start:
|
||||||
|
description:
|
||||||
|
- Unix timestamp of event start, defaults to now
|
||||||
|
required: false
|
||||||
|
stop:
|
||||||
|
description:
|
||||||
|
- Unix timestamp of event end, defaults to now + duration
|
||||||
|
required: false
|
||||||
|
duration:
|
||||||
|
description:
|
||||||
|
- Duration in seconds of annotation, defaults to 0
|
||||||
|
required: false
|
||||||
|
'''
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a simple annotation event with a source, defaults to start and end time of now
|
||||||
|
- circonus_annotation:
|
||||||
|
api_key: XXXXXXXXXXXXXXXXX
|
||||||
|
title: 'App Config Change'
|
||||||
|
description: 'This is a detailed description of the config change'
|
||||||
|
category: 'This category groups like annotations'
|
||||||
|
# Create an annotation with a duration of 5 minutes and a default start time of now
|
||||||
|
- circonus_annotation:
|
||||||
|
api_key: XXXXXXXXXXXXXXXXX
|
||||||
|
title: 'App Config Change'
|
||||||
|
description: 'This is a detailed description of the config change'
|
||||||
|
category: 'This category groups like annotations'
|
||||||
|
duration: 300
|
||||||
|
# Create an annotation with a start_time and end_time
|
||||||
|
- circonus_annotation:
|
||||||
|
api_key: XXXXXXXXXXXXXXXXX
|
||||||
|
title: 'App Config Change'
|
||||||
|
description: 'This is a detailed description of the config change'
|
||||||
|
category: 'This category groups like annotations'
|
||||||
|
start_time: 1395940006
|
||||||
|
end_time: 1395954407
|
||||||
|
'''
|
||||||
|
def post_annotation(annotation, api_key):
|
||||||
|
''' Takes annotation dict and api_key string'''
|
||||||
|
base_url = 'https://api.circonus.com/v2'
|
||||||
|
anootate_post_endpoint = '/annotation'
|
||||||
|
resp = requests.post(base_url + anootate_post_endpoint,
|
||||||
|
headers=build_headers(api_key), data=json.dumps(annotation))
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp
|
||||||
|
|
||||||
|
def create_annotation(module):
|
||||||
|
''' Takes ansible module object '''
|
||||||
|
annotation = {}
|
||||||
|
if module.params['duration'] != None:
|
||||||
|
duration = module.params['duration']
|
||||||
|
else:
|
||||||
|
duration = 0
|
||||||
|
if module.params['start'] != None:
|
||||||
|
start = module.params['start']
|
||||||
|
else:
|
||||||
|
start = int(time.time())
|
||||||
|
if module.params['stop'] != None:
|
||||||
|
stop = module.params['stop']
|
||||||
|
else:
|
||||||
|
stop = int(time.time())+ duration
|
||||||
|
annotation['start'] = int(start)
|
||||||
|
annotation['stop'] = int(stop)
|
||||||
|
annotation['category'] = module.params['category']
|
||||||
|
annotation['description'] = module.params['description']
|
||||||
|
annotation['title'] = module.params['title']
|
||||||
|
return annotation
|
||||||
|
def build_headers(api_token):
|
||||||
|
'''Takes api token, returns headers with it included.'''
|
||||||
|
headers = {'X-Circonus-App-Name': 'ansible',
|
||||||
|
'Host': 'api.circonus.com', 'X-Circonus-Auth-Token': api_token,
|
||||||
|
'Accept': 'application/json'}
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def main():
|
||||||
|
'''Main function, dispatches logic'''
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
start=dict(required=False, type='int'),
|
||||||
|
stop=dict(required=False, type='int'),
|
||||||
|
category=dict(required=True),
|
||||||
|
title=dict(required=True),
|
||||||
|
description=dict(required=True),
|
||||||
|
duration=dict(required=False, type='int'),
|
||||||
|
api_key=dict(required=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
annotation = create_annotation(module)
|
||||||
|
try:
|
||||||
|
resp = post_annotation(annotation, module.params['api_key'])
|
||||||
|
except requests.exceptions.RequestException, err_str:
|
||||||
|
module.fail_json(msg='Request Failed', reason=err_str)
|
||||||
|
module.exit_json(changed=True, annotation=resp.json())
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,283 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2015, Sebastian Kornehl <sebastian.kornehl@asideas.de>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
# import module snippets
|
||||||
|
|
||||||
|
# Import Datadog
|
||||||
|
try:
|
||||||
|
from datadog import initialize, api
|
||||||
|
HAS_DATADOG = True
|
||||||
|
except:
|
||||||
|
HAS_DATADOG = False
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: datadog_monitor
|
||||||
|
short_description: Manages Datadog monitors
|
||||||
|
description:
|
||||||
|
- "Manages monitors within Datadog"
|
||||||
|
- "Options like described on http://docs.datadoghq.com/api/"
|
||||||
|
version_added: "2.0"
|
||||||
|
author: "Sebastian Kornehl (@skornehl)"
|
||||||
|
notes: []
|
||||||
|
requirements: [datadog]
|
||||||
|
options:
|
||||||
|
api_key:
|
||||||
|
description: ["Your DataDog API key."]
|
||||||
|
required: true
|
||||||
|
app_key:
|
||||||
|
description: ["Your DataDog app key."]
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description: ["The designated state of the monitor."]
|
||||||
|
required: true
|
||||||
|
choices: ['present', 'absent', 'muted', 'unmuted']
|
||||||
|
type:
|
||||||
|
description: ["The type of the monitor."]
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
choices: ['metric alert', 'service check']
|
||||||
|
query:
|
||||||
|
description: ["he monitor query to notify on with syntax varying depending on what type of monitor you are creating."]
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
name:
|
||||||
|
description: ["The name of the alert."]
|
||||||
|
required: true
|
||||||
|
message:
|
||||||
|
description: ["A message to include with notifications for this monitor. Email notifications can be sent to specific users by using the same '@username' notation as events."]
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
silenced:
|
||||||
|
description: ["Dictionary of scopes to timestamps or None. Each scope will be muted until the given POSIX timestamp or forever if the value is None. "]
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
notify_no_data:
|
||||||
|
description: ["A boolean indicating whether this monitor will notify when data stops reporting.."]
|
||||||
|
required: false
|
||||||
|
default: False
|
||||||
|
no_data_timeframe:
|
||||||
|
description: ["The number of minutes before a monitor will notify when data stops reporting. Must be at least 2x the monitor timeframe for metric alerts or 2 minutes for service checks."]
|
||||||
|
required: false
|
||||||
|
default: 2x timeframe for metric, 2 minutes for service
|
||||||
|
timeout_h:
|
||||||
|
description: ["The number of hours of the monitor not reporting data before it will automatically resolve from a triggered state."]
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
renotify_interval:
|
||||||
|
description: ["The number of minutes after the last notification before a monitor will re-notify on the current status. It will only re-notify if it's not resolved."]
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
escalation_message:
|
||||||
|
description: ["A message to include with a re-notification. Supports the '@username' notification we allow elsewhere. Not applicable if renotify_interval is None"]
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
notify_audit:
|
||||||
|
description: ["A boolean indicating whether tagged users will be notified on changes to this monitor."]
|
||||||
|
required: false
|
||||||
|
default: False
|
||||||
|
thresholds:
|
||||||
|
description: ["A dictionary of thresholds by status. Because service checks can have multiple thresholds, we don't define them directly in the query."]
|
||||||
|
required: false
|
||||||
|
default: {'ok': 1, 'critical': 1, 'warning': 1}
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create a metric monitor
|
||||||
|
datadog_monitor:
|
||||||
|
type: "metric alert"
|
||||||
|
name: "Test monitor"
|
||||||
|
state: "present"
|
||||||
|
query: "datadog.agent.up".over("host:host1").last(2).count_by_status()"
|
||||||
|
message: "Some message."
|
||||||
|
api_key: "9775a026f1ca7d1c6c5af9d94d9595a4"
|
||||||
|
app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff"
|
||||||
|
|
||||||
|
# Deletes a monitor
|
||||||
|
datadog_monitor:
|
||||||
|
name: "Test monitor"
|
||||||
|
state: "absent"
|
||||||
|
api_key: "9775a026f1ca7d1c6c5af9d94d9595a4"
|
||||||
|
app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff"
|
||||||
|
|
||||||
|
# Mutes a monitor
|
||||||
|
datadog_monitor:
|
||||||
|
name: "Test monitor"
|
||||||
|
state: "mute"
|
||||||
|
silenced: '{"*":None}'
|
||||||
|
api_key: "9775a026f1ca7d1c6c5af9d94d9595a4"
|
||||||
|
app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff"
|
||||||
|
|
||||||
|
# Unmutes a monitor
|
||||||
|
datadog_monitor:
|
||||||
|
name: "Test monitor"
|
||||||
|
state: "unmute"
|
||||||
|
api_key: "9775a026f1ca7d1c6c5af9d94d9595a4"
|
||||||
|
app_key: "87ce4a24b5553d2e482ea8a8500e71b8ad4554ff"
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
api_key=dict(required=True),
|
||||||
|
app_key=dict(required=True),
|
||||||
|
state=dict(required=True, choises=['present', 'absent', 'mute', 'unmute']),
|
||||||
|
type=dict(required=False, choises=['metric alert', 'service check']),
|
||||||
|
name=dict(required=True),
|
||||||
|
query=dict(required=False),
|
||||||
|
message=dict(required=False, default=None),
|
||||||
|
silenced=dict(required=False, default=None, type='dict'),
|
||||||
|
notify_no_data=dict(required=False, default=False, choices=BOOLEANS),
|
||||||
|
no_data_timeframe=dict(required=False, default=None),
|
||||||
|
timeout_h=dict(required=False, default=None),
|
||||||
|
renotify_interval=dict(required=False, default=None),
|
||||||
|
escalation_message=dict(required=False, default=None),
|
||||||
|
notify_audit=dict(required=False, default=False, choices=BOOLEANS),
|
||||||
|
thresholds=dict(required=False, type='dict', default={'ok': 1, 'critical': 1, 'warning': 1}),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare Datadog
|
||||||
|
if not HAS_DATADOG:
|
||||||
|
module.fail_json(msg='datadogpy required for this module')
|
||||||
|
|
||||||
|
options = {
|
||||||
|
'api_key': module.params['api_key'],
|
||||||
|
'app_key': module.params['app_key']
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(**options)
|
||||||
|
|
||||||
|
if module.params['state'] == 'present':
|
||||||
|
install_monitor(module)
|
||||||
|
elif module.params['state'] == 'absent':
|
||||||
|
delete_monitor(module)
|
||||||
|
elif module.params['state'] == 'mute':
|
||||||
|
mute_monitor(module)
|
||||||
|
elif module.params['state'] == 'unmute':
|
||||||
|
unmute_monitor(module)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_monitor(module):
|
||||||
|
for monitor in api.Monitor.get_all():
|
||||||
|
if monitor['name'] == module.params['name']:
|
||||||
|
return monitor
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def _post_monitor(module, options):
|
||||||
|
try:
|
||||||
|
msg = api.Monitor.create(type=module.params['type'], query=module.params['query'],
|
||||||
|
name=module.params['name'], message=module.params['message'],
|
||||||
|
options=options)
|
||||||
|
if 'errors' in msg:
|
||||||
|
module.fail_json(msg=str(msg['errors']))
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=True, msg=msg)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
def _equal_dicts(a, b, ignore_keys):
|
||||||
|
ka = set(a).difference(ignore_keys)
|
||||||
|
kb = set(b).difference(ignore_keys)
|
||||||
|
return ka == kb and all(a[k] == b[k] for k in ka)
|
||||||
|
|
||||||
|
def _update_monitor(module, monitor, options):
|
||||||
|
try:
|
||||||
|
msg = api.Monitor.update(id=monitor['id'], query=module.params['query'],
|
||||||
|
name=module.params['name'], message=module.params['message'],
|
||||||
|
options=options)
|
||||||
|
if 'errors' in msg:
|
||||||
|
module.fail_json(msg=str(msg['errors']))
|
||||||
|
elif _equal_dicts(msg, monitor, ['creator', 'overall_state']):
|
||||||
|
module.exit_json(changed=False, msg=msg)
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=True, msg=msg)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def install_monitor(module):
|
||||||
|
options = {
|
||||||
|
"silenced": module.params['silenced'],
|
||||||
|
"notify_no_data": module.boolean(module.params['notify_no_data']),
|
||||||
|
"no_data_timeframe": module.params['no_data_timeframe'],
|
||||||
|
"timeout_h": module.params['timeout_h'],
|
||||||
|
"renotify_interval": module.params['renotify_interval'],
|
||||||
|
"escalation_message": module.params['escalation_message'],
|
||||||
|
"notify_audit": module.boolean(module.params['notify_audit']),
|
||||||
|
}
|
||||||
|
|
||||||
|
if module.params['type'] == "service check":
|
||||||
|
options["thresholds"] = module.params['thresholds']
|
||||||
|
|
||||||
|
monitor = _get_monitor(module)
|
||||||
|
if not monitor:
|
||||||
|
_post_monitor(module, options)
|
||||||
|
else:
|
||||||
|
_update_monitor(module, monitor, options)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_monitor(module):
|
||||||
|
monitor = _get_monitor(module)
|
||||||
|
if not monitor:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
try:
|
||||||
|
msg = api.Monitor.delete(monitor['id'])
|
||||||
|
module.exit_json(changed=True, msg=msg)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def mute_monitor(module):
|
||||||
|
monitor = _get_monitor(module)
|
||||||
|
if not monitor:
|
||||||
|
module.fail_json(msg="Monitor %s not found!" % module.params['name'])
|
||||||
|
elif monitor['options']['silenced']:
|
||||||
|
module.fail_json(msg="Monitor is already muted. Datadog does not allow to modify muted alerts, consider unmuting it first.")
|
||||||
|
elif (module.params['silenced'] is not None
|
||||||
|
and len(set(monitor['options']['silenced']) - set(module.params['silenced'])) == 0):
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
try:
|
||||||
|
if module.params['silenced'] is None or module.params['silenced'] == "":
|
||||||
|
msg = api.Monitor.mute(id=monitor['id'])
|
||||||
|
else:
|
||||||
|
msg = api.Monitor.mute(id=monitor['id'], silenced=module.params['silenced'])
|
||||||
|
module.exit_json(changed=True, msg=msg)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
def unmute_monitor(module):
|
||||||
|
monitor = _get_monitor(module)
|
||||||
|
if not monitor:
|
||||||
|
module.fail_json(msg="Monitor %s not found!" % module.params['name'])
|
||||||
|
elif not monitor['options']['silenced']:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
try:
|
||||||
|
msg = api.Monitor.unmute(monitor['id'])
|
||||||
|
module.exit_json(changed=True, msg=msg)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
from ansible.module_utils.urls import *
|
||||||
|
main()
|
@ -0,0 +1,336 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2014, Anders Ingemann <aim@secoya.dk>
|
||||||
|
#
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: sensu_check
|
||||||
|
short_description: Manage Sensu checks
|
||||||
|
version_added: 2.0
|
||||||
|
description:
|
||||||
|
- Manage the checks that should be run on a machine by I(Sensu).
|
||||||
|
- Most options do not have a default and will not be added to the check definition unless specified.
|
||||||
|
- All defaults except I(path), I(state), I(backup) and I(metric) are not managed by this module,
|
||||||
|
- they are simply specified for your convenience.
|
||||||
|
options:
|
||||||
|
name:
|
||||||
|
description:
|
||||||
|
- The name of the check
|
||||||
|
- This is the key that is used to determine whether a check exists
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description: Whether the check should be present or not
|
||||||
|
choices: [ 'present', 'absent' ]
|
||||||
|
required: false
|
||||||
|
default: present
|
||||||
|
path:
|
||||||
|
description:
|
||||||
|
- Path to the json file of the check to be added/removed.
|
||||||
|
- Will be created if it does not exist (unless I(state=absent)).
|
||||||
|
- The parent folders need to exist when I(state=present), otherwise an error will be thrown
|
||||||
|
required: false
|
||||||
|
default: /etc/sensu/conf.d/checks.json
|
||||||
|
backup:
|
||||||
|
description:
|
||||||
|
- Create a backup file (if yes), including the timestamp information so
|
||||||
|
- you can get the original file back if you somehow clobbered it incorrectly.
|
||||||
|
choices: [ 'yes', 'no' ]
|
||||||
|
required: false
|
||||||
|
default: no
|
||||||
|
command:
|
||||||
|
description:
|
||||||
|
- Path to the sensu check to run (not required when I(state=absent))
|
||||||
|
required: true
|
||||||
|
handlers:
|
||||||
|
description:
|
||||||
|
- List of handlers to notify when the check fails
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
subscribers:
|
||||||
|
description:
|
||||||
|
- List of subscribers/channels this check should run for
|
||||||
|
- See sensu_subscribers to subscribe a machine to a channel
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
interval:
|
||||||
|
description:
|
||||||
|
- Check interval in seconds
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- Timeout for the check
|
||||||
|
required: false
|
||||||
|
default: 10
|
||||||
|
handle:
|
||||||
|
description:
|
||||||
|
- Whether the check should be handled or not
|
||||||
|
choices: [ 'yes', 'no' ]
|
||||||
|
required: false
|
||||||
|
default: yes
|
||||||
|
subdue_begin:
|
||||||
|
description:
|
||||||
|
- When to disable handling of check failures
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
subdue_end:
|
||||||
|
description:
|
||||||
|
- When to enable handling of check failures
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
dependencies:
|
||||||
|
description:
|
||||||
|
- Other checks this check depends on, if dependencies fail,
|
||||||
|
- handling of this check will be disabled
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
metric:
|
||||||
|
description: Whether the check is a metric
|
||||||
|
choices: [ 'yes', 'no' ]
|
||||||
|
required: false
|
||||||
|
default: no
|
||||||
|
standalone:
|
||||||
|
description:
|
||||||
|
- Whether the check should be scheduled by the sensu client or server
|
||||||
|
- This option obviates the need for specifying the I(subscribers) option
|
||||||
|
choices: [ 'yes', 'no' ]
|
||||||
|
required: false
|
||||||
|
default: no
|
||||||
|
publish:
|
||||||
|
description:
|
||||||
|
- Whether the check should be scheduled at all.
|
||||||
|
- You can still issue it via the sensu api
|
||||||
|
choices: [ 'yes', 'no' ]
|
||||||
|
required: false
|
||||||
|
default: yes
|
||||||
|
occurrences:
|
||||||
|
description:
|
||||||
|
- Number of event occurrences before the handler should take action
|
||||||
|
required: false
|
||||||
|
default: 1
|
||||||
|
refresh:
|
||||||
|
description:
|
||||||
|
- Number of seconds handlers should wait before taking second action
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
aggregate:
|
||||||
|
description:
|
||||||
|
- Classifies the check as an aggregate check,
|
||||||
|
- making it available via the aggregate API
|
||||||
|
choices: [ 'yes', 'no' ]
|
||||||
|
required: false
|
||||||
|
default: no
|
||||||
|
low_flap_threshold:
|
||||||
|
description:
|
||||||
|
- The low threshhold for flap detection
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
high_flap_threshold:
|
||||||
|
description:
|
||||||
|
- The low threshhold for flap detection
|
||||||
|
required: false
|
||||||
|
default: null
|
||||||
|
requirements: [ ]
|
||||||
|
author: Anders Ingemann
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Fetch metrics about the CPU load every 60 seconds,
|
||||||
|
# the sensu server has a handler called 'relay' which forwards stats to graphite
|
||||||
|
- name: get cpu metrics
|
||||||
|
sensu_check: name=cpu_load
|
||||||
|
command=/etc/sensu/plugins/system/cpu-mpstat-metrics.rb
|
||||||
|
metric=yes handlers=relay subscribers=common interval=60
|
||||||
|
|
||||||
|
# Check whether nginx is running
|
||||||
|
- name: check nginx process
|
||||||
|
sensu_check: name=nginx_running
|
||||||
|
command='/etc/sensu/plugins/processes/check-procs.rb -f /var/run/nginx.pid'
|
||||||
|
handlers=default subscribers=nginx interval=60
|
||||||
|
|
||||||
|
# Stop monitoring the disk capacity.
|
||||||
|
# Note that the check will still show up in the sensu dashboard,
|
||||||
|
# to remove it completely you need to issue a DELETE request to the sensu api.
|
||||||
|
- name: check disk
|
||||||
|
sensu_check: name=check_disk_capacity
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
def sensu_check(module, path, name, state='present', backup=False):
|
||||||
|
changed = False
|
||||||
|
reasons = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
stream = open(path, 'r')
|
||||||
|
config = json.load(stream.read())
|
||||||
|
except IOError, e:
|
||||||
|
if e.errno is 2: # File not found, non-fatal
|
||||||
|
if state == 'absent':
|
||||||
|
reasons.append('file did not exist and state is `absent\'')
|
||||||
|
return changed, reasons
|
||||||
|
config = {}
|
||||||
|
else:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
except ValueError:
|
||||||
|
msg = '{path} contains invalid JSON'.format(path=path)
|
||||||
|
module.fail_json(msg=msg)
|
||||||
|
finally:
|
||||||
|
if stream:
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
if 'checks' not in config:
|
||||||
|
if state == 'absent':
|
||||||
|
reasons.append('`checks\' section did not exist and state is `absent\'')
|
||||||
|
return changed, reasons
|
||||||
|
config['checks'] = {}
|
||||||
|
changed = True
|
||||||
|
reasons.append('`checks\' section did not exist')
|
||||||
|
|
||||||
|
if state == 'absent':
|
||||||
|
if name in config['checks']:
|
||||||
|
del config['checks'][name]
|
||||||
|
changed = True
|
||||||
|
reasons.append('check was present and state is `absent\'')
|
||||||
|
|
||||||
|
if state == 'present':
|
||||||
|
if name not in config['checks']:
|
||||||
|
check = {}
|
||||||
|
config['checks'][name] = check
|
||||||
|
changed = True
|
||||||
|
reasons.append('check was absent and state is `present\'')
|
||||||
|
else:
|
||||||
|
check = config['checks'][name]
|
||||||
|
simple_opts = ['command',
|
||||||
|
'handlers',
|
||||||
|
'subscribers',
|
||||||
|
'interval',
|
||||||
|
'timeout',
|
||||||
|
'handle',
|
||||||
|
'dependencies',
|
||||||
|
'standalone',
|
||||||
|
'publish',
|
||||||
|
'occurrences',
|
||||||
|
'refresh',
|
||||||
|
'aggregate',
|
||||||
|
'low_flap_threshold',
|
||||||
|
'high_flap_threshold',
|
||||||
|
]
|
||||||
|
for opt in simple_opts:
|
||||||
|
if module.params[opt] is not None:
|
||||||
|
if opt not in check or check[opt] != module.params[opt]:
|
||||||
|
check[opt] = module.params[opt]
|
||||||
|
changed = True
|
||||||
|
reasons.append('`{opt}\' did not exist or was different'.format(opt=opt))
|
||||||
|
else:
|
||||||
|
if opt in check:
|
||||||
|
del check[opt]
|
||||||
|
changed = True
|
||||||
|
reasons.append('`{opt}\' was removed'.format(opt=opt))
|
||||||
|
|
||||||
|
if module.params['metric']:
|
||||||
|
if 'type' not in check or check['type'] != 'metric':
|
||||||
|
check['type'] = 'metric'
|
||||||
|
changed = True
|
||||||
|
reasons.append('`type\' was not defined or not `metric\'')
|
||||||
|
if not module.params['metric'] and 'type' in check:
|
||||||
|
del check['type']
|
||||||
|
changed = True
|
||||||
|
reasons.append('`type\' was defined')
|
||||||
|
|
||||||
|
if module.params['subdue_begin'] is not None and module.params['subdue_end'] is not None:
|
||||||
|
subdue = {'begin': module.params['subdue_begin'],
|
||||||
|
'end': module.params['subdue_end'],
|
||||||
|
}
|
||||||
|
if 'subdue' not in check or check['subdue'] != subdue:
|
||||||
|
check['subdue'] = subdue
|
||||||
|
changed = True
|
||||||
|
reasons.append('`subdue\' did not exist or was different')
|
||||||
|
else:
|
||||||
|
if 'subdue' in check:
|
||||||
|
del check['subdue']
|
||||||
|
changed = True
|
||||||
|
reasons.append('`subdue\' was removed')
|
||||||
|
|
||||||
|
if changed and not module.check_mode:
|
||||||
|
if backup:
|
||||||
|
module.backup_local(path)
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
stream = open(path, 'w')
|
||||||
|
stream.write(json.dumps(config, indent=2) + '\n')
|
||||||
|
except IOError, e:
|
||||||
|
module.fail_json(msg=str(e))
|
||||||
|
finally:
|
||||||
|
if stream:
|
||||||
|
stream.close()
|
||||||
|
|
||||||
|
return changed, reasons
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
|
||||||
|
arg_spec = {'name': {'type': 'str', 'required': True},
|
||||||
|
'path': {'type': 'str', 'default': '/etc/sensu/conf.d/checks.json'},
|
||||||
|
'state': {'type': 'str', 'default': 'present', 'choices': ['present', 'absent']},
|
||||||
|
'backup': {'type': 'bool', 'default': 'no'},
|
||||||
|
'command': {'type': 'str'},
|
||||||
|
'handlers': {'type': 'list'},
|
||||||
|
'subscribers': {'type': 'list'},
|
||||||
|
'interval': {'type': 'int'},
|
||||||
|
'timeout': {'type': 'int'},
|
||||||
|
'handle': {'type': 'bool'},
|
||||||
|
'subdue_begin': {'type': 'str'},
|
||||||
|
'subdue_end': {'type': 'str'},
|
||||||
|
'dependencies': {'type': 'list'},
|
||||||
|
'metric': {'type': 'bool', 'default': 'no'},
|
||||||
|
'standalone': {'type': 'bool'},
|
||||||
|
'publish': {'type': 'bool'},
|
||||||
|
'occurrences': {'type': 'int'},
|
||||||
|
'refresh': {'type': 'int'},
|
||||||
|
'aggregate': {'type': 'bool'},
|
||||||
|
'low_flap_threshold': {'type': 'int'},
|
||||||
|
'high_flap_threshold': {'type': 'int'},
|
||||||
|
}
|
||||||
|
|
||||||
|
required_together = [['subdue_begin', 'subdue_end']]
|
||||||
|
|
||||||
|
module = AnsibleModule(argument_spec=arg_spec,
|
||||||
|
required_together=required_together,
|
||||||
|
supports_check_mode=True)
|
||||||
|
if module.params['state'] != 'absent' and module.params['command'] is None:
|
||||||
|
module.fail_json(msg="missing required arguments: %s" % ",".join(['command']))
|
||||||
|
|
||||||
|
path = module.params['path']
|
||||||
|
name = module.params['name']
|
||||||
|
state = module.params['state']
|
||||||
|
backup = module.params['backup']
|
||||||
|
|
||||||
|
changed, reasons = sensu_check(module, path, name, state, backup)
|
||||||
|
|
||||||
|
module.exit_json(path=path, changed=changed, msg='OK', name=name, reasons=reasons)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
@ -0,0 +1,485 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2013-2014, Epic Games, 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: zabbix_host
|
||||||
|
short_description: Zabbix host creates/updates/deletes
|
||||||
|
description:
|
||||||
|
- This module allows you to create, modify and delete Zabbix host entries and associated group and template data.
|
||||||
|
version_added: "2.0"
|
||||||
|
author:
|
||||||
|
- "(@cove)"
|
||||||
|
- "Tony Minfei Ding"
|
||||||
|
- "Harrison Gu (@harrisongu)"
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- zabbix-api
|
||||||
|
options:
|
||||||
|
server_url:
|
||||||
|
description:
|
||||||
|
- Url of Zabbix server, with protocol (http or https).
|
||||||
|
required: true
|
||||||
|
aliases: [ "url" ]
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- Zabbix user name, used to authenticate against the server.
|
||||||
|
required: true
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- Zabbix user password.
|
||||||
|
required: true
|
||||||
|
host_name:
|
||||||
|
description:
|
||||||
|
- Name of the host in Zabbix.
|
||||||
|
- host_name is the unique identifier used and cannot be updated using this module.
|
||||||
|
required: true
|
||||||
|
host_groups:
|
||||||
|
description:
|
||||||
|
- List of host groups the host is part of.
|
||||||
|
required: false
|
||||||
|
link_templates:
|
||||||
|
description:
|
||||||
|
- List of templates linked to the host.
|
||||||
|
required: false
|
||||||
|
default: None
|
||||||
|
status:
|
||||||
|
description:
|
||||||
|
- Monitoring status of the host.
|
||||||
|
required: false
|
||||||
|
choices: ['enabled', 'disabled']
|
||||||
|
default: "enabled"
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the host.
|
||||||
|
- On C(present), it will create if host does not exist or update the host if the associated data is different.
|
||||||
|
- On C(absent) will remove a host if it exists.
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- The timeout of API request (seconds).
|
||||||
|
default: 10
|
||||||
|
proxy:
|
||||||
|
description:
|
||||||
|
- The name of the Zabbix Proxy to be used
|
||||||
|
default: None
|
||||||
|
interfaces:
|
||||||
|
description:
|
||||||
|
- List of interfaces to be created for the host (see example below).
|
||||||
|
- 'Available values are: dns, ip, main, port, type and useip.'
|
||||||
|
- Please review the interface documentation for more information on the supported properties
|
||||||
|
- 'https://www.zabbix.com/documentation/2.0/manual/appendix/api/hostinterface/definitions#host_interface'
|
||||||
|
required: false
|
||||||
|
default: []
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create a new host or update an existing host's info
|
||||||
|
local_action:
|
||||||
|
module: zabbix_host
|
||||||
|
server_url: http://monitor.example.com
|
||||||
|
login_user: username
|
||||||
|
login_password: password
|
||||||
|
host_name: ExampleHost
|
||||||
|
host_groups:
|
||||||
|
- Example group1
|
||||||
|
- Example group2
|
||||||
|
link_templates:
|
||||||
|
- Example template1
|
||||||
|
- Example template2
|
||||||
|
status: enabled
|
||||||
|
state: present
|
||||||
|
interfaces:
|
||||||
|
- type: 1
|
||||||
|
main: 1
|
||||||
|
useip: 1
|
||||||
|
ip: 10.xx.xx.xx
|
||||||
|
dns: ""
|
||||||
|
port: 10050
|
||||||
|
- type: 4
|
||||||
|
main: 1
|
||||||
|
useip: 1
|
||||||
|
ip: 10.xx.xx.xx
|
||||||
|
dns: ""
|
||||||
|
port: 12345
|
||||||
|
proxy: a.zabbix.proxy
|
||||||
|
'''
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import copy
|
||||||
|
|
||||||
|
try:
|
||||||
|
from zabbix_api import ZabbixAPI, ZabbixAPISubClass
|
||||||
|
|
||||||
|
HAS_ZABBIX_API = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ZABBIX_API = False
|
||||||
|
|
||||||
|
|
||||||
|
# Extend the ZabbixAPI
|
||||||
|
# Since the zabbix-api python module too old (version 1.0, no higher version so far),
|
||||||
|
# it does not support the 'hostinterface' api calls,
|
||||||
|
# so we have to inherit the ZabbixAPI class to add 'hostinterface' support.
|
||||||
|
class ZabbixAPIExtends(ZabbixAPI):
|
||||||
|
hostinterface = None
|
||||||
|
|
||||||
|
def __init__(self, server, timeout, **kwargs):
|
||||||
|
ZabbixAPI.__init__(self, server, timeout=timeout)
|
||||||
|
self.hostinterface = ZabbixAPISubClass(self, dict({"prefix": "hostinterface"}, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
class Host(object):
|
||||||
|
def __init__(self, module, zbx):
|
||||||
|
self._module = module
|
||||||
|
self._zapi = zbx
|
||||||
|
|
||||||
|
# exist host
|
||||||
|
def is_host_exist(self, host_name):
|
||||||
|
result = self._zapi.host.exists({'host': host_name})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# check if host group exists
|
||||||
|
def check_host_group_exist(self, group_names):
|
||||||
|
for group_name in group_names:
|
||||||
|
result = self._zapi.hostgroup.exists({'name': group_name})
|
||||||
|
if not result:
|
||||||
|
self._module.fail_json(msg="Hostgroup not found: %s" % group_name)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_template_ids(self, template_list):
|
||||||
|
template_ids = []
|
||||||
|
if template_list is None or len(template_list) == 0:
|
||||||
|
return template_ids
|
||||||
|
for template in template_list:
|
||||||
|
template_list = self._zapi.template.get({'output': 'extend', 'filter': {'host': template}})
|
||||||
|
if len(template_list) < 1:
|
||||||
|
self._module.fail_json(msg="Template not found: %s" % template)
|
||||||
|
else:
|
||||||
|
template_id = template_list[0]['templateid']
|
||||||
|
template_ids.append(template_id)
|
||||||
|
return template_ids
|
||||||
|
|
||||||
|
def add_host(self, host_name, group_ids, status, interfaces, proxy_id):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
parameters = {'host': host_name, 'interfaces': interfaces, 'groups': group_ids, 'status': status}
|
||||||
|
if proxy_id:
|
||||||
|
parameters['proxy_hostid'] = proxy_id
|
||||||
|
host_list = self._zapi.host.create(parameters)
|
||||||
|
if len(host_list) >= 1:
|
||||||
|
return host_list['hostids'][0]
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to create host %s: %s" % (host_name, e))
|
||||||
|
|
||||||
|
def update_host(self, host_name, group_ids, status, host_id, interfaces, exist_interface_list, proxy_id):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
parameters = {'hostid': host_id, 'groups': group_ids, 'status': status, 'proxy_hostid': proxy_id}
|
||||||
|
self._zapi.host.update(parameters)
|
||||||
|
interface_list_copy = exist_interface_list
|
||||||
|
if interfaces:
|
||||||
|
for interface in interfaces:
|
||||||
|
flag = False
|
||||||
|
interface_str = interface
|
||||||
|
for exist_interface in exist_interface_list:
|
||||||
|
interface_type = interface['type']
|
||||||
|
exist_interface_type = int(exist_interface['type'])
|
||||||
|
if interface_type == exist_interface_type:
|
||||||
|
# update
|
||||||
|
interface_str['interfaceid'] = exist_interface['interfaceid']
|
||||||
|
self._zapi.hostinterface.update(interface_str)
|
||||||
|
flag = True
|
||||||
|
interface_list_copy.remove(exist_interface)
|
||||||
|
break
|
||||||
|
if not flag:
|
||||||
|
# add
|
||||||
|
interface_str['hostid'] = host_id
|
||||||
|
self._zapi.hostinterface.create(interface_str)
|
||||||
|
# remove
|
||||||
|
remove_interface_ids = []
|
||||||
|
for remove_interface in interface_list_copy:
|
||||||
|
interface_id = remove_interface['interfaceid']
|
||||||
|
remove_interface_ids.append(interface_id)
|
||||||
|
if len(remove_interface_ids) > 0:
|
||||||
|
self._zapi.hostinterface.delete(remove_interface_ids)
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to update host %s: %s" % (host_name, e))
|
||||||
|
|
||||||
|
def delete_host(self, host_id, host_name):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.host.delete({'hostid': host_id})
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to delete host %s: %s" % (host_name, e))
|
||||||
|
|
||||||
|
# get host by host name
|
||||||
|
def get_host_by_host_name(self, host_name):
|
||||||
|
host_list = self._zapi.host.get({'output': 'extend', 'filter': {'host': [host_name]}})
|
||||||
|
if len(host_list) < 1:
|
||||||
|
self._module.fail_json(msg="Host not found: %s" % host_name)
|
||||||
|
else:
|
||||||
|
return host_list[0]
|
||||||
|
|
||||||
|
# get proxyid by proxy name
|
||||||
|
def get_proxyid_by_proxy_name(self, proxy_name):
|
||||||
|
proxy_list = self._zapi.proxy.get({'output': 'extend', 'filter': {'host': [proxy_name]}})
|
||||||
|
if len(proxy_list) < 1:
|
||||||
|
self._module.fail_json(msg="Proxy not found: %s" % proxy_name)
|
||||||
|
else:
|
||||||
|
return proxy_list[0]['proxyid']
|
||||||
|
|
||||||
|
# get group ids by group names
|
||||||
|
def get_group_ids_by_group_names(self, group_names):
|
||||||
|
group_ids = []
|
||||||
|
if self.check_host_group_exist(group_names):
|
||||||
|
group_list = self._zapi.hostgroup.get({'output': 'extend', 'filter': {'name': group_names}})
|
||||||
|
for group in group_list:
|
||||||
|
group_id = group['groupid']
|
||||||
|
group_ids.append({'groupid': group_id})
|
||||||
|
return group_ids
|
||||||
|
|
||||||
|
# get host templates by host id
|
||||||
|
def get_host_templates_by_host_id(self, host_id):
|
||||||
|
template_ids = []
|
||||||
|
template_list = self._zapi.template.get({'output': 'extend', 'hostids': host_id})
|
||||||
|
for template in template_list:
|
||||||
|
template_ids.append(template['templateid'])
|
||||||
|
return template_ids
|
||||||
|
|
||||||
|
# get host groups by host id
|
||||||
|
def get_host_groups_by_host_id(self, host_id):
|
||||||
|
exist_host_groups = []
|
||||||
|
host_groups_list = self._zapi.hostgroup.get({'output': 'extend', 'hostids': host_id})
|
||||||
|
|
||||||
|
if len(host_groups_list) >= 1:
|
||||||
|
for host_groups_name in host_groups_list:
|
||||||
|
exist_host_groups.append(host_groups_name['name'])
|
||||||
|
return exist_host_groups
|
||||||
|
|
||||||
|
# check the exist_interfaces whether it equals the interfaces or not
|
||||||
|
def check_interface_properties(self, exist_interface_list, interfaces):
|
||||||
|
interfaces_port_list = []
|
||||||
|
if len(interfaces) >= 1:
|
||||||
|
for interface in interfaces:
|
||||||
|
interfaces_port_list.append(int(interface['port']))
|
||||||
|
|
||||||
|
exist_interface_ports = []
|
||||||
|
if len(exist_interface_list) >= 1:
|
||||||
|
for exist_interface in exist_interface_list:
|
||||||
|
exist_interface_ports.append(int(exist_interface['port']))
|
||||||
|
|
||||||
|
if set(interfaces_port_list) != set(exist_interface_ports):
|
||||||
|
return True
|
||||||
|
|
||||||
|
for exist_interface in exist_interface_list:
|
||||||
|
exit_interface_port = int(exist_interface['port'])
|
||||||
|
for interface in interfaces:
|
||||||
|
interface_port = int(interface['port'])
|
||||||
|
if interface_port == exit_interface_port:
|
||||||
|
for key in interface.keys():
|
||||||
|
if str(exist_interface[key]) != str(interface[key]):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# get the status of host by host
|
||||||
|
def get_host_status_by_host(self, host):
|
||||||
|
return host['status']
|
||||||
|
|
||||||
|
# check all the properties before link or clear template
|
||||||
|
def check_all_properties(self, host_id, host_groups, status, interfaces, template_ids,
|
||||||
|
exist_interfaces, host, proxy_id):
|
||||||
|
# get the existing host's groups
|
||||||
|
exist_host_groups = self.get_host_groups_by_host_id(host_id)
|
||||||
|
if set(host_groups) != set(exist_host_groups):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# get the existing status
|
||||||
|
exist_status = self.get_host_status_by_host(host)
|
||||||
|
if int(status) != int(exist_status):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# check the exist_interfaces whether it equals the interfaces or not
|
||||||
|
if self.check_interface_properties(exist_interfaces, interfaces):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# get the existing templates
|
||||||
|
exist_template_ids = self.get_host_templates_by_host_id(host_id)
|
||||||
|
if set(list(template_ids)) != set(exist_template_ids):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if host['proxy_hostid'] != proxy_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# link or clear template of the host
|
||||||
|
def link_or_clear_template(self, host_id, template_id_list):
|
||||||
|
# get host's exist template ids
|
||||||
|
exist_template_id_list = self.get_host_templates_by_host_id(host_id)
|
||||||
|
|
||||||
|
exist_template_ids = set(exist_template_id_list)
|
||||||
|
template_ids = set(template_id_list)
|
||||||
|
template_id_list = list(template_ids)
|
||||||
|
|
||||||
|
# get unlink and clear templates
|
||||||
|
templates_clear = exist_template_ids.difference(template_ids)
|
||||||
|
templates_clear_list = list(templates_clear)
|
||||||
|
request_str = {'hostid': host_id, 'templates': template_id_list, 'templates_clear': templates_clear_list}
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.host.update(request_str)
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to link template to host: %s" % e)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
server_url=dict(required=True, aliases=['url']),
|
||||||
|
login_user=dict(required=True),
|
||||||
|
login_password=dict(required=True, no_log=True),
|
||||||
|
host_name=dict(required=True),
|
||||||
|
host_groups=dict(required=False),
|
||||||
|
link_templates=dict(required=False),
|
||||||
|
status=dict(default="enabled", choices=['enabled', 'disabled']),
|
||||||
|
state=dict(default="present", choices=['present', 'absent']),
|
||||||
|
timeout=dict(type='int', default=10),
|
||||||
|
interfaces=dict(required=False),
|
||||||
|
proxy=dict(required=False)
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_ZABBIX_API:
|
||||||
|
module.fail_json(msg="Missing requried zabbix-api module (check docs or install with: pip install zabbix-api)")
|
||||||
|
|
||||||
|
server_url = module.params['server_url']
|
||||||
|
login_user = module.params['login_user']
|
||||||
|
login_password = module.params['login_password']
|
||||||
|
host_name = module.params['host_name']
|
||||||
|
host_groups = module.params['host_groups']
|
||||||
|
link_templates = module.params['link_templates']
|
||||||
|
status = module.params['status']
|
||||||
|
state = module.params['state']
|
||||||
|
timeout = module.params['timeout']
|
||||||
|
interfaces = module.params['interfaces']
|
||||||
|
proxy = module.params['proxy']
|
||||||
|
|
||||||
|
# convert enabled to 0; disabled to 1
|
||||||
|
status = 1 if status == "disabled" else 0
|
||||||
|
|
||||||
|
zbx = None
|
||||||
|
# login to zabbix
|
||||||
|
try:
|
||||||
|
zbx = ZabbixAPIExtends(server_url, timeout=timeout)
|
||||||
|
zbx.login(login_user, login_password)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failed to connect to Zabbix server: %s" % e)
|
||||||
|
|
||||||
|
host = Host(module, zbx)
|
||||||
|
|
||||||
|
template_ids = []
|
||||||
|
if link_templates:
|
||||||
|
template_ids = host.get_template_ids(link_templates)
|
||||||
|
|
||||||
|
group_ids = []
|
||||||
|
|
||||||
|
if host_groups:
|
||||||
|
group_ids = host.get_group_ids_by_group_names(host_groups)
|
||||||
|
|
||||||
|
ip = ""
|
||||||
|
if interfaces:
|
||||||
|
for interface in interfaces:
|
||||||
|
if interface['type'] == 1:
|
||||||
|
ip = interface['ip']
|
||||||
|
|
||||||
|
proxy_id = "0"
|
||||||
|
|
||||||
|
if proxy:
|
||||||
|
proxy_id = host.get_proxyid_by_proxy_name(proxy)
|
||||||
|
|
||||||
|
# check if host exist
|
||||||
|
is_host_exist = host.is_host_exist(host_name)
|
||||||
|
|
||||||
|
if is_host_exist:
|
||||||
|
# get host id by host name
|
||||||
|
zabbix_host_obj = host.get_host_by_host_name(host_name)
|
||||||
|
host_id = zabbix_host_obj['hostid']
|
||||||
|
|
||||||
|
if state == "absent":
|
||||||
|
# remove host
|
||||||
|
host.delete_host(host_id, host_name)
|
||||||
|
module.exit_json(changed=True, result="Successfully delete host %s" % host_name)
|
||||||
|
else:
|
||||||
|
if not group_ids:
|
||||||
|
module.fail_json(msg="Specify at least one group for updating host '%s'." % host_name)
|
||||||
|
|
||||||
|
# get exist host's interfaces
|
||||||
|
exist_interfaces = host._zapi.hostinterface.get({'output': 'extend', 'hostids': host_id})
|
||||||
|
exist_interfaces_copy = copy.deepcopy(exist_interfaces)
|
||||||
|
|
||||||
|
# update host
|
||||||
|
interfaces_len = len(interfaces) if interfaces else 0
|
||||||
|
|
||||||
|
if len(exist_interfaces) > interfaces_len:
|
||||||
|
if host.check_all_properties(host_id, host_groups, status, interfaces, template_ids,
|
||||||
|
exist_interfaces, zabbix_host_obj, proxy_id):
|
||||||
|
host.link_or_clear_template(host_id, template_ids)
|
||||||
|
host.update_host(host_name, group_ids, status, host_id,
|
||||||
|
interfaces, exist_interfaces, proxy_id)
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
result="Successfully update host %s (%s) and linked with template '%s'"
|
||||||
|
% (host_name, ip, link_templates))
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
else:
|
||||||
|
if host.check_all_properties(host_id, host_groups, status, interfaces, template_ids,
|
||||||
|
exist_interfaces_copy, zabbix_host_obj, proxy_id):
|
||||||
|
host.update_host(host_name, group_ids, status, host_id, interfaces, exist_interfaces, proxy_id)
|
||||||
|
host.link_or_clear_template(host_id, template_ids)
|
||||||
|
module.exit_json(changed=True,
|
||||||
|
result="Successfully update host %s (%s) and linked with template '%s'"
|
||||||
|
% (host_name, ip, link_templates))
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
else:
|
||||||
|
if not group_ids:
|
||||||
|
module.fail_json(msg="Specify at least one group for creating host '%s'." % host_name)
|
||||||
|
|
||||||
|
if not interfaces or (interfaces and len(interfaces) == 0):
|
||||||
|
module.fail_json(msg="Specify at least one interface for creating host '%s'." % host_name)
|
||||||
|
|
||||||
|
# create host
|
||||||
|
host_id = host.add_host(host_name, group_ids, status, interfaces, proxy_id)
|
||||||
|
host.link_or_clear_template(host_id, template_ids)
|
||||||
|
module.exit_json(changed=True, result="Successfully added host %s (%s) and linked with template '%s'" % (
|
||||||
|
host_name, ip, link_templates))
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,230 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2013-2014, Epic Games, 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: zabbix_hostmacro
|
||||||
|
short_description: Zabbix host macro creates/updates/deletes
|
||||||
|
description:
|
||||||
|
- manages Zabbix host macros, it can create, update or delete them.
|
||||||
|
version_added: "2.0"
|
||||||
|
author:
|
||||||
|
- "(@cave)"
|
||||||
|
- Dean Hailin Song
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- zabbix-api
|
||||||
|
options:
|
||||||
|
server_url:
|
||||||
|
description:
|
||||||
|
- Url of Zabbix server, with protocol (http or https).
|
||||||
|
required: true
|
||||||
|
aliases: [ "url" ]
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- Zabbix user name.
|
||||||
|
required: true
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- Zabbix user password.
|
||||||
|
required: true
|
||||||
|
host_name:
|
||||||
|
description:
|
||||||
|
- Name of the host.
|
||||||
|
required: true
|
||||||
|
macro_name:
|
||||||
|
description:
|
||||||
|
- Name of the host macro.
|
||||||
|
required: true
|
||||||
|
macro_value:
|
||||||
|
description:
|
||||||
|
- Value of the host macro.
|
||||||
|
required: true
|
||||||
|
state:
|
||||||
|
description:
|
||||||
|
- State of the macro.
|
||||||
|
- On C(present), it will create if macro does not exist or update the macro if the associated data is different.
|
||||||
|
- On C(absent) will remove a macro if it exists.
|
||||||
|
required: false
|
||||||
|
choices: ['present', 'absent']
|
||||||
|
default: "present"
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- The timeout of API request (seconds).
|
||||||
|
default: 10
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
- name: Create a new host macro or update an existing macro's value
|
||||||
|
local_action:
|
||||||
|
module: zabbix_hostmacro
|
||||||
|
server_url: http://monitor.example.com
|
||||||
|
login_user: username
|
||||||
|
login_password: password
|
||||||
|
host_name: ExampleHost
|
||||||
|
macro_name:Example macro
|
||||||
|
macro_value:Example value
|
||||||
|
state: present
|
||||||
|
'''
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import copy
|
||||||
|
|
||||||
|
try:
|
||||||
|
from zabbix_api import ZabbixAPI, ZabbixAPISubClass
|
||||||
|
|
||||||
|
HAS_ZABBIX_API = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ZABBIX_API = False
|
||||||
|
|
||||||
|
|
||||||
|
# Extend the ZabbixAPI
|
||||||
|
# Since the zabbix-api python module too old (version 1.0, no higher version so far).
|
||||||
|
class ZabbixAPIExtends(ZabbixAPI):
|
||||||
|
def __init__(self, server, timeout, **kwargs):
|
||||||
|
ZabbixAPI.__init__(self, server, timeout=timeout)
|
||||||
|
|
||||||
|
|
||||||
|
class HostMacro(object):
|
||||||
|
def __init__(self, module, zbx):
|
||||||
|
self._module = module
|
||||||
|
self._zapi = zbx
|
||||||
|
|
||||||
|
# exist host
|
||||||
|
def is_host_exist(self, host_name):
|
||||||
|
result = self._zapi.host.exists({'host': host_name})
|
||||||
|
return result
|
||||||
|
|
||||||
|
# get host id by host name
|
||||||
|
def get_host_id(self, host_name):
|
||||||
|
try:
|
||||||
|
host_list = self._zapi.host.get({'output': 'extend', 'filter': {'host': host_name}})
|
||||||
|
if len(host_list) < 1:
|
||||||
|
self._module.fail_json(msg="Host not found: %s" % host_name)
|
||||||
|
else:
|
||||||
|
host_id = host_list[0]['hostid']
|
||||||
|
return host_id
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to get the host %s id: %s." % (host_name, e))
|
||||||
|
|
||||||
|
# get host macro
|
||||||
|
def get_host_macro(self, macro_name, host_id):
|
||||||
|
try:
|
||||||
|
host_macro_list = self._zapi.usermacro.get(
|
||||||
|
{"output": "extend", "selectSteps": "extend", 'hostids': [host_id], 'filter': {'macro': '{$' + macro_name + '}'}})
|
||||||
|
if len(host_macro_list) > 0:
|
||||||
|
return host_macro_list[0]
|
||||||
|
return None
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to get host macro %s: %s" % (macro_name, e))
|
||||||
|
|
||||||
|
# create host macro
|
||||||
|
def create_host_macro(self, macro_name, macro_value, host_id):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.usermacro.create({'hostid': host_id, 'macro': '{$' + macro_name + '}', 'value': macro_value})
|
||||||
|
self._module.exit_json(changed=True, result="Successfully added host macro %s " % macro_name)
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to create host macro %s: %s" % (macro_name, e))
|
||||||
|
|
||||||
|
# update host macro
|
||||||
|
def update_host_macro(self, host_macro_obj, macro_name, macro_value):
|
||||||
|
host_macro_id = host_macro_obj['hostmacroid']
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.usermacro.update({'hostmacroid': host_macro_id, 'value': macro_value})
|
||||||
|
self._module.exit_json(changed=True, result="Successfully updated host macro %s " % macro_name)
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to updated host macro %s: %s" % (macro_name, e))
|
||||||
|
|
||||||
|
# delete host macro
|
||||||
|
def delete_host_macro(self, host_macro_obj, macro_name):
|
||||||
|
host_macro_id = host_macro_obj['hostmacroid']
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.usermacro.delete([host_macro_id])
|
||||||
|
self._module.exit_json(changed=True, result="Successfully deleted host macro %s " % macro_name)
|
||||||
|
except Exception, e:
|
||||||
|
self._module.fail_json(msg="Failed to delete host macro %s: %s" % (macro_name, e))
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
server_url=dict(required=True, aliases=['url']),
|
||||||
|
login_user=dict(required=True),
|
||||||
|
login_password=dict(required=True, no_log=True),
|
||||||
|
host_name=dict(required=True),
|
||||||
|
macro_name=dict(required=True),
|
||||||
|
macro_value=dict(required=True),
|
||||||
|
state=dict(default="present", choices=['present', 'absent']),
|
||||||
|
timeout=dict(type='int', default=10)
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_ZABBIX_API:
|
||||||
|
module.fail_json(msg="Missing requried zabbix-api module (check docs or install with: pip install zabbix-api)")
|
||||||
|
|
||||||
|
server_url = module.params['server_url']
|
||||||
|
login_user = module.params['login_user']
|
||||||
|
login_password = module.params['login_password']
|
||||||
|
host_name = module.params['host_name']
|
||||||
|
macro_name = (module.params['macro_name']).upper()
|
||||||
|
macro_value = module.params['macro_value']
|
||||||
|
state = module.params['state']
|
||||||
|
timeout = module.params['timeout']
|
||||||
|
|
||||||
|
zbx = None
|
||||||
|
# login to zabbix
|
||||||
|
try:
|
||||||
|
zbx = ZabbixAPIExtends(server_url, timeout=timeout)
|
||||||
|
zbx.login(login_user, login_password)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failed to connect to Zabbix server: %s" % e)
|
||||||
|
|
||||||
|
host_macro_class_obj = HostMacro(module, zbx)
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
if host_name:
|
||||||
|
host_id = host_macro_class_obj.get_host_id(host_name)
|
||||||
|
host_macro_obj = host_macro_class_obj.get_host_macro(macro_name, host_id)
|
||||||
|
|
||||||
|
if state == 'absent':
|
||||||
|
if not host_macro_obj:
|
||||||
|
module.exit_json(changed=False, msg="Host Macro %s does not exist" % macro_name)
|
||||||
|
else:
|
||||||
|
# delete a macro
|
||||||
|
host_macro_class_obj.delete_host_macro(host_macro_obj, macro_name)
|
||||||
|
else:
|
||||||
|
if not host_macro_obj:
|
||||||
|
# create host macro
|
||||||
|
host_macro_class_obj.create_host_macro(macro_name, macro_value, host_id)
|
||||||
|
else:
|
||||||
|
# update host macro
|
||||||
|
host_macro_class_obj.update_host_macro(host_macro_obj, macro_name, macro_value)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,419 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2013-2014, Epic Games, 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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = '''
|
||||||
|
---
|
||||||
|
module: zabbix_screen
|
||||||
|
short_description: Zabbix screen creates/updates/deletes
|
||||||
|
description:
|
||||||
|
- This module allows you to create, modify and delete Zabbix screens and associated graph data.
|
||||||
|
version_added: "2.0"
|
||||||
|
author:
|
||||||
|
- "(@cove)"
|
||||||
|
- "Tony Minfei Ding"
|
||||||
|
- "Harrison Gu (@harrisongu)"
|
||||||
|
requirements:
|
||||||
|
- "python >= 2.6"
|
||||||
|
- zabbix-api
|
||||||
|
options:
|
||||||
|
server_url:
|
||||||
|
description:
|
||||||
|
- Url of Zabbix server, with protocol (http or https).
|
||||||
|
required: true
|
||||||
|
aliases: [ "url" ]
|
||||||
|
login_user:
|
||||||
|
description:
|
||||||
|
- Zabbix user name.
|
||||||
|
required: true
|
||||||
|
login_password:
|
||||||
|
description:
|
||||||
|
- Zabbix user password.
|
||||||
|
required: true
|
||||||
|
timeout:
|
||||||
|
description:
|
||||||
|
- The timeout of API request (seconds).
|
||||||
|
default: 10
|
||||||
|
zabbix_screens:
|
||||||
|
description:
|
||||||
|
- List of screens to be created/updated/deleted(see example).
|
||||||
|
- If the screen(s) already been added, the screen(s) name won't be updated.
|
||||||
|
- When creating or updating screen(s), C(screen_name), C(host_group) are required.
|
||||||
|
- When deleting screen(s), the C(screen_name) is required.
|
||||||
|
- 'The available states are: C(present) (default) and C(absent). If the screen(s) already exists, and the state is not C(absent), the screen(s) will just be updated as needed.'
|
||||||
|
required: true
|
||||||
|
notes:
|
||||||
|
- Too many concurrent updates to the same screen may cause Zabbix to return errors, see examples for a workaround if needed.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = '''
|
||||||
|
# Create/update a screen.
|
||||||
|
- name: Create a new screen or update an existing screen's items
|
||||||
|
local_action:
|
||||||
|
module: zabbix_screen
|
||||||
|
server_url: http://monitor.example.com
|
||||||
|
login_user: username
|
||||||
|
login_password: password
|
||||||
|
screens:
|
||||||
|
- screen_name: ExampleScreen1
|
||||||
|
host_group: Example group1
|
||||||
|
state: present
|
||||||
|
graph_names:
|
||||||
|
- Example graph1
|
||||||
|
- Example graph2
|
||||||
|
graph_width: 200
|
||||||
|
graph_height: 100
|
||||||
|
|
||||||
|
# Create/update multi-screen
|
||||||
|
- name: Create two of new screens or update the existing screens' items
|
||||||
|
local_action:
|
||||||
|
module: zabbix_screen
|
||||||
|
server_url: http://monitor.example.com
|
||||||
|
login_user: username
|
||||||
|
login_password: password
|
||||||
|
screens:
|
||||||
|
- screen_name: ExampleScreen1
|
||||||
|
host_group: Example group1
|
||||||
|
state: present
|
||||||
|
graph_names:
|
||||||
|
- Example graph1
|
||||||
|
- Example graph2
|
||||||
|
graph_width: 200
|
||||||
|
graph_height: 100
|
||||||
|
- screen_name: ExampleScreen2
|
||||||
|
host_group: Example group2
|
||||||
|
state: present
|
||||||
|
graph_names:
|
||||||
|
- Example graph1
|
||||||
|
- Example graph2
|
||||||
|
graph_width: 200
|
||||||
|
graph_height: 100
|
||||||
|
|
||||||
|
# Limit the Zabbix screen creations to one host since Zabbix can return an error when doing concurent updates
|
||||||
|
- name: Create a new screen or update an existing screen's items
|
||||||
|
local_action:
|
||||||
|
module: zabbix_screen
|
||||||
|
server_url: http://monitor.example.com
|
||||||
|
login_user: username
|
||||||
|
login_password: password
|
||||||
|
state: present
|
||||||
|
screens:
|
||||||
|
- screen_name: ExampleScreen
|
||||||
|
host_group: Example group
|
||||||
|
state: present
|
||||||
|
graph_names:
|
||||||
|
- Example graph1
|
||||||
|
- Example graph2
|
||||||
|
graph_width: 200
|
||||||
|
graph_height: 100
|
||||||
|
when: inventory_hostname==groups['group_name'][0]
|
||||||
|
'''
|
||||||
|
|
||||||
|
try:
|
||||||
|
from zabbix_api import ZabbixAPI, ZabbixAPISubClass
|
||||||
|
from zabbix_api import ZabbixAPIException
|
||||||
|
from zabbix_api import Already_Exists
|
||||||
|
HAS_ZABBIX_API = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ZABBIX_API = False
|
||||||
|
|
||||||
|
|
||||||
|
# Extend the ZabbixAPI
|
||||||
|
# Since the zabbix-api python module too old (version 1.0, and there's no higher version so far), it doesn't support the 'screenitem' api call,
|
||||||
|
# we have to inherit the ZabbixAPI class to add 'screenitem' support.
|
||||||
|
class ZabbixAPIExtends(ZabbixAPI):
|
||||||
|
screenitem = None
|
||||||
|
|
||||||
|
def __init__(self, server, timeout, **kwargs):
|
||||||
|
ZabbixAPI.__init__(self, server, timeout=timeout)
|
||||||
|
self.screenitem = ZabbixAPISubClass(self, dict({"prefix": "screenitem"}, **kwargs))
|
||||||
|
|
||||||
|
|
||||||
|
class Screen(object):
|
||||||
|
def __init__(self, module, zbx):
|
||||||
|
self._module = module
|
||||||
|
self._zapi = zbx
|
||||||
|
|
||||||
|
# get group id by group name
|
||||||
|
def get_host_group_id(self, group_name):
|
||||||
|
if group_name == "":
|
||||||
|
self._module.fail_json(msg="group_name is required")
|
||||||
|
hostGroup_list = self._zapi.hostgroup.get({'output': 'extend', 'filter': {'name': group_name}})
|
||||||
|
if len(hostGroup_list) < 1:
|
||||||
|
self._module.fail_json(msg="Host group not found: %s" % group_name)
|
||||||
|
else:
|
||||||
|
hostGroup_id = hostGroup_list[0]['groupid']
|
||||||
|
return hostGroup_id
|
||||||
|
|
||||||
|
# get monitored host_id by host_group_id
|
||||||
|
def get_host_ids_by_group_id(self, group_id):
|
||||||
|
host_list = self._zapi.host.get({'output': 'extend', 'groupids': group_id, 'monitored_hosts': 1})
|
||||||
|
if len(host_list) < 1:
|
||||||
|
self._module.fail_json(msg="No host in the group.")
|
||||||
|
else:
|
||||||
|
host_ids = []
|
||||||
|
for i in host_list:
|
||||||
|
host_id = i['hostid']
|
||||||
|
host_ids.append(host_id)
|
||||||
|
return host_ids
|
||||||
|
|
||||||
|
# get screen
|
||||||
|
def get_screen_id(self, screen_name):
|
||||||
|
if screen_name == "":
|
||||||
|
self._module.fail_json(msg="screen_name is required")
|
||||||
|
try:
|
||||||
|
screen_id_list = self._zapi.screen.get({'output': 'extend', 'search': {"name": screen_name}})
|
||||||
|
if len(screen_id_list) >= 1:
|
||||||
|
screen_id = screen_id_list[0]['screenid']
|
||||||
|
return screen_id
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
self._module.fail_json(msg="Failed to get screen %s from Zabbix: %s" % (screen_name, e))
|
||||||
|
|
||||||
|
# create screen
|
||||||
|
def create_screen(self, screen_name, h_size, v_size):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
screen = self._zapi.screen.create({'name': screen_name, 'hsize': h_size, 'vsize': v_size})
|
||||||
|
return screen['screenids'][0]
|
||||||
|
except Exception as e:
|
||||||
|
self._module.fail_json(msg="Failed to create screen %s: %s" % (screen_name, e))
|
||||||
|
|
||||||
|
# update screen
|
||||||
|
def update_screen(self, screen_id, screen_name, h_size, v_size):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.screen.update({'screenid': screen_id, 'hsize': h_size, 'vsize': v_size})
|
||||||
|
except Exception as e:
|
||||||
|
self._module.fail_json(msg="Failed to update screen %s: %s" % (screen_name, e))
|
||||||
|
|
||||||
|
# delete screen
|
||||||
|
def delete_screen(self, screen_id, screen_name):
|
||||||
|
try:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.screen.delete([screen_id])
|
||||||
|
except Exception as e:
|
||||||
|
self._module.fail_json(msg="Failed to delete screen %s: %s" % (screen_name, e))
|
||||||
|
|
||||||
|
# get graph ids
|
||||||
|
def get_graph_ids(self, hosts, graph_name_list):
|
||||||
|
graph_id_lists = []
|
||||||
|
vsize = 1
|
||||||
|
for host in hosts:
|
||||||
|
graph_id_list = self.get_graphs_by_host_id(graph_name_list, host)
|
||||||
|
size = len(graph_id_list)
|
||||||
|
if size > 0:
|
||||||
|
graph_id_lists.extend(graph_id_list)
|
||||||
|
if vsize < size:
|
||||||
|
vsize = size
|
||||||
|
return graph_id_lists, vsize
|
||||||
|
|
||||||
|
# getGraphs
|
||||||
|
def get_graphs_by_host_id(self, graph_name_list, host_id):
|
||||||
|
graph_ids = []
|
||||||
|
for graph_name in graph_name_list:
|
||||||
|
graphs_list = self._zapi.graph.get({'output': 'extend', 'search': {'name': graph_name}, 'hostids': host_id})
|
||||||
|
graph_id_list = []
|
||||||
|
if len(graphs_list) > 0:
|
||||||
|
for graph in graphs_list:
|
||||||
|
graph_id = graph['graphid']
|
||||||
|
graph_id_list.append(graph_id)
|
||||||
|
if len(graph_id_list) > 0:
|
||||||
|
graph_ids.extend(graph_id_list)
|
||||||
|
return graph_ids
|
||||||
|
|
||||||
|
# get screen items
|
||||||
|
def get_screen_items(self, screen_id):
|
||||||
|
screen_item_list = self._zapi.screenitem.get({'output': 'extend', 'screenids': screen_id})
|
||||||
|
return screen_item_list
|
||||||
|
|
||||||
|
# delete screen items
|
||||||
|
def delete_screen_items(self, screen_id, screen_item_id_list):
|
||||||
|
try:
|
||||||
|
if len(screen_item_id_list) == 0:
|
||||||
|
return True
|
||||||
|
screen_item_list = self.get_screen_items(screen_id)
|
||||||
|
if len(screen_item_list) > 0:
|
||||||
|
if self._module.check_mode:
|
||||||
|
self._module.exit_json(changed=True)
|
||||||
|
self._zapi.screenitem.delete(screen_item_id_list)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except ZabbixAPIException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# get screen's hsize and vsize
|
||||||
|
def get_hsize_vsize(self, hosts, v_size):
|
||||||
|
h_size = len(hosts)
|
||||||
|
if h_size == 1:
|
||||||
|
if v_size == 1:
|
||||||
|
h_size = 1
|
||||||
|
elif v_size in range(2, 9):
|
||||||
|
h_size = 2
|
||||||
|
else:
|
||||||
|
h_size = 3
|
||||||
|
v_size = (v_size - 1) / h_size + 1
|
||||||
|
return h_size, v_size
|
||||||
|
|
||||||
|
# create screen_items
|
||||||
|
def create_screen_items(self, screen_id, hosts, graph_name_list, width, height, h_size):
|
||||||
|
if len(hosts) < 4:
|
||||||
|
if width is None or width < 0:
|
||||||
|
width = 500
|
||||||
|
else:
|
||||||
|
if width is None or width < 0:
|
||||||
|
width = 200
|
||||||
|
if height is None or height < 0:
|
||||||
|
height = 100
|
||||||
|
|
||||||
|
try:
|
||||||
|
# when there're only one host, only one row is not good.
|
||||||
|
if len(hosts) == 1:
|
||||||
|
graph_id_list = self.get_graphs_by_host_id(graph_name_list, hosts[0])
|
||||||
|
for i, graph_id in enumerate(graph_id_list):
|
||||||
|
if graph_id is not None:
|
||||||
|
self._zapi.screenitem.create({'screenid': screen_id, 'resourcetype': 0, 'resourceid': graph_id,
|
||||||
|
'width': width, 'height': height,
|
||||||
|
'x': i % h_size, 'y': i / h_size, 'colspan': 1, 'rowspan': 1,
|
||||||
|
'elements': 0, 'valign': 0, 'halign': 0,
|
||||||
|
'style': 0, 'dynamic': 0, 'sort_triggers': 0})
|
||||||
|
else:
|
||||||
|
for i, host in enumerate(hosts):
|
||||||
|
graph_id_list = self.get_graphs_by_host_id(graph_name_list, host)
|
||||||
|
for j, graph_id in enumerate(graph_id_list):
|
||||||
|
if graph_id is not None:
|
||||||
|
self._zapi.screenitem.create({'screenid': screen_id, 'resourcetype': 0, 'resourceid': graph_id,
|
||||||
|
'width': width, 'height': height,
|
||||||
|
'x': i, 'y': j, 'colspan': 1, 'rowspan': 1,
|
||||||
|
'elements': 0, 'valign': 0, 'halign': 0,
|
||||||
|
'style': 0, 'dynamic': 0, 'sort_triggers': 0})
|
||||||
|
except Already_Exists:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
module = AnsibleModule(
|
||||||
|
argument_spec=dict(
|
||||||
|
server_url=dict(required=True, aliases=['url']),
|
||||||
|
login_user=dict(required=True),
|
||||||
|
login_password=dict(required=True, no_log=True),
|
||||||
|
timeout=dict(type='int', default=10),
|
||||||
|
screens=dict(type='dict', required=True)
|
||||||
|
),
|
||||||
|
supports_check_mode=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HAS_ZABBIX_API:
|
||||||
|
module.fail_json(msg="Missing requried zabbix-api module (check docs or install with: pip install zabbix-api)")
|
||||||
|
|
||||||
|
server_url = module.params['server_url']
|
||||||
|
login_user = module.params['login_user']
|
||||||
|
login_password = module.params['login_password']
|
||||||
|
timeout = module.params['timeout']
|
||||||
|
screens = module.params['screens']
|
||||||
|
|
||||||
|
zbx = None
|
||||||
|
# login to zabbix
|
||||||
|
try:
|
||||||
|
zbx = ZabbixAPIExtends(server_url, timeout=timeout)
|
||||||
|
zbx.login(login_user, login_password)
|
||||||
|
except Exception, e:
|
||||||
|
module.fail_json(msg="Failed to connect to Zabbix server: %s" % e)
|
||||||
|
|
||||||
|
screen = Screen(module, zbx)
|
||||||
|
created_screens = []
|
||||||
|
changed_screens = []
|
||||||
|
deleted_screens = []
|
||||||
|
|
||||||
|
for zabbix_screen in screens:
|
||||||
|
screen_name = zabbix_screen['screen_name']
|
||||||
|
screen_id = screen.get_screen_id(screen_name)
|
||||||
|
state = "absent" if "state" in zabbix_screen and zabbix_screen['state'] == "absent" else "present"
|
||||||
|
|
||||||
|
if state == "absent":
|
||||||
|
if screen_id:
|
||||||
|
screen_item_list = screen.get_screen_items(screen_id)
|
||||||
|
screen_item_id_list = []
|
||||||
|
for screen_item in screen_item_list:
|
||||||
|
screen_item_id = screen_item['screenitemid']
|
||||||
|
screen_item_id_list.append(screen_item_id)
|
||||||
|
screen.delete_screen_items(screen_id, screen_item_id_list)
|
||||||
|
screen.delete_screen(screen_id, screen_name)
|
||||||
|
|
||||||
|
deleted_screens.append(screen_name)
|
||||||
|
else:
|
||||||
|
host_group = zabbix_screen['host_group']
|
||||||
|
graph_names = zabbix_screen['graph_names']
|
||||||
|
graph_width = None
|
||||||
|
if 'graph_width' in zabbix_screen:
|
||||||
|
graph_width = zabbix_screen['graph_width']
|
||||||
|
graph_height = None
|
||||||
|
if 'graph_height' in zabbix_screen:
|
||||||
|
graph_height = zabbix_screen['graph_height']
|
||||||
|
host_group_id = screen.get_host_group_id(host_group)
|
||||||
|
hosts = screen.get_host_ids_by_group_id(host_group_id)
|
||||||
|
|
||||||
|
screen_item_id_list = []
|
||||||
|
resource_id_list = []
|
||||||
|
|
||||||
|
graph_ids, v_size = screen.get_graph_ids(hosts, graph_names)
|
||||||
|
h_size, v_size = screen.get_hsize_vsize(hosts, v_size)
|
||||||
|
|
||||||
|
if not screen_id:
|
||||||
|
# create screen
|
||||||
|
screen_id = screen.create_screen(screen_name, h_size, v_size)
|
||||||
|
screen.create_screen_items(screen_id, hosts, graph_names, graph_width, graph_height, h_size)
|
||||||
|
created_screens.append(screen_name)
|
||||||
|
else:
|
||||||
|
screen_item_list = screen.get_screen_items(screen_id)
|
||||||
|
|
||||||
|
for screen_item in screen_item_list:
|
||||||
|
screen_item_id = screen_item['screenitemid']
|
||||||
|
resource_id = screen_item['resourceid']
|
||||||
|
screen_item_id_list.append(screen_item_id)
|
||||||
|
resource_id_list.append(resource_id)
|
||||||
|
|
||||||
|
# when the screen items changed, then update
|
||||||
|
if graph_ids != resource_id_list:
|
||||||
|
deleted = screen.delete_screen_items(screen_id, screen_item_id_list)
|
||||||
|
if deleted:
|
||||||
|
screen.update_screen(screen_id, screen_name, h_size, v_size)
|
||||||
|
screen.create_screen_items(screen_id, hosts, graph_names, graph_width, graph_height, h_size)
|
||||||
|
changed_screens.append(screen_name)
|
||||||
|
|
||||||
|
if created_screens and changed_screens:
|
||||||
|
module.exit_json(changed=True, result="Successfully created screen(s): %s, and updated screen(s): %s" % (",".join(created_screens), ",".join(changed_screens)))
|
||||||
|
elif created_screens:
|
||||||
|
module.exit_json(changed=True, result="Successfully created screen(s): %s" % ",".join(created_screens))
|
||||||
|
elif changed_screens:
|
||||||
|
module.exit_json(changed=True, result="Successfully updated screen(s): %s" % ",".join(changed_screens))
|
||||||
|
elif deleted_screens:
|
||||||
|
module.exit_json(changed=True, result="Successfully deleted screen(s): %s" % ",".join(deleted_screens))
|
||||||
|
else:
|
||||||
|
module.exit_json(changed=False)
|
||||||
|
|
||||||
|
from ansible.module_utils.basic import *
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue