Merge "Android.mk, setup.py, __main__.py to package acloud. Update README.md"
diff --git a/internal/lib/android_build_client.py b/internal/lib/android_build_client.py
new file mode 100644
index 0000000..b459f62
--- /dev/null
+++ b/internal/lib/android_build_client.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A client that talks to Android Build APIs."""
+
+import io
+import logging
+import os
+
+import apiclient
+
+from acloud.internal.lib import base_cloud_client
+from acloud.public import errors
+
+logger = logging.getLogger(__name__)
+
+
+class AndroidBuildClient(base_cloud_client.BaseCloudApiClient):
+    """Client that manages Android Build."""
+
+    # API settings, used by BaseCloudApiClient.
+    API_NAME = "androidbuildinternal"
+    API_VERSION = "v2beta1"
+    SCOPE = "https://www.googleapis.com/auth/androidbuild.internal"
+
+    # other variables.
+    DEFAULT_RESOURCE_ID = "0"
+    # TODO(fdeng): We should use "latest".
+    DEFAULT_ATTEMPT_ID = "0"
+    DEFAULT_CHUNK_SIZE = 20 * 1024 * 1024
+    NO_ACCESS_ERROR_PATTERN = "does not have storage.objects.create access"
+
+    # Message constant
+    COPY_TO_MSG = ("build artifact (target: %s, build_id: %s, "
+                   "artifact: %s, attempt_id: %s) to "
+                   "google storage (bucket: %s, path: %s)")
+
+    def DownloadArtifact(self,
+                         build_target,
+                         build_id,
+                         resource_id,
+                         local_dest,
+                         attempt_id=None):
+        """Get Android build attempt information.
+
+        Args:
+            build_target: Target name, e.g. "gce_x86-userdebug"
+            build_id: Build id, a string, e.g. "2263051", "P2804227"
+            resource_id: Id of the resource, e.g "avd-system.tar.gz".
+            local_dest: A local path where the artifact should be stored.
+                        e.g. "/tmp/avd-system.tar.gz"
+            attempt_id: String, attempt id, will default to DEFAULT_ATTEMPT_ID.
+        """
+        attempt_id = attempt_id or self.DEFAULT_ATTEMPT_ID
+        api = self.service.buildartifact().get_media(
+            buildId=build_id,
+            target=build_target,
+            attemptId=attempt_id,
+            resourceId=resource_id)
+        logger.info("Downloading artifact: target: %s, build_id: %s, "
+                    "resource_id: %s, dest: %s", build_target, build_id,
+                    resource_id, local_dest)
+        try:
+            with io.FileIO(local_dest, mode="wb") as fh:
+                downloader = apiclient.http.MediaIoBaseDownload(
+                    fh, api, chunksize=self.DEFAULT_CHUNK_SIZE)
+                done = False
+                while not done:
+                    _, done = downloader.next_chunk()
+            logger.info("Downloaded artifact: %s", local_dest)
+        except OSError as e:
+            logger.error("Downloading artifact failed: %s", str(e))
+            raise errors.DriverError(str(e))
+
+    def CopyTo(self,
+               build_target,
+               build_id,
+               artifact_name,
+               destination_bucket,
+               destination_path,
+               attempt_id=None):
+        """Copy an Android Build artifact to a storage bucket.
+
+        Args:
+            build_target: Target name, e.g. "gce_x86-userdebug"
+            build_id: Build id, a string, e.g. "2263051", "P2804227"
+            artifact_name: Name of the artifact, e.g "avd-system.tar.gz".
+            destination_bucket: String, a google storage bucket name.
+            destination_path: String, "path/inside/bucket"
+            attempt_id: String, attempt id, will default to DEFAULT_ATTEMPT_ID.
+        """
+        attempt_id = attempt_id or self.DEFAULT_ATTEMPT_ID
+        copy_msg = "Copying %s" % self.COPY_TO_MSG
+        logger.info(copy_msg, build_target, build_id, artifact_name,
+                    attempt_id, destination_bucket, destination_path)
+        api = self.service.buildartifact().copyTo(
+            buildId=build_id,
+            target=build_target,
+            attemptId=attempt_id,
+            artifactName=artifact_name,
+            destinationBucket=destination_bucket,
+            destinationPath=destination_path)
+        try:
+            self.Execute(api)
+            finish_msg = "Finished copying %s" % self.COPY_TO_MSG
+            logger.info(finish_msg, build_target, build_id, artifact_name,
+                        attempt_id, destination_bucket, destination_path)
+        except errors.HttpError as e:
+            if e.code == 503:
+                if self.NO_ACCESS_ERROR_PATTERN in str(e):
+                    error_msg = "Please grant android build team's service account "
+                    error_msg += "write access to bucket %s. Original error: %s"
+                    error_msg %= (destination_bucket, str(e))
+                    raise errors.HttpError(e.code, message=error_msg)
+            raise
diff --git a/internal/lib/android_build_client_test.py b/internal/lib/android_build_client_test.py
new file mode 100644
index 0000000..db097bc
--- /dev/null
+++ b/internal/lib/android_build_client_test.py
@@ -0,0 +1,132 @@
+#!/usr/bin/env python
+#
+# Copyright 2016 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for acloud.internal.lib.android_build_client."""
+
+import io
+import time
+
+import apiclient
+import mock
+
+import unittest
+from acloud.internal.lib import android_build_client
+from acloud.internal.lib import driver_test_lib
+from acloud.public import errors
+
+
+class AndroidBuildClientTest(driver_test_lib.BaseDriverTest):
+    """Test AndroidBuildClient."""
+
+    BUILD_TARGET = "fake_target"
+    BUILD_ID = 12345
+    RESOURCE_ID = "avd-system.tar.gz"
+    LOCAL_DEST = "/fake/local/path"
+    DESTINATION_BUCKET = "fake_bucket"
+
+    def setUp(self):
+        """Set up test."""
+        super(AndroidBuildClientTest, self).setUp()
+        self.Patch(android_build_client.AndroidBuildClient,
+                   "InitResourceHandle")
+        self.client = android_build_client.AndroidBuildClient(mock.MagicMock())
+        self.client._service = mock.MagicMock()
+
+    def testDownloadArtifact(self):
+        """Test DownloadArtifact."""
+        # Create mocks.
+        mock_file = mock.MagicMock()
+        mock_file_io = mock.MagicMock()
+        mock_file_io.__enter__.return_value = mock_file
+        mock_downloader = mock.MagicMock()
+        mock_downloader.next_chunk = mock.MagicMock(
+            side_effect=[(mock.MagicMock(), False), (mock.MagicMock(), True)])
+        mock_api = mock.MagicMock()
+        self.Patch(io, "FileIO", return_value=mock_file_io)
+        self.Patch(
+            apiclient.http,
+            "MediaIoBaseDownload",
+            return_value=mock_downloader)
+        mock_resource = mock.MagicMock()
+        self.client._service.buildartifact = mock.MagicMock(
+            return_value=mock_resource)
+        mock_resource.get_media = mock.MagicMock(return_value=mock_api)
+        # Make the call to the api
+        self.client.DownloadArtifact(self.BUILD_TARGET, self.BUILD_ID,
+                                     self.RESOURCE_ID, self.LOCAL_DEST)
+        # Verify
+        mock_resource.get_media.assert_called_with(
+            buildId=self.BUILD_ID,
+            target=self.BUILD_TARGET,
+            attemptId="0",
+            resourceId=self.RESOURCE_ID)
+        io.FileIO.assert_called_with(self.LOCAL_DEST, mode="wb")
+        mock_call = mock.call(
+            mock_file,
+            mock_api,
+            chunksize=android_build_client.AndroidBuildClient.
+            DEFAULT_CHUNK_SIZE)
+        apiclient.http.MediaIoBaseDownload.assert_has_calls([mock_call])
+        self.assertEqual(mock_downloader.next_chunk.call_count, 2)
+
+    def testDownloadArtifactOSError(self):
+        """Test DownloadArtifact when OSError is raised."""
+        self.Patch(io, "FileIO", side_effect=OSError("fake OSError"))
+        self.assertRaises(errors.DriverError, self.client.DownloadArtifact,
+                          self.BUILD_TARGET, self.BUILD_ID, self.RESOURCE_ID,
+                          self.LOCAL_DEST)
+
+    def testCopyTo(self):
+        """Test CopyTo."""
+        mock_resource = mock.MagicMock()
+        self.client._service.buildartifact = mock.MagicMock(
+            return_value=mock_resource)
+        self.client.CopyTo(
+            build_target=self.BUILD_TARGET,
+            build_id=self.BUILD_ID,
+            artifact_name=self.RESOURCE_ID,
+            destination_bucket=self.DESTINATION_BUCKET,
+            destination_path=self.RESOURCE_ID)
+        mock_resource.copyTo.assert_called_once_with(
+            buildId=self.BUILD_ID,
+            target=self.BUILD_TARGET,
+            attemptId=self.client.DEFAULT_ATTEMPT_ID,
+            artifactName=self.RESOURCE_ID,
+            destinationBucket=self.DESTINATION_BUCKET,
+            destinationPath=self.RESOURCE_ID)
+
+    def testCopyToWithRetry(self):
+        """Test CopyTo with retry."""
+        self.Patch(time, "sleep")
+        mock_resource = mock.MagicMock()
+        mock_api_request = mock.MagicMock()
+        mock_resource.copyTo.return_value = mock_api_request
+        self.client._service.buildartifact.return_value = mock_resource
+        mock_api_request.execute.side_effect = errors.HttpError(503,
+                                                                "fake error")
+        self.assertRaises(
+            errors.HttpError,
+            self.client.CopyTo,
+            build_id=self.BUILD_ID,
+            build_target=self.BUILD_TARGET,
+            artifact_name=self.RESOURCE_ID,
+            destination_bucket=self.DESTINATION_BUCKET,
+            destination_path=self.RESOURCE_ID)
+        self.assertEqual(mock_api_request.execute.call_count, 6)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/internal/lib/android_compute_client.py b/internal/lib/android_compute_client.py
index 0aef253..a3167fd 100755
--- a/internal/lib/android_compute_client.py
+++ b/internal/lib/android_compute_client.py
@@ -51,12 +51,15 @@
 class AndroidComputeClient(gcompute_client.ComputeClient):
     """Client that manages Anadroid Virtual Device."""
 
