[autotest] Serialize foreign key we don't want to follow

In some case, we don't want to follow a relationship when
serialize and de-serialize an object. However, we may
want to serialize the value of the foreign key value.

For example, we follow Host.hostattribute_set relationship to
serialize HostAttribute objects associated with a host. We
don't want to follow HostAttribute.host_id to go back to Host.

This CL allows a model to claim foreign keys that will be
serialized but will not be followed.

BUG=None
TEST=In python
>>> import common
>>> from autotest_lib.frontend import setup_django_environment
>>> from autotest_lib.frontend.afe import models
>>> models.HostAttribute.objects.filter(host_id=20)[0].serialize()
{'host_id': 20L, u'id': 203L, 'value': u'2271', 'attribute':
u'need_crash_log'}
>>> models.HostAttribute.deserialize({'host_id': 20L, u'id': 251L,
'value': u'2271', 'attribute': u'need_crash_log'})
<HostAttribute: HostAttribute object>
TEST=Test shard heartbeat works when host attributes present.
DEPLOY=apache, shard_client

Change-Id: I8e0fbb1b7eb037f0968c1599334446301ddbbcb0
Reviewed-on: https://chromium-review.googlesource.com/236708
Reviewed-by: Fang Deng <fdeng@chromium.org>
Tested-by: Fang Deng <fdeng@chromium.org>
Reviewed-by: Prashanth B <beeps@chromium.org>
Tested-by: Prashanth B <beeps@chromium.org>
Commit-Queue: Prashanth B <beeps@chromium.org>
Trybot-Ready: Prashanth B <beeps@chromium.org>
diff --git a/frontend/afe/model_logic.py b/frontend/afe/model_logic.py
index 5781646..a0923ba 100644
--- a/frontend/afe/model_logic.py
+++ b/frontend/afe/model_logic.py
@@ -2,8 +2,6 @@
 Extensions to Django's model logic.
 """
 
-import re
-import time
 import django.core.exceptions
 from django.db import backend
 from django.db import connection
@@ -531,6 +529,14 @@
     """
 
 
+    SERIALIZATION_LINKS_TO_KEEP = set()
+    """This set stores foreign keys which we don't want to follow, but
+    still want to include in the serialized dictionary. For
+    example, we follow the relationship `Host.hostattribute_set`,
+    but we do not want to follow `HostAttributes.host_id` back to
+    to Host, which would otherwise lead to a circle. However, we still
+    like to serialize HostAttribute.`host_id`."""
+
     SERIALIZATION_LOCAL_LINKS_TO_UPDATE = set()
     """
     On deserializion, if the object to persist already exists, local fields
@@ -938,6 +944,12 @@
         for field in self._meta.concrete_model._meta.local_fields:
             if field.rel is None:
                 serialized[field.name] = field._get_val_from_obj(self)
+            elif (include_dependencies and
+                  field.name in self.SERIALIZATION_LINKS_TO_KEEP):
+                # attname will contain "_id" suffix for foreign keys,
+                # e.g. HostAttribute.host will be serialized as 'host_id'.
+                # Use it for easy deserialization.
+                serialized[field.attname] = field._get_val_from_obj(self)
 
         if include_dependencies:
             for link in self.SERIALIZATION_LINKS_TO_FOLLOW:
@@ -984,7 +996,8 @@
                 # It's a foreign key
                 links_to_related_values.append((link, value))
             else:
-                # It's a local attribute
+                # It's a local attribute or a foreign key
+                # we don't want to follow.
                 links_to_local_values.append((link, value))
         return links_to_local_values, links_to_related_values