2016-11-19 18:10:50 +00:00
|
|
|
|
# -*- coding: utf-8 -*-
|
2016-11-19 18:11:29 +00:00
|
|
|
|
# above is for compatibility of python2.7.11
|
2016-11-19 18:10:50 +00:00
|
|
|
|
|
2015-12-31 04:10:59 +00:00
|
|
|
|
import subprocess
|
|
|
|
|
import os
|
2017-03-31 18:32:47 +00:00
|
|
|
|
import sys
|
2015-12-31 04:10:59 +00:00
|
|
|
|
import json
|
|
|
|
|
import logging
|
2016-12-20 10:53:01 +00:00
|
|
|
|
import tempfile
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
from python_terraform.tfstate import Tfstate
|
|
|
|
|
|
2015-12-31 04:10:59 +00:00
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
2016-11-19 10:24:33 +00:00
|
|
|
|
class IsFlagged:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2017-01-03 17:34:37 +00:00
|
|
|
|
class IsNotFlagged:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
2016-12-20 10:53:01 +00:00
|
|
|
|
class Terraform(object):
|
2016-11-18 07:35:14 +00:00
|
|
|
|
"""
|
|
|
|
|
Wrapper of terraform command line tool
|
|
|
|
|
https://www.terraform.io/
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, working_dir=None,
|
|
|
|
|
targets=None,
|
|
|
|
|
state=None,
|
|
|
|
|
variables=None,
|
|
|
|
|
parallelism=None,
|
2016-11-18 09:20:32 +00:00
|
|
|
|
var_file=None,
|
|
|
|
|
terraform_bin_path=None):
|
2016-11-19 10:24:33 +00:00
|
|
|
|
"""
|
2017-01-03 17:02:11 +00:00
|
|
|
|
:param working_dir: the folder of the working folder, if not given,
|
|
|
|
|
will be current working folder
|
2016-11-19 10:24:33 +00:00
|
|
|
|
:param targets: list of target
|
2017-01-03 17:02:11 +00:00
|
|
|
|
as default value of apply/destroy/plan command
|
|
|
|
|
:param state: path of state file relative to working folder,
|
|
|
|
|
as a default value of apply/destroy/plan command
|
|
|
|
|
:param variables: default variables for apply/destroy/plan command,
|
|
|
|
|
will be override by variable passing by apply/destroy/plan method
|
|
|
|
|
:param parallelism: default parallelism value for apply/destroy command
|
|
|
|
|
:param var_file: passed as value of -var-file option,
|
|
|
|
|
could be string or list, list stands for multiple -var-file option
|
2016-11-19 10:24:33 +00:00
|
|
|
|
:param terraform_bin_path: binary path of terraform
|
|
|
|
|
"""
|
2016-11-18 07:35:14 +00:00
|
|
|
|
self.working_dir = working_dir
|
|
|
|
|
self.state = state
|
2015-12-31 04:10:59 +00:00
|
|
|
|
self.targets = [] if targets is None else targets
|
|
|
|
|
self.variables = dict() if variables is None else variables
|
2016-11-18 07:35:14 +00:00
|
|
|
|
self.parallelism = parallelism
|
2016-11-18 09:20:32 +00:00
|
|
|
|
self.terraform_bin_path = terraform_bin_path \
|
|
|
|
|
if terraform_bin_path else 'terraform'
|
2016-11-18 07:35:14 +00:00
|
|
|
|
self.var_file = var_file
|
2016-12-20 16:18:57 +00:00
|
|
|
|
self.temp_var_files = VariableFiles()
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
|
|
|
|
# store the tfstate data
|
2016-12-20 10:53:01 +00:00
|
|
|
|
self.tfstate = None
|
|
|
|
|
self.read_state_file(self.state)
|
|
|
|
|
|
|
|
|
|
def __getattr__(self, item):
|
|
|
|
|
def wrapper(*args, **kwargs):
|
2017-05-09 07:48:28 +00:00
|
|
|
|
cmd_name = str(item)
|
|
|
|
|
if cmd_name.endswith('_cmd'):
|
|
|
|
|
cmd_name = cmd_name[:-4]
|
2016-12-20 16:18:57 +00:00
|
|
|
|
logging.debug('called with %r and %r' % (args, kwargs))
|
2017-05-09 07:48:28 +00:00
|
|
|
|
return self.cmd(cmd_name, *args, **kwargs)
|
2016-12-20 10:53:01 +00:00
|
|
|
|
|
|
|
|
|
return wrapper
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
2017-01-03 17:34:37 +00:00
|
|
|
|
def apply(self, dir_or_plan=None, input=False, no_color=IsFlagged, **kwargs):
|
2015-12-31 06:48:26 +00:00
|
|
|
|
"""
|
|
|
|
|
refer to https://terraform.io/docs/commands/apply.html
|
2016-11-24 07:09:16 +00:00
|
|
|
|
no-color is flagged by default
|
2017-01-03 17:34:37 +00:00
|
|
|
|
:param no_color: disable color of stdout
|
|
|
|
|
:param input: disable prompt for a missing variable
|
2016-11-24 07:09:16 +00:00
|
|
|
|
:param dir_or_plan: folder relative to working folder
|
2016-11-19 10:24:33 +00:00
|
|
|
|
:param kwargs: same as kwags in method 'cmd'
|
2016-02-25 09:22:11 +00:00
|
|
|
|
:returns return_code, stdout, stderr
|
2015-12-31 06:48:26 +00:00
|
|
|
|
"""
|
2017-01-03 17:34:37 +00:00
|
|
|
|
default = kwargs
|
|
|
|
|
default['input'] = input
|
|
|
|
|
default['no_color'] = no_color
|
|
|
|
|
option_dict = self._generate_default_options(default)
|
|
|
|
|
args = self._generate_default_args(dir_or_plan)
|
2016-11-19 10:24:33 +00:00
|
|
|
|
return self.cmd('apply', *args, **option_dict)
|
|
|
|
|
|
2017-01-03 17:34:37 +00:00
|
|
|
|
def _generate_default_args(self, dir_or_plan):
|
|
|
|
|
return [dir_or_plan] if dir_or_plan else []
|
|
|
|
|
|
|
|
|
|
def _generate_default_options(self, input_options):
|
|
|
|
|
option_dict = dict()
|
2016-11-18 07:35:14 +00:00
|
|
|
|
option_dict['state'] = self.state
|
|
|
|
|
option_dict['target'] = self.targets
|
|
|
|
|
option_dict['var'] = self.variables
|
|
|
|
|
option_dict['var_file'] = self.var_file
|
|
|
|
|
option_dict['parallelism'] = self.parallelism
|
2016-11-24 07:09:16 +00:00
|
|
|
|
option_dict['no_color'] = IsFlagged
|
2016-12-20 10:53:01 +00:00
|
|
|
|
option_dict['input'] = False
|
2017-01-03 17:34:37 +00:00
|
|
|
|
option_dict.update(input_options)
|
|
|
|
|
return option_dict
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
2017-01-03 17:34:37 +00:00
|
|
|
|
def destroy(self, dir_or_plan=None, force=IsFlagged, **kwargs):
|
2016-11-19 10:24:33 +00:00
|
|
|
|
"""
|
|
|
|
|
refer to https://www.terraform.io/docs/commands/destroy.html
|
2016-11-24 07:09:16 +00:00
|
|
|
|
force/no-color option is flagged by default
|
2016-11-19 10:24:33 +00:00
|
|
|
|
:return: ret_code, stdout, stderr
|
|
|
|
|
"""
|
2017-01-03 17:34:37 +00:00
|
|
|
|
default = kwargs
|
|
|
|
|
default['force'] = force
|
2017-01-04 05:21:30 +00:00
|
|
|
|
options = self._generate_default_options(default)
|
|
|
|
|
args = self._generate_default_args(dir_or_plan)
|
|
|
|
|
return self.cmd('destroy', *args, **options)
|
|
|
|
|
|
|
|
|
|
def plan(self, dir_or_plan=None, detailed_exitcode=IsFlagged, **kwargs):
|
|
|
|
|
"""
|
|
|
|
|
refert to https://www.terraform.io/docs/commands/plan.html
|
|
|
|
|
:param detailed_exitcode: Return a detailed exit code when the command exits.
|
|
|
|
|
:param dir_or_plan: relative path to plan/folder
|
|
|
|
|
:param kwargs: options
|
|
|
|
|
:return: ret_code, stdout, stderr
|
|
|
|
|
"""
|
|
|
|
|
options = kwargs
|
|
|
|
|
options['detailed_exitcode'] = detailed_exitcode
|
|
|
|
|
options = self._generate_default_options(options)
|
2017-01-03 17:34:37 +00:00
|
|
|
|
args = self._generate_default_args(dir_or_plan)
|
2017-01-04 05:21:30 +00:00
|
|
|
|
return self.cmd('plan', *args, **options)
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
|
|
|
|
def generate_cmd_string(self, cmd, *args, **kwargs):
|
2016-02-25 09:22:11 +00:00
|
|
|
|
"""
|
2016-11-18 07:35:14 +00:00
|
|
|
|
for any generate_cmd_string doesn't written as public method of terraform
|
|
|
|
|
|
|
|
|
|
examples:
|
|
|
|
|
1. call import command,
|
|
|
|
|
ref to https://www.terraform.io/docs/commands/import.html
|
|
|
|
|
--> generate_cmd_string call:
|
|
|
|
|
terraform import -input=true aws_instance.foo i-abcd1234
|
|
|
|
|
--> python call:
|
|
|
|
|
tf.generate_cmd_string('import', 'aws_instance.foo', 'i-abcd1234', input=True)
|
|
|
|
|
|
|
|
|
|
2. call apply command,
|
|
|
|
|
--> generate_cmd_string call:
|
|
|
|
|
terraform apply -var='a=b' -var='c=d' -no-color the_folder
|
|
|
|
|
--> python call:
|
2016-11-19 10:24:33 +00:00
|
|
|
|
tf.generate_cmd_string('apply', the_folder, no_color=IsFlagged, var={'a':'b', 'c':'d'})
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
|
|
|
|
:param cmd: command and sub-command of terraform, seperated with space
|
|
|
|
|
refer to https://www.terraform.io/docs/commands/index.html
|
2016-11-19 10:24:33 +00:00
|
|
|
|
:param args: arguments of a command
|
2016-11-18 07:35:14 +00:00
|
|
|
|
:param kwargs: same as kwags in method 'cmd'
|
|
|
|
|
:return: string of valid terraform command
|
2016-02-25 09:22:11 +00:00
|
|
|
|
"""
|
2016-11-18 07:35:14 +00:00
|
|
|
|
cmds = cmd.split()
|
|
|
|
|
cmds = [self.terraform_bin_path] + cmds
|
|
|
|
|
|
|
|
|
|
for k, v in kwargs.items():
|
|
|
|
|
if '_' in k:
|
|
|
|
|
k = k.replace('_', '-')
|
|
|
|
|
|
|
|
|
|
if type(v) is list:
|
|
|
|
|
for sub_v in v:
|
|
|
|
|
cmds += ['-{k}={v}'.format(k=k, v=sub_v)]
|
|
|
|
|
continue
|
|
|
|
|
|
2016-12-20 10:53:01 +00:00
|
|
|
|
# right now we assume only variables will be passed as dict
|
|
|
|
|
# since map type sent in string won't work, create temp var file for
|
|
|
|
|
# variables, and clean it up later
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if type(v) is dict:
|
2016-12-20 10:53:01 +00:00
|
|
|
|
filename = self.temp_var_files.create(v)
|
|
|
|
|
cmds += ['-var-file={0}'.format(filename)]
|
2016-11-18 07:35:14 +00:00
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# simple flag,
|
2016-11-19 10:24:33 +00:00
|
|
|
|
if v is IsFlagged:
|
2016-11-18 07:35:14 +00:00
|
|
|
|
cmds += ['-{k}'.format(k=k)]
|
|
|
|
|
continue
|
|
|
|
|
|
2017-01-03 17:34:37 +00:00
|
|
|
|
if v is None or v is IsNotFlagged:
|
2016-11-19 10:24:33 +00:00
|
|
|
|
continue
|
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if type(v) is bool:
|
|
|
|
|
v = 'true' if v else 'false'
|
|
|
|
|
|
|
|
|
|
cmds += ['-{k}={v}'.format(k=k, v=v)]
|
|
|
|
|
|
|
|
|
|
cmds += args
|
|
|
|
|
cmd = ' '.join(cmds)
|
|
|
|
|
return cmd
|
|
|
|
|
|
|
|
|
|
def cmd(self, cmd, *args, **kwargs):
|
2015-12-31 04:10:59 +00:00
|
|
|
|
"""
|
2016-11-18 07:35:14 +00:00
|
|
|
|
run a terraform command, if success, will try to read state file
|
|
|
|
|
:param cmd: command and sub-command of terraform, seperated with space
|
|
|
|
|
refer to https://www.terraform.io/docs/commands/index.html
|
2016-11-19 10:24:33 +00:00
|
|
|
|
:param args: arguments of a command
|
|
|
|
|
:param kwargs: any option flag with key value without prefixed dash character
|
|
|
|
|
if there's a dash in the option name, use under line instead of dash,
|
|
|
|
|
ex. -no-color --> no_color
|
|
|
|
|
if it's a simple flag with no value, value should be IsFlagged
|
|
|
|
|
ex. cmd('taint', allow_missing=IsFlagged)
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if it's a boolean value flag, assign True or false
|
|
|
|
|
if it's a flag could be used multiple times, assign list to it's value
|
|
|
|
|
if it's a "var" variable flag, assign dictionary to it
|
|
|
|
|
if a value is None, will skip this option
|
2017-03-31 18:32:47 +00:00
|
|
|
|
if the option 'capture_output' is passed (with any value other than
|
|
|
|
|
True), terraform output will be printed to stdout/stderr and
|
|
|
|
|
"None" will be returned as out and err.
|
2016-11-18 07:35:14 +00:00
|
|
|
|
:return: ret_code, out, err
|
2015-12-31 04:10:59 +00:00
|
|
|
|
"""
|
2017-03-31 18:32:47 +00:00
|
|
|
|
|
|
|
|
|
capture_output = kwargs.pop('capture_output', True)
|
|
|
|
|
if capture_output is True:
|
|
|
|
|
stderr = subprocess.PIPE
|
|
|
|
|
stdout = subprocess.PIPE
|
|
|
|
|
else:
|
|
|
|
|
stderr = sys.stderr
|
|
|
|
|
stdout = sys.stdout
|
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
cmd_string = self.generate_cmd_string(cmd, *args, **kwargs)
|
|
|
|
|
log.debug('command: {c}'.format(c=cmd_string))
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-19 10:24:33 +00:00
|
|
|
|
working_folder = self.working_dir if self.working_dir else None
|
|
|
|
|
|
2017-03-31 18:32:47 +00:00
|
|
|
|
p = subprocess.Popen(cmd_string, stdout=stdout,
|
|
|
|
|
stderr=stderr, shell=True,
|
2016-11-19 10:24:33 +00:00
|
|
|
|
cwd=working_folder)
|
2016-11-18 07:35:14 +00:00
|
|
|
|
out, err = p.communicate()
|
|
|
|
|
ret_code = p.returncode
|
|
|
|
|
log.debug('output: {o}'.format(o=out))
|
2016-02-25 09:22:11 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if ret_code == 0:
|
|
|
|
|
self.read_state_file()
|
|
|
|
|
else:
|
|
|
|
|
log.warn('error: {e}'.format(e=err))
|
2016-12-20 10:53:01 +00:00
|
|
|
|
|
|
|
|
|
self.temp_var_files.clean_up()
|
2017-03-31 18:32:47 +00:00
|
|
|
|
if capture_output is True:
|
|
|
|
|
return ret_code, out.decode('utf-8'), err.decode('utf-8')
|
|
|
|
|
else:
|
|
|
|
|
return ret_code, None, None
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
|
|
|
|
def output(self, name):
|
2016-02-25 09:22:11 +00:00
|
|
|
|
"""
|
2016-11-18 07:35:14 +00:00
|
|
|
|
https://www.terraform.io/docs/commands/output.html
|
|
|
|
|
:param name: name of output
|
|
|
|
|
:return: output value
|
|
|
|
|
"""
|
2016-11-19 10:24:33 +00:00
|
|
|
|
ret, out, err = self.cmd('output', name, json=IsFlagged)
|
2016-11-18 07:35:14 +00:00
|
|
|
|
|
2016-11-19 10:24:33 +00:00
|
|
|
|
log.debug('output raw string: {0}'.format(out))
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if ret != 0:
|
2015-12-31 04:10:59 +00:00
|
|
|
|
return None
|
2016-11-18 07:35:14 +00:00
|
|
|
|
out = out.lstrip()
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
output_dict = json.loads(out)
|
|
|
|
|
return output_dict['value']
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
def read_state_file(self, file_path=None):
|
|
|
|
|
"""
|
|
|
|
|
read .tfstate file
|
|
|
|
|
:param file_path: relative path to working dir
|
|
|
|
|
:return: states file in dict type
|
|
|
|
|
"""
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if not file_path:
|
|
|
|
|
file_path = self.state
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if not file_path:
|
|
|
|
|
file_path = 'terraform.tfstate'
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
if self.working_dir:
|
|
|
|
|
file_path = os.path.join(self.working_dir, file_path)
|
2015-12-31 04:10:59 +00:00
|
|
|
|
|
2016-11-18 07:35:14 +00:00
|
|
|
|
self.tfstate = Tfstate.load_file(file_path)
|
2016-12-20 10:53:01 +00:00
|
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
|
|
|
self.temp_var_files.clean_up()
|
|
|
|
|
|
|
|
|
|
|
2016-12-20 16:18:57 +00:00
|
|
|
|
class VariableFiles(object):
|
2016-12-20 10:53:01 +00:00
|
|
|
|
def __init__(self):
|
|
|
|
|
self.files = []
|
|
|
|
|
|
|
|
|
|
def create(self, variables):
|
|
|
|
|
with tempfile.NamedTemporaryFile('w+t', delete=False) as temp:
|
|
|
|
|
logging.debug('{0} is created'.format(temp.name))
|
|
|
|
|
self.files.append(temp)
|
|
|
|
|
temp.write(json.dumps(variables))
|
|
|
|
|
file_name = temp.name
|
|
|
|
|
|
|
|
|
|
return file_name
|
|
|
|
|
|
|
|
|
|
def clean_up(self):
|
|
|
|
|
for f in self.files:
|
|
|
|
|
os.unlink(f.name)
|
|
|
|
|
|
|
|
|
|
self.files = []
|