1# -*- coding: utf-8 -*-
2# This code is part of Ansible, but is an independent component.
3# This particular file snippet, and this file snippet only, is BSD licensed.
4# Modules you write using this snippet, which is embedded dynamically by Ansible
5# still belong to the author of the module, and may assign their own license
6# to the complete work.
7#
8# (c) 2018 Red Hat Inc.
9#
10# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
11
12from __future__ import (absolute_import, division, print_function)
13__metaclass__ = type
14
15
16import os
17from functools import partial
18from ansible.module_utils.common.text.converters import to_native
19from ansible.module_utils.six import iteritems
20from ansible.module_utils.common.text.converters import to_text
21from ansible.module_utils.basic import env_fallback
22from ansible.module_utils.common.validation import check_type_dict
23
24try:
25    from infoblox_client.connector import Connector
26    from infoblox_client.exceptions import InfobloxException
27    HAS_INFOBLOX_CLIENT = True
28except ImportError:
29    HAS_INFOBLOX_CLIENT = False
30
31# defining nios constants
32NIOS_DNS_VIEW = 'view'
33NIOS_NETWORK_VIEW = 'networkview'
34NIOS_HOST_RECORD = 'record:host'
35NIOS_IPV4_NETWORK = 'network'
36NIOS_IPV6_NETWORK = 'ipv6network'
37NIOS_ZONE = 'zone_auth'
38NIOS_PTR_RECORD = 'record:ptr'
39NIOS_A_RECORD = 'record:a'
40NIOS_AAAA_RECORD = 'record:aaaa'
41NIOS_CNAME_RECORD = 'record:cname'
42NIOS_MX_RECORD = 'record:mx'
43NIOS_SRV_RECORD = 'record:srv'
44NIOS_NAPTR_RECORD = 'record:naptr'
45NIOS_TXT_RECORD = 'record:txt'
46NIOS_NSGROUP = 'nsgroup'
47NIOS_IPV4_FIXED_ADDRESS = 'fixedaddress'
48NIOS_IPV6_FIXED_ADDRESS = 'ipv6fixedaddress'
49NIOS_NEXT_AVAILABLE_IP = 'func:nextavailableip'
50NIOS_IPV4_NETWORK_CONTAINER = 'networkcontainer'
51NIOS_IPV6_NETWORK_CONTAINER = 'ipv6networkcontainer'
52NIOS_MEMBER = 'member'
53
54NIOS_PROVIDER_SPEC = {
55    'host': dict(fallback=(env_fallback, ['INFOBLOX_HOST'])),
56    'username': dict(fallback=(env_fallback, ['INFOBLOX_USERNAME'])),
57    'password': dict(fallback=(env_fallback, ['INFOBLOX_PASSWORD']), no_log=True),
58    'validate_certs': dict(type='bool', default=False, fallback=(env_fallback, ['INFOBLOX_SSL_VERIFY']), aliases=['ssl_verify']),
59    'silent_ssl_warnings': dict(type='bool', default=True),
60    'http_request_timeout': dict(type='int', default=10, fallback=(env_fallback, ['INFOBLOX_HTTP_REQUEST_TIMEOUT'])),
61    'http_pool_connections': dict(type='int', default=10),
62    'http_pool_maxsize': dict(type='int', default=10),
63    'max_retries': dict(type='int', default=3, fallback=(env_fallback, ['INFOBLOX_MAX_RETRIES'])),
64    'wapi_version': dict(default='2.1', fallback=(env_fallback, ['INFOBLOX_WAP_VERSION'])),
65    'max_results': dict(type='int', default=1000, fallback=(env_fallback, ['INFOBLOX_MAX_RETRIES']))
66}
67
68
69def get_connector(*args, **kwargs):
70    ''' Returns an instance of infoblox_client.connector.Connector
71    :params args: positional arguments are silently ignored
72    :params kwargs: dict that is passed to Connector init
73    :returns: Connector
74    '''
75    if not HAS_INFOBLOX_CLIENT:
76        raise Exception('infoblox-client is required but does not appear '
77                        'to be installed.  It can be installed using the '
78                        'command `pip install infoblox-client`')
79
80    if not set(kwargs.keys()).issubset(list(NIOS_PROVIDER_SPEC.keys()) + ['ssl_verify']):
81        raise Exception('invalid or unsupported keyword argument for connector')
82    for key, value in iteritems(NIOS_PROVIDER_SPEC):
83        if key not in kwargs:
84            # apply default values from NIOS_PROVIDER_SPEC since we cannot just
85            # assume the provider values are coming from AnsibleModule
86            if 'default' in value:
87                kwargs[key] = value['default']
88
89            # override any values with env variables unless they were
90            # explicitly set
91            env = ('INFOBLOX_%s' % key).upper()
92            if env in os.environ:
93                kwargs[key] = os.environ.get(env)
94
95    if 'validate_certs' in kwargs.keys():
96        kwargs['ssl_verify'] = kwargs['validate_certs']
97        kwargs.pop('validate_certs', None)
98
99    return Connector(kwargs)
100
101
102def normalize_extattrs(value):
103    ''' Normalize extattrs field to expected format
104    The module accepts extattrs as key/value pairs.  This method will
105    transform the key/value pairs into a structure suitable for
106    sending across WAPI in the format of:
107        extattrs: {
108            key: {
109                value: <value>
110            }
111        }
112    '''
113    return dict([(k, {'value': v}) for k, v in iteritems(value)])
114
115
116def flatten_extattrs(value):
117    ''' Flatten the key/value struct for extattrs
118    WAPI returns extattrs field as a dict in form of:
119        extattrs: {
120            key: {
121                value: <value>
122            }
123        }
124    This method will flatten the structure to:
125        extattrs: {
126            key: value
127        }
128    '''
129    return dict([(k, v['value']) for k, v in iteritems(value)])
130
131
132def member_normalize(member_spec):
133    ''' Transforms the member module arguments into a valid WAPI struct
134    This function will transform the arguments into a structure that
135    is a valid WAPI structure in the format of:
136        {
137            key: <value>,
138        }
139    It will remove any arguments that are set to None since WAPI will error on
140    that condition.
141    The remainder of the value validation is performed by WAPI
142    Some parameters in ib_spec are passed as a list in order to pass the validation for elements.
143    In this function, they are converted to dictionary.
144    '''
145    member_elements = ['vip_setting', 'ipv6_setting', 'lan2_port_setting', 'mgmt_port_setting',
146                       'pre_provisioning', 'network_setting', 'v6_network_setting',
147                       'ha_port_setting', 'lan_port_setting', 'lan2_physical_setting',
148                       'lan_ha_port_setting', 'mgmt_network_setting', 'v6_mgmt_network_setting']
149    for key in list(member_spec.keys()):
150        if key in member_elements and member_spec[key] is not None:
151            member_spec[key] = member_spec[key][0]
152        if isinstance(member_spec[key], dict):
153            member_spec[key] = member_normalize(member_spec[key])
154        elif isinstance(member_spec[key], list):
155            for x in member_spec[key]:
156                if isinstance(x, dict):
157                    x = member_normalize(x)
158        elif member_spec[key] is None:
159            del member_spec[key]
160    return member_spec
161
162
163def normalize_ib_spec(ib_spec):
164    result = {}
165    for arg in ib_spec:
166        result[arg] = dict([(k, v)
167                            for k, v in iteritems(ib_spec[arg])
168                            if k not in ('ib_req', 'transform', 'update')])
169    return result
170
171
172class WapiBase(object):
173    ''' Base class for implementing Infoblox WAPI API '''
174    provider_spec = {'provider': dict(type='dict', options=NIOS_PROVIDER_SPEC)}
175
176    def __init__(self, provider):
177        self.connector = get_connector(**provider)
178
179    def __getattr__(self, name):
180        try:
181            return self.__dict__[name]
182        except KeyError:
183            if name.startswith('_'):
184                raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, name))
185            return partial(self._invoke_method, name)
186
187    def _invoke_method(self, name, *args, **kwargs):
188        try:
189            method = getattr(self.connector, name)
190            return method(*args, **kwargs)
191        except InfobloxException as exc:
192            if hasattr(self, 'handle_exception'):
193                self.handle_exception(name, exc)
194            else:
195                raise
196
197
198class WapiLookup(WapiBase):
199    ''' Implements WapiBase for lookup plugins '''
200    def handle_exception(self, method_name, exc):
201        if ('text' in exc.response):
202            raise Exception(exc.response['text'])
203        else:
204            raise Exception(exc)
205
206
207class WapiInventory(WapiBase):
208    ''' Implements WapiBase for dynamic inventory script '''
209    pass
210
211
212class WapiModule(WapiBase):
213    ''' Implements WapiBase for executing a NIOS module '''
214    def __init__(self, module):
215        self.module = module
216        provider = module.params['provider']
217        try:
218            super(WapiModule, self).__init__(provider)
219        except Exception as exc:
220            self.module.fail_json(msg=to_text(exc))
221
222    def handle_exception(self, method_name, exc):
223        ''' Handles any exceptions raised
224        This method will be called if an InfobloxException is raised for
225        any call to the instance of Connector and also, in case of generic
226        exception. This method will then gracefully fail the module.
227        :args exc: instance of InfobloxException
228        '''
229        if ('text' in exc.response):
230            self.module.fail_json(
231                msg=exc.response['text'],
232                type=exc.response['Error'].split(':')[0],
233                code=exc.response.get('code'),
234                operation=method_name
235            )
236        else:
237            self.module.fail_json(msg=to_native(exc))
238
239    def run(self, ib_obj_type, ib_spec):
240        ''' Runs the module and performans configuration tasks
241        :args ib_obj_type: the WAPI object type to operate against
242        :args ib_spec: the specification for the WAPI object as a dict
243        :returns: a results dict
244        '''
245
246        update = new_name = None
247        state = self.module.params['state']
248        if state not in ('present', 'absent'):
249            self.module.fail_json(msg='state must be one of `present`, `absent`, got `%s`' % state)
250
251        result = {'changed': False}
252
253        obj_filter = dict([(k, self.module.params[k]) for k, v in iteritems(ib_spec) if v.get('ib_req')])
254
255        # get object reference
256        ib_obj_ref, update, new_name = self.get_object_ref(self.module, ib_obj_type, obj_filter, ib_spec)
257        proposed_object = {}
258        for key, value in iteritems(ib_spec):
259            if self.module.params[key] is not None:
260                if 'transform' in value:
261                    proposed_object[key] = value['transform'](self.module)
262                else:
263                    proposed_object[key] = self.module.params[key]
264
265        # If configure_by_dns is set to False and view is 'default', then delete the default dns
266        if not proposed_object.get('configure_for_dns') and proposed_object.get('view') == 'default'\
267                and ib_obj_type == NIOS_HOST_RECORD:
268            del proposed_object['view']
269
270        if ib_obj_ref:
271            if len(ib_obj_ref) > 1:
272                for each in ib_obj_ref:
273                    # To check for existing A_record with same name with input A_record by IP
274                    if each.get('ipv4addr') and each.get('ipv4addr') == proposed_object.get('ipv4addr'):
275                        current_object = each
276                    # To check for existing Host_record with same name with input Host_record by IP
277                    elif each.get('ipv4addrs')[0].get('ipv4addr') and each.get('ipv4addrs')[0].get('ipv4addr')\
278                            == proposed_object.get('ipv4addrs')[0].get('ipv4addr'):
279                        current_object = each
280                    # Else set the current_object with input value
281                    else:
282                        current_object = obj_filter
283                        ref = None
284            else:
285                current_object = ib_obj_ref[0]
286            if 'extattrs' in current_object:
287                current_object['extattrs'] = flatten_extattrs(current_object['extattrs'])
288            if current_object.get('_ref'):
289                ref = current_object.pop('_ref')
290        else:
291            current_object = obj_filter
292            ref = None
293        # checks if the object type is member to normalize the attributes being passed
294        if (ib_obj_type == NIOS_MEMBER):
295            proposed_object = member_normalize(proposed_object)
296
297        # checks if the name's field has been updated
298        if update and new_name:
299            proposed_object['name'] = new_name
300
301        check_remove = []
302        if (ib_obj_type == NIOS_HOST_RECORD):
303            # this check is for idempotency, as if the same ip address shall be passed
304            # add param will be removed, and same exists true for remove case as well.
305            if 'ipv4addrs' in [current_object and proposed_object]:
306                for each in current_object['ipv4addrs']:
307                    if each['ipv4addr'] == proposed_object['ipv4addrs'][0]['ipv4addr']:
308                        if 'add' in proposed_object['ipv4addrs'][0]:
309                            del proposed_object['ipv4addrs'][0]['add']
310                            break
311                    check_remove += each.values()
312                if proposed_object['ipv4addrs'][0]['ipv4addr'] not in check_remove:
313                    if 'remove' in proposed_object['ipv4addrs'][0]:
314                        del proposed_object['ipv4addrs'][0]['remove']
315
316        res = None
317        modified = not self.compare_objects(current_object, proposed_object)
318        if 'extattrs' in proposed_object:
319            proposed_object['extattrs'] = normalize_extattrs(proposed_object['extattrs'])
320
321        # Checks if nios_next_ip param is passed in ipv4addrs/ipv4addr args
322        proposed_object = self.check_if_nios_next_ip_exists(proposed_object)
323
324        if state == 'present':
325            if ref is None:
326                if not self.module.check_mode:
327                    self.create_object(ib_obj_type, proposed_object)
328                result['changed'] = True
329            # Check if NIOS_MEMBER and the flag to call function create_token is set
330            elif (ib_obj_type == NIOS_MEMBER) and (proposed_object['create_token']):
331                proposed_object = None
332                # the function creates a token that can be used by a pre-provisioned member to join the grid
333                result['api_results'] = self.call_func('create_token', ref, proposed_object)
334                result['changed'] = True
335            elif modified:
336                if 'ipv4addrs' in proposed_object:
337                    if ('add' not in proposed_object['ipv4addrs'][0]) and ('remove' not in proposed_object['ipv4addrs'][0]):
338                        self.check_if_recordname_exists(obj_filter, ib_obj_ref, ib_obj_type, current_object, proposed_object)
339
340                if (ib_obj_type in (NIOS_HOST_RECORD, NIOS_NETWORK_VIEW, NIOS_DNS_VIEW)):
341                    run_update = True
342                    proposed_object = self.on_update(proposed_object, ib_spec)
343                    if 'ipv4addrs' in proposed_object:
344                        if ('add' or 'remove') in proposed_object['ipv4addrs'][0]:
345                            run_update, proposed_object = self.check_if_add_remove_ip_arg_exists(proposed_object)
346                            if run_update:
347                                res = self.update_object(ref, proposed_object)
348                                result['changed'] = True
349                            else:
350                                res = ref
351                if (ib_obj_type in (NIOS_A_RECORD, NIOS_AAAA_RECORD, NIOS_PTR_RECORD, NIOS_SRV_RECORD)):
352                    # popping 'view' key as update of 'view' is not supported with respect to a:record/aaaa:record/srv:record/ptr:record
353                    proposed_object = self.on_update(proposed_object, ib_spec)
354                    del proposed_object['view']
355                    if not self.module.check_mode:
356                        res = self.update_object(ref, proposed_object)
357                    result['changed'] = True
358                elif 'network_view' in proposed_object:
359                    proposed_object.pop('network_view')
360                    result['changed'] = True
361                if not self.module.check_mode and res is None:
362                    proposed_object = self.on_update(proposed_object, ib_spec)
363                    self.update_object(ref, proposed_object)
364                    result['changed'] = True
365
366        elif state == 'absent':
367            if ref is not None:
368                if 'ipv4addrs' in proposed_object:
369                    if 'remove' in proposed_object['ipv4addrs'][0]:
370                        self.check_if_add_remove_ip_arg_exists(proposed_object)
371                        self.update_object(ref, proposed_object)
372                        result['changed'] = True
373                elif not self.module.check_mode:
374                    self.delete_object(ref)
375                    result['changed'] = True
376
377        return result
378
379    def check_if_recordname_exists(self, obj_filter, ib_obj_ref, ib_obj_type, current_object, proposed_object):
380        ''' Send POST request if host record input name and retrieved ref name is same,
381            but input IP and retrieved IP is different'''
382
383        if 'name' in (obj_filter and ib_obj_ref[0]) and ib_obj_type == NIOS_HOST_RECORD:
384            obj_host_name = obj_filter['name']
385            ref_host_name = ib_obj_ref[0]['name']
386            if 'ipv4addrs' in (current_object and proposed_object):
387                current_ip_addr = current_object['ipv4addrs'][0]['ipv4addr']
388                proposed_ip_addr = proposed_object['ipv4addrs'][0]['ipv4addr']
389            elif 'ipv6addrs' in (current_object and proposed_object):
390                current_ip_addr = current_object['ipv6addrs'][0]['ipv6addr']
391                proposed_ip_addr = proposed_object['ipv6addrs'][0]['ipv6addr']
392
393            if obj_host_name == ref_host_name and current_ip_addr != proposed_ip_addr:
394                self.create_object(ib_obj_type, proposed_object)
395
396    def check_if_nios_next_ip_exists(self, proposed_object):
397        ''' Check if nios_next_ip argument is passed in ipaddr while creating
398            host record, if yes then format proposed object ipv4addrs and pass
399            func:nextavailableip and ipaddr range to create hostrecord with next
400             available ip in one call to avoid any race condition '''
401
402        if 'ipv4addrs' in proposed_object:
403            if 'nios_next_ip' in proposed_object['ipv4addrs'][0]['ipv4addr']:
404                ip_range = check_type_dict(proposed_object['ipv4addrs'][0]['ipv4addr'])['nios_next_ip']
405                proposed_object['ipv4addrs'][0]['ipv4addr'] = NIOS_NEXT_AVAILABLE_IP + ':' + ip_range
406        elif 'ipv4addr' in proposed_object:
407            if 'nios_next_ip' in proposed_object['ipv4addr']:
408                ip_range = check_type_dict(proposed_object['ipv4addr'])['nios_next_ip']
409                proposed_object['ipv4addr'] = NIOS_NEXT_AVAILABLE_IP + ':' + ip_range
410
411        return proposed_object
412
413    def check_if_add_remove_ip_arg_exists(self, proposed_object):
414        '''
415            This function shall check if add/remove param is set to true and
416            is passed in the args, then we will update the proposed dictionary
417            to add/remove IP to existing host_record, if the user passes false
418            param with the argument nothing shall be done.
419            :returns: True if param is changed based on add/remove, and also the
420            changed proposed_object.
421        '''
422        update = False
423        if 'add' in proposed_object['ipv4addrs'][0]:
424            if proposed_object['ipv4addrs'][0]['add']:
425                proposed_object['ipv4addrs+'] = proposed_object['ipv4addrs']
426                del proposed_object['ipv4addrs']
427                del proposed_object['ipv4addrs+'][0]['add']
428                update = True
429            else:
430                del proposed_object['ipv4addrs'][0]['add']
431        elif 'remove' in proposed_object['ipv4addrs'][0]:
432            if proposed_object['ipv4addrs'][0]['remove']:
433                proposed_object['ipv4addrs-'] = proposed_object['ipv4addrs']
434                del proposed_object['ipv4addrs']
435                del proposed_object['ipv4addrs-'][0]['remove']
436                update = True
437            else:
438                del proposed_object['ipv4addrs'][0]['remove']
439        return update, proposed_object
440
441    def issubset(self, item, objects):
442        ''' Checks if item is a subset of objects
443        :args item: the subset item to validate
444        :args objects: superset list of objects to validate against
445        :returns: True if item is a subset of one entry in objects otherwise
446            this method will return None
447        '''
448        for obj in objects:
449            if isinstance(item, dict):
450                if all(entry in obj.items() for entry in item.items()):
451                    return True
452            else:
453                if item in obj:
454                    return True
455
456    def compare_objects(self, current_object, proposed_object):
457        for key, proposed_item in iteritems(proposed_object):
458            current_item = current_object.get(key)
459
460            # if proposed has a key that current doesn't then the objects are
461            # not equal and False will be immediately returned
462            if current_item is None:
463                return False
464
465            elif isinstance(proposed_item, list):
466                if key == 'aliases':
467                    if set(current_item) != set(proposed_item):
468                        return False
469                for subitem in proposed_item:
470                    if not self.issubset(subitem, current_item):
471                        return False
472
473            elif isinstance(proposed_item, dict):
474                return self.compare_objects(current_item, proposed_item)
475
476            else:
477                if current_item != proposed_item:
478                    return False
479
480        return True
481
482    def get_object_ref(self, module, ib_obj_type, obj_filter, ib_spec):
483        ''' this function gets the reference object of pre-existing nios objects '''
484
485        update = False
486        old_name = new_name = None
487        if ('name' in obj_filter):
488            # gets and returns the current object based on name/old_name passed
489            try:
490                name_obj = check_type_dict(obj_filter['name'])
491                old_name = name_obj['old_name']
492                new_name = name_obj['new_name']
493            except TypeError:
494                name = obj_filter['name']
495
496            if old_name and new_name:
497                if (ib_obj_type == NIOS_HOST_RECORD):
498                    test_obj_filter = dict([('name', old_name), ('view', obj_filter['view'])])
499                elif (ib_obj_type in (NIOS_AAAA_RECORD, NIOS_A_RECORD)):
500                    test_obj_filter = obj_filter
501                else:
502                    test_obj_filter = dict([('name', old_name)])
503                # get the object reference
504                ib_obj = self.get_object(ib_obj_type, test_obj_filter, return_fields=list(ib_spec.keys()))
505                if ib_obj:
506                    obj_filter['name'] = new_name
507                else:
508                    test_obj_filter['name'] = new_name
509                    ib_obj = self.get_object(ib_obj_type, test_obj_filter, return_fields=list(ib_spec.keys()))
510                update = True
511                return ib_obj, update, new_name
512            if (ib_obj_type == NIOS_HOST_RECORD):
513                # to check only by name if dns bypassing is set
514                if not obj_filter['configure_for_dns']:
515                    test_obj_filter = dict([('name', name)])
516                else:
517                    test_obj_filter = dict([('name', name), ('view', obj_filter['view'])])
518            elif (ib_obj_type == NIOS_IPV4_FIXED_ADDRESS or ib_obj_type == NIOS_IPV6_FIXED_ADDRESS and 'mac' in obj_filter):
519                test_obj_filter = dict([['mac', obj_filter['mac']]])
520            elif (ib_obj_type == NIOS_A_RECORD):
521                # resolves issue where a_record with uppercase name was returning null and was failing
522                test_obj_filter = obj_filter
523                test_obj_filter['name'] = test_obj_filter['name'].lower()
524                # resolves issue where multiple a_records with same name and different IP address
525                try:
526                    ipaddr_obj = check_type_dict(obj_filter['ipv4addr'])
527                    ipaddr = ipaddr_obj['old_ipv4addr']
528                except TypeError:
529                    ipaddr = obj_filter['ipv4addr']
530                test_obj_filter['ipv4addr'] = ipaddr
531            elif (ib_obj_type == NIOS_TXT_RECORD):
532                # resolves issue where multiple txt_records with same name and different text
533                test_obj_filter = obj_filter
534                try:
535                    text_obj = check_type_dict(obj_filter['text'])
536                    txt = text_obj['old_text']
537                except TypeError:
538                    txt = obj_filter['text']
539                test_obj_filter['text'] = txt
540            # check if test_obj_filter is empty copy passed obj_filter
541            else:
542                test_obj_filter = obj_filter
543            ib_obj = self.get_object(ib_obj_type, test_obj_filter.copy(), return_fields=list(ib_spec.keys()))
544        elif (ib_obj_type == NIOS_A_RECORD):
545            # resolves issue where multiple a_records with same name and different IP address
546            test_obj_filter = obj_filter
547            try:
548                ipaddr_obj = check_type_dict(obj_filter['ipv4addr'])
549                ipaddr = ipaddr_obj['old_ipv4addr']
550            except TypeError:
551                ipaddr = obj_filter['ipv4addr']
552            test_obj_filter['ipv4addr'] = ipaddr
553            ib_obj = self.get_object(ib_obj_type, test_obj_filter.copy(), return_fields=list(ib_spec.keys()))
554        elif (ib_obj_type == NIOS_TXT_RECORD):
555            # resolves issue where multiple txt_records with same name and different text
556            test_obj_filter = obj_filter
557            try:
558                text_obj = check_type_dict(obj_filter['text'])
559                txt = text_obj['old_text']
560            except TypeError:
561                txt = obj_filter['text']
562            test_obj_filter['text'] = txt
563            ib_obj = self.get_object(ib_obj_type, test_obj_filter.copy(), return_fields=list(ib_spec.keys()))
564        elif (ib_obj_type == NIOS_ZONE):
565            # del key 'restart_if_needed' as nios_zone get_object fails with the key present
566            temp = ib_spec['restart_if_needed']
567            del ib_spec['restart_if_needed']
568            ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=list(ib_spec.keys()))
569            # reinstate restart_if_needed if ib_obj is none, meaning there's no existing nios_zone ref
570            if not ib_obj:
571                ib_spec['restart_if_needed'] = temp
572        elif (ib_obj_type == NIOS_MEMBER):
573            # del key 'create_token' as nios_member get_object fails with the key present
574            temp = ib_spec['create_token']
575            del ib_spec['create_token']
576            ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=list(ib_spec.keys()))
577            if temp:
578                # reinstate 'create_token' key
579                ib_spec['create_token'] = temp
580        else:
581            ib_obj = self.get_object(ib_obj_type, obj_filter.copy(), return_fields=list(ib_spec.keys()))
582        return ib_obj, update, new_name
583
584    def on_update(self, proposed_object, ib_spec):
585        ''' Event called before the update is sent to the API endpoing
586        This method will allow the final proposed object to be changed
587        and/or keys filtered before it is sent to the API endpoint to
588        be processed.
589        :args proposed_object: A dict item that will be encoded and sent
590            the API endpoint with the updated data structure
591        :returns: updated object to be sent to API endpoint
592        '''
593        keys = set()
594        for key, value in iteritems(proposed_object):
595            update = ib_spec[key].get('update', True)
596            if not update:
597                keys.add(key)
598        return dict([(k, v) for k, v in iteritems(proposed_object) if k not in keys])
599