Add blueprint wiring for loopback controller-config 05/47905/13
authorAlexis de Talhouët <adetalhouet@inocybe.com>
Thu, 3 Nov 2016 20:25:18 +0000 (16:25 -0400)
committerJakub Morvay <jmorvay@cisco.com>
Mon, 28 Nov 2016 09:40:31 +0000 (09:40 +0000)
Change-Id: I0bf0664e35f17ba449d71a6555359cc42b61da07
Signed-off-by: Alexis de Talhouët <adetalhouet@inocybe.com>
features/netconf-connector/pom.xml
features/netconf-connector/src/main/features/features.xml
features/netconf/pom.xml
netconf/netconf-artifacts/pom.xml
netconf/netconf-connector-config/pom.xml
netconf/netconf-connector-config/src/main/resources/initial/99-netconf-connector.xml [deleted file]
netconf/netconf-connector-config/src/main/resources/org/opendaylight/blueprint/netconf-connector.xml [new file with mode: 0644]
netconf/netconf-topology-config/src/main/resources/org/opendaylight/blueprint/netconf-topology.xml
netconf/netconf-topology/pom.xml
netconf/netconf-topology/src/main/java/org/opendaylight/netconf/topology/api/NetconfConnectorFactory.java [new file with mode: 0644]
netconf/netconf-topology/src/main/java/org/opendaylight/netconf/topology/impl/NetconfConnectorFactoryImpl.java [new file with mode: 0644]

index ecd3e5abe631bf4d5a886a66d11f64c101ded35c..c3f8d6aa061c399340aa3af641e54ac69793ff7f 100644 (file)
@@ -34,7 +34,6 @@
     <features.file>features.xml</features.file>
     <config.configfile.directory>etc/opendaylight/karaf</config.configfile.directory>
     <config.netconf.client.configfile>01-netconf.xml</config.netconf.client.configfile>
-    <config.netconf.connector.configfile>99-netconf-connector.xml</config.netconf.connector.configfile>
   </properties>
 
   <dependencyManagement>
       <groupId>${project.groupId}</groupId>
       <artifactId>netconf-topology-singleton</artifactId>
     </dependency>
-
     <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>netconf-connector-config</artifactId>
-      <version>${netconf.version}</version>
-      <type>xml</type>
-      <classifier>config</classifier>
     </dependency>
   </dependencies>
 
index a911dcf56585b349936af25e26966e3f727166cd..cdbe097eb0e5306a387515a9dbba03fa8d0f5c70 100644 (file)
@@ -43,7 +43,7 @@
 
     <feature name='odl-netconf-connector-ssh' version='${project.version}' description="OpenDaylight :: Netconf Connector :: Netconf Connector + Netconf SSH Server + loopback connection configuration">
         <feature version='${project.version}'>odl-netconf-topology</feature>
-        <configfile finalname="${config.configfile.directory}/${config.netconf.connector.configfile}">mvn:org.opendaylight.netconf/netconf-connector-config/{{VERSION}}/xml/config</configfile>
+        <bundle>mvn:org.opendaylight.netconf/netconf-connector-config/{{VERSION}}</bundle>
     </feature>
 
     <feature name='odl-netconf-topology' version='${project.version}' description="OpenDaylight :: Netconf Topology :: Netconf Connector + Netconf SSH Server + Netconf configuration via config topology datastore">
index 502ae76fef8e5e79ec44ea7ac23656e6c4983afe..bd68cb8448ac724dd647200759e06a1ff8ad221b 100644 (file)
       <type>xml</type>
       <classifier>config</classifier>
     </dependency>
-    <dependency>
-      <groupId>${project.groupId}</groupId>
-      <artifactId>netconf-connector-config</artifactId>
-      <type>xml</type>
-      <classifier>config</classifier>
-    </dependency>
     <dependency>
       <groupId>${project.groupId}</groupId>
       <artifactId>netconf-monitoring</artifactId>
index 32bcf637d3bbb56e4026b18fe168109c59d8877a..cb03a4d2cf7e0db4d4ed91eedd105f331ffcf58a 100644 (file)
@@ -75,8 +75,6 @@
                 <groupId>${project.groupId}</groupId>
                 <artifactId>netconf-connector-config</artifactId>
                 <version>${project.version}</version>
-                <type>xml</type>
-                <classifier>config</classifier>
             </dependency>
             <dependency>
                 <groupId>${project.groupId}</groupId>
