Add docker info to collect_logs.
BUG=1112422
TESTING=Manual testing. Unit testing.
Change-Id: I1621f9e788601bbd79d10a12fa599fa1b06169bf
diff --git a/src/moblab_common/feedback_connector.py b/src/moblab_common/feedback_connector.py
index 0dc0bd5..501b9f6 100644
--- a/src/moblab_common/feedback_connector.py
+++ b/src/moblab_common/feedback_connector.py
@@ -4,15 +4,16 @@
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import base64
+import logging
import urllib.parse
-from moblab_common import moblab_info
-
from datetime import datetime
# pylint: disable=no-name-in-module, import-error
from google.cloud import storage
-import logging
+from moblab_common import moblab_info
+from moblab_common.utils.cli import CopyFile
+
class FeedbackPath():
FEEDBACK = 'feedback'
@@ -30,13 +31,15 @@
# TODO: Feedback will eventually be extended to include additional data beyond image and feedback text.
raise NotImplementedError
- def upload_feedback(self, contact_email=None, feedback=None, screenshot=None, filenames=[]):
+ def upload_feedback(self, contact_email=None, feedback=None, screenshot=None, files=[]):
"""Uploads image and feedback text to GCS.
Args:
contact_email: string email by which feedback submitter can be contacted.
feedback: Text string of feedback.
screenshot: Base 64 byte string representing a png screenshot image.
+ files: A list of CopyFile tuples (src, name) where src is the location of the
+ file on the local machine, and name is the desired name to upload as.
Returns:
A tuple (path, url) where path is the directory of the uploaded info, and
@@ -54,9 +57,9 @@
feedback_blob = storage.Blob(feedback_dir_path + '/' + FeedbackPath.FEEDBACK, bucket)
feedback_blob.upload_from_string("Contact email: " + contact_email + "\n" + feedback)
- for filename in filenames:
- filename_blob = storage.Blob(feedback_dir_path + filename, bucket)
- filename_blob.upload_from_filename(filename)
+ for path, filename in files:
+ filename_blob = storage.Blob(feedback_dir_path + '/' + filename, bucket)
+ filename_blob.upload_from_filename(path)
url = FeedbackPath.GCS_URL + '/' +\
self.moblab_bucket_name + '/' +\
diff --git a/src/moblab_common/feedback_connector_unittest.py b/src/moblab_common/feedback_connector_unittest.py
index 452a204..7684898 100644
--- a/src/moblab_common/feedback_connector_unittest.py
+++ b/src/moblab_common/feedback_connector_unittest.py
@@ -32,7 +32,11 @@
mock_moblab_info.get_serial_number.return_value = MOCK_MOBLAB_SERIAL_NO
mock_datetime.utcnow.return_value = '2020-01-01 11:11:11'
- screenshot_path, screenshot_url = self.feedback_connector.upload_feedback()
+ screenshot_path, screenshot_url = self.feedback_connector.upload_feedback(
+ contact_email='fake@email.com',
+ feedback='Feedback feedback feedback.',
+ files=[('/mock/path/to/tar', 'files.tgz')]
+ )
assert(screenshot_path == 'feedback/4HBWUSIE15646458/2020-01-01 11:11:11')
assert(screenshot_url ==
'https://pantheon.corp.google.com/storage/browser/_details/moblab-mock-bucket/' +
diff --git a/src/moblab_common/monitoring/collect_logs.py b/src/moblab_common/monitoring/collect_logs.py
index e850698..3255fcd 100644
--- a/src/moblab_common/monitoring/collect_logs.py
+++ b/src/moblab_common/monitoring/collect_logs.py
@@ -5,10 +5,13 @@
"""Simple log collection script for Mob* Monitor"""
+import docker
import glob
+import json
import os
-import tempfile
+import re
import shutil
+import tempfile
from moblab_common import afe_connector
from moblab_common import config_connector
@@ -26,44 +29,108 @@
'moblab_id': '/home/moblab/.moblab_id',
}
+DOCKER_DIR = 'docker'
+INSPECT_PREFIX = 'inspect_'
+DOCKER_PS_DETAILED = 'docker_ps_detailed'
+DOCKER_PS_SUCCINCT = 'docker_ps_succinct'
+DOCKER_SUCCINCT_KEYS = ["Command", "Created", "Id", "Image", "ImageID", "Names", "State", "Status"]
+
+
def remove_old_tarballs():
+ """
+ Removes all files in the tmp dir where all log tarballs are created. This is a
+ backup, calls to collect_logs should clean up after themselves.
+ """
paths = glob.iglob(os.path.join(TMPDIR, '%s*.tgz' % TMPDIR_PREFIX))
for path in paths:
os.remove(path)
+def _write_to_file(filename, content):
+ """
+ :param filename: complete path of file to write content to.
+ :param content: String content to be written to disk.
+ :return: None
+ """
+ with open(filename, 'w+') as f:
+ f.write(content)
+
+def _filtered_docker_container_info(docker_containers_info):
+ """
+ :param docker_containers_info: A list of dicts, each dict container a container's
+ info.
+ :return: A list of dicts, like the input, only with each dict filtered for target
+ keys.
+ """
+ succinct_containers_info = []
+ for container in docker_containers_info:
+ succinct_containers_info.append({ key: container.get(key, None) for key in DOCKER_SUCCINCT_KEYS})
+ return succinct_containers_info
+
+
+def _get_docker_container_details(write_dir):
+ """
+ :param filename: complete path of file to write content to.
+ :param content: String content to be written to disk.
+ :return: None
+ """
+ docker_api_client = docker.APIClient()
+ docker_client = docker.from_env()
+
+ cli.safe_mkdir(os.path.join(write_dir, DOCKER_DIR))
+
+ detailed_container_info = docker_api_client.containers()
+ _write_to_file(
+ os.path.join(write_dir, DOCKER_DIR, DOCKER_PS_DETAILED + '.log'),
+ json.dumps(detailed_container_info, indent=4, sort_keys=True)
+ )
+
+ succinct_container_info = _filtered_docker_container_info(detailed_container_info)
+ _write_to_file(
+ os.path.join(write_dir, DOCKER_DIR, DOCKER_PS_SUCCINCT + '.log'),
+ json.dumps(succinct_container_info, indent=4, sort_keys=True)
+ )
+
+ for container in docker_client.containers.list():
+ _write_to_file(
+ os.path.join(write_dir, DOCKER_DIR, INSPECT_PREFIX + container.name + '.log'),
+ json.dumps(docker_api_client.inspect_container(container.id), indent=4, sort_keys=True)
+ )
def collect_logs():
+ """
+ Collects all available Moblab logs and docker configuration information.
+ :return: Path to tarball of collected logs and info.
+ """
remove_old_tarballs()
cli.safe_mkdir(TMPDIR)
- tempdir = tempfile.mkdtemp(prefix=TMPDIR_PREFIX, dir=TMPDIR)
- os.chmod(tempdir, 0o777)
- try:
- for name, path in LOG_DIRS.items():
- if not os.path.exists(path):
- continue
- if os.path.isdir(path):
- shutil.copytree(path, os.path.join(tempdir, name), ignore_dangling_symlinks=True)
- else:
- shutil.copyfile(path, os.path.join(tempdir, name), follow_symlinks=False)
+ with tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX, dir=TMPDIR) as tempdir:
+ os.chmod(tempdir, 0o777)
+ try:
+ for name, path in LOG_DIRS.items():
+ if not os.path.exists(path):
+ continue
+ if os.path.isdir(path):
+ shutil.copytree(path, os.path.join(tempdir, name), ignore_dangling_symlinks=True)
+ else:
+ shutil.copyfile(path, os.path.join(tempdir, name), follow_symlinks=False)
- status_file = os.path.join(tempdir, 'mobmonitor_getstatus')
- status = cli.run_command([
- 'curl', 'http://localhost:9991/GetStatus', '-o', status_file])
+ status_file = os.path.join(tempdir, 'mobmonitor_getstatus')
+ status = cli.run_command([
+ 'curl', 'http://localhost:9991/GetStatus', '-o', status_file])
- serialfile = os.path.join(tempdir, 'serialno')
- with open(serialfile, 'w+') as f:
- f.write(host_connector.HostServicesConnector().get_host_identifier())
+ serialfile = os.path.join(tempdir, 'serialno')
+ _write_to_file(serialfile, host_connector.HostServicesConnector().get_host_identifier())
- afe = afe_connector.AFEConnector()
- conf = config_connector.MoblabConfigConnector(afe)
- conf.load_config()
- configfile = os.path.join(tempdir, 'config')
- with open(configfile, 'w+') as f:
- f.write(str(conf.config))
+ afe = afe_connector.AFEConnector()
+ conf = config_connector.MoblabConfigConnector(afe)
+ conf.load_config()
+ configfile = os.path.join(tempdir, 'config')
+ _write_to_file(configfile, str(conf.config))
- finally:
- tarball = '%s.tgz' % tempdir
- cli.create_tarball(tarball, tempdir)
- cli.rm_dir(tempdir, ignore_missing=True)
+ _get_docker_container_details(tempdir)
+ finally:
+ tarball = '%s.tgz' % tempdir
+ cli.create_tarball(tarball, tempdir[len(TMPDIR + '/'):], TMPDIR)
+
return tarball
diff --git a/src/moblab_common/monitoring/collect_logs_unittest.py b/src/moblab_common/monitoring/collect_logs_unittest.py
new file mode 100644
index 0000000..74466d2
--- /dev/null
+++ b/src/moblab_common/monitoring/collect_logs_unittest.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Unit tests for the collect logs utils"""
+import copy
+import json
+import sys
+import unittest
+
+from unittest import mock
+from unittest.mock import MagicMock #, PropertyMock
+
+MOCKED_DOCKER = MagicMock()
+sys.modules['docker'] = MOCKED_DOCKER
+sys.modules['moblab_common'] = MagicMock()
+sys.modules['moblab_common.utils'] = MagicMock()
+sys.modules['shutil'] = MagicMock()
+
+from collect_logs import collect_logs
+
+
+_NUM_MOCK_CONTAINERS = 10
+_MOCK_MOBLAB_ID = 'mock_moblab_id'
+_MOCK_CONTAINER_NAME = 'mock_container'
+_MOCK_CONTAINER_SUCCINCT = {
+ "Command" : 'a',
+ "Created" : 'b',
+ "Id" : 'c',
+ "Image" : 'd',
+ "ImageID" : 'e',
+ "Names" : 'f',
+ "State" : 'g',
+ "Status": 'h'
+}
+_MOCK_CONTAINER_DETAILED = copy.deepcopy(_MOCK_CONTAINER_SUCCINCT)
+_MOCK_CONTAINER_DETAILED.update({"OtherAttribute" : 'i'})
+_MOCK_CONTAINERS_SUCCINCT = [_MOCK_CONTAINER_SUCCINCT for _ in range(_NUM_MOCK_CONTAINERS)]
+_MOCK_CONTAINERS_DETAILED = [_MOCK_CONTAINER_DETAILED for _ in range(_NUM_MOCK_CONTAINERS)]
+
+_MOCK_CONTAINER_INSPECT_DICT = {
+ 'attr1': 'value1',
+ 'attr2': 'value2'
+}
+
+_MOCK_TEMPDIR_PATH = '/tmp/moblab_logs_xxxxxxxx/'
+
+class CollectLogsTest(unittest.TestCase):
+ """Testing the collect logs code."""
+
+ def _create_mock_api_client(self):
+ mock_api_client = MagicMock()
+ mock_api_client.containers.return_value = _MOCK_CONTAINERS_DETAILED
+ mock_api_client.inspect_container.return_value = _MOCK_CONTAINER_INSPECT_DICT
+ return mock_api_client
+
+ def _create_mock_docker_client(self):
+ mock_docker_client = MagicMock()
+ mock_container = MagicMock()
+ mock_container.name = _MOCK_CONTAINER_NAME
+ mock_docker_client.containers.list.return_value = [mock_container for _ in range(_NUM_MOCK_CONTAINERS)]
+ return mock_docker_client
+
+ def _create_mock_host_connector(self):
+ mock_host_connector = MagicMock()
+ mock_host_connector.get_host_identifier.return_value = _MOCK_MOBLAB_ID
+ return mock_host_connector
+
+ @mock.patch("os.chmod")
+ @mock.patch("tempfile.TemporaryDirectory")
+ @mock.patch("collect_logs.cli")
+ @mock.patch("collect_logs._write_to_file")
+ @mock.patch("collect_logs.docker.from_env")
+ @mock.patch("collect_logs.docker.APIClient")
+ @mock.patch("collect_logs.host_connector.HostServicesConnector")
+ def test_collect_logs(self, mock_host_services_connector, mock_docker_api_client, mock_docker_from_env, mock_write_to_file, mock_cli, mock_temp_dir, mock_chmod):
+ mock_temp_dir.return_value.__enter__.return_value = _MOCK_TEMPDIR_PATH
+ mock_docker_api_client.return_value = self._create_mock_api_client()
+ mock_docker_from_env.return_value = self._create_mock_docker_client()
+ mock_host_services_connector.return_value = self._create_mock_host_connector()
+
+ collect_logs()
+
+ self.assertEqual(mock_write_to_file.call_count, 4 + _NUM_MOCK_CONTAINERS)
+ mock_write_to_file.assert_has_calls([
+ mock.call(_MOCK_TEMPDIR_PATH + 'docker/inspect_mock_container.log', json.dumps(_MOCK_CONTAINER_INSPECT_DICT, indent=4, sort_keys=True)),
+ mock.call(_MOCK_TEMPDIR_PATH + 'docker/docker_ps_detailed.log', json.dumps(_MOCK_CONTAINERS_DETAILED, indent=4, sort_keys=True)),
+ mock.call(_MOCK_TEMPDIR_PATH + 'docker/docker_ps_succinct.log', json.dumps(_MOCK_CONTAINERS_SUCCINCT, indent=4, sort_keys=True)),
+ mock.call(_MOCK_TEMPDIR_PATH + 'config', mock.ANY),
+ mock.call(_MOCK_TEMPDIR_PATH + 'serialno', _MOCK_MOBLAB_ID)
+ ], any_order=True)
+ mock_cli.create_tarball.assert_called_once()
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/moblab_common/utils/cli.py b/src/moblab_common/utils/cli.py
index 373a48d..e6c887c 100644
--- a/src/moblab_common/utils/cli.py
+++ b/src/moblab_common/utils/cli.py
@@ -15,6 +15,11 @@
LOGGER = logging.getLogger(__name__)
+# tuple for carrying holding arguments for a cp operation.
+# src: path to file to copy on local machine
+# name: filename of the copy
+CopyFile = collections.namedtuple('CopyFile', ['src', 'name'])
+
class RunCommandError(subprocess.CalledProcessError):
def __init__(self, returncode, cmd):
super(RunCommandError, self).__init__(returncode, cmd)
@@ -63,14 +68,20 @@
return run_command(cmd, log=True, error_code_ok=error_code_ok,
shell=shell)
-def create_tarball(tarball_name, directory):
+def create_tarball(tarball_name, directory, starting_directory=None):
"""Creates a gzipped tar archive
Args:
tarball_name: string name for the tarball
directory: directory to archive the contents of
+ starting_directory: context directory to find directory within (
+ useful for hiding undesired parent directory structures )
"""
- run_command(['tar', '-czf', tarball_name, directory])
+ args = ['-czf', tarball_name, directory]
+ if starting_directory:
+ args = ['-C', starting_directory] + args
+ args = ['tar'] + args
+ run_command(args)
def safe_mkdir(path):
"""Create a directory and any parent directories if they don't exist.
diff --git a/src/mobmonitor/mobmonitor.py b/src/mobmonitor/mobmonitor.py
index 8a10da8..72f0889 100644
--- a/src/mobmonitor/mobmonitor.py
+++ b/src/mobmonitor/mobmonitor.py
@@ -15,15 +15,14 @@
import argparse
from cherrypy.lib.static import serve_file
+
from checkfile import manager as cf_manager
-
from diagnostic_checks import manager as dc_manager
-
from moblab_common import afe_connector
-from moblab_common.monitoring import collect_logs
from moblab_common import config_connector
from moblab_common import feedback_connector
-
+from moblab_common.monitoring import collect_logs
+from moblab_common.utils.cli import CopyFile
STATICDIR = '/etc/moblab/mobmonitor/static/'
@@ -131,8 +130,11 @@
conf = config_connector.MoblabConfigConnector(afe)
conf.load_config()
conn = feedback_connector.MoblabFeedbackConnector(conf.get_cloud_bucket())
- tarfile = collect_logs.collect_logs()
- feedback_dir_path, url = conn.upload_feedback(filenames=[tarfile])
+ tarfile_path = collect_logs.collect_logs()
+ feedback_dir_path, url = conn.upload_feedback(
+ # upload tar as a *.tgz without exposing any of the Moblab dir structure
+ files=[CopyFile(src=tarfile_path, name=tarfile_path.split('/')[-1])]
+ )
return ("Your logs have been uploaded to your Google Cloud bucket "
"<a href=\"https://console.developers.google.com/storage/%s/%s\">Link to Logs</a>" % (conf.get_cloud_bucket(), feedback_dir_path))