[ChaosDatastore]Checkin latest version of the app engine code

Get all the working code and add date-util package to requirements and a README.

Bug: 112916493
Test: Tested locally and with the gcloud server
Change-Id: I5e8e31ef44bc0d6827de926d8d39e125653273f4
Reviewed-on: https://chromium-review.googlesource.com/1183818
Tested-by: Bindu Mahadev <bmahadev@chromium.org>
Reviewed-by: Bindu Mahadev <bmahadev@chromium.org>
Reviewed-by: Kris Rambish <krisr@chromium.org>
Commit-Queue: Bindu Mahadev <bmahadev@chromium.org>
Trybot-Ready: Bindu Mahadev <bmahadev@chromium.org>
diff --git a/provingground/chaosap/.gcloudignore b/provingground/chaosap/.gcloudignore
new file mode 100644
index 0000000..8e1329f
--- /dev/null
+++ b/provingground/chaosap/.gcloudignore
@@ -0,0 +1,16 @@
+# This file specifies files that are *not* uploaded to Google Cloud Platform
+# using gcloud. It follows the same syntax as .gitignore, with the addition of
+# "#!include" directives (which insert the entries of the given .gitignore-style
+# file at that point).
+#
+# For more information, run:
+#   $ gcloud topic gcloudignore
+#
+.gcloudignore
+# If you would like to upload your .git directory, .gitignore file or files
+# from your .gitignore file, remove the corresponding line
+# below:
+.git
+.gitignore
+# Python pycache:
+__pycache__/
diff --git a/provingground/chaosap/.gitignore b/provingground/chaosap/.gitignore
new file mode 100644
index 0000000..3631dee
--- /dev/null
+++ b/provingground/chaosap/.gitignore
@@ -0,0 +1,2 @@
+lib/*
+venv/*
diff --git a/provingground/chaosap/README.txt b/provingground/chaosap/README.txt
new file mode 100644
index 0000000..25707db
--- /dev/null
+++ b/provingground/chaosap/README.txt
@@ -0,0 +1,12 @@
+Setup and deploy appengine.
+============================
+In the ./chaosp directory:
+1. Run the following command to install python dependencies:
+   # pip install -r requirements.txt -t lib
+
+2. To deploy the app engine:
+   # gcloud app deploy app.yaml
+
+3. To test changes using local server:
+   # dev_appserver.py app.yaml
+This will start a server on localport 8080.
diff --git a/provingground/chaosap/ap_spec.py b/provingground/chaosap/ap_spec.py
new file mode 100644
index 0000000..12f5f13
--- /dev/null
+++ b/provingground/chaosap/ap_spec.py
@@ -0,0 +1,306 @@
+# Copyright (c) 2013 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# Supported bands
+BAND_2GHZ = '2.4GHz'
+BAND_5GHZ = '5GHz'
+
+# List of valid bands.
+VALID_BANDS = [BAND_2GHZ, BAND_5GHZ]
+
+# List of valid 802.11 protocols (modes).
+MODE_A = 0x01
+MODE_B = 0x02
+MODE_G = 0x04
+MODE_N = 0x08
+MODE_AC = 0x10
+MODE_AUTO = 0x20
+MODE_M = MODE_A | MODE_B | MODE_G # Used for standard maintenance
+MODE_D = MODE_A | MODE_B | MODE_N # International roaming extensions
+MODE_B_G = MODE_B | MODE_G
+MODE_B_G_N = MODE_B | MODE_G | MODE_N
+MODE_AC_N = MODE_AC | MODE_N
+MODE_A_N = MODE_A | MODE_N
+
+# List of valid modes.
+VALID_MODES = [MODE_A, MODE_AC, MODE_AUTO, MODE_B, MODE_D, MODE_G, MODE_M,
+               MODE_N, MODE_B_G, MODE_B_G_N, MODE_A_N, MODE_AC_N]
+VALID_2GHZ_MODES = [MODE_B, MODE_G, MODE_N, MODE_B_G, MODE_B_G_N]
+VALID_5GHZ_MODES = [MODE_A, MODE_AC, MODE_N, MODE_A_N, MODE_AC_N]
+
+# Supported security types
+SECURITY_TYPE_DISABLED = 'open'
+SECURITY_TYPE_WEP = 'wep'
+SECURITY_TYPE_WPAPSK = 'wpa'
+SECURITY_TYPE_WPA2PSK = 'wpa2'
+# Mixed mode security is wpa/wpa2
+SECURITY_TYPE_MIXED = 'mixed'
+
+WEP_AUTHENTICATION_OPEN = object()
+WEP_AUTHENTICATION_SHARED = object()
+
+# List of valid securities.
+# TODO (krisr) the configurators do not support WEP at this time.
+VALID_SECURITIES = [SECURITY_TYPE_DISABLED,
+                    SECURITY_TYPE_WPAPSK,
+                    SECURITY_TYPE_WPA2PSK,
+                    SECURITY_TYPE_MIXED,
+                    SECURITY_TYPE_WEP]
+
+# List of valid channels.
+VALID_2GHZ_CHANNELS = range(1,15)
+VALID_5GHZ_CHANNELS = [36, 40, 44, 48, 149, 153, 157, 161, 165]
+
+# Frequency to channel conversion table
+CHANNEL_TABLE = {2412: 1, 2417: 2, 2422: 3,
+                 2427: 4, 2432: 5, 2437: 6,
+                 2442: 7, 2447: 8, 2452: 9,
+                 2457: 10, 2462: 11, 2467: 12,
+                 2472: 13, 2484: 14, 5180: 36,
+                 5200: 40, 5220: 44, 5240: 48,
+                 5745: 149, 5765: 153, 5785: 157,
+                 5805: 161, 5825: 165}
+
+# This only works because the frequency table is one to one
+# for channels/frequencies.
+FREQUENCY_TABLE = dict((v,k) for k,v in CHANNEL_TABLE.iteritems())
+
+# Configurator type
+CONFIGURATOR_STATIC = 1
+CONFIGURATOR_DYNAMIC = 2
+CONFIGURATOR_ANY = 3
+
+# Default values
+DEFAULT_BAND = BAND_2GHZ
+
+DEFAULT_2GHZ_MODE = MODE_G
+DEFAULT_5GHZ_MODE = MODE_A
+
+DEFAULT_SECURITY_TYPE = SECURITY_TYPE_DISABLED
+
+DEFAULT_2GHZ_CHANNEL = 5
+DEFAULT_5GHZ_CHANNEL = 149
+
+# Convenience method to convert modes and bands to human readable strings.
+def band_string_for_band(band):
+    """Returns a human readable string of the band
+
+    @param band: band object
+    @returns: string representation of the band
+    """
+    if band == BAND_2GHZ:
+        return '2.4 GHz'
+    elif band == BAND_5GHZ:
+        return '5 GHz'
+
+
+def mode_string_for_mode(mode):
+    """Returns a human readable string of the mode.
+
+    @param mode: integer, the mode to convert.
+    @returns: string representation of the mode
+    """
+    string_table = {MODE_A:'a', MODE_AC:'ac', MODE_B:'b', MODE_G:'g',
+                    MODE_N:'n'}
+
+    if mode == MODE_AUTO:
+        return 'Auto'
+    total = 0
+    string = ''
+    for current_mode in sorted(string_table.keys()):
+        i = current_mode & mode
+        total = total | i
+        if i in string_table:
+            string = string + string_table[i] + '/'
+    if total == MODE_M:
+        string = 'm'
+    elif total == MODE_D:
+        string = 'd'
+    if string[-1] == '/':
+        return string[:-1]
+    return string
+
+
+class APSpec(object):
+    """Object to specify an APs desired capabilities.
+
+    The APSpec object is immutable.  All of the parameters are optional.
+    For those not given the defaults listed above will be used.  Validation
+    is done on the values to make sure the spec created is valid.  If
+    validation fails a ValueError is raised.
+    """
+
+
+    def __init__(self, visible=True, security=SECURITY_TYPE_DISABLED,
+                 band=None, mode=None, channel=None, hostnames=None,
+                 configurator_type=CONFIGURATOR_ANY,
+                 # lab_ap set to true means the AP must be in the lab;
+                 # if it set to false the AP is outside of the lab.
+                 lab_ap=True):
+        super(APSpec, self).__init__()
+        self._visible = visible
+        self._security = security
+        self._mode = mode
+        self._channel = channel
+        self._hostnames = hostnames
+        self._configurator_type = configurator_type
+        self._lab_ap = lab_ap
+        self._webdriver_hostname = None
+
+        if not self._channel and (self._mode == MODE_N or not self._mode):
+            if band == BAND_2GHZ or not band:
+                self._channel = DEFAULT_2GHZ_CHANNEL
+                if not self._mode:
+                    self._mode = DEFAULT_2GHZ_MODE
+            elif band == BAND_5GHZ:
+                self._channel = DEFAULT_5GHZ_CHANNEL
+                if not self._mode:
+                    self._mode = DEFAULT_5GHZ_MODE
+            else:
+                raise ValueError('Invalid Band.')
+
+        self._validate_channel_and_mode()
+
+        if ((band == BAND_2GHZ and self._mode not in VALID_2GHZ_MODES) or
+            (band == BAND_5GHZ and self._mode not in VALID_5GHZ_MODES)):
+            raise ValueError('Conflicting band and modes/channels.')
+
+        self._validate_security()
+
+
+    def __str__(self):
+        return ('AP Specification:\n'
+                'visible=%r\n'
+                'security=%s\n'
+                'band=%s\n'
+                'mode=%s\n'
+                'channel=%d\n'
+                'password=%s' % (self._visible, self._security,
+                                 band_string_for_band(self.band),
+                                 mode_string_for_mode(self._mode),
+                                 self._channel, self._password))
+
+
+    @property
+    def password(self):
+        """Returns the password for password supported secured networks."""
+        return self._password
+
+
+
+    @property
+    def visible(self):
+        """Returns if the SSID is visible or not."""
+        return self._visible
+
+
+    @property
+    def security(self):
+        """Returns the type of security."""
+        return self._security
+
+
+    @property
+    def band(self):
+        """Return the band."""
+        if self._channel in VALID_2GHZ_CHANNELS:
+            return BAND_2GHZ
+        return BAND_5GHZ
+
+
+    @property
+    def mode(self):
+        """Return the mode."""
+        return self._mode
+
+
+    @property
+    def channel(self):
+        """Return the channel."""
+        return self._channel
+
+
+    @property
+    def frequency(self):
+        """Return the frequency equivalent of the channel."""
+        return FREQUENCY_TABLE[self._channel]
+
+
+    @property
+    def hostnames(self):
+        """Return the hostnames; this may be None."""
+        return self._hostnames
+
+
+    @property
+    def configurator_type(self):
+        """Returns the configurator type."""
+        return self._configurator_type
+
+
+    @property
+    def lab_ap(self):
+        """Returns if the AP should be in the lab or not."""
+        return self._lab_ap
+
+
+    @property
+    def webdriver_hostname(self):
+        """Returns locked webdriver hostname."""
+        return self._webdriver_hostname
+
+
+    @webdriver_hostname.setter
+    def webdriver_hostname(self, value):
+        """Sets webdriver_hostname to locked instance.
+
+        @param value: locked webdriver hostname
+
+        """
+        self._webdriver_hostname = value
+
+
+    def _validate_channel_and_mode(self):
+        """Validates the channel and mode selected are correct.
+
+        raises ValueError: if the channel or mode selected is invalid
+        """
+        if self._channel and self._mode:
+            if ((self._channel in VALID_2GHZ_CHANNELS and
+                 self._mode not in VALID_2GHZ_MODES) or
+                (self._channel in VALID_5GHZ_CHANNELS and
+                 self._mode not in VALID_5GHZ_MODES)):
+                raise ValueError('Conflicting mode/channel has been selected.')
+        elif self._channel:
+            if self._channel in VALID_2GHZ_CHANNELS:
+                self._mode = DEFAULT_2GHZ_MODE
+            elif self._channel in VALID_5GHZ_CHANNELS:
+                self._mode = DEFAULT_5GHZ_MODE
+            else:
+                raise ValueError('Invalid channel passed.')
+        else:
+            if self._mode in VALID_2GHZ_MODES:
+                self._channel = DEFAULT_2GHZ_CHANNEL
+            elif self._mode in VALID_5GHZ_MODES:
+                self._channel = DEFAULT_5GHZ_CHANNEL
+            else:
+                raise ValueError('Invalid mode passed.')
+
+
+    def _validate_security(self):
+        """Sets a password for security settings that need it.
+
+        raises ValueError: if the security setting passed is invalid.
+        """
+        if self._security == SECURITY_TYPE_DISABLED:
+            self._password = None
+        elif (self._security == SECURITY_TYPE_WPAPSK or
+             self._security == SECURITY_TYPE_WPA2PSK or
+             self._security == SECURITY_TYPE_MIXED):
+             self._password = 'chromeos'
+        elif (self._security==SECURITY_TYPE_WEP):
+             self._password = 'chros'
+        else:
+            raise ValueError('Invalid security passed.')
+
diff --git a/provingground/chaosap/app.yaml b/provingground/chaosap/app.yaml
index 1522c0b..f63c8df 100644
--- a/provingground/chaosap/app.yaml
+++ b/provingground/chaosap/app.yaml
@@ -6,4 +6,3 @@
 - url: /.*
   script: main.app
   secure: always
-  login: required
\ No newline at end of file
diff --git a/provingground/chaosap/appengine_config.py b/provingground/chaosap/appengine_config.py
index 49511fc..893e0bd 100644
--- a/provingground/chaosap/appengine_config.py
+++ b/provingground/chaosap/appengine_config.py
@@ -18,4 +18,3 @@
 # Add any libraries installed in the "lib" folder.
 vendor.add('lib')
 # [END vendor]
-
diff --git a/provingground/chaosap/main.py b/provingground/chaosap/main.py
index 54aa42e..96cd619 100644
--- a/provingground/chaosap/main.py
+++ b/provingground/chaosap/main.py
@@ -68,6 +68,37 @@
     new_device.put()
     return jsonify(result=True, id=hostname)
 
+
+@app.route('/devices/<hostname>', methods=['PUT', 'GET'])
+def edit_ap_device(hostname):
+    """Get and/or edit an AP.
+
+    curl -i -H "Content-Type: application/json" <URL>/devices/<HOSTNAME>
+
+    curl -i -H "Content-Type: application/json" -X PUT -d '{"ap_label":"<AP_LABEL>"}' \
+    <URL>/devices/<HOSTNAME>
+
+    """
+    if request.method == 'GET':
+        device_key = ndb.Key(ApDevice, hostname)
+        device = device_key.get()
+        device_js = ndb_json.dumps(device, indent=0)
+        return device_js
+
+    if request.method == 'PUT':
+        device_key = ndb.Key(ApDevice, hostname)
+        device = device_key.get()
+        if 'router_name' in request.json:
+            device.router_name = request.json['router_name']
+        if 'ap_label' in request.json:
+            device.ap_label = request.json['ap_label']
+        if 'lab_label' in request.json:
+            device.lab_label = request.json['lab_label']
+        device.put()
+        return jsonify(result=True, id=hostname, ap_label=device.ap_label, \
+               lab_label=device.lab_label, router_name=device.router_name)
+
+
 @app.route('/devices/lock', methods=['PUT'])
 def lock_devices():
     """
diff --git a/provingground/chaosap/requirements.txt b/provingground/chaosap/requirements.txt
index d0be1d6..0755008 100644
--- a/provingground/chaosap/requirements.txt
+++ b/provingground/chaosap/requirements.txt
@@ -3,3 +3,4 @@
 Flask-RESTful==0.3.6
 Flask-HTTPAuth==3.2.3
 PassLib==1.7.1
+python-dateutil==2.7.3