index ff6e0ec1776b4c5b5bc9525ccdef9c37c6b5ed64..771a3bea453fabd1ad1b11a95569bc25f2d5b876 100644 (file)
@@ -6,46 +6,55 @@
  terms of the Eclipse Public License v1.0 which accompanies this distribution,
  and is available at http://www.eclipse.org/legal/epl-v10.html
 -->
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 
-  <modelVersion>4.0.0</modelVersion>
-  <parent>
-    <groupId>org.opendaylight.odlparent</groupId>
-    <artifactId>odlparent-lite</artifactId>
-    <version>1.8.0-SNAPSHOT</version>
-    <relativePath/>
-  </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.opendaylight.odlparent</groupId>
+        <artifactId>bundle-parent</artifactId>
+        <version>1.8.0-SNAPSHOT</version>
+        <relativePath/>
+    </parent>
 
-  <groupId>org.opendaylight.netconf</groupId>
-  <artifactId>netconf-connector-config</artifactId>
-  <description>Configuration files for netconf-connector</description>
-  <version>1.2.0-SNAPSHOT</version>
-  <packaging>jar</packaging>
+    <groupId>org.opendaylight.netconf</groupId>
+    <artifactId>netconf-connector-config</artifactId>
+    <description>Configuration files for netconf-connector</description>
+    <version>1.2.0-SNAPSHOT</version>
+    <packaging>bundle</packaging>
 
-  <build>
-    <plugins>
-        <plugin>
-        <groupId>org.codehaus.mojo</groupId>
-        <artifactId>build-helper-maven-plugin</artifactId>
-        <executions>
-          <execution>
-            <id>attach-artifacts</id>
-            <goals>
-              <goal>attach-artifact</goal>
-            </goals>
-            <phase>package</phase>
-            <configuration>
-              <artifacts>
-                <artifact>
-                  <file>${project.build.directory}/classes/initial/99-netconf-connector.xml</file>
-                  <type>xml</type>
-                  <classifier>config</classifier>
-                </artifact>
-              </artifacts>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-    </plugins>
-  </build>
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.opendaylight.netconf</groupId>
+                <artifactId>netconf-subsystem</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>netconf-topology</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                        <Embed-Dependency>netconf-topology</Embed-Dependency>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
 </project>
