1/*-
2 * Copyright (c) 2005-2006 The FreeBSD Project
3 * All rights reserved.
4 *
5 * Author: Victor Cruceru <soc-victor@freebsd.org>
6 *
7 * Redistribution of this software and documentation and use in source and
8 * binary forms, with or without modification, are permitted provided that
9 * the following conditions are met:
10 *
11 * 1. Redistributions of source code or documentation must retain the above
12 *    copyright notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 *    notice, this list of conditions and the following disclaimer in the
15 *    documentation and/or other materials provided with the distribution.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
18 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
21 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
23 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
24 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
26 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
27 * SUCH DAMAGE.
28 *
29 * $FreeBSD$
30 */
31
32/*
33 * Host Resources MIB for SNMPd. Implementation for hrProcessorTable
34 */
35
36#include <sys/param.h>
37#include <sys/sysctl.h>
38#include <sys/user.h>
39
40#include <assert.h>
41#include <math.h>
42#include <stdlib.h>
43#include <string.h>
44#include <syslog.h>
45
46#include "hostres_snmp.h"
47#include "hostres_oid.h"
48#include "hostres_tree.h"
49
50/*
51 * This structure is used to hold a SNMP table entry
52 * for HOST-RESOURCES-MIB's hrProcessorTable.
53 * Note that index is external being allocated & maintained
54 * by the hrDeviceTable code..
55 */
56struct processor_entry {
57	int32_t		index;
58	const struct asn_oid *frwId;
59	int32_t		load;		/* average cpu usage */
60	int32_t		sample_cnt;	/* number of usage samples */
61	int32_t		cur_sample_idx;	/* current valid sample */
62	TAILQ_ENTRY(processor_entry) link;
63	u_char		cpu_no;		/* which cpu, counted from 0 */
64
65	/* the samples from the last minute, as required by MIB */
66	double		samples[MAX_CPU_SAMPLES];
67	long		states[MAX_CPU_SAMPLES][CPUSTATES];
68};
69TAILQ_HEAD(processor_tbl, processor_entry);
70
71/* the head of the list with hrDeviceTable's entries */
72static struct processor_tbl processor_tbl =
73    TAILQ_HEAD_INITIALIZER(processor_tbl);
74
75/* number of processors in dev tbl */
76static int32_t detected_processor_count;
77
78/* sysctlbyname(hw.ncpu) */
79static int hw_ncpu;
80
81/* sysctlbyname(kern.cp_times) */
82static int cpmib[2];
83static size_t cplen;
84
85/* periodic timer used to get cpu load stats */
86static void *cpus_load_timer;
87
88/**
89 * Returns the CPU usage of a given processor entry.
90 *
91 * It needs at least two cp_times "tick" samples to calculate a delta and
92 * thus, the usage over the sampling period.
93 */
94static int
95get_avg_load(struct processor_entry *e)
96{
97	u_int i, oldest;
98	long delta = 0;
99	double usage = 0.0;
100
101	assert(e != NULL);
102
103	/* Need two samples to perform delta calculation. */
104	if (e->sample_cnt <= 1)
105		return (0);
106
107	/* Oldest usable index, we wrap around. */
108	if (e->sample_cnt == MAX_CPU_SAMPLES)
109		oldest = (e->cur_sample_idx + 1) % MAX_CPU_SAMPLES;
110	else
111		oldest = 0;
112
113	/* Sum delta for all states. */
114	for (i = 0; i < CPUSTATES; i++) {
115		delta += e->states[e->cur_sample_idx][i];
116		delta -= e->states[oldest][i];
117	}
118	if (delta == 0)
119		return 0;
120
121	/* Take idle time from the last element and convert to
122	 * percent usage by contrasting with total ticks delta. */
123	usage = (double)(e->states[e->cur_sample_idx][CPUSTATES-1] -
124	    e->states[oldest][CPUSTATES-1]) / delta;
125	usage = 100 - (usage * 100);
126	HRDBG("CPU no. %d, delta ticks %ld, pct usage %.2f", e->cpu_no,
127	    delta, usage);
128
129	return ((int)(usage));
130}
131
132/**
133 * Save a new sample to proc entry and get the average usage.
134 *
135 * Samples are stored in a ringbuffer from 0..(MAX_CPU_SAMPLES-1)
136 */
137static void
138save_sample(struct processor_entry *e, long *cp_times)
139{
140	int i;
141
142	e->cur_sample_idx = (e->cur_sample_idx + 1) % MAX_CPU_SAMPLES;
143	for (i = 0; cp_times != NULL && i < CPUSTATES; i++)
144		e->states[e->cur_sample_idx][i] = cp_times[i];
145
146	e->sample_cnt++;
147	if (e->sample_cnt > MAX_CPU_SAMPLES)
148		e->sample_cnt = MAX_CPU_SAMPLES;
149
150	HRDBG("sample count for CPU no. %d went to %d", e->cpu_no, e->sample_cnt);
151	e->load = get_avg_load(e);
152
153}
154
155/**
156 * Create a new entry into the processor table.
157 */
158static struct processor_entry *
159proc_create_entry(u_int cpu_no, struct device_map_entry *map)
160{
161	struct device_entry *dev;
162	struct processor_entry *entry;
163	char name[128];
164
165	/*
166	 * If there is no map entry create one by creating a device table
167	 * entry.
168	 */
169	if (map == NULL) {
170		snprintf(name, sizeof(name), "cpu%u", cpu_no);
171		if ((dev = device_entry_create(name, "", "")) == NULL)
172			return (NULL);
173		dev->flags |= HR_DEVICE_IMMUTABLE;
174		STAILQ_FOREACH(map, &device_map, link)
175			if (strcmp(map->name_key, name) == 0)
176				break;
177		if (map == NULL)
178			abort();
179	}
180
181	if ((entry = malloc(sizeof(*entry))) == NULL) {
182		syslog(LOG_ERR, "hrProcessorTable: %s malloc "
183		    "failed: %m", __func__);
184		return (NULL);
185	}
186	memset(entry, 0, sizeof(*entry));
187
188	entry->index = map->hrIndex;
189	entry->load = 0;
190	entry->sample_cnt = 0;
191	entry->cur_sample_idx = -1;
192	entry->cpu_no = (u_char)cpu_no;
193	entry->frwId = &oid_zeroDotZero; /* unknown id FIXME */
194
195	INSERT_OBJECT_INT(entry, &processor_tbl);
196
197	HRDBG("CPU %d added with SNMP index=%d",
198	    entry->cpu_no, entry->index);
199
200	return (entry);
201}
202
203/**
204 * Scan the device map table for CPUs and create an entry into the
205 * processor table for each CPU.
206 *
207 * Make sure that the number of processors announced by the kernel hw.ncpu
208 * is equal to the number of processors we have found in the device table.
209 */
210static void
211create_proc_table(void)
212{
213	struct device_map_entry *map;
214	struct processor_entry *entry;
215	int cpu_no;
216	size_t len;
217
218	detected_processor_count = 0;
219
220	/*
221	 * Because hrProcessorTable depends on hrDeviceTable,
222	 * the device detection must be performed at this point.
223	 * If not, no entries will be present in the hrProcessor Table.
224	 *
225	 * For non-ACPI system the processors are not in the device table,
226	 * therefore insert them after checking hw.ncpu.
227	 */
228	STAILQ_FOREACH(map, &device_map, link)
229		if (strncmp(map->name_key, "cpu", strlen("cpu")) == 0 &&
230		    strstr(map->location_key, ".CPU") != NULL) {
231			if (sscanf(map->name_key,"cpu%d", &cpu_no) != 1) {
232				syslog(LOG_ERR, "hrProcessorTable: Failed to "
233				    "get cpu no. from device named '%s'",
234				    map->name_key);
235				continue;
236			}
237
238			if ((entry = proc_create_entry(cpu_no, map)) == NULL)
239				continue;
240
241			detected_processor_count++;
242		}
243
244	len = sizeof(hw_ncpu);
245	if (sysctlbyname("hw.ncpu", &hw_ncpu, &len, NULL, 0) == -1 ||
246	    len != sizeof(hw_ncpu)) {
247		syslog(LOG_ERR, "hrProcessorTable: sysctl(hw.ncpu) failed");
248		hw_ncpu = 0;
249	}
250
251	HRDBG("%d CPUs detected via device table; hw.ncpu is %d",
252	    detected_processor_count, hw_ncpu);
253
254	/* XXX Can happen on non-ACPI systems? Create entries by hand. */
255	for (; detected_processor_count < hw_ncpu; detected_processor_count++)
256		proc_create_entry(detected_processor_count, NULL);
257
258	len = 2;
259	if (sysctlnametomib("kern.cp_times", cpmib, &len)) {
260		syslog(LOG_ERR, "hrProcessorTable: sysctlnametomib(kern.cp_times) failed");
261		cpmib[0] = 0;
262		cpmib[1] = 0;
263		cplen = 0;
264	} else if (sysctl(cpmib, 2, NULL, &len, NULL, 0)) {
265		syslog(LOG_ERR, "hrProcessorTable: sysctl(kern.cp_times) length query failed");
266		cplen = 0;
267	} else {
268		cplen = len / sizeof(long);
269	}
270	HRDBG("%zu entries for kern.cp_times", cplen);
271
272}
273
274/**
275 * Free the processor table
276 */
277static void
278free_proc_table(void)
279{
280	struct processor_entry *n1;
281
282	while ((n1 = TAILQ_FIRST(&processor_tbl)) != NULL) {
283		TAILQ_REMOVE(&processor_tbl, n1, link);
284		free(n1);
285		detected_processor_count--;
286	}
287
288	assert(detected_processor_count == 0);
289	detected_processor_count = 0;
290}
291
292/**
293 * Refresh all values in the processor table. We call this once for
294 * every PDU that accesses the table.
295 */
296static void
297refresh_processor_tbl(void)
298{
299	struct processor_entry *entry;
300	size_t size;
301
302	long pcpu_cp_times[cplen];
303	memset(pcpu_cp_times, 0, sizeof(pcpu_cp_times));
304
305	size = cplen * sizeof(long);
306	if (sysctl(cpmib, 2, pcpu_cp_times, &size, NULL, 0) == -1 &&
307	    !(errno == ENOMEM && size >= cplen * sizeof(long))) {
308		syslog(LOG_ERR, "hrProcessorTable: sysctl(kern.cp_times) failed");
309		return;
310	}
311
312	TAILQ_FOREACH(entry, &processor_tbl, link) {
313		assert(hr_kd != NULL);
314		save_sample(entry, &pcpu_cp_times[entry->cpu_no * CPUSTATES]);
315	}
316
317}
318
319/**
320 * This function is called MAX_CPU_SAMPLES times per minute to collect the
321 * CPU load.
322 */
323static void
324get_cpus_samples(void *arg __unused)
325{
326
327	HRDBG("[%llu] ENTER", (unsigned long long)get_ticks());
328	refresh_processor_tbl();
329	HRDBG("[%llu] EXIT", (unsigned long long)get_ticks());
330}
331
332/**
333 * Called to start this table. We need to start the periodic idle
334 * time collection.
335 */
336void
337start_processor_tbl(struct lmodule *mod)
338{
339
340	/*
341	 * Start the cpu stats collector
342	 * The semantics of timer_start parameters is in "SNMP ticks";
343	 * we have 100 "SNMP ticks" per second, thus we are trying below
344	 * to get MAX_CPU_SAMPLES per minute
345	 */
346	cpus_load_timer = timer_start_repeat(100, 100 * 60 / MAX_CPU_SAMPLES,
347	    get_cpus_samples, NULL, mod);
348}
349
350/**
351 * Init the things for hrProcessorTable.
352 * Scan the device table for processor entries.
353 */
354void
355init_processor_tbl(void)
356{
357
358	/* create the initial processor table */
359	create_proc_table();
360	/* and get first samples */
361	refresh_processor_tbl();
362}
363
364/**
365 * Finalization routine for hrProcessorTable.
366 * It destroys the lists and frees any allocated heap memory.
367 */
368void
369fini_processor_tbl(void)
370{
371
372	if (cpus_load_timer != NULL) {
373		timer_stop(cpus_load_timer);
374		cpus_load_timer = NULL;
375	}
376
377	free_proc_table();
378}
379
380/**
381 * Access routine for the processor table.
382 */
383int
384op_hrProcessorTable(struct snmp_context *ctx __unused,
385    struct snmp_value *value, u_int sub, u_int iidx __unused,
386    enum snmp_op curr_op)
387{
388	struct processor_entry *entry;
389
390	switch (curr_op) {
391
392	case SNMP_OP_GETNEXT:
393		if ((entry = NEXT_OBJECT_INT(&processor_tbl,
394		    &value->var, sub)) == NULL)
395			return (SNMP_ERR_NOSUCHNAME);
396		value->var.len = sub + 1;
397		value->var.subs[sub] = entry->index;
398		goto get;
399
400	case SNMP_OP_GET:
401		if ((entry = FIND_OBJECT_INT(&processor_tbl,
402		    &value->var, sub)) == NULL)
403			return (SNMP_ERR_NOSUCHNAME);
404		goto get;
405
406	case SNMP_OP_SET:
407		if ((entry = FIND_OBJECT_INT(&processor_tbl,
408		    &value->var, sub)) == NULL)
409			return (SNMP_ERR_NO_CREATION);
410		return (SNMP_ERR_NOT_WRITEABLE);
411
412	case SNMP_OP_ROLLBACK:
413	case SNMP_OP_COMMIT:
414		abort();
415	}
416	abort();
417
418  get:
419	switch (value->var.subs[sub - 1]) {
420
421	case LEAF_hrProcessorFrwID:
422		assert(entry->frwId != NULL);
423		value->v.oid = *entry->frwId;
424		return (SNMP_ERR_NOERROR);
425
426	case LEAF_hrProcessorLoad:
427		value->v.integer = entry->load;
428		return (SNMP_ERR_NOERROR);
429	}
430	abort();
431}
432