@ -3,180 +3,234 @@ import os
import json
import json
import logging
import logging
from python_terraform . tfstate import Tfstate
log = logging . getLogger ( __name__ )
log = logging . getLogger ( __name__ )
class IsFlagged :
pass
class IsNotFlagged :
pass
class Terraform :
class Terraform :
def __init__ ( self , targets = None , state = ' terraform.tfstate ' , variables = None ) :
"""
Wrapper of terraform command line tool
https : / / www . terraform . io /
"""
def __init__ ( self , working_dir = None ,
targets = None ,
state = None ,
variables = None ,
parallelism = None ,
var_file = None ,
terraform_bin_path = None ) :
"""
: param working_dir : the folder of the working folder , if not given , will be where python
: param targets : list of target
: param state : path of state file relative to working folder
: param variables : variables for apply / destroy / plan command
: param parallelism : parallelism for apply / destroy command
: param var_file : if specified , variables will not be used
: param terraform_bin_path : binary path of terraform
"""
self . working_dir = working_dir
self . state = state
self . targets = [ ] if targets is None else targets
self . targets = [ ] if targets is None else targets
self . variables = dict ( ) if variables is None else variables
self . variables = dict ( ) if variables is None else variables
self . parallelism = parallelism
self . state_filename = state
self . terraform_bin_path = terraform_bin_path \
self . state_data = dict ( )
if terraform_bin_path else ' terraform '
self . parallelism = 50
self . var_file = var_file
def apply ( self , targets = None , variables = None , * * kargs ) :
# store the tfstate data
self . tfstate = dict ( )
def apply ( self ,
dir = None ,
is_no_color = True ,
is_input = False ,
* * kwargs ) :
"""
"""
refer to https : / / terraform . io / docs / commands / apply . html
refer to https : / / terraform . io / docs / commands / apply . html
: param variables : variables in dict type
: raise RuntimeError when return code is not zero
: param targets : targets in list
: param is_no_color : if True , add flag - no - color
: param is_input : if True , add option - input = true
: param dir : folder relative to working folder
: param kwargs : same as kwags in method ' cmd '
: returns return_code , stdout , stderr
: returns return_code , stdout , stderr
"""
"""
variables = self . variables if variables is None else variables
targets = self . targets if targets is None else targets
parameters = [ ]
parameters + = self . _generate_targets ( targets )
parameters + = self . _generate_var_string ( variables )
parameters + = self . _gen_param_string ( kargs )
parameters = \
[ ' terraform ' , ' apply ' , ' -state= %s ' % self . state_filename ] + parameters
cmd = ' ' . join ( parameters )
return self . _run_cmd ( cmd )
def _gen_param_string ( self , kargs ) :
params = [ ]
for key , value in kargs . items ( ) :
if value :
params + = [ ' - %s = %s ' % ( key , value ) ]
else :
params + = [ ' - %s ' % key ]
return params
def _run_cmd ( self , cmd ) :
log . debug ( ' command: ' + cmd )
p = subprocess . Popen (
cmd , stdout = subprocess . PIPE , stderr = subprocess . PIPE , shell = True )
out , err = p . communicate ( )
ret_code = p . returncode
log . debug ( ' output: ' + out )
if ret_code == 0 :
args , option_dict = self . _create_cmd_args ( is_input ,
log . debug ( ' error: ' + err )
is_no_color ,
self . read_state_file ( )
dir ,
return ret_code , out , err
kwargs )
def destroy ( self , targets = None , variables = None , * * kwargs ) :
return self . cmd ( ' apply ' , * args , * * option_dict )
variables = self . variables if variables is None else variables
targets = self . targets if targets is None else targets
def _create_cmd_args ( self , is_input , is_no_color , dir , kwargs ) :
option_dict = dict ( )
parameters = [ ]
option_dict [ ' state ' ] = self . state
parameters + = self . _generate_targets ( targets )
option_dict [ ' target ' ] = self . targets
parameters + = self . _generate_var_string ( variables )
option_dict [ ' var ' ] = self . variables
option_dict [ ' var_file ' ] = self . var_file
parameters = \
option_dict [ ' parallelism ' ] = self . parallelism
[ ' terraform ' , ' destroy ' , ' -force ' , ' -state= %s ' % self . state_filename ] + \
if is_no_color :
parameters
option_dict [ ' no_color ' ] = IsFlagged
cmd = ' ' . join ( parameters )
option_dict [ ' input ' ] = is_input
return self . _run_cmd ( cmd )
option_dict . update ( kwargs )
args = [ dir ] if dir else [ ]
def refresh ( self , targets = None , variables = None ) :
return args , option_dict
variables = self . variables if variables is None else variables
targets = self . targets if targets is None else targets
def destroy ( self , working_dir = None , is_force = True ,
is_no_color = True , is_input = False , * * kwargs ) :
parameters = [ ]
parameters + = self . _generate_targets ( targets )
parameters + = self . _generate_var_string ( variables )
parameters = \
[ ' terraform ' , ' refresh ' , ' -state= %s ' % self . state_filename ] + \
parameters
cmd = ' ' . join ( parameters )
return self . _run_cmd ( cmd )
def read_state_file ( self ) :
"""
"""
read . tfstate file
refer to https : / / www . terraform . io / docs / commands / destroy . html
: return : states file in dict type
: raise RuntimeError when return code is not zero
: return : ret_code , stdout , stderr
"""
"""
if os . path . exists ( self . state_filename ) :
with open ( self . state_filename ) as f :
args , option_dict = self . _create_cmd_args ( is_input ,
json_data = json . load ( f )
is_no_color ,
self . state_data = json_data
working_dir ,
log . debug ( " state_data= %s " % str ( self . state_data ) )
kwargs )
return json_data
if is_force :
option_dict [ ' force ' ] = IsFlagged
return dict ( )
return self . cmd ( ' destroy ' , * args , * * option_dict )
def is_any_aws_instance_alive ( self ) :
self . refresh ( )
def generate_cmd_string ( self , cmd , * args , * * kwargs ) :
if not os . path . exists ( self . state_filename ) :
log . debug ( " can ' t find %s " % self . state_data )
return False
self . read_state_file ( )
try :
main_module = self . _get_main_module ( )
for resource_key , info in main_module [ ' resources ' ] . items ( ) :
if ' aws_instance ' in resource_key :
log . debug ( " %s is found when read state " % resource_key )
return True
log . debug ( " no aws_instance found in resource key " )
return False
except KeyError as err :
log . debug ( str ( err ) )
return False
except TypeError as err :
log . debug ( str ( err ) )
return False
def _get_main_module ( self ) :
return self . state_data [ ' modules ' ] [ 0 ]
def get_aws_instances ( self ) :
instances = dict ( )
try :
main_module = self . _get_main_module ( )
for resource_key , info in main_module [ ' resources ' ] . items ( ) :
if ' aws_instance ' in resource_key :
instances [ resource_key ] = info
except KeyError :
return instances
except TypeError :
return instances
return instances
def get_aws_instance ( self , resource_name ) :
"""
"""
: param resource_name :
for any generate_cmd_string doesn ' t written as public method of terraform
name of terraform resource , make source count is attached
: return : return None if not exist , dict type if exist
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 :
tf . generate_cmd_string ( ' apply ' , the_folder , no_color = IsFlagged , var = { ' a ' : ' b ' , ' c ' : ' d ' } )
: param cmd : command and sub - command of terraform , seperated with space
refer to https : / / www . terraform . io / docs / commands / index . html
: param args : arguments of a command
: param kwargs : same as kwags in method ' cmd '
: return : string of valid terraform command
"""
"""
try :
cmds = cmd . split ( )
return self . get_aws_instances ( ) [ resource_name ]
cmds = [ self . terraform_bin_path ] + cmds
except KeyError :
return None
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
if type ( v ) is dict :
for sub_k , sub_v in v . items ( ) :
cmds + = [ " - {k} = ' {var_k} = {var_v} ' " . format ( k = k ,
var_k = sub_k ,
var_v = sub_v ) ]
continue
def get_output_value ( self , output_name ) :
# simple flag,
if v is IsFlagged :
cmds + = [ ' - {k} ' . format ( k = k ) ]
continue
if v is IsNotFlagged :
continue
if not v :
continue
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 ) :
"""
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
: 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 )
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
: return : ret_code , out , err
"""
"""
cmd_string = self . generate_cmd_string ( cmd , * args , * * kwargs )
log . debug ( ' command: {c} ' . format ( c = cmd_string ) )
working_folder = self . working_dir if self . working_dir else None
p = subprocess . Popen ( cmd_string , stdout = subprocess . PIPE ,
stderr = subprocess . PIPE , shell = True ,
cwd = working_folder )
out , err = p . communicate ( )
ret_code = p . returncode
log . debug ( ' output: {o} ' . format ( o = out ) )
: param output_name :
if ret_code == 0 :
: return :
self . read_state_file ( )
else :
log . warn ( ' error: {e} ' . format ( e = err ) )
return ret_code , out . decode ( ' utf-8 ' ) , err . decode ( ' utf-8 ' )
def output ( self , name ) :
"""
https : / / www . terraform . io / docs / commands / output . html
: param name : name of output
: return : output value
"""
"""
try :
ret , out , err = self . cmd ( ' output ' , name , json = IsFlagged )
main_module = self . _get_main_module ( )
return main_module [ ' outputs ' ] [ output_name ]
log . debug ( ' output raw string: {0} ' . format ( out ) )
except KeyError :
if ret != 0 :
return None
return None
out = out . lstrip ( )
@staticmethod
output_dict = json . loads ( out )
def _generate_var_string ( d ) :
return output_dict [ ' value ' ]
str_t = [ ]
for k , v in d . iteritems ( ) :
str_t + = [ ' -var ' ] + [ " %s = %s " % ( k , v ) ]
return str_t
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
"""
@staticmethod
if not file_path :
def _generate_targets ( targets ) :
file_path = self . state
str_t = [ ]
for t in targets :
str_t + = [ ' -target= %s ' % t ]
return str_t
if not file_path :
file_path = ' terraform.tfstate '
if self . working_dir :
file_path = os . path . join ( self . working_dir , file_path )
self . tfstate = Tfstate . load_file ( file_path )