-    INSTANCE_NAME_FMT = "{build_target}-{build_id}-{uuid}"
-    IMAGE_NAME_FMT = "image-{build_target}-{build_id}-{uuid}"
-    DATA_DISK_NAME_FMT = "{instance}-data"
+    INSTANCE_NAME_FMT = "ins-{uuid}-{build_id}-{build_target}"
+    IMAGE_NAME_FMT = "img-{uuid}-{build_id}-{build_target}"
+    DATA_DISK_NAME_FMT = "data-{instance}"
     BOOT_COMPLETED_MSG = "VIRTUAL_DEVICE_BOOT_COMPLETED"
     BOOT_TIMEOUT_SECS = 5 * 60  # 5 mins, usually it should take ~2 mins
     BOOT_CHECK_INTERVAL_SECS = 10
+    NAME_LENGTH_LIMIT = 63
+    # If the generated name ends with '-', replace it with REPLACER.
+    REPLACER = "e"
 
     def __init__(self, acloud_config, oauth2_credentials):
         """Initialize.
@@ -75,6 +78,28 @@
         self._resolution = acloud_config.resolution
         self._metadata = acloud_config.metadata_variable.copy()
 
+    @classmethod
+    def _FormalizeName(cls, name):
+        """Formalize the name to comply with RFC1035.
+
+        The name must be 1-63 characters long and match the regular expression
+        [a-z]([-a-z0-9]*[a-z0-9])? which means the first character must be a
+        lowercase letter, and all following characters must be a dash,
+        lowercase letter, or digit, except the last character, which cannot be
+        a dash.
+
+        Args:
+          name: A string.
+
+        Returns:
+          name: A string that complies with RFC1035.
+        """
+        name = name.replace("_", "-").lower()
+        name = name[:cls.NAME_LENGTH_LIMIT]
+        if name[-1] == "-":
+          name = name[:-1] + cls.REPLACER
+        return name
+
     def _CheckMachineSize(self):
         """Check machine size.
 
