summaryrefslogtreecommitdiff
path: root/gcimagebundle/gcimagebundlelib/imagebundle.py
diff options
context:
space:
mode:
Diffstat (limited to 'gcimagebundle/gcimagebundlelib/imagebundle.py')
-rwxr-xr-xgcimagebundle/gcimagebundlelib/imagebundle.py244
1 files changed, 244 insertions, 0 deletions
diff --git a/gcimagebundle/gcimagebundlelib/imagebundle.py b/gcimagebundle/gcimagebundlelib/imagebundle.py
new file mode 100755
index 0000000..b188e3b
--- /dev/null
+++ b/gcimagebundle/gcimagebundlelib/imagebundle.py
@@ -0,0 +1,244 @@
+# -*- coding: utf-8 -*-
+# Copyright 2013 Google Inc. All Rights Reserved.
+#
+# 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.
+
+
+"""Tool to bundle root filesystem to a tarball.
+
+Creates a tar bundle and a Manifest, which can be uploaded to image store.
+"""
+
+
+
+import logging
+from optparse import OptionParser
+import os
+import shutil
+import subprocess
+import tempfile
+
+from gcimagebundlelib import block_disk
+from gcimagebundlelib import exclude_spec
+from gcimagebundlelib import platform_factory
+from gcimagebundlelib import utils
+
+def SetupArgsParser():
+ """Sets up the command line flags."""
+ parser = OptionParser()
+ parser.add_option('-d', '--disk', dest='disk',
+ default='/dev/sda',
+ help='Disk to bundle.')
+ parser.add_option('-r', '--root', dest='root_directory',
+ default='/', metavar='ROOT',
+ help='Root of the file system to bundle.'
+ ' Recursively bundles all sub directories.')
+ parser.add_option('-e', '--excludes', dest='excludes',
+ help='Comma separated list of sub directories to exclude.'
+ ' The defaults are platform specific.')
+ parser.add_option('-o', '--output_directory', dest='output_directory',
+ default='/tmp/', metavar='DIR',
+ help='Output directory for image.')
+ parser.add_option('--output_file_name', dest='output_file_name',
+ default=None, metavar='FILENAME',
+ help=('Output filename for the image. Default is a digest'
+ ' of the image bytes.'))
+ parser.add_option('--include_mounts', dest='include_mounts',
+ help='Don\'t ignore mounted filesystems under ROOT.',
+ action='store_true', default=False)
+ parser.add_option('-v', '--version',
+ action='store_true', dest='display_version', default=False,
+ help='Print the tool version.')
+ parser.add_option('--loglevel', dest='log_level',
+ help='Debug logging level.', default='INFO',
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR' 'CRITICAL'])
+ parser.add_option('--log_file', dest='log_file',
+ help='Output file for log messages.')
+ parser.add_option('-k', '--key', dest='key', default='nebula',
+ help='Public key used for signing the image.')
+ parser.add_option('--nocleanup', dest='cleanup',
+ action='store_false', default=True,
+ help=' Do not clean up temporary and log files.')
+ #TODO(user): Get dehumanize.
+ parser.add_option('--fssize', dest='fs_size', default=10*1024*1024*1024,
+ type='int', help='File system size in bytes')
+ parser.add_option('-b', '--bucket', dest='bucket',
+ help='Destination storage bucket')
+ parser.add_option('-f', '--filesystem', dest='file_system',
+ default=None,
+ help='File system type for the image.')
+ return parser
+
+
+def VerifyArgs(parser, options):
+ """Verifies that commandline flags are consistent."""
+ if not options.output_directory:
+ parser.error('output bundle directory must be specified.')
+ if not os.path.exists(options.output_directory):
+ parser.error('output bundle directory does not exist.')
+
+ # TODO(user): add more verification as needed
+
+def EnsureSuperUser():
+ """Ensures that current user has super user privileges."""
+ if os.getuid() != 0:
+ logging.warning('Tool must be run as root.')
+ exit(-1)
+
+
+def GetLogLevel(options):
+ """Log Level string to logging.LogLevel mapping."""
+ level = {
+ 'DEBUG': logging.DEBUG,
+ 'INFO': logging.INFO,
+ 'WARNING': logging.WARNING,
+ 'ERROR': logging.ERROR,
+ 'CRITICAL': logging.CRITICAL
+ }
+ if options.log_level in level:
+ return level[options.log_level]
+ print 'Invalid logging level. defaulting to INFO.'
+ return logging.INFO
+
+
+def SetupLogging(options, log_dir='/tmp'):
+ """Set up logging.
+
+ All messages above INFO level are also logged to console.
+
+ Args:
+ options: collection of command line options.
+ log_dir: directory used to generate log files.
+ """
+ if options.log_file:
+ logfile = options.log_file
+ else:
+ logfile = tempfile.mktemp(dir=log_dir, prefix='bundle_log_')
+ print 'Starting logging in %s' % logfile
+ logging.basicConfig(filename=logfile, level=GetLogLevel(options))
+ console = logging.StreamHandler()
+ console.setLevel(GetLogLevel(options))
+ logging.getLogger().addHandler(console)
+
+
+def PrintVersionInfo():
+ #TODO: Should read from the VERSION file instead.
+ logging.info('version 1.1.0')
+
+
+def GetTargetFilesystem(options, guest_platform):
+ if options.file_system:
+ return options.file_system
+ else:
+ return guest_platform.GetPreferredFilesystemType()
+
+
+def main():
+ parser = SetupArgsParser()
+ (options, _) = parser.parse_args()
+ if options.display_version:
+ PrintVersionInfo()
+ return 0
+ EnsureSuperUser()
+ VerifyArgs(parser, options)
+
+ scratch_dir = tempfile.mkdtemp(dir=options.output_directory)
+ SetupLogging(options, scratch_dir)
+ try:
+ guest_platform = platform_factory.PlatformFactory(
+ options.root_directory).GetPlatform()
+ except platform_factory.UnknownPlatformException:
+ logging.critical('Platform is not supported.'
+ ' Platform rules can be added to platform_factory.py.')
+ return -1
+
+ temp_file_name = tempfile.mktemp(dir=scratch_dir, suffix='.tar.gz')
+
+ file_system = GetTargetFilesystem(options, guest_platform)
+ logging.info('File System: %s', file_system)
+ logging.info('Disk Size: %s bytes', options.fs_size)
+ bundle = block_disk.RootFsRaw(options.fs_size, file_system)
+ bundle.SetTarfile(temp_file_name)
+ if options.disk:
+ readlink_command = ['readlink', '-f', options.disk]
+ final_path = utils.RunCommand(readlink_command).strip()
+ logging.info('Resolved %s to %s', options.disk, final_path)
+ bundle.AddDisk(final_path)
+ # TODO(user): Find the location where the first partition of the disk
+ # is mounted and add it as the source instead of relying on the source
+ # param flag
+ bundle.AddSource(options.root_directory)
+ bundle.SetKey(options.key)
+ bundle.SetScratchDirectory(scratch_dir)
+
+ # Merge platform specific exclude list, mounts points
+ # and user specified excludes
+ excludes = guest_platform.GetExcludeList()
+ if options.excludes:
+ excludes.extend([exclude_spec.ExcludeSpec(x) for x in
+ options.excludes.split(',')])
+ logging.info('exclude list: %s', ' '.join([x.GetSpec() for x in excludes]))
+ bundle.AppendExcludes(excludes)
+ if not options.include_mounts:
+ mount_points = utils.GetMounts(options.root_directory)
+ logging.info('ignoring mounts %s', ' '.join(mount_points))
+ bundle.AppendExcludes([exclude_spec.ExcludeSpec(x, preserve_dir=True) for x
+ in utils.GetMounts(options.root_directory)])
+ bundle.SetPlatform(guest_platform)
+
+ # Verify that bundle attributes are correct and create tar bundle.
+ bundle.Verify()
+ (fs_size, digest) = bundle.Bundleup()
+ if not digest:
+ logging.critical('Could not get digest for the bundle.'
+ ' The bundle may not be created correctly')
+ return -1
+ if fs_size > options.fs_size:
+ logging.critical('Size of tar %d exceeds the file system size %d.', fs_size,
+ options.fs_size)
+ return -1
+
+ if options.output_file_name:
+ output_file = os.path.join(
+ options.output_directory, options.output_file_name)
+ else:
+ output_file = os.path.join(
+ options.output_directory, '%s.image.tar.gz' % digest)
+
+ os.rename(temp_file_name, output_file)
+ logging.info('Created tar.gz file at %s' % output_file)
+
+ if options.bucket:
+ bucket = options.bucket
+ if bucket.startswith('gs://'):
+ output_bucket = '%s/%s' % (
+ bucket, os.path.basename(output_file))
+ else:
+ output_bucket = 'gs://%s/%s' % (
+ bucket, os.path.basename(output_file))
+ # TODO: Consider using boto library directly.
+ cmd = ['gsutil', 'cp', output_file, output_bucket]
+ retcode = subprocess.call(cmd)
+ if retcode != 0:
+ logging.critical('Failed to copy image to bucket. '
+ 'gsutil returned %d. To retry, run the command: %s',
+ retcode, ' '.join(cmd))
+
+ return -1
+ logging.info('Uploaded image to %s', output_bucket)
+
+ # If we've uploaded, then we can remove the local file.
+ os.remove(output_file)
+
+ if options.cleanup:
+ shutil.rmtree(scratch_dir)