diff --git a/lib/ansible/module_utils/gcp_utils.py b/lib/ansible/module_utils/gcp_utils.py index a938b5237c4..599e8fca3e0 100644 --- a/lib/ansible/module_utils/gcp_utils.py +++ b/lib/ansible/module_utils/gcp_utils.py @@ -17,10 +17,15 @@ except ImportError: HAS_GOOGLE_LIBRARIES = False from ansible.module_utils.basic import AnsibleModule, env_fallback +from ansible.module_utils.six import string_types +from ansible.module_utils._text import to_text import os def navigate_hash(source, path, default=None): + if not source: + return None + key = path[0] path = path[1:] if key not in source: @@ -36,6 +41,28 @@ class GcpRequestException(Exception): pass +def remove_nones_from_dict(obj): + new_obj = {} + for key in obj: + value = obj[key] + if value is not None and value != {} and value != []: + new_obj[key] = value + return new_obj + + +# Handles the replacement of dicts with values -> the needed value for GCP API +def replace_resource_dict(item, value): + if isinstance(item, list): + items = [] + for i in item: + items.append(replace_resource_dict(i, value)) + return items + else: + if not item: + return item + return item.get(value) + + # Handles all authentation and HTTP sessions for GCP API calls. class GcpSession(object): def __init__(self, module, product): @@ -47,25 +74,25 @@ class GcpSession(object): try: return self.session().get(url, json=body, headers=self._headers()) except getattr(requests.exceptions, 'RequestException') as inst: - raise GcpRequestException(inst.message) + self.module.fail_json(msg=inst.message) def post(self, url, body=None): try: return self.session().post(url, json=body, headers=self._headers()) except getattr(requests.exceptions, 'RequestException') as inst: - raise GcpRequestException(inst.message) + self.module.fail_json(msg=inst.message) def delete(self, url, body=None): try: return self.session().delete(url, json=body, headers=self._headers()) except getattr(requests.exceptions, 'RequestException') as inst: - raise GcpRequestException(inst.message) + self.module.fail_json(msg=inst.message) def put(self, url, body=None): try: return self.session().put(url, json=body, headers=self._headers()) except getattr(requests.exceptions, 'RequestException') as inst: - raise GcpRequestException(inst.message) + self.module.fail_json(msg=inst.message) def session(self): return AuthorizedSession( @@ -148,7 +175,86 @@ class GcpModule(AnsibleModule): AnsibleModule.__init__(self, *args, **kwargs) + def raise_for_status(self, response): + try: + response.raise_for_status() + except getattr(requests.exceptions, 'RequestException') as inst: + self.fail_json(msg="GCP returned error: %s" % response.json()) + def _merge_dictionaries(self, a, b): new = a.copy() new.update(b) return new + + +# This class takes in two dictionaries `a` and `b`. +# These are dictionaries of arbitrary depth, but made up of standard Python +# types only. +# This differ will compare all values in `a` to those in `b`. +# Note: Only keys in `a` will be compared. Extra keys in `b` will be ignored. +# Note: On all lists, order does matter. +class GcpRequest(object): + def __init__(self, request): + self.request = request + + def __eq__(self, other): + return not self.difference(other) + + def __ne__(self, other): + return not self.__eq__(other) + + # Returns the difference between `self.request` and `b` + def difference(self, b): + return self._compare_dicts(self.request, b.request) + + def _compare_dicts(self, dict1, dict2): + difference = {} + for key in dict1: + difference[key] = self._compare_value(dict1.get(key), dict2.get(key)) + + # Remove all empty values from difference. + difference2 = {} + for key in difference: + if difference[key]: + difference2[key] = difference[key] + + return difference2 + + # Takes in two lists and compares them. + def _compare_lists(self, list1, list2): + difference = [] + for index in range(len(list1)): + value1 = list1[index] + if index < len(list2): + value2 = list2[index] + difference.append(self._compare_value(value1, value2)) + + difference2 = [] + for value in difference: + if value: + difference2.append(value) + + return difference2 + + def _compare_value(self, value1, value2): + diff = None + # If a None is found, a difference does not exist. + # Only differing values matter. + if not value2: + return None + + # Can assume non-None types at this point. + try: + if isinstance(value1, list): + diff = self._compare_lists(value1, value2) + elif isinstance(value2, dict): + diff = self._compare_dicts(value1, value2) + # Always use to_text values to avoid unicode issues. + elif to_text(value1) != to_text(value2): + diff = value1 + # to_text may throw UnicodeErrors. + # These errors shouldn't crash Ansible and should be hidden. + except UnicodeError: + pass + + return diff diff --git a/lib/ansible/modules/cloud/google/gcp_dns_resource_record_set.py b/lib/ansible/modules/cloud/google/gcp_dns_resource_record_set.py new file mode 100644 index 00000000000..cd1ec280c44 --- /dev/null +++ b/lib/ansible/modules/cloud/google/gcp_dns_resource_record_set.py @@ -0,0 +1,461 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Google +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file at +# https://www.github.com/GoogleCloudPlatform/magic-modules +# +# ---------------------------------------------------------------------------- + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +################################################################################ +# Documentation +################################################################################ + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ["preview"], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: gcp_dns_resource_record_set +description: + - A single DNS record that exists on a domain name (i.e. in a managed zone). + - This record defines the information about the domain and where the domain + / subdomains direct to. + - The record will include the domain/subdomain name, a type (i.e. A, AAA, + CAA, MX, CNAME, NS, etc). +short_description: Creates a GCP ResourceRecordSet +version_added: 2.6 +author: Google Inc. (@googlecloudplatform) +requirements: + - python >= 2.6 + - requests >= 2.18.4 + - google-auth >= 1.3.0 +options: + state: + description: + - Whether the given object should exist in GCP + required: true + choices: ['present', 'absent'] + default: 'present' + name: + description: + - For example, www.example.com. + required: true + type: + description: + - One of valid DNS resource types. + required: true + choices: ['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SOA', 'SPF', 'SRV', 'TXT'] + ttl: + description: + - Number of seconds that this ResourceRecordSet can be cached by + resolvers. + required: false + target: + description: + - As defined in RFC 1035 (section 5) and RFC 1034 (section 3.6.1). + required: false + managed_zone: + description: + - A reference to ManagedZone resource. + required: true +extends_documentation_fragment: gcp +''' + +EXAMPLES = ''' +- name: create a managed zone + gcp_dns_managed_zone: + name: 'managedzone-rrs' + dns_name: 'testzone-4.com.' + description: 'test zone' + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: present + register: managed_zone +- name: create a resource record set + gcp_dns_resource_record_set: + name: 'www.testzone-4.com.' + managed_zone: "{{ managed_zone }}" + type: 'A' + ttl: 600 + target: + - 10.1.2.3 + - 40.5.6.7 + project: testProject + auth_kind: service_account + service_account_file: /tmp/auth.pem + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: present +''' + +RETURN = ''' + name: + description: + - For example, www.example.com. + returned: success + type: str + type: + description: + - One of valid DNS resource types. + returned: success + type: str + ttl: + description: + - Number of seconds that this ResourceRecordSet can be cached by + resolvers. + returned: success + type: int + target: + description: + - As defined in RFC 1035 (section 5) and RFC 1034 (section 3.6.1). + returned: success + type: list + managed_zone: + description: + - A reference to ManagedZone resource. + returned: success + type: dict +''' + +################################################################################ +# Imports +################################################################################ + +from ansible.module_utils.gcp_utils import navigate_hash, GcpSession, GcpModule, GcpRequest, replace_resource_dict +import json +import copy +import datetime +import time + +################################################################################ +# Main +################################################################################ + + +def main(): + """Main function""" + + module = GcpModule( + argument_spec=dict( + state=dict(default='present', choices=['present', 'absent'], type='str'), + name=dict(required=True, type='str'), + type=dict(required=True, type='str', choices=['A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SOA', 'SPF', 'SRV', 'TXT']), + ttl=dict(type='int'), + target=dict(type='list', elements='str'), + managed_zone=dict(required=True, type='dict') + ) + ) + + state = module.params['state'] + kind = 'dns#resourceRecordSet' + + fetch = fetch_wrapped_resource(module, 'dns#resourceRecordSet', + 'dns#resourceRecordSetsListResponse', + 'rrsets') + changed = False + + if fetch: + if state == 'present': + if is_different(module, fetch): + fetch = update(module, self_link(module), kind, fetch) + changed = True + else: + delete(module, self_link(module), kind, fetch) + fetch = {} + changed = True + else: + if state == 'present': + fetch = create(module, collection(module), kind) + changed = True + else: + fetch = {} + + fetch.update({'changed': changed}) + + module.exit_json(**fetch) + + +def create(module, link, kind): + change = create_change(None, updated_record(module), module) + change_id = int(change['id']) + if change['status'] == 'pending': + wait_for_change_to_complete(change_id, module) + return fetch_wrapped_resource(module, 'dns#resourceRecordSet', + 'dns#resourceRecordSetsListResponse', + 'rrsets') + + +def update(module, link, kind, fetch): + change = create_change(fetch, updated_record(module), module) + change_id = int(change['id']) + if change['status'] == 'pending': + wait_for_change_to_complete(change_id, module) + return fetch_wrapped_resource(module, 'dns#resourceRecordSet', + 'dns#resourceRecordSetsListResponse', + 'rrsets') + + +def delete(module, link, kind, fetch): + change = create_change(fetch, None, module) + change_id = int(change['id']) + if change['status'] == 'pending': + wait_for_change_to_complete(change_id, module) + return fetch_wrapped_resource(module, 'dns#resourceRecordSet', + 'dns#resourceRecordSetsListResponse', + 'rrsets') + + +def resource_to_request(module): + request = { + u'kind': 'dns#resourceRecordSet', + u'managed_zone': replace_resource_dict(module.params.get(u'managed_zone', {}), 'name'), + u'name': module.params.get('name'), + u'type': module.params.get('type'), + u'ttl': module.params.get('ttl'), + u'rrdatas': module.params.get('target') + } + return_vals = {} + for k, v in request.items(): + if v: + return_vals[k] = v + + return return_vals + + +def fetch_resource(module, link, kind): + auth = GcpSession(module, 'dns') + return return_if_object(module, auth.get(link), kind) + + +def fetch_wrapped_resource(module, kind, wrap_kind, wrap_path): + result = fetch_resource(module, self_link(module), wrap_kind) + if result is None or wrap_path not in result: + return None + + result = unwrap_resource(result[wrap_path], module) + + if result is None: + return None + + if result['kind'] != kind: + module.fail_json(msg="Incorrect result: {kind}".format(**result)) + + return result + + +def self_link(module): + return "https://www.googleapis.com/dns/v1/projects/{project}/managedZones/{managed_zone}/rrsets?name={name}&type={type}".format(**module.params) + + +def collection(module, extra_url=''): + return "https://www.googleapis.com/dns/v1/projects/{project}/managedZones/{managed_zone}/changes".format(**module.params) + extra_url + + +def return_if_object(module, response, kind): + # If not found, return nothing. + if response.status_code == 404: + return None + + # If no content, return nothing. + if response.status_code == 204: + return None + + try: + module.raise_for_status(response) + result = response.json() + except getattr(json.decoder, 'JSONDecodeError', ValueError) as inst: + module.fail_json(msg="Invalid JSON response with error: %s" % inst) + + if navigate_hash(result, ['error', 'errors']): + module.fail_json(msg=navigate_hash(result, ['error', 'errors'])) + if result['kind'] != kind: + module.fail_json(msg="Incorrect result: {kind}".format(**result)) + + return result + + +def is_different(module, response): + request = resource_to_request(module) + response = response_to_hash(module, response) + + # Remove all output-only from response. + response_vals = {} + for k, v in response.items(): + if k in request: + response_vals[k] = v + + request_vals = {} + for k, v in request.items(): + if k in response: + request_vals[k] = v + + return GcpRequest(request_vals) != GcpRequest(response_vals) + + +# Remove unnecessary properties from the response. +# This is for doing comparisons with Ansible's current parameters. +def response_to_hash(module, response): + return { + u'name': response.get(u'name'), + u'type': response.get(u'type'), + u'ttl': response.get(u'ttl'), + u'rrdatas': response.get(u'target') + } + + +def updated_record(module): + return { + 'kind': 'dns#resourceRecordSet', + 'name': module.params['name'], + 'type': module.params['type'], + 'ttl': module.params['ttl'] if module.params['ttl'] else 900, + 'rrdatas': module.params['target'] + } + + +def unwrap_resource(result, module): + if not result: + return None + return result[0] + + +class SOAForwardable(object): + def __init__(self, params, module): + self.params = params + self.module = module + + def fail_json(self, *args, **kwargs): + self.module.fail_json(*args, **kwargs) + + +def prefetch_soa_resource(module): + name = module.params['name'].split('.')[1:] + + resource = SOAForwardable({ + 'type': 'SOA', + 'managed_zone': module.params['managed_zone'], + 'name': '.'.join(name), + 'project': module.params['project'], + 'scopes': module.params['scopes'], + 'service_account_file': module.params['service_account_file'], + 'auth_kind': module.params['auth_kind'], + 'service_account_email': module.params['service_account_email'] + }, module) + + result = fetch_wrapped_resource(resource, 'dns#resourceRecordSet', + 'dns#resourceRecordSetsListResponse', + 'rrsets') + if not result: + raise ValueError("Google DNS Managed Zone %s not found" % module.params['managed_zone']) + return result + + +def create_change(original, updated, module): + auth = GcpSession(module, 'dns') + return return_if_change_object(module, + auth.post(collection(module), + resource_to_change_request( + original, updated, module) + )) + + +# Fetch current SOA. We need the last SOA so we can increment its serial +def update_soa(module): + original_soa = prefetch_soa_resource(module) + + # Create a clone of the SOA record so we can update it + updated_soa = copy.deepcopy(original_soa) + + soa_parts = updated_soa['rrdatas'][0].split(' ') + soa_parts[2] = str(int(soa_parts[2]) + 1) + updated_soa['rrdatas'][0] = ' '.join(soa_parts) + return [original_soa, updated_soa] + + +def resource_to_change_request(original_record, updated_record, module): + original_soa, updated_soa = update_soa(module) + result = new_change_request() + add_additions(result, updated_soa, updated_record) + add_deletions(result, original_soa, original_record) + return result + + +def add_additions(result, updated_soa, updated_record): + if updated_soa: + result['additions'].append(updated_soa) + if updated_record: + result['additions'].append(updated_record) + + +def add_deletions(result, original_soa, original_record): + if original_soa: + result['deletions'].append(original_soa) + + if original_record: + result['deletions'].append(original_record) + + +# TODO(nelsonjr): Merge and delete this code once async operation +# declared in api.yaml +def wait_for_change_to_complete(change_id, module): + status = 'pending' + while status == 'pending': + status = get_change_status(change_id, module) + if status != 'done': + time.sleep(0.5) + + +def get_change_status(change_id, module): + auth = GcpSession(module, 'dns') + link = collection(module, "/%s" % change_id) + return return_if_change_object(module, auth.get(link))['status'] + + +def new_change_request(): + return { + 'kind': 'dns#change', + 'additions': [], + 'deletions': [], + 'start_time': datetime.datetime.now().isoformat() + } + + +def return_if_change_object(module, response): + # If not found, return nothing. + if response.status_code == 404: + return None + + if response.status_code == 204: + return None + + try: + response.raise_for_status() + result = response.json() + except getattr(json.decoder, 'JSONDecodeError', ValueError) as inst: + module.fail_json(msg="Invalid JSON response with error: %s" % inst) + + if result['kind'] != 'dns#change': + module.fail_json(msg="Invalid result: %s" % result['kind']) + + return result + +if __name__ == '__main__': + main() diff --git a/test/integration/targets/gcp_dns_resource_record_set/aliases b/test/integration/targets/gcp_dns_resource_record_set/aliases new file mode 100644 index 00000000000..26507c23cd3 --- /dev/null +++ b/test/integration/targets/gcp_dns_resource_record_set/aliases @@ -0,0 +1 @@ +cloud/gcp diff --git a/test/integration/targets/gcp_dns_resource_record_set/defaults/main.yml b/test/integration/targets/gcp_dns_resource_record_set/defaults/main.yml new file mode 100644 index 00000000000..aa87a2a8e0e --- /dev/null +++ b/test/integration/targets/gcp_dns_resource_record_set/defaults/main.yml @@ -0,0 +1,3 @@ +--- +# defaults file +resource_name: '{{resource_prefix}}' diff --git a/test/integration/targets/gcp_dns_resource_record_set/meta/main.yml b/test/integration/targets/gcp_dns_resource_record_set/meta/main.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/integration/targets/gcp_dns_resource_record_set/tasks/main.yml b/test/integration/targets/gcp_dns_resource_record_set/tasks/main.yml new file mode 100644 index 00000000000..c0f5d2be2cb --- /dev/null +++ b/test/integration/targets/gcp_dns_resource_record_set/tasks/main.yml @@ -0,0 +1,144 @@ +--- +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file at +# https://www.github.com/GoogleCloudPlatform/magic-modules +# +# ---------------------------------------------------------------------------- +# Pre-test setup +- name: create a managed zone + gcp_dns_managed_zone: + name: 'managedzone-rrs' + dns_name: 'testzone-4.com.' + description: 'test zone' + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: present + register: managed_zone +- name: delete a resource record set + gcp_dns_resource_record_set: + name: 'www.testzone-4.com.' + managed_zone: "{{ managed_zone }}" + type: 'A' + ttl: 600 + target: + - 10.1.2.3 + - 40.5.6.7 + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: absent +#---------------------------------------------------------- +- name: create a resource record set + gcp_dns_resource_record_set: + name: 'www.testzone-4.com.' + managed_zone: "{{ managed_zone }}" + type: 'A' + ttl: 600 + target: + - 10.1.2.3 + - 40.5.6.7 + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: present + register: result +- name: assert changed is true + assert: + that: + - result.changed == true + - "result.kind == 'dns#resourceRecordSet'" +# ---------------------------------------------------------------------------- +- name: create a resource record set that already exists + gcp_dns_resource_record_set: + name: 'www.testzone-4.com.' + managed_zone: "{{ managed_zone }}" + type: 'A' + ttl: 600 + target: + - 10.1.2.3 + - 40.5.6.7 + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: present + register: result +- name: assert changed is false + assert: + that: + - result.changed == false + - "result.kind == 'dns#resourceRecordSet'" +#---------------------------------------------------------- +- name: delete a resource record set + gcp_dns_resource_record_set: + name: 'www.testzone-4.com.' + managed_zone: "{{ managed_zone }}" + type: 'A' + ttl: 600 + target: + - 10.1.2.3 + - 40.5.6.7 + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: absent + register: result +- name: assert changed is true + assert: + that: + - result.changed == true + - result.has_key('kind') == False +# ---------------------------------------------------------------------------- +- name: delete a resource record set that does not exist + gcp_dns_resource_record_set: + name: 'www.testzone-4.com.' + managed_zone: "{{ managed_zone }}" + type: 'A' + ttl: 600 + target: + - 10.1.2.3 + - 40.5.6.7 + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: absent + register: result +- name: assert changed is false + assert: + that: + - result.changed == false + - result.has_key('kind') == False +#--------------------------------------------------------- +# Post-test teardown +- name: delete a managed zone + gcp_dns_managed_zone: + name: 'managedzone-rrs' + dns_name: 'testzone-4.com.' + description: 'test zone' + project: "{{ gcp_project }}" + auth_kind: "{{ gcp_cred_kind }}" + service_account_file: "{{ gcp_cred_file }}" + scopes: + - https://www.googleapis.com/auth/ndev.clouddns.readwrite + state: absent + register: managed_zone diff --git a/test/units/module_utils/gcp/test_gcp_utils.py b/test/units/module_utils/gcp/test_gcp_utils.py new file mode 100644 index 00000000000..c6c6958f857 --- /dev/null +++ b/test/units/module_utils/gcp/test_gcp_utils.py @@ -0,0 +1,176 @@ +# -*- coding: utf-8 -*- +# (c) 2016, Tom Melendez +# +# 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 . +import os +import sys + +from ansible.compat.tests import mock, unittest +from ansible.module_utils.gcp_utils import GcpRequest + + +class GCPRequestDifferenceTestCase(unittest.TestCase): + def test_simple_no_difference(self): + value1 = { + 'foo': 'bar', + 'test': 'original' + } + request = GcpRequest(value1) + self.assertEquals(request == request, True) + + def test_simple_different(self): + value1 = { + 'foo': 'bar', + 'test': 'original' + } + value2 = { + 'foo': 'bar', + 'test': 'different' + } + difference = { + 'test': 'original' + } + request1 = GcpRequest(value1) + request2 = GcpRequest(value2) + self.assertEquals(request1 == request2, False) + self.assertEquals(request1.difference(request2), difference) + + def test_nested_dictionaries_no_difference(self): + value1 = { + 'foo': { + 'quiet': { + 'tree': 'test' + }, + 'bar': 'baz' + }, + 'test': 'original' + } + request = GcpRequest(value1) + self.assertEquals(request == request, True) + + def test_nested_dictionaries_with_difference(self): + value1 = { + 'foo': { + 'quiet': { + 'tree': 'test' + }, + 'bar': 'baz' + }, + 'test': 'original' + } + value2 = { + 'foo': { + 'quiet': { + 'tree': 'baz' + }, + 'bar': 'hello' + }, + 'test': 'original' + } + difference = { + 'foo': { + 'quiet': { + 'tree': 'test' + }, + 'bar': 'baz' + } + } + request1 = GcpRequest(value1) + request2 = GcpRequest(value2) + self.assertEquals(request1 == request2, False) + self.assertEquals(request1.difference(request2), difference) + + def test_arrays_strings_no_difference(self): + value1 = { + 'foo': [ + 'baz', + 'bar' + ] + } + request = GcpRequest(value1) + self.assertEquals(request == request, True) + + def test_arrays_strings_with_difference(self): + value1 = { + 'foo': [ + 'baz', + 'bar', + ] + } + + value2 = { + 'foo': [ + 'baz', + 'hello' + ] + } + difference = { + 'foo': [ + 'bar', + ] + } + request1 = GcpRequest(value1) + request2 = GcpRequest(value2) + self.assertEquals(request1 == request2, False) + self.assertEquals(request1.difference(request2), difference) + + def test_arrays_dicts_with_no_difference(self): + value1 = { + 'foo': [ + { + 'test': 'value', + 'foo': 'bar' + }, + { + 'different': 'dict' + } + ] + } + request = GcpRequest(value1) + self.assertEquals(request == request, True) + + def test_arrays_dicts_with_difference(self): + value1 = { + 'foo': [ + { + 'test': 'value', + 'foo': 'bar' + }, + { + 'different': 'dict' + } + ] + } + value2 = { + 'foo': [ + { + 'test': 'value2', + 'foo': 'bar2' + }, + ] + } + difference = { + 'foo': [ + { + 'test': 'value', + 'foo': 'bar' + } + ] + } + request1 = GcpRequest(value1) + request2 = GcpRequest(value2) + self.assertEquals(request1 == request2, False) + self.assertEquals(request1.difference(request2), difference)