@@ -107,7 +132,7 @@
         name = cls.IMAGE_NAME_FMT.format(build_target=build_target,
                                          build_id=build_id,
                                          uuid=uuid.uuid4().hex[:8])
-        return name.replace("_", "-").lower()
+        return cls._FormalizeName(name)
 
     @classmethod
     def GetDataDiskName(cls, instance):
@@ -119,7 +144,8 @@
         Returns:
             The corresponding data disk name.
         """
-        return cls.DATA_DISK_NAME_FMT.format(instance=instance)
+        name = cls.DATA_DISK_NAME_FMT.format(instance=instance)
+        return cls._FormalizeName(name)
 
     @classmethod
     def GenerateInstanceName(cls, build_target=None, build_id=None):
@@ -136,10 +162,11 @@
         """
         if not build_target and not build_id:
             return "instance-" + uuid.uuid4().hex
-        return cls.INSTANCE_NAME_FMT.format(
+        name = cls.INSTANCE_NAME_FMT.format(
             build_target=build_target,
             build_id=build_id,
             uuid=uuid.uuid4().hex[:8]).replace("_", "-").lower()
+        return cls._FormalizeName(name)
 
     def CreateDisk(self, disk_name, source_image, size_gb):
         """Create a gce disk.
diff --git a/internal/lib/base_cloud_client.py b/internal/lib/base_cloud_client.py
index 5566793..f876118 100755
--- a/internal/lib/base_cloud_client.py
+++ b/internal/lib/base_cloud_client.py
@@ -24,8 +24,6 @@
 import socket
 import ssl
 
-import google3
-
 from apiclient import errors as gerrors
 from apiclient.discovery import build
 import apiclient.http
diff --git a/internal/lib/gcompute_client.py b/internal/lib/gcompute_client.py
index 76e3bf9..8cd32b3 100755
--- a/internal/lib/gcompute_client.py
+++ b/internal/lib/gcompute_client.py
@@ -30,8 +30,6 @@
 import logging
 import os
 
-import google3
-
 from acloud.internal.lib import base_cloud_client
 from acloud.internal.lib import utils
 from acloud.public import errors
diff --git a/internal/lib/gstorage_client.py b/internal/lib/gstorage_client.py
index 2dbaaf6..686d8af 100755
--- a/internal/lib/gstorage_client.py
+++ b/internal/lib/gstorage_client.py
@@ -20,11 +20,10 @@
 import logging
 import os
 
-import google3
-
 import apiclient
 
 from acloud.internal.lib import base_cloud_client
+from acloud.internal.lib import utils
 from acloud.public import errors
 
 logger = logging.getLogger(__name__)
diff --git a/internal/proto/internal_config_pb2.py b/internal/proto/internal_config_pb2.py
new file mode 100644
index 0000000..1fcb955
--- /dev/null
+++ b/internal/proto/internal_config_pb2.py
@@ -0,0 +1,466 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: internal_config.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='internal_config.proto',
+  package='acloud.internal.proto',
+  syntax='proto2',
+  serialized_pb=_b('\n\x15internal_config.proto\x12\x15\x61\x63loud.internal.proto\"\xef\x01\n\x11\x44\x65\x66\x61ultUserConfig\x12\x14\n\x0cmachine_type\x18\x01 \x01(\t\x12\x0f\n\x07network\x18\x02 \x01(\t\x12\x1f\n\x17\x65xtra_data_disk_size_gb\x18\x03 \x01(\x05\x12Y\n\x11metadata_variable\x18\x04 \x03(\x0b\x32>.acloud.internal.proto.DefaultUserConfig.MetadataVariableEntry\x1a\x37\n\x15MetadataVariableEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\xee\x07\n\x0eInternalConfig\x12\x41\n\x0f\x64\x65\x66\x61ult_usr_cfg\x18\x01 \x01(\x0b\x32(.acloud.internal.proto.DefaultUserConfig\x12]\n\x15\x64\x65vice_resolution_map\x18\x02 \x03(\x0b\x32>.acloud.internal.proto.InternalConfig.DeviceResolutionMapEntry\x12n\n\x1e\x64\x65vice_default_orientation_map\x18\x03 \x03(\x0b\x32\x46.acloud.internal.proto.InternalConfig.DeviceDefaultOrientationMapEntry\x12\x18\n\x10min_machine_size\x18\x04 \x01(\t\x12\x17\n\x0f\x64isk_image_name\x18\x05 \x01(\t\x12\x1c\n\x14\x64isk_image_mime_type\x18\x06 \x01(\t\x12\x1c\n\x14\x64isk_image_extension\x18\x07 \x01(\t\x12\x1b\n\x13\x64isk_raw_image_name\x18\x08 \x01(\t\x12 \n\x18\x64isk_raw_image_extension\x18\t \x01(\t\x12&\n\x1e\x64\x65\x66\x61ult_extra_data_disk_device\x18\n \x01(\t\x12]\n\x15precreated_data_image\x18\x0b \x03(\x0b\x32>.acloud.internal.proto.InternalConfig.PrecreatedDataImageEntry\x12j\n\x1dvalid_branch_and_min_build_id\x18\x0c \x03(\x0b\x32\x43.acloud.internal.proto.InternalConfig.ValidBranchAndMinBuildIdEntry\x12\x18\n\x10\x63reds_cache_file\x18\r \x01(\t\x12\x12\n\nuser_agent\x18\x0e \x01(\t\x1a:\n\x18\x44\x65viceResolutionMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a\x42\n DeviceDefaultOrientationMapEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a:\n\x18PrecreatedDataImageEntry\x12\x0b\n\x03key\x18\x01 \x01(\x05\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x1a?\n\x1dValidBranchAndMinBuildIdEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x05:\x02\x38\x01')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_DEFAULTUSERCONFIG_METADATAVARIABLEENTRY = _descriptor.Descriptor(
+  name='MetadataVariableEntry',
+  full_name='acloud.internal.proto.DefaultUserConfig.MetadataVariableEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='acloud.internal.proto.DefaultUserConfig.MetadataVariableEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='acloud.internal.proto.DefaultUserConfig.MetadataVariableEntry.value', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=233,
+  serialized_end=288,
+)
+
+_DEFAULTUSERCONFIG = _descriptor.Descriptor(
+  name='DefaultUserConfig',
+  full_name='acloud.internal.proto.DefaultUserConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='machine_type', full_name='acloud.internal.proto.DefaultUserConfig.machine_type', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='network', full_name='acloud.internal.proto.DefaultUserConfig.network', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='extra_data_disk_size_gb', full_name='acloud.internal.proto.DefaultUserConfig.extra_data_disk_size_gb', index=2,
+      number=3, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='metadata_variable', full_name='acloud.internal.proto.DefaultUserConfig.metadata_variable', index=3,
+      number=4, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_DEFAULTUSERCONFIG_METADATAVARIABLEENTRY, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=49,
+  serialized_end=288,
+)
+
+
+_INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY = _descriptor.Descriptor(
+  name='DeviceResolutionMapEntry',
+  full_name='acloud.internal.proto.InternalConfig.DeviceResolutionMapEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='acloud.internal.proto.InternalConfig.DeviceResolutionMapEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='acloud.internal.proto.InternalConfig.DeviceResolutionMapEntry.value', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1046,
+  serialized_end=1104,
+)
+
+_INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY = _descriptor.Descriptor(
+  name='DeviceDefaultOrientationMapEntry',
+  full_name='acloud.internal.proto.InternalConfig.DeviceDefaultOrientationMapEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='acloud.internal.proto.InternalConfig.DeviceDefaultOrientationMapEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='acloud.internal.proto.InternalConfig.DeviceDefaultOrientationMapEntry.value', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1106,
+  serialized_end=1172,
+)
+
+_INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY = _descriptor.Descriptor(
+  name='PrecreatedDataImageEntry',
+  full_name='acloud.internal.proto.InternalConfig.PrecreatedDataImageEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='acloud.internal.proto.InternalConfig.PrecreatedDataImageEntry.key', index=0,
+      number=1, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='acloud.internal.proto.InternalConfig.PrecreatedDataImageEntry.value', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1174,
+  serialized_end=1232,
+)
+
+_INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY = _descriptor.Descriptor(
+  name='ValidBranchAndMinBuildIdEntry',
+  full_name='acloud.internal.proto.InternalConfig.ValidBranchAndMinBuildIdEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='acloud.internal.proto.InternalConfig.ValidBranchAndMinBuildIdEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='acloud.internal.proto.InternalConfig.ValidBranchAndMinBuildIdEntry.value', index=1,
+      number=2, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=1234,
+  serialized_end=1297,
+)
+
+_INTERNALCONFIG = _descriptor.Descriptor(
+  name='InternalConfig',
+  full_name='acloud.internal.proto.InternalConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='default_usr_cfg', full_name='acloud.internal.proto.InternalConfig.default_usr_cfg', index=0,
+      number=1, type=11, cpp_type=10, label=1,
+      has_default_value=False, default_value=None,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='device_resolution_map', full_name='acloud.internal.proto.InternalConfig.device_resolution_map', index=1,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='device_default_orientation_map', full_name='acloud.internal.proto.InternalConfig.device_default_orientation_map', index=2,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='min_machine_size', full_name='acloud.internal.proto.InternalConfig.min_machine_size', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='disk_image_name', full_name='acloud.internal.proto.InternalConfig.disk_image_name', index=4,
+      number=5, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='disk_image_mime_type', full_name='acloud.internal.proto.InternalConfig.disk_image_mime_type', index=5,
+      number=6, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='disk_image_extension', full_name='acloud.internal.proto.InternalConfig.disk_image_extension', index=6,
+      number=7, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='disk_raw_image_name', full_name='acloud.internal.proto.InternalConfig.disk_raw_image_name', index=7,
+      number=8, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='disk_raw_image_extension', full_name='acloud.internal.proto.InternalConfig.disk_raw_image_extension', index=8,
+      number=9, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='default_extra_data_disk_device', full_name='acloud.internal.proto.InternalConfig.default_extra_data_disk_device', index=9,
+      number=10, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='precreated_data_image', full_name='acloud.internal.proto.InternalConfig.precreated_data_image', index=10,
+      number=11, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='valid_branch_and_min_build_id', full_name='acloud.internal.proto.InternalConfig.valid_branch_and_min_build_id', index=11,
+      number=12, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='creds_cache_file', full_name='acloud.internal.proto.InternalConfig.creds_cache_file', index=12,
+      number=13, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='user_agent', full_name='acloud.internal.proto.InternalConfig.user_agent', index=13,
+      number=14, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY, _INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY, _INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY, _INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=291,
+  serialized_end=1297,
+)
+
+_DEFAULTUSERCONFIG_METADATAVARIABLEENTRY.containing_type = _DEFAULTUSERCONFIG
+_DEFAULTUSERCONFIG.fields_by_name['metadata_variable'].message_type = _DEFAULTUSERCONFIG_METADATAVARIABLEENTRY
+_INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY.containing_type = _INTERNALCONFIG
+_INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY.containing_type = _INTERNALCONFIG
+_INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY.containing_type = _INTERNALCONFIG
+_INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY.containing_type = _INTERNALCONFIG
+_INTERNALCONFIG.fields_by_name['default_usr_cfg'].message_type = _DEFAULTUSERCONFIG
+_INTERNALCONFIG.fields_by_name['device_resolution_map'].message_type = _INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY
+_INTERNALCONFIG.fields_by_name['device_default_orientation_map'].message_type = _INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY
+_INTERNALCONFIG.fields_by_name['precreated_data_image'].message_type = _INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY
+_INTERNALCONFIG.fields_by_name['valid_branch_and_min_build_id'].message_type = _INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY
+DESCRIPTOR.message_types_by_name['DefaultUserConfig'] = _DEFAULTUSERCONFIG
+DESCRIPTOR.message_types_by_name['InternalConfig'] = _INTERNALCONFIG
+
+DefaultUserConfig = _reflection.GeneratedProtocolMessageType('DefaultUserConfig', (_message.Message,), dict(
+
+  MetadataVariableEntry = _reflection.GeneratedProtocolMessageType('MetadataVariableEntry', (_message.Message,), dict(
+    DESCRIPTOR = _DEFAULTUSERCONFIG_METADATAVARIABLEENTRY,
+    __module__ = 'internal_config_pb2'
+    # @@protoc_insertion_point(class_scope:acloud.internal.proto.DefaultUserConfig.MetadataVariableEntry)
+    ))
+  ,
+  DESCRIPTOR = _DEFAULTUSERCONFIG,
+  __module__ = 'internal_config_pb2'
+  # @@protoc_insertion_point(class_scope:acloud.internal.proto.DefaultUserConfig)
+  ))
+_sym_db.RegisterMessage(DefaultUserConfig)
+_sym_db.RegisterMessage(DefaultUserConfig.MetadataVariableEntry)
+
+InternalConfig = _reflection.GeneratedProtocolMessageType('InternalConfig', (_message.Message,), dict(
+
+  DeviceResolutionMapEntry = _reflection.GeneratedProtocolMessageType('DeviceResolutionMapEntry', (_message.Message,), dict(
+    DESCRIPTOR = _INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY,
+    __module__ = 'internal_config_pb2'
+    # @@protoc_insertion_point(class_scope:acloud.internal.proto.InternalConfig.DeviceResolutionMapEntry)
+    ))
+  ,
+
+  DeviceDefaultOrientationMapEntry = _reflection.GeneratedProtocolMessageType('DeviceDefaultOrientationMapEntry', (_message.Message,), dict(
+    DESCRIPTOR = _INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY,
+    __module__ = 'internal_config_pb2'
+    # @@protoc_insertion_point(class_scope:acloud.internal.proto.InternalConfig.DeviceDefaultOrientationMapEntry)
+    ))
+  ,
+
+  PrecreatedDataImageEntry = _reflection.GeneratedProtocolMessageType('PrecreatedDataImageEntry', (_message.Message,), dict(
+    DESCRIPTOR = _INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY,
+    __module__ = 'internal_config_pb2'
+    # @@protoc_insertion_point(class_scope:acloud.internal.proto.InternalConfig.PrecreatedDataImageEntry)
+    ))
+  ,
+
+  ValidBranchAndMinBuildIdEntry = _reflection.GeneratedProtocolMessageType('ValidBranchAndMinBuildIdEntry', (_message.Message,), dict(
+    DESCRIPTOR = _INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY,
+    __module__ = 'internal_config_pb2'
+    # @@protoc_insertion_point(class_scope:acloud.internal.proto.InternalConfig.ValidBranchAndMinBuildIdEntry)
+    ))
+  ,
+  DESCRIPTOR = _INTERNALCONFIG,
+  __module__ = 'internal_config_pb2'
+  # @@protoc_insertion_point(class_scope:acloud.internal.proto.InternalConfig)
+  ))
+_sym_db.RegisterMessage(InternalConfig)
+_sym_db.RegisterMessage(InternalConfig.DeviceResolutionMapEntry)
+_sym_db.RegisterMessage(InternalConfig.DeviceDefaultOrientationMapEntry)
+_sym_db.RegisterMessage(InternalConfig.PrecreatedDataImageEntry)
+_sym_db.RegisterMessage(InternalConfig.ValidBranchAndMinBuildIdEntry)
+
+
+_DEFAULTUSERCONFIG_METADATAVARIABLEENTRY.has_options = True
+_DEFAULTUSERCONFIG_METADATAVARIABLEENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY.has_options = True
+_INTERNALCONFIG_DEVICERESOLUTIONMAPENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY.has_options = True
+_INTERNALCONFIG_DEVICEDEFAULTORIENTATIONMAPENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY.has_options = True
+_INTERNALCONFIG_PRECREATEDDATAIMAGEENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+_INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY.has_options = True
+_INTERNALCONFIG_VALIDBRANCHANDMINBUILDIDENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+# @@protoc_insertion_point(module_scope)
diff --git a/internal/proto/user_config_pb2.py b/internal/proto/user_config_pb2.py
new file mode 100644
index 0000000..1e69cf3
--- /dev/null
+++ b/internal/proto/user_config_pb2.py
@@ -0,0 +1,209 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: user_config.proto
+
+import sys
+_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='user_config.proto',
+  package='acloud.internal.proto',
+  syntax='proto2',
+  serialized_pb=_b('\n\x11user_config.proto\x12\x15\x61\x63loud.internal.proto\"\xd6\x03\n\nUserConfig\x12\x1c\n\x14service_account_name\x18\x01 \x01(\t\x12(\n service_account_private_key_path\x18\x02 \x01(\t\x12\x0f\n\x07project\x18\x03 \x01(\t\x12\x0c\n\x04zone\x18\x04 \x01(\t\x12\x14\n\x0cmachine_type\x18\x05 \x01(\t\x12\x0f\n\x07network\x18\x06 \x01(\t\x12\x1c\n\x14ssh_private_key_path\x18\x07 \x01(\t\x12\x1b\n\x13storage_bucket_name\x18\x08 \x01(\t\x12\x13\n\x0borientation\x18\t \x01(\t\x12\x12\n\nresolution\x18\n \x01(\t\x12\x1f\n\x17\x65xtra_data_disk_size_gb\x18\x0b \x01(\x05\x12R\n\x11metadata_variable\x18\x0c \x03(\x0b\x32\x37.acloud.internal.proto.UserConfig.MetadataVariableEntry\x12\x11\n\tclient_id\x18\r \x01(\t\x12\x15\n\rclient_secret\x18\x0e \x01(\t\x1a\x37\n\x15MetadataVariableEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01')
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+
+_USERCONFIG_METADATAVARIABLEENTRY = _descriptor.Descriptor(
+  name='MetadataVariableEntry',
+  full_name='acloud.internal.proto.UserConfig.MetadataVariableEntry',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='key', full_name='acloud.internal.proto.UserConfig.MetadataVariableEntry.key', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='value', full_name='acloud.internal.proto.UserConfig.MetadataVariableEntry.value', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001')),
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=460,
+  serialized_end=515,
+)
+
+_USERCONFIG = _descriptor.Descriptor(
+  name='UserConfig',
+  full_name='acloud.internal.proto.UserConfig',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='service_account_name', full_name='acloud.internal.proto.UserConfig.service_account_name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='service_account_private_key_path', full_name='acloud.internal.proto.UserConfig.service_account_private_key_path', index=1,
+      number=2, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='project', full_name='acloud.internal.proto.UserConfig.project', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='zone', full_name='acloud.internal.proto.UserConfig.zone', index=3,
+      number=4, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='machine_type', full_name='acloud.internal.proto.UserConfig.machine_type', index=4,
+      number=5, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='network', full_name='acloud.internal.proto.UserConfig.network', index=5,
+      number=6, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='ssh_private_key_path', full_name='acloud.internal.proto.UserConfig.ssh_private_key_path', index=6,
+      number=7, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='storage_bucket_name', full_name='acloud.internal.proto.UserConfig.storage_bucket_name', index=7,
+      number=8, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='orientation', full_name='acloud.internal.proto.UserConfig.orientation', index=8,
+      number=9, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='resolution', full_name='acloud.internal.proto.UserConfig.resolution', index=9,
+      number=10, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='extra_data_disk_size_gb', full_name='acloud.internal.proto.UserConfig.extra_data_disk_size_gb', index=10,
+      number=11, type=5, cpp_type=1, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='metadata_variable', full_name='acloud.internal.proto.UserConfig.metadata_variable', index=11,
+      number=12, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='client_id', full_name='acloud.internal.proto.UserConfig.client_id', index=12,
+      number=13, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='client_secret', full_name='acloud.internal.proto.UserConfig.client_secret', index=13,
+      number=14, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=_b("").decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_USERCONFIG_METADATAVARIABLEENTRY, ],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=45,
+  serialized_end=515,
+)
+
+_USERCONFIG_METADATAVARIABLEENTRY.containing_type = _USERCONFIG
+_USERCONFIG.fields_by_name['metadata_variable'].message_type = _USERCONFIG_METADATAVARIABLEENTRY
+DESCRIPTOR.message_types_by_name['UserConfig'] = _USERCONFIG
+
+UserConfig = _reflection.GeneratedProtocolMessageType('UserConfig', (_message.Message,), dict(
+
+  MetadataVariableEntry = _reflection.GeneratedProtocolMessageType('MetadataVariableEntry', (_message.Message,), dict(
+    DESCRIPTOR = _USERCONFIG_METADATAVARIABLEENTRY,
+    __module__ = 'user_config_pb2'
+    # @@protoc_insertion_point(class_scope:acloud.internal.proto.UserConfig.MetadataVariableEntry)
+    ))
+  ,
+  DESCRIPTOR = _USERCONFIG,
+  __module__ = 'user_config_pb2'
+  # @@protoc_insertion_point(class_scope:acloud.internal.proto.UserConfig)
+  ))
+_sym_db.RegisterMessage(UserConfig)
+_sym_db.RegisterMessage(UserConfig.MetadataVariableEntry)
+
+
+_USERCONFIG_METADATAVARIABLEENTRY.has_options = True
+_USERCONFIG_METADATAVARIABLEENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\001'))
+# @@protoc_insertion_point(module_scope)
diff --git a/public/acloud.py b/public/acloud_main.py
similarity index 98%
rename from public/acloud.py
rename to public/acloud_main.py
index 5badcef..bd89929 100755
--- a/public/acloud.py
+++ b/public/acloud_main.py
@@ -74,7 +74,7 @@
 from acloud.public import errors
 
 LOGGING_FMT = "%(asctime)s |%(levelname)s| %(module)s:%(lineno)s| %(message)s"
-LOGGER_NAME = "google3.cloud.android.driver"
+LOGGER_NAME = "acloud_main"
 
 # Commands
 CMD_CREATE = "create"
@@ -349,3 +349,7 @@
         sys.stderr.write("Encountered the following errors:\n%s\n" % msg)
         return 1
     return 0
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
diff --git a/public/data/default.config b/public/data/default.config
index 8c6e8fb..88ee324 100644
--- a/public/data/default.config
+++ b/public/data/default.config
@@ -35,7 +35,7 @@
 
   metadata_variable {
     key: "cfg_sta_persistent_data_device"
-    value: "/dev/block/sda2"
+    value: "default"
   }
 
   metadata_variable {
diff --git a/public/device_driver.py b/public/device_driver.py
index b0fb139..d0bdf35 100755
--- a/public/device_driver.py
+++ b/public/device_driver.py
@@ -31,8 +31,6 @@
 import logging
 import os
 
-import google3
-
 import dateutil.parser
 import dateutil.tz
 
@@ -348,9 +346,8 @@
         cleanup: boolean, if True clean up compute engine image and
                  disk image in storage after creating the instance.
         serial_log_file: A path to a file where serial output should
-                         be saved to. Logs will be fetch only on boot failure.
+                         be saved to.
         logcat_file: A path to a file where logcat logs should be saved.
-                     Logs will be fetch only on boot failure.
 
     Returns:
         A Report instance.
@@ -388,15 +385,17 @@
 
         # Dump serial and logcat logs.
         if serial_log_file:
-            _FetchSerialLogsFromDevices(compute_client,
-                                        instance_names=failures.keys(),
-                                        port=constants.DEFAULT_SERIAL_PORT,
-                                        output_file=serial_log_file)
+            _FetchSerialLogsFromDevices(
+                compute_client,
+                instance_names=[d.instance_name for d in device_pool.devices],
+                port=constants.DEFAULT_SERIAL_PORT,
+                output_file=serial_log_file)
         if logcat_file:
-            _FetchSerialLogsFromDevices(compute_client,
-                                        instance_names=failures.keys(),
-                                        port=constants.LOGCAT_SERIAL_PORT,
-                                        output_file=logcat_file)
+            _FetchSerialLogsFromDevices(
+                compute_client,
+                instance_names=[d.instance_name for d in device_pool.devices],
+                port=constants.LOGCAT_SERIAL_PORT,
+                output_file=logcat_file)
     except errors.DriverError as e:
         r.AddError(str(e))
         r.SetStatus(report.Status.FAIL)
diff --git a/regen_proto.sh b/regen_proto.sh
new file mode 100644
index 0000000..9a38eab
--- /dev/null
+++ b/regen_proto.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+## Compiles proto files to .py files
+## Note that proto version 3.0.0 is needed for successful compilation.
+protoc -I=internal/proto --python_out=internal/proto internal/proto/internal_config.proto
+protoc -I=internal/proto --python_out=internal/proto internal/proto/user_config.proto