Merge ~blake-rouse/maas:remote-syslog into maas:master

Proposed by Blake Rouse
Status: Merged
Approved by: Blake Rouse
Approved revision: b67e2f7c052c40252f9f9a931eb245bedaa88a8d
Merge reported by: MAAS Lander
Merged at revision: not available
Proposed branch: ~blake-rouse/maas:remote-syslog
Merge into: maas:master
Diff against target: 388 lines (+178/-7)
13 files modified
src/maasserver/compose_preseed.py (+5/-1)
src/maasserver/forms/__init__.py (+5/-0)
src/maasserver/forms/settings.py (+30/-0)
src/maasserver/forms/tests/test_settings.py (+15/-0)
src/maasserver/models/config.py (+2/-0)
src/maasserver/preseed.py (+4/-1)
src/maasserver/rpc/boot.py (+15/-4)
src/maasserver/templates/maasserver/settings_network.html (+18/-0)
src/maasserver/tests/test_compose_preseed.py (+18/-0)
src/maasserver/tests/test_preseed.py (+6/-0)
src/maasserver/views/settings.py (+9/-0)
src/provisioningserver/utils/tests/test_url.py (+33/-1)
src/provisioningserver/utils/url.py (+18/-0)
Reviewer Review Type Date Requested Status
MAAS Lander Approve
Mike Pontillo (community) Approve
Review via email: mp+354143@code.launchpad.net

Commit message

Add ability for an administrator to configure a remote syslog server.

To post a comment you must log in.
Revision history for this message
Mike Pontillo (mpontillo) wrote :

Looks good to me!

One suggestion: I would just make it clearer in the help text how to set the syslog settings back to the default, and what the default behavior actually is.

review: Approve
Revision history for this message
MAAS Lander (maas-lander) wrote :

UNIT TESTS
-b remote-syslog lp:~blake-rouse/maas/+git/maas into -b master lp:~maas-committers/maas

STATUS: SUCCESS
COMMIT: 54cc56121a328004c6cd12b7ba04d116d92a5037

review: Approve
~blake-rouse/maas:remote-syslog updated
b67e2f7... by Blake Rouse

