Giuseppe Lavagetto has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/369385 )
Change subject: Use own differ instead of puppet catalog diff ...................................................................... Use own differ instead of puppet catalog diff This allows faster execution and better control. Change-Id: I03d4319b49f3c7a195e076decc6a7b741e13d399 --- A puppet_compiler/differ.py M puppet_compiler/filter.py M puppet_compiler/presentation/html.py A puppet_compiler/templates/diffs.jinja2 M puppet_compiler/templates/hostpage.future.jinja2 M puppet_compiler/templates/hostpage.jinja2 A puppet_compiler/tests/fixtures/catalog-change.pson A puppet_compiler/tests/fixtures/catalog.pson M puppet_compiler/tests/test_controller.py A puppet_compiler/tests/test_differ.py M puppet_compiler/tests/test_hostworker.py A puppet_compiler/tests/test_html.py M puppet_compiler/worker.py M setup.py 14 files changed, 894 insertions(+), 96 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/software/puppet-compiler refs/changes/85/369385/1 diff --git a/puppet_compiler/differ.py b/puppet_compiler/differ.py new file mode 100644 index 0000000..9f5cbb6 --- /dev/null +++ b/puppet_compiler/differ.py @@ -0,0 +1,113 @@ +import difflib +import json + +import datadiff + + +class PuppetResource(object): + + def __init__(self, data, resource_filter=None): + self.resource_type = data['type'] + self.title = data['title'] + self.exported = data['exported'] + if resource_filter is None: + self._filter = lambda x: x + else: + self._filter = resource_filter + self._init_params(data.get('parameters', {})) + + def _init_params(self, kwargs): + self.parameters = {} + self.content = '' + self.source = None + # Let's first look for special + for k, v in kwargs.items(): + # Filter out null values + if v is None: + pass + elif k == 'content': + self.content = v + else: + self.parameters[k] = self._filter(v) + + def __str__(self): + return "{res}[{title}]".format(res=self.resource_type, + title=self.title) + + def is_same_of(self, other): + """ + True if it designates the same resource + """ + return (str(self) == str(other)) + + def __eq__(self, other): + if not self.is_same_of(other): + return False + return (self.content == other.content and + self.source == other.source and + self.parameters == other.parameters) + + def __ne__(self, other): + return not self.__eq__(other) + + def diff_if_present(self, other): + if self == other: + return None + + out = {'resource': str(self)} + if self.content != other.content and self.resource_type == 'File': + other_content = other.content.split('\n') + my_content = self.content.split('\n') + content_diff = [ + line for line in difflib.unified_diff( + my_content, other_content, + fromfile='{}.orig'.format(self.title), + tofile=self.title) + ] + out['content'] = content_diff + if self.parameters != other.parameters: + out['parameters'] = datadiff.diff( + self.parameters, other.parameters, + fromfile='{}.orig'.format(str(self)), tofile=str(self) + ) + return out + + +class PuppetCatalog(object): + def __init__(self, filename, resource_filter=None): + self.resources = {} + with open(filename, 'r') as fh: + catalog = json.load(fh, 'latin_1') + for resource in catalog['data']['resources']: + r = PuppetResource(resource, resource_filter) + self.resources[str(r)] = r + self.all_resources = set(self.resources.keys()) + self.name = catalog['data']['name'] + + def diff_if_present(self, other): + diffs = [] + only_in_other = other.all_resources - self.all_resources + only_in_self = self.all_resources - other.all_resources + + for resource in self.all_resources & other.all_resources: + mine = self.resources[resource] + theirs = other.resources[resource] + out = mine.diff_if_present(theirs) + if out is not None: + diffs.append(out) + + num_changed = len(diffs) + num_other = len(only_in_other) + num_self = len(only_in_self) + total_affected = (num_changed + num_other + num_self) + num_resources = len(self.all_resources) + perc_changed = '%.2f%%' % (100 * float(total_affected) / num_resources) + if (total_affected) == 0: + return None + return { + 'total': len(self.all_resources), + 'only_in_self': only_in_self, + 'only_in_other': only_in_other, + 'resource_diffs': diffs, + 'perc_changed': perc_changed + } diff --git a/puppet_compiler/filter.py b/puppet_compiler/filter.py index e1a6efa..508e243 100644 --- a/puppet_compiler/filter.py +++ b/puppet_compiler/filter.py @@ -76,3 +76,23 @@ def setup_filters(self): return [self.flatten_one_element_arrays, self.cast_integers_to_string] + + +def itos(value): + """Transform all integers in strings""" + if type(value) == int: + return str(value) + elif type(value) == list: + return [itos(v) for v in value] + elif type(value) == dict: + return {k: itos(v) for k, v in value.items()} + else: + return value + + +def flatten(value): + """Flatten one-element arrays""" + if type(value) == list and len(value) == 1: + return value[0] + else: + return value diff --git a/puppet_compiler/presentation/html.py b/puppet_compiler/presentation/html.py index 5510cc7..e120b33 100644 --- a/puppet_compiler/presentation/html.py +++ b/puppet_compiler/presentation/html.py @@ -15,7 +15,6 @@ self.retcode = retcode self.hostname = hostname self.outdir = files.outdir - self.diff_file = files.file_for('change', 'diff') def _retcode_to_desc(self): if self.retcode == 'noop': @@ -27,18 +26,17 @@ else: return 'compiler failure' - def htmlpage(self): + def htmlpage(self, diffs=None): """ Create the html page """ _log.debug("Rendering index page for %s", self.hostname) - data = {'retcode': self.retcode} - if self.retcode == 'diff': - with open(self.diff_file, 'r') as f: - data['diffs'] = f.read() + data = {'retcode': self.retcode, 'host': self.hostname} + if self.retcode == 'diff' and diffs is not None: + data['diffs'] = diffs data['desc'] = self._retcode_to_desc() t = env.get_template(self.tpl) - page = t.render(host=self.hostname, jid=job_id, chid=change_id, **data) + page = t.render(jid=job_id, chid=change_id, **data) with open(os.path.join(self.outdir, self.page_name), 'w') as f: f.write(page) @@ -49,7 +47,6 @@ def __init__(self, hostname, files, retcode): super(FutureHost, self).__init__(hostname, files, retcode) - self.diff_file = files.file_for('future', 'diff') def _retcode_to_desc(self): if self.retcode == 'break': diff --git a/puppet_compiler/templates/diffs.jinja2 b/puppet_compiler/templates/diffs.jinja2 new file mode 100644 index 0000000..3e11d7f --- /dev/null +++ b/puppet_compiler/templates/diffs.jinja2 @@ -0,0 +1,26 @@ +{% macro bigsep() %}{{ "%-80s"|format("=" * 80) }}{% endmacro %} +{% macro smallsep() %}{{ "%-80s"|format("-" * 80) }}{% endmacro %} +{{ bigsep() }} +{{ "%-70s%10s"|format(host, diffs.perc_changed) }} +{{ bigsep() }} +Total Resources: {{ diffs.total }} +{% if diffs.only_in_self %}Only in old: +{% for resource in diffs.only_in_self %} {{ resource }} +{% endfor %}{% endif %} + +{% if diffs.only_in_other %}Only in new: +{% for resource in diffs.only_in_other %} {{ resource }} +{% endfor %}{% endif %} + +{% if diffs.resource_diffs %}Resources changed:{% for diff in diffs.resource_diffs %} +{{ smallsep() }} +Resource: {{ diff.resource}} +{% if diff.parameters %} +Parameters differences: +{{ diff.parameters }} +{% endif %} +{% if diff.content %} +Content differences: +{{ diff.content }} +{% endif %}{{smallsep()}}{% endfor %} +{% endif %} diff --git a/puppet_compiler/templates/hostpage.future.jinja2 b/puppet_compiler/templates/hostpage.future.jinja2 index 47f731a..d047000 100644 --- a/puppet_compiler/templates/hostpage.future.jinja2 +++ b/puppet_compiler/templates/hostpage.future.jinja2 @@ -28,7 +28,7 @@ <h1>Compilation results for {{ host }}: <span class="{{retcode}}">{{ desc }}</span></h1> {% if retcode == "diff" %} <h2>Catalog differences</h2> - <textarea rows="20" cols="82" class="source">{{ diffs }}</textarea> + <textarea rows="20" cols="82" class="source">{% include "diffs.jinja2" %}</textarea> {% endif %} <h2>Relevant files</h2> <ul> diff --git a/puppet_compiler/templates/hostpage.jinja2 b/puppet_compiler/templates/hostpage.jinja2 index 1fd4e60..3a3de79 100644 --- a/puppet_compiler/templates/hostpage.jinja2 +++ b/puppet_compiler/templates/hostpage.jinja2 @@ -28,7 +28,7 @@ <h1>Compilation results for {{ host }}: <span class="{{retcode}}">{{ desc }}</span></h1> {% if retcode == "diff" %} <h2>Catalog differences</h2> - <textarea rows="20" cols="82" class="source">{{ diffs }}</textarea> + <textarea rows="20" cols="82" class="source">{% include "diffs.jinja2" %}</textarea> {% endif %} <h2>Relevant files</h2> <ul> diff --git a/puppet_compiler/tests/fixtures/catalog-change.pson b/puppet_compiler/tests/fixtures/catalog-change.pson new file mode 100644 index 0000000..1dd6d19 --- /dev/null +++ b/puppet_compiler/tests/fixtures/catalog-change.pson @@ -0,0 +1,261 @@ +{ + "document_type": "Catalog", + "data": { + "tags": ["settings","systemd","node","class"], + "name": "test123.test", + "version": 1500366969, + "environment": "production", + "resources": [ + { + "type": "Stage", + "title": "main", + "tags": ["stage"], + "exported": false, + "parameters": { + "name": "main" + } + }, + { + "type": "Class", + "title": "Settings", + "tags": ["class","settings"], + "exported": false + }, + { + "type": "Class", + "title": "main", + "tags": ["class"], + "exported": false, + "parameters": { + "name": "main" + } + }, + { + "type": "Node", + "title": "test123.test", + "tags": ["node","test123.test","class"], + "exported": false + }, + { + "type": "Class", + "title": "Role::Configcluster", + "tags": ["class","role::configcluster","role","configcluster","node","test123.test"], + "exported": false + }, + { + "type": "Class", + "title": "Standard", + "tags": ["class","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false, + "parameters": { + "has_default_mail_relay": true, + "has_admin": true, + "has_ganglia": true + } + }, + { + "type": "Class", + "title": "Profile::Base", + "tags": ["class","profile::base","profile","base","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false, + "parameters": { + "puppetmaster": "puppet", + "dns_alt_names": "foobar.test", + "environment": "future", + "use_apt_proxy": true, + "domain_search": "test", + "remote_syslog": [ + "syslog.eqiad.wmnet", + "syslog.test" + ], + "monitoring": true, + "core_dump_pattern": "/var/tmp/core/core.%h.%e.%p.%t", + "ssh_server_settings": { + + }, + "nrpe_allowed_hosts": "127.0.0.1,208.80.154.14,208.80.153.74,208.80.155.119", + "group_contact": "admins", + "check_disk_options": "-w 6% -c 3% -l -e -A -i \"/srv/sd[a-b][1-3]\" --exclude-type=tracefs", + "check_disk_critical": false, + "check_raid_policy": "", + "require": "Class[Profile::Base::Certificates]" + } + }, + { + "type": "Class", + "title": "Profile::Base::Certificates", + "tags": ["class","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false + }, + { + "type": "Class", + "title": "Sslcert3", + "tags": ["class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false + }, + { + "type": "Package", + "title": "openssl", + "tags": ["package","openssl","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 15, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Package", + "title": "ssl-cert", + "tags": ["package","ssl-cert","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 15, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Package", + "title": "ca-certificates", + "tags": ["package","ca-certificates","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 15, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Exec", + "title": "update-ca-certificates", + "tags": ["exec","update-ca-certificates","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 21, + "exported": false, + "parameters": { + "command": "/usr/sbin/update-ca-certificates -a", + "refreshonly": true, + "require": "Package[ca-certificates]" + } + }, + { + "type": "File", + "title": "/etc/ssl/localcerts", + "tags": ["file","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 31, + "exported": false, + "parameters": { + "ensure": "directory", + "owner": "root", + "group": "ssl-cert", + "mode": "0755", + "require": "Package[ssl-cert]" + } + }, + { + "type": "File", + "title": "/etc/ssl/private", + "tags": ["file","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 41, + "exported": false, + "parameters": { + "ensure": "directory", + "owner": "root", + "group": "ssl-cert", + "mode": "0711", + "require": "Package[ssl-cert]" + } + }, + { + "type": "File", + "title": "/usr/local/sbin/x509-bundle", + "tags": ["file","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 50, + "exported": false, + "parameters": { + "ensure": "present", + "owner": "root", + "group": "root", + "mode": "0755", + "source": "puppet:///modules/sslcert/x509-bundle" + } + }, + { + "type": "Class", + "title": "Base::Kernel", + "tags": ["class","base::kernel","base","kernel","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/profile/manifests/base.pp", + "line": 84, + "exported": false + }, + { + "type": "File", + "title": "/etc/modprobe.d/blacklist-wmf.conf", + "tags": ["file","class","base::kernel","base","kernel","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/base/manifests/kernel.pp", + "line": 26, + "exported": false, + "parameters": { + "ensure": "present", + "owner": "root", + "group": "root", + "mode": "0444", + "source": "puppet:///modules/base/kernel/blacklist-wmf.conf" + } + }, + { + "type": "File", + "title": "/etc/modprobe.d/blacklist-linux44.conf", + "tags": ["file","class","base::kernel","base","kernel","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/base/manifests/kernel.pp", + "line": 35, + "exported": false, + "parameters": { + "ensure": "present", + "owner": "root", + "group": "root", + "mode": "0444", + "source": "puppet:///modules/base/kernel/blacklist-linux44.conf" + } + }, + { + "type": "Class", + "title": "Base::Debdeploy", + "tags": ["class","base::debdeploy","base","debdeploy","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/profile/manifests/base.pp", + "line": 85, + "exported": false + }, + { + "type": "Package", + "title": "debdeploy-minion", + "tags": ["package","debdeploy-minion","class","base::debdeploy","base","debdeploy","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/base/manifests/debdeploy.pp", + "line": 10, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Salt::Grain", + "title": "debdeploy-etcd-codfw", + "tags": ["salt::grain","salt","grain","debdeploy-etcd-codfw","class","base::debdeploy","base","debdeploy","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false, + "parameters": { + "value": "standard", + "grain": "debdeploy-etcd-codfw", + "ensure": "present", + "replace": false + } + } + ] + }, + "metadata": { + "api_version": 1 + } +} diff --git a/puppet_compiler/tests/fixtures/catalog.pson b/puppet_compiler/tests/fixtures/catalog.pson new file mode 100644 index 0000000..c4c9692 --- /dev/null +++ b/puppet_compiler/tests/fixtures/catalog.pson @@ -0,0 +1,261 @@ +{ + "document_type": "Catalog", + "data": { + "tags": ["settings","systemd","node","class"], + "name": "test123.test", + "version": 1500366969, + "environment": "production", + "resources": [ + { + "type": "Stage", + "title": "main", + "tags": ["stage"], + "exported": false, + "parameters": { + "name": "main" + } + }, + { + "type": "Class", + "title": "Settings", + "tags": ["class","settings"], + "exported": false + }, + { + "type": "Class", + "title": "main", + "tags": ["class"], + "exported": false, + "parameters": { + "name": "main" + } + }, + { + "type": "Node", + "title": "test123.test", + "tags": ["node","test123.test","class"], + "exported": false + }, + { + "type": "Class", + "title": "Role::Configcluster", + "tags": ["class","role::configcluster","role","configcluster","node","test123.test"], + "exported": false + }, + { + "type": "Class", + "title": "Standard", + "tags": ["class","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false, + "parameters": { + "has_default_mail_relay": true, + "has_admin": true, + "has_ganglia": true + } + }, + { + "type": "Class", + "title": "Profile::Base", + "tags": ["class","profile::base","profile","base","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false, + "parameters": { + "puppetmaster": "puppet", + "dns_alt_names": false, + "environment": "future", + "use_apt_proxy": true, + "domain_search": "test", + "remote_syslog": [ + "syslog.eqiad.wmnet", + "syslog.test" + ], + "monitoring": true, + "core_dump_pattern": "/var/tmp/core/core.%h.%e.%p.%t", + "ssh_server_settings": { + + }, + "nrpe_allowed_hosts": "127.0.0.1,208.80.154.14,208.80.153.74,208.80.155.119", + "group_contact": "admins", + "check_disk_options": "-w 6% -c 3% -l -e -A -i \"/srv/sd[a-b][1-3]\" --exclude-type=tracefs", + "check_disk_critical": false, + "check_raid_policy": "", + "require": "Class[Profile::Base::Certificates]" + } + }, + { + "type": "Class", + "title": "Profile::Base::Certificates", + "tags": ["class","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false + }, + { + "type": "Class", + "title": "Sslcert", + "tags": ["class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false + }, + { + "type": "Package", + "title": "openssl", + "tags": ["package","openssl","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 15, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Package", + "title": "ssl-cert", + "tags": ["package","ssl-cert","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 15, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Package", + "title": "ca-certificates", + "tags": ["package","ca-certificates","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 15, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Exec", + "title": "update-ca-certificates", + "tags": ["exec","update-ca-certificates","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 21, + "exported": false, + "parameters": { + "command": "/usr/sbin/update-ca-certificates", + "refreshonly": true, + "require": "Package[ca-certificates]" + } + }, + { + "type": "File", + "title": "/etc/ssl/localcerts", + "tags": ["file","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 31, + "exported": false, + "parameters": { + "ensure": "directory", + "owner": "root", + "group": "ssl-cert", + "mode": "0755", + "require": "Package[ssl-cert]" + } + }, + { + "type": "File", + "title": "/etc/ssl/private", + "tags": ["file","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 41, + "exported": false, + "parameters": { + "ensure": "directory", + "owner": "root", + "group": "ssl-cert", + "mode": "0711", + "require": "Package[ssl-cert]" + } + }, + { + "type": "File", + "title": "/usr/local/sbin/x509-bundle", + "tags": ["file","class","sslcert","profile::base::certificates","profile","base","certificates","profile::base","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/sslcert/manifests/init.pp", + "line": 50, + "exported": false, + "parameters": { + "ensure": "present", + "owner": "root", + "group": "root", + "mode": "0755", + "source": "puppet:///modules/sslcert/x509-bundle" + } + }, + { + "type": "Class", + "title": "Base::Kernel", + "tags": ["class","base::kernel","base","kernel","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/profile/manifests/base.pp", + "line": 84, + "exported": false + }, + { + "type": "File", + "title": "/etc/modprobe.d/blacklist-wmf.conf", + "tags": ["file","class","base::kernel","base","kernel","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/base/manifests/kernel.pp", + "line": 26, + "exported": false, + "parameters": { + "ensure": "present", + "owner": "root", + "group": "root", + "mode": "0444", + "source": "puppet:///modules/base/kernel/blacklist-wmf.conf" + } + }, + { + "type": "File", + "title": "/etc/modprobe.d/blacklist-linux44.conf", + "tags": ["file","class","base::kernel","base","kernel","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/base/manifests/kernel.pp", + "line": 35, + "exported": false, + "parameters": { + "ensure": "present", + "owner": "root", + "group": "root", + "mode": "0444", + "source": "puppet:///modules/base/kernel/blacklist-linux44.conf" + } + }, + { + "type": "Class", + "title": "Base::Debdeploy", + "tags": ["class","base::debdeploy","base","debdeploy","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/profile/manifests/base.pp", + "line": 85, + "exported": false + }, + { + "type": "Package", + "title": "debdeploy-minion", + "tags": ["package","debdeploy-minion","class","base::debdeploy","base","debdeploy","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "file": "/tmp/testspc/change/src/modules/base/manifests/debdeploy.pp", + "line": 10, + "exported": false, + "parameters": { + "ensure": "present" + } + }, + { + "type": "Salt::Grain", + "title": "debdeploy-etcd-codfw", + "tags": ["salt::grain","salt","grain","debdeploy-etcd-codfw","class","base::debdeploy","base","debdeploy","profile::base","profile","standard","role::configcluster","role","configcluster","node","test123.test"], + "exported": false, + "parameters": { + "value": "standard", + "grain": "debdeploy-etcd-codfw", + "ensure": "present", + "replace": false + } + } + ] + }, + "metadata": { + "api_version": 1 + } +} diff --git a/puppet_compiler/tests/test_controller.py b/puppet_compiler/tests/test_controller.py index f04c68c..b6d2ffe 100644 --- a/puppet_compiler/tests/test_controller.py +++ b/puppet_compiler/tests/test_controller.py @@ -40,7 +40,8 @@ c.m.refresh = mock.MagicMock() c.m.refresh.return_value = True - c.run() + with mock.patch('time.sleep'): + c.run() c.m.prepare.assert_called_once_with() c.m.refresh.assert_not_called() self.assertEquals(c.state.modes['change']['fail'], set(['test.eqiad.wmnet'])) @@ -48,7 +49,8 @@ c.config['puppet_src'] = '/src' c.config['puppet_private'] = '/private' mocker.return_value = (False, False, None) - c.run() + with mock.patch('time.sleep'): + c.run() c.m.refresh.assert_has_calls([mock.call('/src'), mock.call('/private')]) self.assertEquals(c.state.modes['change']['noop'], set(['test.eqiad.wmnet'])) diff --git a/puppet_compiler/tests/test_differ.py b/puppet_compiler/tests/test_differ.py new file mode 100644 index 0000000..5e1e580 --- /dev/null +++ b/puppet_compiler/tests/test_differ.py @@ -0,0 +1,110 @@ +import mock +import os +import unittest + +from puppet_compiler.differ import PuppetResource, PuppetCatalog +from puppet_compiler.worker import future_filter + + +class TestPuppetResource(unittest.TestCase): + + def setUp(self): + self.raw_resource = { + 'type': 'Class', + 'title': 'hhvm', + 'exported': False, + 'parameters': { + 'ensure': 'present', + 'type': 'fcgi', + 'settings': {'foo': 1, 'bar': [2, 3]}, + 'before': ['Class[apache2]'], + 'nope': None, + } + } + self.r = PuppetResource( + self.raw_resource, + future_filter, + ) + + def test_init(self): + """Test initialization""" + self.assertEqual(self.r.resource_type, 'Class') + self.assertEqual(self.r.title, 'hhvm') + self.assertEqual(self.r._filter, future_filter) + self.assertEqual(self.r.content, '') + self.assertEqual(self.r.parameters['before'], 'Class[apache2]') + self.assertEqual(self.r.parameters['settings'], {'foo': '1', 'bar': ['2', '3']}) + self.assertNotIn('nope', self.r.parameters) + + def test_str(self): + """Test stringification""" + self.assertEqual(str(self.r), 'Class[hhvm]') + + def test_is_same(self): + """Test function `PuppetResource.is_same_of`""" + other = mock.MagicMock() + other.__str__.return_value = 'Class[hhvm]' + self.assertTrue(self.r.is_same_of(other)) + other.__str__.return_value = 'Hhvm::monitoring[test]' + self.assertFalse(self.r.is_same_of(other)) + + def test_equality(self): + # Not filtered resource, it should be different + other = PuppetResource(self.raw_resource) + self.assertNotEqual(self.r, other) + other = PuppetResource(self.raw_resource, future_filter) + self.assertEqual(self.r, other) + other.content = 'lala' + self.assertNotEqual(self.r, other) + + @mock.patch('difflib.unified_diff') + @mock.patch('datadiff.diff') + def test_diff_if_present(self, datadiff_mock, difflib_mock): + other = PuppetResource(self.raw_resource, future_filter) + self.assertIsNone(self.r.diff_if_present(other)) + # Now let's add some content + self.r.resource_type = 'File' + other.resource_type = 'File' + other.content = "lala\nalal" + diff = self.r.diff_if_present(other) + datadiff_mock.assert_not_called() + difflib_mock.assert_called_with( + [''], ['lala', 'alal'], + fromfile='hhvm.orig', + tofile='hhvm' + ) + self.r.resource_type = 'Class' + datadiff_mock.reset_mock() + difflib_mock.reset_mock() + # Not filtered resource, it should be different + other = PuppetResource(self.raw_resource) + diff = self.r.diff_if_present(other) + difflib_mock.assert_not_called() + datadiff_mock.assert_called_with( + self.r.parameters, other.parameters, + fromfile='Class[hhvm].orig', tofile='Class[hhvm]' + ) + self.assertNotIn('content', diff) + self.assertIn('parameters', diff) + + +class TestPuppetCatalog(unittest.TestCase): + + def setUp(self): + fixtures = os.path.join(os.path.dirname(__file__), 'fixtures') + self.orig = PuppetCatalog(os.path.join(fixtures, 'catalog.pson')) + self.change = PuppetCatalog(os.path.join(fixtures, 'catalog-change.pson')) + + def test_init(self): + """Test initialization.""" + self.assertEqual(self.orig.name, 'test123.test') + self.assertIsInstance(self.orig.resources, dict) + + def test_diff_is_present(self): + self.assertIsNone(self.orig.diff_if_present(self.orig)) + diffs = self.orig.diff_if_present(self.change) + self.assertEqual(diffs['total'], 22) + self.assertEqual(diffs['only_in_self'], set(['Class[Sslcert]'])) + self.assertEqual(diffs['only_in_other'], set(['Class[Sslcert3]'])) + self.assertEqual(len(diffs['resource_diffs']), 2) + self.assertEqual(diffs['perc_changed'], '18.18%') diff --git a/puppet_compiler/tests/test_hostworker.py b/puppet_compiler/tests/test_hostworker.py index c6f7aaf..013e0e9 100644 --- a/puppet_compiler/tests/test_hostworker.py +++ b/puppet_compiler/tests/test_hostworker.py @@ -17,6 +17,8 @@ self.assertEquals(self.hw.hostname, 'test.example.com') self.assertItemsEqual(['prod', 'change'], self.hw._envs) + self.assertIsNone(self.hw.diffs) + self.assertEqual(self.hw.resource_filter, worker.future_filter) @mock.patch('os.path.isfile') def test_facts_file(self, isfile): @@ -58,18 +60,28 @@ err = self.hw._compile_all() self.assertEquals(err, 2) - @mock.patch('puppet_compiler.puppet.diff') + @mock.patch('puppet_compiler.worker.PuppetCatalog') def test_make_diff(self, mocker): - mocker.return_value = True - self.hw._get_diff = mock.Mock(return_value=True) - retval = self.hw._make_diff() - mocker.assert_called_with('change', 'test.example.com', base='prod') - self.assertEquals(retval, True) - self.hw._get_diff = mock.Mock(return_value=False) - self.assertEquals(self.hw._make_diff(), None) - mocker.reset_mock() - mocker.side_effect = subprocess.CalledProcessError(cmd="ehehe", returncode=30) - self.assertEquals(self.hw._make_diff(), False) + instance_mock = mocker.return_value + instance_mock.diff_if_present.return_value = None + self.assertIsNone(self.hw._make_diff()) + self.assertIsNone(self.hw.diffs) + + mocker.assert_has_calls( + [ + mock.call( + '/mnt/jenkins-workspace/19/production/catalogs/test.example.com.pson', worker.future_filter + ), + mock.call( + '/mnt/jenkins-workspace/19/change/catalogs/test.example.com.pson', worker.future_filter + ) + ] + ) + instance_mock.diff_if_present.return_value = {'foo': 'bar'} + self.assertTrue(self.hw._make_diff()) + self.assertEqual(self.hw.diffs, {'foo': 'bar'}) + instance_mock.diff_if_present.side_effect = ValueError("ehehe") + self.assertFalse(self.hw._make_diff()) @mock.patch('os.path.isfile') @mock.patch('os.makedirs') @@ -80,35 +92,15 @@ mock_makedirs.assert_called_with(self.hw._files.outdir, 0755) assert not mock_copy.called mock_isfile.return_value = True - self.hw._get_diff = mock.MagicMock(return_value=False) self.hw._make_output() mock_copy.assert_called_with( '/mnt/jenkins-workspace/19/change/catalogs/test.example.com.err', '/mnt/jenkins-workspace/output/19/test.example.com/change.test.example.com.err', ) - self.hw._get_diff = mock.MagicMock(return_value='test_value') - self.hw._make_output() - mock_copy.assert_called_with( - 'test_value', - '/mnt/jenkins-workspace/output/19/test.example.com' - ) - - @mock.patch('os.path') - def test_get_diff(self, mock_path): - mock_path.join.return_value = self.fixtures + '/' + 'exit_nodiff' - mock_path.isfile.return_value = True - mock_path.getsize.return_value = 100 - self.assertEquals(self.hw._get_diff(), False) - filename = self.fixtures + '/' + 'exit_diff' - mock_path.join.return_value = filename - self.assertEquals(self.hw._get_diff(), filename) - - def test_build_html(self): - pass def test_run_host(self): self.hw.facts_file = mock.Mock(return_value=False) - self.assertEquals(self.hw.run_host(), 'fail') + self.assertEquals(self.hw.run_host(), (True, True, None)) self.hw.facts_file = mock.Mock(return_value=True) self.hw._compile_all = mock.Mock(return_value=0) self.hw._make_diff = mock.Mock(return_value=True) @@ -141,8 +133,6 @@ def test_compile_all(self): self.hw._compile = mock.Mock(return_value=True) - self.hw.filter_future.run = mock.Mock() - self.hw.filter_change.run = mock.Mock() self.assertEquals(self.hw._compile_all(), 0) future_parser = [ '--environment=future', @@ -154,5 +144,3 @@ mock.call('change', []), mock.call('future', future_parser) ]) - self.hw.filter_future.run.assert_called_with() - self.hw.filter_change.run.assert_called_with() diff --git a/puppet_compiler/tests/test_html.py b/puppet_compiler/tests/test_html.py new file mode 100644 index 0000000..372850a --- /dev/null +++ b/puppet_compiler/tests/test_html.py @@ -0,0 +1,40 @@ +import os +import shutil +import tempfile +import unittest + +from puppet_compiler.presentation import html +from puppet_compiler.differ import PuppetCatalog +from puppet_compiler.directories import FHS, HostFiles + + +class TestHost(unittest.TestCase): + + def setUp(self): + self.tempdir = tempfile.mkdtemp(prefix='puppet-compiler') + fixtures = os.path.join(os.path.dirname(__file__), 'fixtures') + orig = PuppetCatalog(os.path.join(fixtures, 'catalog.pson')) + change = PuppetCatalog(os.path.join(fixtures, 'catalog-change.pson')) + self.diffs = orig.diff_if_present(change) + FHS.setup('10', self.tempdir) + self.files = HostFiles('test.example.com') + os.makedirs(self.files.outdir) + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def test_init(self): + h = html.Host('test.example.com', self.files, 'diff') + self.assertEqual(h.retcode, 'diff') + self.assertEqual(h.hostname, 'test.example.com') + self.assertEqual(h.outdir, self.files.outdir) + + def test_htmlpage(self): + h = html.Host('test.example.com', self.files, 'diff') + h.htmlpage(self.diffs) + # Test the test file has been produced + output = os.path.join(h.outdir, h.page_name) + assert os.path.isfile(output) + with open(output, 'r') as fh: + html_data = fh.read() + assert len(html_data) > 0 diff --git a/puppet_compiler/worker.py b/puppet_compiler/worker.py index 1f5406b..654c4b2 100644 --- a/puppet_compiler/worker.py +++ b/puppet_compiler/worker.py @@ -1,13 +1,18 @@ import os -import re import shutil import subprocess from puppet_compiler import puppet, _log -from puppet_compiler.filter import FilterFutureParser +from puppet_compiler.filter import itos, flatten +from puppet_compiler.differ import PuppetCatalog from puppet_compiler.directories import HostFiles, FHS from puppet_compiler.presentation import html from puppet_compiler.state import ChangeState, FutureState + + +def future_filter(value): + """This filter must be used during the transition to the future parser""" + return itos(flatten(value)) class HostWorker(object): @@ -23,6 +28,8 @@ self._files = HostFiles(hostname) self._envs = ['prod', 'change'] self.hostname = hostname + self.diffs = None + self.resource_filter = future_filter def facts_file(self): """ Finds facts file for the current hostname """ @@ -39,7 +46,7 @@ if not self.facts_file(): _log.error('Unable to find facts for host %s, skipping', self.hostname) - return 'fail' + return (True, True, None) errors = self._compile_all() if errors == self.E_OK: diff = self._make_diff() @@ -112,21 +119,26 @@ def _make_diff(self): """ Will produce diffs. + + Returns True if there are diffs, None if no diffs are found, + False if diffing failed """ - # Both nodes compiled correctly _log.info("Calculating diffs for %s", self.hostname) try: - puppet.diff(self._envs[1], self.hostname, base=self._envs[0]) - except subprocess.CalledProcessError as e: + original = PuppetCatalog(self._files.file_for(self._envs[0], 'catalog'), + self.resource_filter) + new = PuppetCatalog(self._files.file_for(self._envs[1], 'catalog'), + self.resource_filter) + self.diffs = original.diff_if_present(new) + except Exception as e: _log.error("Diffing the catalogs failed: %s", self.hostname) - _log.info("Diffing exited with code %d", e.returncode) - _log.debug("Failed command: %s", e.cmd) + _log.info("Diffing failed with exception %s", e) return False else: - if self._get_diff(): - return True - else: + if self.diffs is None: return None + else: + return True def _make_output(self): """ @@ -142,38 +154,14 @@ newname = self._files.outfile_for(env, label) shutil.copy(filename, newname) - diff = self._get_diff() - if diff: - shutil.copy(diff, self._files.outdir) - return True - - return False - - def _get_diff(self): - """ - Get diffs name if the file exists - """ - diff = self._files.file_for(self._envs[1], 'diff') - - if os.path.isfile(diff) and os.path.getsize(diff) > 0: - with open(diff, 'r') as f: - for line in f: - # If no changes were detected, puppet catalog diff outputs - # a line with 'fqdn 0.0%' - # we use that to detect a noop change since we have no other - # means to detect that. - if re.match('^{}\s+0.0\%'.format(self.hostname), line): - return False - return diff return False def _build_html(self, retcode): """ build the HTML output """ - # TODO: implement the actual html parsing host = self.html_page(self.hostname, self._files, retcode) - host.htmlpage() + host.htmlpage(self.diffs) class FutureHostWorker(HostWorker): @@ -196,8 +184,6 @@ def __init__(self, vardir, hostname): super(FutureHostWorker, self).__init__(vardir, hostname) self._envs = ['change', 'future'] - self.filter_future = FilterFutureParser(self._files.file_for('future', 'catalog')) - self.filter_change = FilterFutureParser(self._files.file_for('change', 'catalog')) def _compile_all(self): future_args = [ @@ -208,15 +194,9 @@ ] args = [] errors = self.E_OK - if self._compile(self._envs[0], args): - _log.info("Filtering the change catalog (%s)", self.hostname) - self.filter_change.run() - else: + if not self._compile(self._envs[0], args): errors += self.E_BASE - if self._compile(self._envs[1], future_args): - _log.info("Filtering the future catalog (%s)", self.hostname) - self.filter_future.run() - else: + if not self._compile(self._envs[1], future_args): errors += self.E_CHANGED return errors diff --git a/setup.py b/setup.py index 3a52ad6..16d771a 100755 --- a/setup.py +++ b/setup.py @@ -12,11 +12,11 @@ setup( name='puppet_compiler', - version='0.2.3', + version='0.3.0', description='Tools to compile puppet catalogs as a service', author='Joe', author_email='glavage...@wikimedia.org', - install_requires=['jinja2', 'requests', 'pyyaml'], + install_requires=['jinja2', 'requests', 'pyyaml', 'datadiff>=2.0.0'], test_suite='nose.collector', tests_require=['mock<1.1.0', 'nose'], zip_safe=True, -- To view, visit https://gerrit.wikimedia.org/r/369385 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I03d4319b49f3c7a195e076decc6a7b741e13d399 Gerrit-PatchSet: 1 Gerrit-Project: operations/software/puppet-compiler Gerrit-Branch: master Gerrit-Owner: Giuseppe Lavagetto <glavage...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits