Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion client/patchman-client
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ build_json_report() {
# Convert tags to JSON array
local tags_json="[]"
if [ ! -z "${tags}" ] && [ "${tags}" != "Default" ] ; then
tags_json=$(echo "${tags}" | tr ',' '\n' | jq -R . | jq -s .)
tags_json=$(echo "${tags}" | jq -R 'if test(",") then split(",") else [splits("\\s+")] end | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0))')
fi

# Convert reboot to boolean
Expand Down
2 changes: 1 addition & 1 deletion client/patchman-client.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ server=https://patchman.example.com
# Options to curl
curl_options="--insecure --connect-timeout 60 --max-time 300"

# Space delimited tags to send
# Comma or space delimited tags to send
tags="Server"

# Does the client output a report of the upload (e.g. for cronjob output)
Expand Down
56 changes: 56 additions & 0 deletions hosts/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2026 Marcus Furlong <furlongm@gmail.com>
#
# This file is part of Patchman.
#
# Patchman 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, version 3 only.
#
# Patchman 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 Patchman. If not, see <http://www.gnu.org/licenses/>

from unittest.mock import patch

from django.test import TestCase

from arch.models import MachineArchitecture
from domains.models import Domain
from hosts.utils import get_or_create_host
from operatingsystems.models import OSRelease, OSVariant
from reports.models import Report


@patch('hosts.models.Host.check_rdns')
class GetOrCreateHostTagTests(TestCase):

def setUp(self):
self.arch = MachineArchitecture.objects.create(name='x86_64')
self.domain = Domain.objects.create(name='example.com')
self.osvariant = OSVariant.objects.create(
name='Debian 13', osrelease=OSRelease.objects.create(name='Debian 13'),
)

def host_tags(self, protocol, tags):
report = Report.objects.create(
host='tags.example.com', report_ip='192.168.1.10', os='Debian 13',
kernel='6.12.57+deb13-amd64', arch='x86_64', protocol=protocol, tags=tags,
)
host = get_or_create_host(report, self.arch, self.osvariant, self.domain)
return sorted(host.tags.names())

def test_protocol1_space_delimited(self, _):
self.assertEqual(self.host_tags('1', 'web prod'), ['prod', 'web'])

def test_protocol1_comma_delimited(self, _):
self.assertEqual(self.host_tags('1', 'web, prod'), ['prod', 'web'])

def test_protocol2_keeps_spaces_within_tags(self, _):
self.assertEqual(self.host_tags('2', 'my tag, web'), ['my tag', 'web'])

def test_protocol2_empty_tags(self, _):
self.assertEqual(self.host_tags('2', ''), [])
12 changes: 11 additions & 1 deletion hosts/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from django.db import IntegrityError, transaction
from taggit.models import Tag
from taggit.utils import parse_tags

from util import get_datetime_now
from util.logging import error_message, info_message
Expand All @@ -36,6 +37,15 @@ def update_rdns(host):
host.save()


def get_report_tags(report):
""" Protocol 2 sends a list (stored comma-joined), protocol 1 sends the
raw config string, which may be comma or space delimited
"""
if report.protocol == '2':
return [t.strip() for t in (report.tags or '').split(',') if t.strip()]
return parse_tags(report.tags or '')


def get_or_create_host(report, arch, osvariant, domain):
""" Get or create a host from from a report
"""
Expand Down Expand Up @@ -65,7 +75,7 @@ def get_or_create_host(report, arch, osvariant, domain):
host.osvariant = osvariant
host.domain = domain
host.lastreport = report.created
host.tags.set(report.tags.split(','), clear=True)
host.tags.set(get_report_tags(report), clear=True)
if report.reboot == 'True':
host.reboot_required = True
else:
Expand Down
Loading