diff --git a/netconf/netconf-connector-config/src/main/resources/initial/99-netconf-connector.xml b/netconf/netconf-connector-config/src/main/resources/initial/99-netconf-connector.xml
deleted file mode 100644 (file)
index 62b2c8d..0000000
+++ /dev/null
@@ -1,55 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- vi: set et smarttab sw=4 tabstop=4: -->
-<!--
- Copyright (c) 2013 Cisco Systems, Inc. and others.  All rights reserved.
-
- This program and the accompanying materials are made available under the
- terms of the Eclipse Public License v1.0 which accompanies this distribution,
- and is available at http://www.eclipse.org/legal/epl-v10.html
--->
-<snapshot>
-  <configuration>
-    <data xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
-      <modules xmlns="urn:opendaylight:params:xml:ns:yang:controller:config">
-        <!-- Loopback connection to netconf server in controller using netconf-connector -->
-        <module>
-          <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">prefix:sal-netconf-connector</type>
-          <name>controller-config</name>
-          <address xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">127.0.0.1</address>
-          <port xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">1830</port>
-          <username xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">admin</username>
-          <password xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">admin</password>
-          <tcp-only xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">false</tcp-only>
-          <reconnect-on-changed-schema xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">true</reconnect-on-changed-schema>
-          <event-executor xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">
-            <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:netty">prefix:netty-event-executor</type>
-            <name>global-event-executor</name>
-          </event-executor>
-          <binding-registry xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">
-            <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:md:sal:binding">prefix:binding-broker-osgi-registry</type>
-            <name>binding-osgi-broker</name>
-          </binding-registry>
-          <dom-registry xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">
-            <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:md:sal:dom">prefix:dom-broker-osgi-registry</type>
-            <name>dom-broker</name>
-          </dom-registry>
-          <client-dispatcher xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">
-            <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:config:netconf">prefix:netconf-client-dispatcher</type>
-            <name>global-netconf-dispatcher</name>
-          </client-dispatcher>
-          <processing-executor xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">
-            <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:threadpool">prefix:threadpool</type>
-            <name>global-netconf-processing-executor</name>
-          </processing-executor>
-          <keepalive-executor xmlns="urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf">
-            <type xmlns:prefix="urn:opendaylight:params:xml:ns:yang:controller:threadpool">prefix:scheduled-threadpool</type>
-            <name>global-netconf-ssh-scheduled-executor</name>
-          </keepalive-executor>
-        </module>
-        </modules>
-    </data>
-  </configuration>
-  <required-capabilities>
-      <capability>urn:opendaylight:params:xml:ns:yang:controller:md:sal:connector:netconf?module=odl-sal-netconf-connector-cfg&amp;revision=2015-08-03</capability>
-  </required-capabilities>
-</snapshot>
diff --git a/netconf/netconf-connector-config/src/main/resources/org/opendaylight/blueprint/netconf-connector.xml b/netconf/netconf-connector-config/src/main/resources/org/opendaylight/blueprint/netconf-connector.xml
new file mode 100644 (file)
index 0000000..7312471
--- /dev/null
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (c) 2016 Inocybe Technologies Inc. and others.  All rights reserved.
+
+ This program and the accompanying materials are made available under the
+ terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ and is available at http://www.eclipse.org/legal/epl-v10.html
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+           xmlns:odl="http://opendaylight.org/xmlns/blueprint/v1.0.0"
+           xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.3.0"
+           odl:use-default-for-reference-types="true">
+
+    <reference id="dataBroker" interface="org.opendaylight.controller.md.sal.binding.api.DataBroker"/>
+    <odl:static-reference id="netconfConnectorFactory" interface="org.opendaylight.netconf.topology.api.NetconfConnectorFactory"/>
+
+    <cm:property-placeholder persistent-id="org.opendaylight.netconf.sal.connect"
+                             update-strategy="none">
+        <cm:default-properties>
+            <cm:property name="name" value="controller-config"/>
+            <cm:property name="address" value="127.0.0.1"/>
+            <cm:property name="port" value="1830"/>
+            <cm:property name="username" value="admin"/>
+            <cm:property name="password" value="admin"/>
+            <cm:property name="tcp-only" value="false"/>
+            <cm:property name="reconnect-on-schema-change" value="true"/>
+        </cm:default-properties>
+    </cm:property-placeholder>
+
+    <bean id="controllerConfig" factory-ref="netconfConnectorFactory" factory-method="newInstance">
+        <argument ref="dataBroker"/>
+        <argument type="java.lang.String" value="${name}"/>
+        <argument type="java.lang.String" value="${address}"/>
+        <argument type="java.lang.Integer" value="${port}"/>
+        <argument type="java.lang.String" value="${username}"/>
+        <argument type="java.lang.String" value="${password}"/>
+        <argument type="java.lang.Boolean" value="${tcp-only}"/>
+        <argument type="java.lang.Boolean" value="${reconnect-on-schema-change}"/>
+    </bean>
+
+</blueprint>
\ No newline at end of file
index cf0401772704ed52ce4e5a2df618d653b6fc3b77..62e5046c7b03be10a224d2db360270c488f8b348 100755 (executable)
@@ -48,4 +48,8 @@
         <argument ref="domMountPointService"/>
     </bean>
 
+    <bean id="netconfConnectorFactory" class="org.opendaylight.netconf.topology.impl.NetconfConnectorFactoryImpl"/>
+    <service ref="netconfConnectorFactory" interface="org.opendaylight.netconf.topology.api.NetconfConnectorFactory"
+             odl:type="default"/>
+
 </blueprint>
\ No newline at end of file
index 5ffd6d8d37aa1c1df9ad5bd54bc24cd0cf3537e5..ae7679f62be5ec733cff35ec327e8be0c7ab6eca 100644 (file)
             <artifactId>yang-model-api</artifactId>
         </dependency>
     </dependencies>
-
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.codehaus.mojo</groupId>
-                <artifactId>build-helper-maven-plugin</artifactId>
-                <version>1.8</version>
-                <executions>
-                    <execution>
-                        <id>add-source</id>
-                        <phase>generate-sources</phase>
-                        <goals>
-                            <goal>add-source</goal>
-                        </goals>
-                        <configuration>
-                            <sources>
-                                <source>${project.build.directory}/generated-sources/config</source>;
-                            </sources>
-                        </configuration>
-                    </execution>
-                </executions>
-            </plugin>
-        </plugins>
-    </build>
-
 </project>
