1# -*- coding: utf-8 -*- 2# 3# Copyright 2018 www.privaz.io Valletech AB 4# 5# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) 6 7from __future__ import (absolute_import, division, print_function) 8__metaclass__ = type 9 10 11import time 12import ssl 13from os import environ 14from ansible.module_utils.six import string_types 15from ansible.module_utils.basic import AnsibleModule 16 17 18HAS_PYONE = True 19 20try: 21 from pyone import OneException 22 from pyone.server import OneServer 23except ImportError: 24 OneException = Exception 25 HAS_PYONE = False 26 27 28class OpenNebulaModule: 29 """ 30 Base class for all OpenNebula Ansible Modules. 31 This is basically a wrapper of the common arguments, the pyone client and 32 some utility methods. 33 """ 34 35 common_args = dict( 36 api_url=dict(type='str', aliases=['api_endpoint'], default=environ.get("ONE_URL")), 37 api_username=dict(type='str', default=environ.get("ONE_USERNAME")), 38 api_password=dict(type='str', no_log=True, aliases=['api_token'], default=environ.get("ONE_PASSWORD")), 39 validate_certs=dict(default=True, type='bool'), 40 wait_timeout=dict(type='int', default=300), 41 ) 42 43 def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None, required_one_of=None, required_if=None): 44 45 module_args = OpenNebulaModule.common_args.copy() 46 module_args.update(argument_spec) 47 48 self.module = AnsibleModule(argument_spec=module_args, 49 supports_check_mode=supports_check_mode, 50 mutually_exclusive=mutually_exclusive, 51 required_one_of=required_one_of, 52 required_if=required_if) 53 self.result = dict(changed=False, 54 original_message='', 55 message='') 56 self.one = self.create_one_client() 57 58 self.resolved_parameters = self.resolve_parameters() 59 60 def create_one_client(self): 61 """ 62 Creates an XMLPRC client to OpenNebula. 63 64 Returns: the new xmlrpc client. 65 66 """ 67 68 # context required for not validating SSL, old python versions won't validate anyway. 69 if hasattr(ssl, '_create_unverified_context'): 70 no_ssl_validation_context = ssl._create_unverified_context() 71 else: 72 no_ssl_validation_context = None 73 74 # Check if the module can run 75 if not HAS_PYONE: 76 self.fail("pyone is required for this module") 77 78 if self.module.params.get("api_url"): 79 url = self.module.params.get("api_url") 80 else: 81 self.fail("Either api_url or the environment variable ONE_URL must be provided") 82 83 if self.module.params.get("api_username"): 84 username = self.module.params.get("api_username") 85 else: 86 self.fail("Either api_username or the environment vairable ONE_USERNAME must be provided") 87 88 if self.module.params.get("api_password"): 89 password = self.module.params.get("api_password") 90 else: 91 self.fail("Either api_password or the environment vairable ONE_PASSWORD must be provided") 92 93 session = "%s:%s" % (username, password) 94 95 if not self.module.params.get("validate_certs") and "PYTHONHTTPSVERIFY" not in environ: 96 return OneServer(url, session=session, context=no_ssl_validation_context) 97 else: 98 return OneServer(url, session) 99 100 def close_one_client(self): 101 """ 102 Close the pyone session. 103 """ 104 self.one.server_close() 105 106 def fail(self, msg): 107 """ 108 Utility failure method, will ensure pyone is properly closed before failing. 109 Args: 110 msg: human readable failure reason. 111 """ 112 if hasattr(self, 'one'): 113 self.close_one_client() 114 self.module.fail_json(msg=msg) 115 116 def exit(self): 117 """ 118 Utility exit method, will ensure pyone is properly closed before exiting. 119 120 """ 121 if hasattr(self, 'one'): 122 self.close_one_client() 123 self.module.exit_json(**self.result) 124 125 def resolve_parameters(self): 126 """ 127 This method resolves parameters provided by a secondary ID to the primary ID. 128 For example if cluster_name is present, cluster_id will be introduced by performing 129 the required resolution 130 131 Returns: a copy of the parameters that includes the resolved parameters. 132 133 """ 134 135 resolved_params = dict(self.module.params) 136 137 if 'cluster_name' in self.module.params: 138 clusters = self.one.clusterpool.info() 139 for cluster in clusters.CLUSTER: 140 if cluster.NAME == self.module.params.get('cluster_name'): 141 resolved_params['cluster_id'] = cluster.ID 142 143 return resolved_params 144 145 def is_parameter(self, name): 146 """ 147 Utility method to check if a parameter was provided or is resolved 148 Args: 149 name: the parameter to check 150 """ 151 if name in self.resolved_parameters: 152 return self.get_parameter(name) is not None 153 else: 154 return False 155 156 def get_parameter(self, name): 157 """ 158 Utility method for accessing parameters that includes resolved ID 159 parameters from provided Name parameters. 160 """ 161 return self.resolved_parameters.get(name) 162 163 def get_host_by_name(self, name): 164 ''' 165 Returns a host given its name. 166 Args: 167 name: the name of the host 168 169 Returns: the host object or None if the host is absent. 170 171 ''' 172 hosts = self.one.hostpool.info() 173 for h in hosts.HOST: 174 if h.NAME == name: 175 return h 176 return None 177 178 def get_cluster_by_name(self, name): 179 """ 180 Returns a cluster given its name. 181 Args: 182 name: the name of the cluster 183 184 Returns: the cluster object or None if the host is absent. 185 """ 186 187 clusters = self.one.clusterpool.info() 188 for c in clusters.CLUSTER: 189 if c.NAME == name: 190 return c 191 return None 192 193 def get_template_by_name(self, name): 194 ''' 195 Returns a template given its name. 196 Args: 197 name: the name of the template 198 199 Returns: the template object or None if the host is absent. 200 201 ''' 202 templates = self.one.templatepool.info() 203 for t in templates.TEMPLATE: 204 if t.NAME == name: 205 return t 206 return None 207 208 def cast_template(self, template): 209 """ 210 OpenNebula handles all template elements as strings 211 At some point there is a cast being performed on types provided by the user 212 This function mimics that transformation so that required template updates are detected properly 213 additionally an array will be converted to a comma separated list, 214 which works for labels and hopefully for something more. 215 216 Args: 217 template: the template to transform 218 219 Returns: the transformed template with data casts applied. 220 """ 221 222 # TODO: check formally available data types in templates 223 # TODO: some arrays might be converted to space separated 224 225 for key in template: 226 value = template[key] 227 if isinstance(value, dict): 228 self.cast_template(template[key]) 229 elif isinstance(value, list): 230 template[key] = ', '.join(value) 231 elif not isinstance(value, string_types): 232 template[key] = str(value) 233 234 def requires_template_update(self, current, desired): 235 """ 236 This function will help decide if a template update is required or not 237 If a desired key is missing from the current dictionary an update is required 238 If the intersection of both dictionaries is not deep equal, an update is required 239 Args: 240 current: current template as a dictionary 241 desired: desired template as a dictionary 242 243 Returns: True if a template update is required 244 """ 245 246 if not desired: 247 return False 248 249 self.cast_template(desired) 250 intersection = dict() 251 for dkey in desired.keys(): 252 if dkey in current.keys(): 253 intersection[dkey] = current[dkey] 254 else: 255 return True 256 return not (desired == intersection) 257 258 def wait_for_state(self, element_name, state, state_name, target_states, 259 invalid_states=None, transition_states=None, 260 wait_timeout=None): 261 """ 262 Args: 263 element_name: the name of the object we are waiting for: HOST, VM, etc. 264 state: lambda that returns the current state, will be queried until target state is reached 265 state_name: lambda that returns the readable form of a given state 266 target_states: states expected to be reached 267 invalid_states: if any of this states is reached, fail 268 transition_states: when used, these are the valid states during the transition. 269 wait_timeout: timeout period in seconds. Defaults to the provided parameter. 270 """ 271 272 if not wait_timeout: 273 wait_timeout = self.module.params.get("wait_timeout") 274 275 start_time = time.time() 276 277 while (time.time() - start_time) < wait_timeout: 278 current_state = state() 279 280 if current_state in invalid_states: 281 self.fail('invalid %s state %s' % (element_name, state_name(current_state))) 282 283 if transition_states: 284 if current_state not in transition_states: 285 self.fail('invalid %s transition state %s' % (element_name, state_name(current_state))) 286 287 if current_state in target_states: 288 return True 289 290 time.sleep(self.one.server_retry_interval()) 291 292 self.fail(msg="Wait timeout has expired!") 293 294 def run_module(self): 295 """ 296 trigger the start of the execution of the module. 297 Returns: 298 299 """ 300 try: 301 self.run(self.one, self.module, self.result) 302 except OneException as e: 303 self.fail(msg="OpenNebula Exception: %s" % e) 304 305 def run(self, one, module, result): 306 """ 307 to be implemented by subclass with the actual module actions. 308 Args: 309 one: the OpenNebula XMLRPC client 310 module: the Ansible Module object 311 result: the Ansible result 312 """ 313 raise NotImplementedError("Method requires implementation") 314