Add message about reseting back to default.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/src/maasserver/compose_preseed.py b/src/maasserver/compose_preseed.py
2index f693ed0..1441a85 100644
3--- a/src/maasserver/compose_preseed.py
4+++ b/src/maasserver/compose_preseed.py
5@@ -266,7 +266,11 @@ def get_cloud_init_reporting(request, node, token):
6
7 def get_rsyslog_host_port(request, node):
8 """Return the rsyslog host and port to use."""
9- return "%s:%d" % (node.boot_cluster_ip, RSYSLOG_PORT)
10+ syslog = Config.objects.get_config('remote_syslog')
11+ if syslog:
12+ return syslog
13+ else:
14+ return "%s:%d" % (node.boot_cluster_ip, RSYSLOG_PORT)
15
16
17 def get_system_info():
18diff --git a/src/maasserver/forms/__init__.py b/src/maasserver/forms/__init__.py
19index 94e52c6..69440fa 100644
20--- a/src/maasserver/forms/__init__.py
21+++ b/src/maasserver/forms/__init__.py
22@@ -1519,6 +1519,11 @@ class NTPForm(ConfigForm):
23 ntp_external_only = get_config_field('ntp_external_only')
24
25
26+class SyslogForm(ConfigForm):
27+ """Settings page, Syslog section."""
28+ remote_syslog = get_config_field('remote_syslog')
29+
30+
31 class NetworkDiscoveryForm(ConfigForm):
32 """Settings page, Network Discovery section."""
33 network_discovery = get_config_field('network_discovery')
34diff --git a/src/maasserver/forms/settings.py b/src/maasserver/forms/settings.py
35index a6ce778..d99152a 100644
36--- a/src/maasserver/forms/settings.py
37+++ b/src/maasserver/forms/settings.py
38@@ -42,6 +42,7 @@ from maasserver.utils.osystems import (
39 release_a_newer_than_b,
40 )
41 from provisioningserver.utils.text import normalise_whitespace
42+from provisioningserver.utils.url import splithost
43
44
45 INVALID_URL_MESSAGE = "Enter a valid url (e.g. http://host.example.com)."
46@@ -231,6 +232,21 @@ def make_maas_internal_domain_field(*args, **kwargs):
47 **kwargs)
48
49
50+class RemoteSyslogField(forms.CharField):
51+ """
52+ A `CharField` that formats the input into the expected value for syslog.
53+ """
54+
55+ def clean(self, value):
56+ value = super(RemoteSyslogField, self).clean(value)
57+ if not value:
58+ return None
59+ host, port = splithost(value)
60+ if not port:
61+ port = 514
62+ return '%s:%d' % (host, port)
63+
64+
65 CONFIG_ITEMS = {
66 'maas_name': {
67 'default': gethostname(),
68@@ -405,6 +421,20 @@ CONFIG_ITEMS = {
69 """),
70 }
71 },
72+ 'remote_syslog': {
73+ 'default': None,
74+ 'form': RemoteSyslogField,
75+ 'form_kwargs': {
76+ 'label': "Remote syslog server to forward machine logs",
77+ 'required': False,
78+ 'help_text': normalise_whitespace("""\
79+ A remote syslog server that MAAS will set on enlisting,
80+ commissioning, testing, and deploying machines to send all
81+ log messages. Clearing this value will restore the default
82+ behaviour of forwarding syslog to MAAS.
83+ """),
84+ }
85+ },
86 'network_discovery': {
87 'default': 'enabled',
88 'form': make_network_discovery_field,
89diff --git a/src/maasserver/forms/tests/test_settings.py b/src/maasserver/forms/tests/test_settings.py
90index a8804d9..ed8368f 100644
91--- a/src/maasserver/forms/tests/test_settings.py
92+++ b/src/maasserver/forms/tests/test_settings.py
93@@ -65,3 +65,18 @@ class TestSpecificConfigSettings(MAASServerTestCase):
94 ips2 = [factory.make_ip_address() for _ in range(3)]
95 input = ' '.join(ips1) + ' ' + ','.join(ips2)
96 self.assertEqual(' '.join(ips1 + ips2), field.clean(input))
97+
98+
99+class TestRemoteSyslogConfigSettings(MAASServerTestCase):
100+
101+ def test_sets_empty_to_none(self):
102+ field = get_config_field('remote_syslog')
103+ self.assertIsNone(field.clean(' '))
104+
105+ def test_adds_port(self):
106+ field = get_config_field('remote_syslog')
107+ self.assertEqual('192.168.1.1:514', field.clean('192.168.1.1'))
108+
109+ def test_wraps_ipv6(self):
110+ field = get_config_field('remote_syslog')
111+ self.assertEqual('[::ffff]:514', field.clean('::ffff'))
112diff --git a/src/maasserver/models/config.py b/src/maasserver/models/config.py
113index 9a0d6c2..8551a81 100644
114--- a/src/maasserver/models/config.py
115+++ b/src/maasserver/models/config.py
116@@ -84,6 +84,8 @@ def get_default_config():
117 'ntp_servers': 'ntp.ubuntu.com',
118 'ntp_external_only': False,
119 'omapi_key': '',
120+ # Syslog settings
121+ 'remote_syslog': None,
122 # Network discovery.
123 'network_discovery': 'enabled',
124 'active_discovery_interval': int(timedelta(hours=3).total_seconds()),
125diff --git a/src/maasserver/preseed.py b/src/maasserver/preseed.py
126index 3056f00..e795f96 100644
127--- a/src/maasserver/preseed.py
128+++ b/src/maasserver/preseed.py
129@@ -753,12 +753,15 @@ def get_preseed_context(
130 rack_controller=rack_controller, default_region_ip=region_ip)
131 server_url = request.build_absolute_uri(reverse('machines_handler'))
132 metadata_enlist_url = request.build_absolute_uri(reverse('enlist'))
133+ syslog = Config.objects.get_config('remote_syslog')
134+ if not syslog:
135+ syslog = '%s:%d' % (server_host, RSYSLOG_PORT)
136 return {
137 'osystem': osystem,
138 'release': release,
139 'server_host': server_host,
140 'server_url': server_url,
141- 'syslog_host_port': '%s:%d' % (server_host, RSYSLOG_PORT),
142+ 'syslog_host_port': syslog,
143 'metadata_enlist_url': metadata_enlist_url,
144 }
145
146diff --git a/src/maasserver/rpc/boot.py b/src/maasserver/rpc/boot.py
147index 7ee36fe..7755761 100644
148--- a/src/maasserver/rpc/boot.py
149+++ b/src/maasserver/rpc/boot.py
150@@ -45,6 +45,7 @@ from provisioningserver.utils.twisted import (
151 synchronous,
152 undefined,
153 )
154+from provisioningserver.utils.url import splithost
155
156
157 DEFAULT_ARCH = 'i386'
158@@ -283,6 +284,7 @@ def get_config(
159 # for arch detection.
160 raise BootConfigNoResponse()
161
162+ # Get all required configuration objects in a single query.
163 configs = Config.objects.get_configs([
164 'commissioning_osystem',
165 'commissioning_distro_series',
166@@ -293,7 +295,16 @@ def get_config(
167 'kernel_opts',
168 'use_rack_proxy',
169 'maas_internal_domain',
170+ 'remote_syslog',
171 ])
172+
173+ # Compute the syslog server.
174+ log_host, log_port = local_ip, RSYSLOG_PORT
175+ if configs['remote_syslog']:
176+ log_host, log_port = splithost(configs['remote_syslog'])
177+ if log_port is None:
178+ log_port = 514 # Fallback to default UDP syslog port.
179+
180 if machine is not None:
181 # Update the last interface, last access cluster IP address, and
182 # the last used BIOS boot method.
183@@ -362,8 +373,8 @@ def get_config(
184 "domain": domain,
185 "preseed_url": preseed_url,
186 "fs_host": local_ip,
187- "log_host": local_ip,
188- "log_port": RSYSLOG_PORT,
189+ "log_host": log_host,
190+ "log_port": log_port,
191 "extra_opts": '',
192 "http_boot": True,
193 }
194@@ -484,8 +495,8 @@ def get_config(
195 "domain": domain,
196 "preseed_url": preseed_url,
197 "fs_host": local_ip,
198- "log_host": local_ip,
199- "log_port": RSYSLOG_PORT,
200+ "log_host": log_host,
201+ "log_port": log_port,
202 "extra_opts": '' if extra_kernel_opts is None else extra_kernel_opts,
203 # As of MAAS 2.4 only HTTP boot is supported. This ensures MAAS 2.3
204 # rack controllers use HTTP boot as well.
205diff --git a/src/maasserver/templates/maasserver/settings_network.html b/src/maasserver/templates/maasserver/settings_network.html
206index a9d3429..ab1de79 100644
207--- a/src/maasserver/templates/maasserver/settings_network.html
208+++ b/src/maasserver/templates/maasserver/settings_network.html
209@@ -104,6 +104,24 @@
210
211 <div class="p-strip is-bordered">
212 <div class="row">
213+ <div id="syslog" class="col-8">
214+ <h2 class="p-heading--four">Syslog</h2>
215+ <form action="{% url 'settings_network' %}" method="post">
216+ {% csrf_token %}
217+ <ul class="p-list">
218+ {% for field in syslog_form %}
219+ {% include "maasserver/form_field.html" %}
220+ {% endfor %}
221+ </ul>
222+ <input type="hidden" name="syslog_submit" value="1" />
223+ <button type="submit" class="p-button--positive u-float--right">Save</button>
224+ </form>
225+ </div>
226+ </div>
227+ </div>
228+
229+ <div class="p-strip is-bordered">
230+ <div class="row">
231 <div id="network" class="col-8">
232 <h2 class="p-heading--four">Network Discovery</h2>
233 <form action="{% url 'settings_network' %}" method="post">
234diff --git a/src/maasserver/tests/test_compose_preseed.py b/src/maasserver/tests/test_compose_preseed.py
235index 2ab0836..4332a1a 100644
236--- a/src/maasserver/tests/test_compose_preseed.py
237+++ b/src/maasserver/tests/test_compose_preseed.py
238@@ -420,6 +420,24 @@ class TestComposePreseed(MAASServerTestCase):
239 reverse('metadata-status', args=[node.system_id])),
240 preseed['reporting']['maas']['endpoint'])
241
242+ def test_compose_preseed_uses_remote_syslog(self):
243+ remote_syslog = '192.168.1.1:514'
244+ Config.objects.set_config('remote_syslog', remote_syslog)
245+ rack_controller = factory.make_RackController(url='')
246+ node = factory.make_Node(
247+ interface=True, status=NODE_STATUS.COMMISSIONING)
248+ nic = node.get_boot_interface()
249+ nic.vlan.dhcp_on = True
250+ nic.vlan.primary_rack = rack_controller
251+ nic.vlan.save()
252+ request = make_HttpRequest()
253+ preseed = yaml.safe_load(
254+ compose_preseed(
255+ request, PRESEED_TYPE.COMMISSIONING, node))
256+ self.assertEqual(
257+ remote_syslog,
258+ preseed['rsyslog']['remotes']['maas'])
259+
260 def test_compose_preseed_for_rescue_mode_does_not_include_poweroff(self):
261 rack_controller = factory.make_RackController()
262 node = factory.make_Node(
263diff --git a/src/maasserver/tests/test_preseed.py b/src/maasserver/tests/test_preseed.py
264index 8ca5a2e..d77cff7 100644
265--- a/src/maasserver/tests/test_preseed.py
266+++ b/src/maasserver/tests/test_preseed.py
267@@ -464,6 +464,12 @@ class TestPreseedContext(MAASServerTestCase):
268 'server_url', 'syslog_host_port'],
269 context.keys())
270
271+ def test_get_preseed_context_includes_remote_syslog(self):
272+ remote_syslog = '192.168.1.1:514'
273+ Config.objects.set_config('remote_syslog', remote_syslog)
274+ context = get_preseed_context(make_HttpRequest())
275+ self.assertEquals(remote_syslog, context['syslog_host_port'])
276+
277
278 class TestNodeDeprecatedPreseedContext(
279 PreseedRPCMixin, BootImageHelperMixin, MAASTransactionServerTestCase):
280diff --git a/src/maasserver/views/settings.py b/src/maasserver/views/settings.py
281index 7087e48..e020480 100644
282--- a/src/maasserver/views/settings.py
283+++ b/src/maasserver/views/settings.py
284@@ -44,6 +44,7 @@ from maasserver.forms import (
285 NTPForm,
286 ProxyForm,
287 StorageSettingsForm,
288+ SyslogForm,
289 ThirdPartyDriversForm,
290 UbuntuForm,
291 WindowsForm,
292@@ -362,6 +363,13 @@ def network(request):
293 if response is not None:
294 return response
295
296+ # Process the Syslog form.
297+ syslog_form, response = process_form(
298+ request, SyslogForm, reverse('settings_network'), 'syslog',
299+ "Configuration updated.")
300+ if response is not None:
301+ return response
302+
303 # Process the network discovery form.
304 network_discovery_form, response = process_form(
305 request, NetworkDiscoveryForm, reverse('settings_network'),
306@@ -376,6 +384,7 @@ def network(request):
307 'proxy_form': proxy_form,
308 'dns_form': dns_form,
309 'ntp_form': ntp_form,
310+ 'syslog_form': syslog_form,
311 'network_discovery_form': network_discovery_form,
312 'show_license_keys': show_license_keys(),
313 })
314diff --git a/src/provisioningserver/utils/tests/test_url.py b/src/provisioningserver/utils/tests/test_url.py
315index f498b22..744cf9a 100644
316--- a/src/provisioningserver/utils/tests/test_url.py
317+++ b/src/provisioningserver/utils/tests/test_url.py
318@@ -9,7 +9,10 @@ from random import randint
319
320 from maastesting.factory import factory
321 from maastesting.testcase import MAASTestCase
322-from provisioningserver.utils.url import compose_URL
323+from provisioningserver.utils.url import (
324+ compose_URL,
325+ splithost,
326+)
327
328
329 class TestComposeURL(MAASTestCase):
330@@ -96,3 +99,32 @@ class TestComposeURL(MAASTestCase):
331 self.assertEqual(
332 'https://%s:%s/' % (hostname, port),
333 compose_URL('https://:%s/' % port, hostname))
334+
335+
336+class TestSplithost(MAASTestCase):
337+
338+ scenarios = (
339+ ('ipv4', {
340+ 'host': '192.168.1.1:21',
341+ 'result': ('192.168.1.1', 21)
342+ }),
343+ ('ipv6', {
344+ 'host': '[::f]:21',
345+ 'result': ('[::f]', 21)
346+ }),
347+ ('ipv4_no_port', {
348+ 'host': '192.168.1.1',
349+ 'result': ('192.168.1.1', None)
350+ }),
351+ ('ipv6_no_port', {
352+ 'host': '[::f]',
353+ 'result': ('[::f]', None)
354+ }),
355+ ('ipv6_no_bracket', {
356+ 'host': '::ffff',
357+ 'result': ('[::ffff]', None)
358+ }),
359+ )
360+
361+ def test__result(self):
362+ self.assertEqual(self.result, splithost(self.host))
363diff --git a/src/provisioningserver/utils/url.py b/src/provisioningserver/utils/url.py
364index b841c80..7935525 100644
365--- a/src/provisioningserver/utils/url.py
366+++ b/src/provisioningserver/utils/url.py
367@@ -43,3 +43,21 @@ def compose_URL(base_url, host):
368 else:
369 netloc = '%s:%d' % (netloc_host, parsed_url.port)
370 return urlunparse(parsed_url._replace(netloc=netloc))
371+
372+
373+def splithost(host):
374+ """Split `host` into hostname and port.
375+
376+ If no :port is in `host` the port with return as None.
377+ """
378+ parsed = urlparse('//' + host)
379+ hostname = parsed.hostname
380+ if hostname is None:
381+ # This only occurs when the `host` is an IPv6 address without brakets.
382+ # Lets try again but add the brackets.
383+ parsed = urlparse('//[%s]' % host)
384+ hostname = parsed.hostname
385+ if ':' in hostname:
386+ # IPv6 hostname, place back into brackets.
387+ hostname = '[%s]' % hostname
388+ return hostname, parsed.port

Subscribers

People subscribed via source and target branches