From b2161b8a2722005c70900ddbf20e9e3cd83e03ee Mon Sep 17 00:00:00 2001 From: Brian O'Connor Date: Wed, 8 Nov 2017 08:13:17 -0800 Subject: [PATCH] VM Updates (#83) - Minor fixes to p4runtime exercise and README - Adding p4runtime/solution - Adding p4runtime/topology.json - Updating .gitignore to include solution directory and topology.json - Fixing root-bootstrap to exit on errors - Updating VM name in Vagrantfile - Setting up VM to automatically log 'p4' user in on startup --- .gitignore | 2 + P4D2_2017_Fall/exercises/p4runtime/README.md | 64 +++++- .../exercises/p4runtime/mycontroller.py | 96 +++++---- .../p4runtime/solution/mycontroller.py | 195 ++++++++++++++++++ .../exercises/p4runtime/topology.json | 16 ++ P4D2_2017_Fall/vm/Vagrantfile | 3 + P4D2_2017_Fall/vm/root-bootstrap.sh | 11 +- 7 files changed, 334 insertions(+), 53 deletions(-) create mode 100755 P4D2_2017_Fall/exercises/p4runtime/solution/mycontroller.py create mode 100755 P4D2_2017_Fall/exercises/p4runtime/topology.json diff --git a/.gitignore b/.gitignore index 7e0adce..93dfdfa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,13 @@ # Compiled JSON *.json !*p4app.json +!topology.json *.pcap # Extracted solutions solution*/ +!solution/ # Build folders build*/ diff --git a/P4D2_2017_Fall/exercises/p4runtime/README.md b/P4D2_2017_Fall/exercises/p4runtime/README.md index e5c340a..bd09ae1 100644 --- a/P4D2_2017_Fall/exercises/p4runtime/README.md +++ b/P4D2_2017_Fall/exercises/p4runtime/README.md @@ -3,11 +3,11 @@ ## Introduction In this exercise, we will be using P4 Runtime to send flow entries to the -switch, instead of using the switch's CLI. We will be using the same P4 -program that you used in the previous in the basic_tunnel exercise. The +switch instead of using the switch's CLI. We will be building on the same P4 +program that you used in the [basic_tunnel](../basic_tunnel) exercise. The P4 program has be renamed to `advanced_tunnel.py` and has been augmented -with a counter, `tunnelCount`, and two new actions, `myTunnel_ingress` -and `myTunnel_egress`. +with two counters (`ingressTunnelCounter`, `egressTunnelCounter`) and +two new actions (`myTunnel_ingress`, `myTunnel_egress`). You will use the starter program, `mycontroller.py`, and a few helper libraries in the `p4runtime_lib` directory to create the table entries @@ -19,12 +19,12 @@ necessary to tunnel traffic between host 1 and 2. ## Step 1: Run the (incomplete) starter code -The starter code for this assignment is in a file called `mycontroller.py` +The starter code for this assignment is in a file called `mycontroller.py`, and it will install only some of the rules that you need tunnel traffic between two hosts. Let's first compile the new P4 program, start the network, use `mycontroller.py` -to install a few rules, and look at the tunnel ingress counter to see that things +to install a few rules, and look at the `ingressTunnelCounter` to see that things are working as expected. 1. In your shell, run: @@ -32,18 +32,17 @@ are working as expected. make ``` This will: - * compile `advanced_tunnel.p4`, and + * compile `advanced_tunnel.p4`, * start a Mininet instance with three switches (`s1`, `s2`, `s3`) - configured in a triangle, each connected to one host (`h1`, `h2`, - and `h3`). - * The hosts are assigned IPs of `10.0.1.1`, `10.0.2.2`, etc. + configured in a triangle, each connected to one host (`h1`, `h2`, `h3`), and + * assign IPs of `10.0.1.1`, `10.0.2.2`, `10.0.3.3` to the respective hosts. 2. You should now see a Mininet command prompt. Start a ping between h1 and h2: ```bash mininet> h1 ping h2 ``` Because there are no rules on the switches, you should **not** receive any - replies yet. + replies yet. You should leave the ping running in this shell. 3. Open another shell and run the starter code: ```bash @@ -65,6 +64,25 @@ Each switch is currently mapping traffic into tunnels based on the destination I address. Your job is to write the rules that forward the traffic between the switches based on the tunnel ID. +### Potential Issues + +If you see the following error message when running `mycontroller.py`, then +the gRPC server is not running on one or more switches. + +``` +p4@p4:~/tutorials/P4D2_2017_Fall/exercises/p4runtime$ ./mycontroller.py +... +grpc._channel._Rendezvous: <_Rendezvous of RPC that terminated with (StatusCode.UNAVAILABLE, Connect Failed)> +``` + +You can check to see which of gRPC ports are listening on the machine by running: +```bash +sudo netstat -lpnt +``` + +The easiest solution is to enter `Ctrl-D` or `exit` in the `mininet>` prompt, +and re-run `make`. + ### A note about the control plane A P4 program defines a packet-processing pipeline, but the rules @@ -96,6 +114,26 @@ that will match on tunnel ID and forward packets to the next hop. ![topology](../basic_tunnel/topo.png) +In this exercise, you will be interacting with some of the classes and methods in +the `p4runtime_lib` directory. Here is a summary of each of the files in the directory: +- `helper.py` + - Contains the `P4InfoHelper` class which is used to parse the `p4info` files. + - Provides translation methods from entity name to and from ID number. + - Builds P4 program-dependendent sections of P4 Runtime table entries. +- `switch.py` + - Contains the `SwitchConnection` class which grabs the gRPC client stub, and + establishes connections to the switches. + - Provides helper methods that construct the P4 Runtime protocol buffer messages + and makes the P4 Runtime gRPC service calls. +- `bmv2.py` + - Contains `Bmv2SwitchConnection` which extends `SwitchConnections` and provides + the BMv2-specific device payload to load the P4 program. +- `convert.py` + - Provides convenience methods to encode and decode from friendly strings and + numbers to the byte strings required for the protocol buffer messages. + - Used by `helper.py` + + ## Step 3: Run your solution Follow the instructions from Step 1. If your Mininet network is still running, @@ -122,6 +160,10 @@ need to change it for a more realistic network? - What is the TTL in the ICMP replies? Why is it the value that it is? Hint: The default TTL is 64 for packets sent by the hosts. +If you are interested, you can find the protocol buffer and gRPC definitions here: +- [P4 Runtime](https://github.com/p4lang/PI/blob/master/proto/p4/p4runtime.proto) +- [P4 Info](https://github.com/p4lang/PI/blob/master/proto/p4/config/p4info.proto) + #### Cleaning up Mininet If the Mininet shell crashes, it may leave a Mininet instance diff --git a/P4D2_2017_Fall/exercises/p4runtime/mycontroller.py b/P4D2_2017_Fall/exercises/p4runtime/mycontroller.py index 6d5e58f..9123a93 100755 --- a/P4D2_2017_Fall/exercises/p4runtime/mycontroller.py +++ b/P4D2_2017_Fall/exercises/p4runtime/mycontroller.py @@ -9,62 +9,69 @@ import p4runtime_lib.helper SWITCH_TO_HOST_PORT = 1 SWITCH_TO_SWITCH_PORT = 2 -def writeTunnelRules(p4info_helper, ingressSw, egressSw, tunnelId, dstEthAddr, dstIpAddr): +def writeTunnelRules(p4info_helper, ingress_sw, egress_sw, tunnel_id, + dst_eth_addr, dst_ip_addr): ''' Installs three rules: - 1) An tunnel ingress rule on the ingress switch in the ipv4_lpm table that encapsulates traffic - into a tunnel with the specified ID - 2) A transit rule on the ingress switch that forwards traffic based on the specified ID - 3) An tunnel egress rule on the egress switch that decapsulates traffic with the specified ID - and sends it to the host + 1) An tunnel ingress rule on the ingress switch in the ipv4_lpm table that + encapsulates traffic into a tunnel with the specified ID + 2) A transit rule on the ingress switch that forwards traffic based on + the specified ID + 3) An tunnel egress rule on the egress switch that decapsulates traffic + with the specified ID and sends it to the host :param p4info_helper: the P4Info helper - :param ingressSw: the ingress switch connection - :param egressSw: the egress switch connection - :param tunnelId: the specified tunnel ID - :param dstEthAddr: the destination IP to match in the ingress rule - :param dstIpAddr: the destination Ethernet address to write in the egress rule + :param ingress_sw: the ingress switch connection + :param egress_sw: the egress switch connection + :param tunnel_id: the specified tunnel ID + :param dst_eth_addr: the destination IP to match in the ingress rule + :param dst_ip_addr: the destination Ethernet address to write in the + egress rule ''' # 1) Tunnel Ingress Rule table_entry = p4info_helper.buildTableEntry( table_name="ipv4_lpm", match_fields={ - "hdr.ipv4.dstAddr": (dstIpAddr, 32) + "hdr.ipv4.dstAddr": (dst_ip_addr, 32) }, action_name="myTunnel_ingress", action_params={ - "dst_id": tunnelId, + "dst_id": tunnel_id, }) - ingressSw.WriteTableEntry(table_entry) - print "Installed ingress tunnel rule on %s" % ingressSw.name + ingress_sw.WriteTableEntry(table_entry) + print "Installed ingress tunnel rule on %s" % ingress_sw.name # 2) Tunnel Transit Rule - # TODO you will need to implement this rule - # The rule will need to be added to the myTunnel_exact table and match on the tunnel ID (hdr.myTunnel.dst_id). - # For our simple topology, transit traffic will need to be forwarded using the myTunnel_egress action to - # the SWITCH_TO_SWITCH_PORT (port 2). - # We will only need on transit rule on the ingress switch because we are using a simple topology. - # In general, you'll need on transit rule for each switch in the path (except the last one) + # The rule will need to be added to the myTunnel_exact table and match on + # the tunnel ID (hdr.myTunnel.dst_id). For our simple topology, transit + # traffic will need to be forwarded on the using the myTunnel_forward action + # on the SWITCH_TO_SWITCH_PORT (port 2). # - # If you are stuck, start by copying the tunnel ingress rule from above. Then, try to make the suggested - # modifications. + # We will only need on transit rule on the ingress switch because we are + # using a simple topology. In general, you'll need on transit rule for + # each switch in the path (except the last one). + + # TODO build the transit rule + # TODO install the transit rule on the ingress switch print "TODO Install transit tunnel rule" # 3) Tunnel Egress Rule - # For our simple topology, the host will always be located on the SWITCH_TO_HOST_PORT (port 1). - # In general, you will need to keep track of which port the host is connected to. + # For our simple topology, the host will always be located on the + # SWITCH_TO_HOST_PORT (port 1). + # In general, you will need to keep track of which port the host is + # connected to. table_entry = p4info_helper.buildTableEntry( table_name="myTunnel_exact", match_fields={ - "hdr.myTunnel.dst_id": tunnelId + "hdr.myTunnel.dst_id": tunnel_id }, action_name="myTunnel_egress", action_params={ - "dstAddr": dstEthAddr, + "dstAddr": dst_eth_addr, "port": SWITCH_TO_HOST_PORT }) - egressSw.WriteTableEntry(table_entry) - print "Installed egress tunnel rule on %s" % egressSw.name + egress_sw.WriteTableEntry(table_entry) + print "Installed egress tunnel rule on %s" % egress_sw.name def readTableRules(p4info_helper, sw): ''' @@ -77,14 +84,16 @@ def readTableRules(p4info_helper, sw): for response in sw.ReadTableEntries(): for entity in response.entities: entry = entity.table_entry - # TODO For extra credit, you can use the p4info_helper to translate the IDs the entry to names + # TODO For extra credit, you can use the p4info_helper to translate + # the IDs the entry to names print entry print '-----' def printCounter(p4info_helper, sw, counter_name, index): ''' - Reads the specified counter at the specified index from the switch. In our program, the index - is the tunnel ID. If the index is 0, it will return all values from the counter. + Reads the specified counter at the specified index from the switch. In our + program, the index is the tunnel ID. If the index is 0, it will return all + values from the counter. :param p4info_helper: the P4Info helper :param sw: the switch connection @@ -104,23 +113,26 @@ def main(p4info_file_path, bmv2_file_path): # Instantiate a P4 Runtime helper from the p4info file p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path) - # Create a switch connection object for s1 and s2; this is backed by a P4 Runtime gRPC connection + # Create a switch connection object for s1 and s2; + # this is backed by a P4 Runtime gRPC connection s1 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s1', address='127.0.0.1:50051') s2 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s2', address='127.0.0.1:50052') # Install the P4 program on the switches - s1.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, bmv2_json_file_path=bmv2_file_path) + s1.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, + bmv2_json_file_path=bmv2_file_path) print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s1.name - s2.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, bmv2_json_file_path=bmv2_file_path) + s2.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, + bmv2_json_file_path=bmv2_file_path) print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s2.name # Write the rules that tunnel traffic from h1 to h2 - writeTunnelRules(p4info_helper, ingressSw=s1, egressSw=s2, tunnelId=100, - dstEthAddr="00:00:00:00:02:02", dstIpAddr="10.0.2.2") + writeTunnelRules(p4info_helper, ingress_sw=s1, egress_sw=s2, tunnel_id=100, + dst_eth_addr="00:00:00:00:02:02", dst_ip_addr="10.0.2.2") # Write the rules that tunnel traffic from h2 to h1 - writeTunnelRules(p4info_helper, ingressSw=s2, egressSw=s1, tunnelId=200, - dstEthAddr="00:00:00:00:01:01", dstIpAddr="10.0.1.1") + writeTunnelRules(p4info_helper, ingress_sw=s2, egress_sw=s1, tunnel_id=200, + dst_eth_addr="00:00:00:00:01:01", dst_ip_addr="10.0.1.1") # TODO Uncomment the following two lines to read table entries from s1 and s2 #readTableRules(p4info_helper, s1) @@ -142,9 +154,11 @@ def main(p4info_file_path, bmv2_file_path): if __name__ == '__main__': parser = argparse.ArgumentParser(description='P4Runtime Controller') parser.add_argument('--p4info', help='p4info proto in text format from p4c', - type=str, action="store", required=False, default='./build/advanced_tunnel.p4info') + type=str, action="store", required=False, + default='./build/advanced_tunnel.p4info') parser.add_argument('--bmv2-json', help='BMv2 JSON file from p4c', - type=str, action="store", required=False, default='./build/advanced_tunnel.json') + type=str, action="store", required=False, + default='./build/advanced_tunnel.json') args = parser.parse_args() if not os.path.exists(args.p4info): diff --git a/P4D2_2017_Fall/exercises/p4runtime/solution/mycontroller.py b/P4D2_2017_Fall/exercises/p4runtime/solution/mycontroller.py new file mode 100755 index 0000000..7396162 --- /dev/null +++ b/P4D2_2017_Fall/exercises/p4runtime/solution/mycontroller.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python2 +import argparse +import os +from time import sleep + +# NOTE: Appending to the PYTHON_PATH is only required in the `solution` directory. +# It is not required for mycontroller.py in the top-level directory. +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +import p4runtime_lib.bmv2 +import p4runtime_lib.helper + +SWITCH_TO_HOST_PORT = 1 +SWITCH_TO_SWITCH_PORT = 2 + +def writeTunnelRules(p4info_helper, ingress_sw, egress_sw, tunnel_id, + dst_eth_addr, dst_ip_addr): + ''' + Installs three rules: + 1) An tunnel ingress rule on the ingress switch in the ipv4_lpm table that + encapsulates traffic into a tunnel with the specified ID + 2) A transit rule on the ingress switch that forwards traffic based on + the specified ID + 3) An tunnel egress rule on the egress switch that decapsulates traffic + with the specified ID and sends it to the host + + :param p4info_helper: the P4Info helper + :param ingress_sw: the ingress switch connection + :param egress_sw: the egress switch connection + :param tunnel_id: the specified tunnel ID + :param dst_eth_addr: the destination IP to match in the ingress rule + :param dst_ip_addr: the destination Ethernet address to write in the + egress rule + ''' + # 1) Tunnel Ingress Rule + table_entry = p4info_helper.buildTableEntry( + table_name="ipv4_lpm", + match_fields={ + "hdr.ipv4.dstAddr": (dst_ip_addr, 32) + }, + action_name="myTunnel_ingress", + action_params={ + "dst_id": tunnel_id, + }) + ingress_sw.WriteTableEntry(table_entry) + print "Installed ingress tunnel rule on %s" % ingress_sw.name + + # 2) Tunnel Transit Rule + # The rule will need to be added to the myTunnel_exact table and match on + # the tunnel ID (hdr.myTunnel.dst_id). For our simple topology, transit + # traffic will need to be forwarded on the using the myTunnel_forward action + # on the SWITCH_TO_SWITCH_PORT (port 2). + # + # We will only need on transit rule on the ingress switch because we are + # using a simple topology. In general, you'll need on transit rule for + # each switch in the path (except the last one). + table_entry = p4info_helper.buildTableEntry( + table_name="myTunnel_exact", + match_fields={ + "hdr.myTunnel.dst_id": tunnel_id + }, + action_name="myTunnel_forward", + action_params={ + "port": SWITCH_TO_SWITCH_PORT + }) + ingress_sw.WriteTableEntry(table_entry) + print "Installed transit tunnel rule on %s" % ingress_sw.name + + # 3) Tunnel Egress Rule + # For our simple topology, the host will always be located on the + # SWITCH_TO_HOST_PORT (port 1). + # In general, you will need to keep track of which port the host is + # connected to. + table_entry = p4info_helper.buildTableEntry( + table_name="myTunnel_exact", + match_fields={ + "hdr.myTunnel.dst_id": tunnel_id + }, + action_name="myTunnel_egress", + action_params={ + "dstAddr": dst_eth_addr, + "port": SWITCH_TO_HOST_PORT + }) + egress_sw.WriteTableEntry(table_entry) + print "Installed egress tunnel rule on %s" % egress_sw.name + +def readTableRules(p4info_helper, sw): + ''' + Reads the table entries from all tables on the switch. + + :param p4info_helper: the P4Info helper + :param sw: the switch connection + ''' + print '\n----- Reading tables rules for %s -----' % sw.name + for response in sw.ReadTableEntries(): + for entity in response.entities: + entry = entity.table_entry + # TODO For extra credit, you can use the p4info_helper to translate + # the IDs the entry to names + table_name = p4info_helper.get_tables_name(entry.table_id) + print '%s: ' % table_name, + for m in entry.match: + print p4info_helper.get_match_field_name(table_name, m.field_id), + print '%r' % (p4info_helper.get_match_field_value(m),), + action = entry.action.action + action_name = p4info_helper.get_actions_name(action.action_id) + print '->', action_name, + for p in action.params: + print p4info_helper.get_action_param_name(action_name, p.param_id), + print '%r' % p.value, + print + +def printCounter(p4info_helper, sw, counter_name, index): + ''' + Reads the specified counter at the specified index from the switch. In our + program, the index is the tunnel ID. If the index is 0, it will return all + values from the counter. + + :param p4info_helper: the P4Info helper + :param sw: the switch connection + :param counter_name: the name of the counter from the P4 program + :param index: the counter index (in our case, the tunnel ID) + ''' + for response in sw.ReadCounters(p4info_helper.get_counters_id(counter_name), index): + for entity in response.entities: + counter = entity.counter_entry + print "%s %s %d: %d packets (%d bytes)" % ( + sw.name, counter_name, index, + counter.data.packet_count, counter.data.byte_count + ) + + +def main(p4info_file_path, bmv2_file_path): + # Instantiate a P4 Runtime helper from the p4info file + p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path) + + # Create a switch connection object for s1 and s2; + # this is backed by a P4 Runtime gRPC connection + s1 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s1', address='127.0.0.1:50051') + s2 = p4runtime_lib.bmv2.Bmv2SwitchConnection('s2', address='127.0.0.1:50052') + + # Install the P4 program on the switches + s1.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, + bmv2_json_file_path=bmv2_file_path) + print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s1.name + s2.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, + bmv2_json_file_path=bmv2_file_path) + print "Installed P4 Program using SetForwardingPipelineConfig on %s" % s2.name + + # Write the rules that tunnel traffic from h1 to h2 + writeTunnelRules(p4info_helper, ingress_sw=s1, egress_sw=s2, tunnel_id=100, + dst_eth_addr="00:00:00:00:02:02", dst_ip_addr="10.0.2.2") + + # Write the rules that tunnel traffic from h2 to h1 + writeTunnelRules(p4info_helper, ingress_sw=s2, egress_sw=s1, tunnel_id=200, + dst_eth_addr="00:00:00:00:01:01", dst_ip_addr="10.0.1.1") + + # TODO Uncomment the following two lines to read table entries from s1 and s2 + readTableRules(p4info_helper, s1) + readTableRules(p4info_helper, s2) + + # Print the tunnel counters every 2 seconds + try: + while True: + sleep(2) + print '\n----- Reading tunnel counters -----' + printCounter(p4info_helper, s1, "ingressTunnelCounter", 100) + printCounter(p4info_helper, s2, "egressTunnelCounter", 100) + printCounter(p4info_helper, s2, "ingressTunnelCounter", 200) + printCounter(p4info_helper, s1, "egressTunnelCounter", 200) + except KeyboardInterrupt: + print " Shutting down." + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='P4Runtime Controller') + parser.add_argument('--p4info', help='p4info proto in text format from p4c', + type=str, action="store", required=False, + default='./build/advanced_tunnel.p4info') + parser.add_argument('--bmv2-json', help='BMv2 JSON file from p4c', + type=str, action="store", required=False, + default='./build/advanced_tunnel.json') + args = parser.parse_args() + + if not os.path.exists(args.p4info): + parser.print_help() + print "\np4info file not found: %s\nHave you run 'make'?" % args.p4info + parser.exit(1) + if not os.path.exists(args.bmv2_json): + parser.print_help() + print "\nBMv2 JSON file not found: %s\nHave you run 'make'?" % args.bmv2_json + parser.exit(1) + + main(args.p4info, args.bmv2_json) diff --git a/P4D2_2017_Fall/exercises/p4runtime/topology.json b/P4D2_2017_Fall/exercises/p4runtime/topology.json new file mode 100755 index 0000000..f8a5b48 --- /dev/null +++ b/P4D2_2017_Fall/exercises/p4runtime/topology.json @@ -0,0 +1,16 @@ +{ + "hosts": [ + "h1", + "h2", + "h3" + ], + "switches": { + "s1": {}, + "s2": {}, + "s3": {} + }, + "links": [ + ["h1", "s1"], ["s1", "s2"], ["s1", "s3"], + ["s3", "s2"], ["s2", "h2"], ["s3", "h3"] + ] +} diff --git a/P4D2_2017_Fall/vm/Vagrantfile b/P4D2_2017_Fall/vm/Vagrantfile index f5bb2f9..6d34ece 100644 --- a/P4D2_2017_Fall/vm/Vagrantfile +++ b/P4D2_2017_Fall/vm/Vagrantfile @@ -3,7 +3,10 @@ Vagrant.configure(2) do |config| config.vm.box = "bento/ubuntu-16.04" + config.vm.define "p4-tutorial" do |tutorial| + end config.vm.provider "virtualbox" do |vb| + vb.name = "P4 Tutorial" + Time.now.strftime(" %Y-%m-%d") vb.gui = true vb.memory = 2048 vb.cpus = 2 diff --git a/P4D2_2017_Fall/vm/root-bootstrap.sh b/P4D2_2017_Fall/vm/root-bootstrap.sh index 8e770d1..0cc24a7 100755 --- a/P4D2_2017_Fall/vm/root-bootstrap.sh +++ b/P4D2_2017_Fall/vm/root-bootstrap.sh @@ -1,6 +1,7 @@ #!/bin/bash -set -x +# Print commands and exit on errors +set -xe sudo add-apt-repository ppa:webupd8team/sublime-text-3 sudo add-apt-repository ppa:webupd8team/atom @@ -79,3 +80,11 @@ sed -i s@#background=@background=/usr/share/lubuntu/wallpapers/1604-lubuntu-defa # Disable screensaver apt-get -y remove light-locker + +# Automatically log into the P4 user +cat << EOF | tee -a /etc/lightdm/lightdm.conf.d/10-lightdm.conf +[SeatDefaults] +autologin-user=p4 +autologin-user-timeout=0 +user-session=Lubuntu +EOF