View Javadoc
1   /*
2    * Copyright 2019 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.dccon.repository.cli;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.StringReader;
22  import java.time.LocalDateTime;
23  import java.time.OffsetDateTime;
24  import java.time.ZoneOffset;
25  import java.time.format.DateTimeFormatter;
26  import java.util.ArrayList;
27  import java.util.Collections;
28  import java.util.List;
29  import java.util.function.BiFunction;
30  import lombok.extern.slf4j.Slf4j;
31  import org.bremersee.dccon.model.DhcpLease;
32  import org.springframework.util.StringUtils;
33  
34  /**
35   * The dhcp lease list parser parses the response of the linux command line tool {@code
36   * dhcp-lease-list}.
37   *
38   * <p>A response of {@code dhcp-lease-list} looks like this:
39   * <pre>
40   * MAC b8:xx:xx:xx:xx:xx IP 192.168.1.109 HOSTNAME ukelei BEGIN 2019-08-18 11:20:33 END 2019-08-18 11:50:33 MANUFACTURER Apple, Inc.
41   * MAC ac:xx:xx:xx:xx:yy IP 192.168.1.188 HOSTNAME -NA- BEGIN 2019-08-18 11:25:48 END 2019-08-18 11:55:48 MANUFACTURER Super Micro Computer, Inc.
42   * </pre>
43   *
44   * @author Christian Bremer
45   */
46  public interface DhcpLeaseParser extends CommandExecutorResponseParser<List<DhcpLease>> {
47  
48    /**
49     * The constant MAC.
50     */
51    String MAC = "MAC ";
52  
53    /**
54     * The constant IP.
55     */
56    String IP = " IP ";
57  
58    /**
59     * The constant HOSTNAME.
60     */
61    String HOSTNAME = " HOSTNAME ";
62  
63    /**
64     * The constant HOSTNAME_UNKNOWN.
65     */
66    String HOSTNAME_UNKNOWN = "-NA-";
67  
68    /**
69     * The constant BEGIN.
70     */
71    String BEGIN = " BEGIN ";
72  
73    /**
74     * The constant END.
75     */
76    String END = " END ";
77  
78    /**
79     * The constant MANUFACTURER.
80     */
81    String MANUFACTURER = " MANUFACTURER ";
82  
83    /**
84     * Default parser dhcp leases parser.
85     *
86     * @return the dhcp leases parser
87     */
88    static DhcpLeaseParser defaultParser() {
89      return new Default();
90    }
91  
92    /**
93     * Default parser dhcp leases parser.
94     *
95     * @param unknownHostConverter the unknown host converter
96     * @return the dhcp leases parser
97     */
98    @SuppressWarnings("unused")
99    static DhcpLeaseParser defaultParser(BiFunction<String, String, String> unknownHostConverter) {
100     return new Default(unknownHostConverter);
101   }
102 
103   /**
104    * The default parser.
105    */
106   @Slf4j
107   class Default implements DhcpLeaseParser {
108 
109     private BiFunction<String, String, String> unknownHostConverter;
110 
111     /**
112      * Instantiates a new default parser.
113      */
114     Default() {
115       this(null);
116     }
117 
118     /**
119      * Instantiates a new default parser.
120      *
121      * <p>The unknown host converter converts the unknown host name {@link
122      * DhcpLeaseParser#HOSTNAME_UNKNOWN} into another host name. The parameters of the function are
123      * MAC and IP.
124      *
125      * @param unknownHostConverter the unknown host converter
126      */
127     Default(
128         BiFunction<String, String, String> unknownHostConverter) {
129       if (unknownHostConverter == null) {
130         this.unknownHostConverter = (mac, ip) -> "dhcp-" + ip.replace(".", "-");
131       } else {
132         this.unknownHostConverter = unknownHostConverter;
133       }
134     }
135 
136     @Override
137     public List<DhcpLease> parse(final CommandExecutorResponse response) {
138       if (!response.stdoutHasText()) {
139         log.warn("Dhcp lease list command did not produce output. Error is [{}].",
140             response.getStderr());
141         return Collections.emptyList();
142       }
143       final String output = response.getStdout();
144       try (final BufferedReader reader = new BufferedReader(new StringReader(output))) {
145         return parseDhcpLeaseList(reader);
146 
147       } catch (IOException e) {
148         log.error("Parsing dhcp lease list failed:\n" + output + "\n", e);
149         return Collections.emptyList();
150       }
151     }
152 
153     private List<DhcpLease> parseDhcpLeaseList(final BufferedReader reader) throws IOException {
154       final List<DhcpLease> leases = new ArrayList<>();
155       String line;
156       while ((line = reader.readLine()) != null) {
157         line = line.trim();
158         final String mac = findDhcpLeasePart(line, MAC, IP);
159         final String ip = findDhcpLeasePart(line, IP, HOSTNAME);
160         String hostname = findDhcpLeasePart(line, HOSTNAME, BEGIN);
161         if (HOSTNAME_UNKNOWN.equalsIgnoreCase(hostname) && ip != null) {
162           hostname = unknownHostConverter.apply(mac, ip);
163         }
164         final String begin = findDhcpLeasePart(line, BEGIN, END);
165         final String end = findDhcpLeasePart(line, END, MANUFACTURER);
166         final String manufacturer = findDhcpLeasePart(line, MANUFACTURER, null);
167         if (mac != null && ip != null && hostname != null && begin != null && end != null) {
168           final DhcpLease lease = new DhcpLease(
169               mac.replace("-", ":").trim().toLowerCase(),
170               ip,
171               hostname,
172               parseDhcpLeaseTime(begin),
173               parseDhcpLeaseTime(end),
174               manufacturer);
175           leases.add(lease);
176         }
177       }
178       return leases;
179     }
180 
181     private String findDhcpLeasePart(String line, String field, String nextField) {
182       if (!StringUtils.hasText(line) || !StringUtils.hasText(field)) {
183         return null;
184       }
185       int start = line.indexOf(field);
186       if (start < 0) {
187         return null;
188       }
189       start = start + field.length();
190       int end = StringUtils.hasText(nextField)
191           ? line.indexOf(nextField, start)
192           : line.length();
193       if (end < 0 || end <= start) {
194         return null;
195       }
196       return line.substring(start, end).trim();
197     }
198 
199     private OffsetDateTime parseDhcpLeaseTime(String time) {
200       if (!StringUtils.hasText(time)) {
201         return null;
202       }
203       final LocalDateTime localDateTime = LocalDateTime.parse(
204           time,
205           DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
206       // As of https://linux.die.net/man/5/dhcpd.leases the time zone is always UTC
207       return OffsetDateTime.of(localDateTime, ZoneOffset.UTC);
208     }
209 
210   }
211 
212 }