diff --git a/netconf/netconf-topology/src/main/java/org/opendaylight/netconf/topology/api/NetconfConnectorFactory.java b/netconf/netconf-topology/src/main/java/org/opendaylight/netconf/topology/api/NetconfConnectorFactory.java
new file mode 100644 (file)
index 0000000..632c2f9
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2016 Inocybe Technologies and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.topology.api;
+
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
+
+/**
+ * Created by adetalhouet on 2016-11-03.
+ */
+public interface NetconfConnectorFactory {
+
+    /**
+     * Create a new netconf connector with default values.
+     * <p>
+     * This method will create a {@link Node} and a {@link NetconfNode} that will be added as an augmentation to the
+     * {@link Node}. Afterward, that {@link Node} will be written in the MDSAL datastore under the {@link NetconfTopology}.
+     * Listeners of that subtree located within network-topology bundle will setup the session.
+     *
+     * @param dataBroker Instance of the {@link DataBroker}
+     * @param instanceName The name of the node
+     * @param address The address
+     * @param port The port
+     * @param username The username of the netconf session
+     * @param password The password of the netconf session
+     * @param tcpOnly Whether to create a TCP or SSH session
+     * @param reconnectOnSchemaChange Whether to enable ietf-netconf-monitoring and register the NETCONF stream.
+     * @return The created {@link Node}
+     */
+    Node newInstance(final DataBroker dataBroker,
+                     final String instanceName,
+                     final String address,
+                     final Integer port,
+                     final String username,
+                     final String password,
+                     final Boolean tcpOnly,
+                     final Boolean reconnectOnSchemaChange);
+}
diff --git a/netconf/netconf-topology/src/main/java/org/opendaylight/netconf/topology/impl/NetconfConnectorFactoryImpl.java b/netconf/netconf-topology/src/main/java/org/opendaylight/netconf/topology/impl/NetconfConnectorFactoryImpl.java
new file mode 100644 (file)
index 0000000..dc5649f
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2016 Inocybe Technologies and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.netconf.topology.impl;
+
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import javax.annotation.Nullable;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.WriteTransaction;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.md.sal.common.api.data.TransactionCommitFailedException;
+import org.opendaylight.netconf.topology.api.NetconfConnectorFactory;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.Host;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.HostBuilder;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNode;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.NetconfNodeBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.netconf.node.credentials.Credentials;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.node.topology.rev150114.netconf.node.credentials.credentials.LoginPasswordBuilder;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NetworkTopology;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.NodeId;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.TopologyId;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.Topology;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.TopologyKey;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.Node;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeBuilder;
+import org.opendaylight.yang.gen.v1.urn.tbd.params.xml.ns.yang.network.topology.rev131021.network.topology.topology.NodeKey;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Created by adetalhouet on 2016-11-03.
+ */
+public class NetconfConnectorFactoryImpl implements NetconfConnectorFactory {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NetconfConnectorFactoryImpl.class);
+
+    private static final InstanceIdentifier<Topology> TOPOLOGY_PATH = InstanceIdentifier.create(NetworkTopology.class)
+            .child(Topology.class, new TopologyKey(new TopologyId("topology-netconf")));
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Node newInstance(final DataBroker dataBroker,
+                            final String instanceName,
+                            final String address,
+                            final Integer port,
+                            final String username,
+                            final String password,
+                            final Boolean tcpOnly,
+                            final Boolean reconnectOnSchemaChange) {
+
+        final NodeId nodeId = new NodeId(instanceName);
+        final NodeKey nodeKey = new NodeKey(nodeId);
+        final Credentials credentials = new LoginPasswordBuilder()
+                .setUsername(username)
+                .setPassword(password)
+                .build();
+        final Host host = HostBuilder.getDefaultInstance(address);
+        final PortNumber portNumber = new PortNumber(port);
+        final NetconfNode netconfNode = new NetconfNodeBuilder()
+                .setHost(host)
+                .setPort(portNumber)
+                .setCredentials(credentials)
+                .setTcpOnly(tcpOnly)
+                .setReconnectOnChangedSchema(reconnectOnSchemaChange)
+                .build();
+        final Node node =  new NodeBuilder()
+                .setNodeId(nodeId)
+                .setKey(nodeKey)
+                .addAugmentation(NetconfNode.class, netconfNode)
+                .build();
+
+        final InstanceIdentifier<Node> nodePath = TOPOLOGY_PATH.child(Node.class, nodeKey);
+        final WriteTransaction transaction = dataBroker.newWriteOnlyTransaction();
+        transaction.put(LogicalDatastoreType.CONFIGURATION, nodePath, node);
+        final CheckedFuture<Void, TransactionCommitFailedException> submitFuture = transaction.submit();
+        Futures.addCallback(submitFuture, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable final Void result) {
+                LOG.debug("Node {} was successfully added to the topology", instanceName);
+            }
+
+            @Override
+            public void onFailure(final Throwable t) {
+                LOG.error("Node {} creation failed: {}", instanceName, t);
+            }
+        });
+        return node;
+    }
+}
\ No newline at end of file