__init__.py 18.6 KB
Newer Older
Nikos Skalkotos's avatar
Nikos Skalkotos committed
1 2
# -*- coding: utf-8 -*-
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
3
# Copyright (C) 2011-2014 GRNET S.A.
4
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
5 6 7 8
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
9
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
10 11 12 13
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
14
#
Nikos Skalkotos's avatar
Nikos Skalkotos committed
15 16
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
Nikos Skalkotos's avatar
Nikos Skalkotos committed
17

Nikos Skalkotos's avatar
Nikos Skalkotos committed
18 19 20 21
"""This package provides various classes for preparing different Operating
Systems for image creation.
"""

22
from image_creator.util import FatalError
Nikos Skalkotos's avatar
Nikos Skalkotos committed
23

24
import textwrap
Nikos Skalkotos's avatar
Nikos Skalkotos committed
25
import re
26
from collections import namedtuple
27
from functools import wraps
Nikos Skalkotos's avatar
Nikos Skalkotos committed
28 29


30
def os_cls(distro, osfamily):
31 32 33 34 35 36 37 38 39 40
    """Given the distro name and the osfamily, return the appropriate OSBase
    derived class
    """

    # hyphens are not allowed in module names
    canonicalize = lambda x: x.replace('-', '_').lower()

    distro = canonicalize(distro)
    osfamily = canonicalize(osfamily)

41
    try:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
42 43
        module = __import__("image_creator.os_type.%s" % distro,
                            fromlist=['image_creator.os_type'])
44 45
        classname = distro.capitalize()
    except ImportError:
46 47 48 49 50 51
        try:
            module = __import__("image_creator.os_type.%s" % osfamily,
                                fromlist=['image_creator.os_type'])
            classname = osfamily.capitalize()
        except ImportError:
            raise FatalError("Unknown OS name: `%s'" % osfamily)
52 53 54 55

    return getattr(module, classname)


56
def add_prefix(target):
57
    """Decorator that adds a prefix to the result of a function"""
58 59
    def wrapper(self, *args):
        prefix = args[0]
60
        return [prefix + path for path in target(self, *args)]
61 62
    return wrapper

63

64
def sysprep(message, enabled=True, **kwargs):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
65
    """Decorator for system preparation tasks"""
66
    def wrapper(method):
67 68 69 70 71
        assert method.__name__.startswith('_'), \
            "Invalid sysprep name:` %s'. Should start with _" % method.__name__

        method._sysprep = True
        method._sysprep_enabled = enabled
Nikos Skalkotos's avatar
Nikos Skalkotos committed
72
        method._sysprep_nomount = False
73

74
        for key, val in kwargs.items():
75
            setattr(method, "_sysprep_%s" % key, val)
76

77
        @wraps(method)
78
        def inner(self, print_message=True):
79 80
            if print_message:
                self.out.output(message)
81
            return method(self)
82

83 84 85 86
        return inner
    return wrapper


87
class SysprepParam(object):
88
    """This class represents a system preparation parameter"""
89

90
    def __init__(self, type, default, description, **kargs):
91

92
        assert hasattr(self, "_check_%s" % type), "Invalid type: %s" % type
93 94 95 96 97 98

        self.type = type
        self.default = default
        self.description = description
        self.value = default
        self.error = None
99 100
        self.check = kargs['check'] if 'check' in kargs else lambda x: x
        self.hidden = kargs['hidden'] if 'hidden' in kargs else False
101 102

    def set_value(self, value):
103
        """Update the value of the parameter"""
104 105

        check_type = getattr(self, "_check_%s" % self.type)
106
        try:
107
            self.value = self.check(check_type(value))
108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
        except ValueError as e:
            self.error = e.message
            return False
        return True

    def _check_posint(self, value):
        """Check if the value is a positive integer"""
        try:
            value = int(value)
        except ValueError:
            raise ValueError("Invalid number")

        if value <= 0:
            raise ValueError("Value is negative or zero")

        return value

    def _check_string(self, value):
        """Check if a value is a string"""
        return str(value)

129
    def _check_file(self, value):
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148
        """Check if the value is a valid filename"""

        value = str(value)
        if len(value) == 0:
            return ""

        import os

        def isblockdev(filename):
            import stat
            try:
                return stat.S_ISBLK(os.stat(filename).st_mode)
            except OSError:
                return False
        if os.path.isfile(value) or isblockdev(value):
            return value

        raise ValueError("Invalid filename")

149
    def _check_dir(self, value):
150 151 152 153 154 155 156 157 158 159 160 161
        """Check if the value is a valid directory"""

        value = str(value)
        if len(value) == 0:
            return ""

        import os
        if os.path.isdir(value):
            return value

        raise ValueError("Invalid dirname")

162

163
def add_sysprep_param(name, type, default, descr, **kargs):
164
    """Decorator for __init__ that adds the definition for a system preparation
165
    parameter in an instance of an os_type class
166
    """
167 168
    extra = kargs

169 170
    def wrapper(init):
        @wraps(init)
171
        def inner(self, *args, **kwargs):
172 173 174 175

            if not hasattr(self, 'sysprep_params'):
                self.sysprep_params = {}

176 177
            self.sysprep_params[name] = \
                SysprepParam(type, default, descr, **extra)
178
            init(self, *args, **kwargs)
179 180 181 182 183
        return inner
    return wrapper


def del_sysprep_param(name):
184 185
    """Decorator for __init__ that deletes a previously added sysprep parameter
    definition from an instance of a os_type class.
186 187 188 189
    """
    def wrapper(func):
        @wraps(func)
        def inner(self, *args, **kwargs):
190
            del self.sysprep_params[name]
191 192 193
            func(self, *args, **kwargs)
        return inner
    return wrapper
Nikos Skalkotos's avatar
Nikos Skalkotos committed
194

195

Nikos Skalkotos's avatar
Nikos Skalkotos committed
196
class OSBase(object):
197
    """Basic operating system class"""
198

199
    def __init__(self, image, **kargs):
200 201 202 203 204
        self.image = image

        self.root = image.root
        self.out = image.out

205 206 207 208 209 210
        # Could be defined in a decorator
        if not hasattr(self, 'sysprep_params'):
            self.sysprep_params = {}

        if 'sysprep_params' in kargs:
            for key, val in kargs['sysprep_params'].items():
211 212 213
                if key not in self.sysprep_params:
                    self.out.warn("Ignoring invalid `%s' parameter." % key)
                    continue
214 215 216 217
                param = self.sysprep_params[key]
                if not param.set_value(val):
                    raise FatalError("Invalid value for sysprep parameter: "
                                     "`%s'. Reason: %s" % (key, param.error))
218

219
        self.meta = {}
Nikos Skalkotos's avatar
Nikos Skalkotos committed
220
        self.shrinked = False
221

222 223
        # This will host the error if mount fails
        self._mount_error = ""
224
        self._mount_warnings = []
225
        self._mounted = False
226

227 228 229
        # Many guestfs compilations don't support scrub
        self._scrub_support = True
        try:
230
            self.image.g.available(['scrub'])
231 232 233
        except RuntimeError:
            self._scrub_support = False

234 235 236 237 238 239 240 241
        # Create a list of available syspreps
        self._sysprep_tasks = {}
        for name in dir(self):
            obj = getattr(self, name)
            if not hasattr(obj, '_sysprep'):
                continue
            self._sysprep_tasks[name] = obj._sysprep_enabled

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
        self._cleanup_jobs = {}

    def _add_cleanup(self, namespace, job, *args):
        """Add a new job in a cleanup list"""

        if namespace not in self._cleanup_jobs:
            self._cleanup_jobs[namespace] = []

        self._cleanup_jobs[namespace].append((job, args))

    def _cleanup(self, namespace):
        """Run the cleanup tasks that are defined under a specific namespace"""

        if namespace not in self._cleanup_jobs:
            self.out.warn("Cleanup namespace: `%s' is not defined", namespace)
            return

        while len(self._cleanup_jobs[namespace]):
            job, args = self._cleanup_jobs[namespace].pop()
            job(*args)

        del self._cleanup_jobs[namespace]

Nikos Skalkotos's avatar
Nikos Skalkotos committed
265
    def inspect(self):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
266
        """Inspect the media to check if it is supported"""
267 268 269 270

        if self.image.is_unsupported():
            return

Nikos Skalkotos's avatar
Nikos Skalkotos committed
271
        self.out.output('Running OS inspection:')
272
        with self.mount(readonly=True, silent=True):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
273
            self._do_inspect()
274 275
        self.out.output()

276 277 278
    def collect_metadata(self):
        """Collect metadata about the OS"""

279 280 281
        self.out.output('Collecting image metadata ...', False)

        with self.mount(readonly=True, silent=True):
282 283
            self._do_collect_metadata()

284
        self.out.success('done')
Nikos Skalkotos's avatar
Nikos Skalkotos committed
285
        self.out.output()
286

287
    def list_syspreps(self):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
288
        """Returns a list of sysprep objects"""
289
        return [getattr(self, name) for name in self._sysprep_tasks]
290

291
    def sysprep_info(self, obj):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
292
        """Returns information about a sysprep object"""
293
        assert hasattr(obj, '_sysprep'), "Object is not a sysprep"
294

295 296
        SysprepInfo = namedtuple("SysprepInfo", "name description")

297 298 299 300
        name = obj.__name__.replace('_', '-')[1:]
        description = textwrap.dedent(obj.__doc__)

        return SysprepInfo(name, description)
301

302 303
    def get_sysprep_by_name(self, name):
        """Returns the sysprep object with the given name"""
304
        error_msg = "Syprep operation %s does not exist for %s" % \
305
                    (name, self.__class__.__name__)
306

307 308 309
        method_name = '_' + name.replace('-', '_')

        if hasattr(self, method_name):
310 311
            method = getattr(self, method_name)

312 313
            if hasattr(method, '_sysprep'):
                return method
314

315
        raise FatalError(error_msg)
316

317
    def enable_sysprep(self, obj):
318
        """Enable a system preparation operation"""
319 320 321 322
        assert hasattr(obj, '_sysprep'), "Object is not a sysprep"
        assert obj.__name__ in self._sysprep_tasks, "Sysprep already executed"

        self._sysprep_tasks[obj.__name__] = True
323

324
    def disable_sysprep(self, obj):
325
        """Disable a system preparation operation"""
326 327 328 329 330 331 332 333 334 335 336
        assert hasattr(obj, '_sysprep'), "Object is not a sysprep"
        assert obj.__name__ in self._sysprep_tasks, "Sysprep already executed"

        self._sysprep_tasks[obj.__name__] = False

    def sysprep_enabled(self, obj):
        """Returns True if this system praparation operation is enabled"""
        assert hasattr(obj, '_sysprep'), "Object is not a sysprep"
        assert obj.__name__ in self._sysprep_tasks, "Sysprep already executed"

        return self._sysprep_tasks[obj.__name__]
337 338

    def print_syspreps(self):
339
        """Print enabled and disabled system preparation operations."""
340

341
        syspreps = self.list_syspreps()
342 343
        enabled = [s for s in syspreps if self.sysprep_enabled(s)]
        disabled = [s for s in syspreps if not self.sysprep_enabled(s)]
344

345 346 347
        wrapper = textwrap.TextWrapper()
        wrapper.subsequent_indent = '\t'
        wrapper.initial_indent = '\t'
348
        wrapper.width = 72
349

350
        self.out.output("Enabled system preparation operations:")
351
        if len(enabled) == 0:
352
            self.out.output("(none)")
353 354
        else:
            for sysprep in enabled:
355
                name = sysprep.__name__.replace('_', '-')[1:]
356
                descr = wrapper.fill(textwrap.dedent(sysprep.__doc__))
357
                self.out.output('    %s:\n%s\n' % (name, descr))
358

359
        self.out.output("Disabled system preparation operations:")
360
        if len(disabled) == 0:
361
            self.out.output("(none)")
362 363
        else:
            for sysprep in disabled:
364
                name = sysprep.__name__.replace('_', '-')[1:]
365
                descr = wrapper.fill(textwrap.dedent(sysprep.__doc__))
366
                self.out.output('    %s:\n%s\n' % (name, descr))
367

368 369 370
    def print_sysprep_params(self):
        """Print the system preparation parameter the user may use"""

371 372
        self.out.output("System preparation parameters:")
        self.out.output()
373

374 375 376
        public_params = [(n, p) for n, p in self.sysprep_params.items()
                         if not p.hidden]
        if len(public_params) == 0:
377 378 379
            self.out.output("(none)")
            return

380 381 382 383
        wrapper = textwrap.TextWrapper()
        wrapper.subsequent_indent = "             "
        wrapper.width = 72

384 385 386
        for name, param in public_params:
            if param.hidden:
                continue
387 388 389 390 391
            self.out.output("NAME:        %s" % name)
            self.out.output("VALUE:       %s" % param.value)
            self.out.output(
                wrapper.fill("DESCRIPTION: %s" % param.description))
            self.out.output()
392

Nikos Skalkotos's avatar
Nikos Skalkotos committed
393 394 395
    def do_sysprep(self):
        """Prepare system for image creation."""

396 397
        self.out.output('Preparing system for image creation:')

398
        if self.image.is_unsupported():
399 400 401 402
            self.out.warn(
                "System preparation is disabled for unsupported media")
            return

403 404
        enabled = [s for s in self.list_syspreps() if self.sysprep_enabled(s)]
        size = len(enabled)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
405 406 407 408 409 410 411
        cnt = 0

        def exec_sysprep(cnt, size, task):
            self.out.output(('(%d/%d)' % (cnt, size)).ljust(7), False)
            task()
            del self._sysprep_tasks[task.__name__]

412
        with self.mount():
Nikos Skalkotos's avatar
Nikos Skalkotos committed
413
            for task in [t for t in enabled if t._sysprep_nomount is False]:
Nikos Skalkotos's avatar
Nikos Skalkotos committed
414
                cnt += 1
Nikos Skalkotos's avatar
Nikos Skalkotos committed
415 416 417 418 419
                exec_sysprep(cnt, size, task)

        for task in [t for t in enabled if t._sysprep_nomount]:
            cnt += 1
            exec_sysprep(cnt, size, task)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
420 421 422

        self.out.output()

Nikos Skalkotos's avatar
Nikos Skalkotos committed
423
    @sysprep('Shrinking image (may take a while)', nomount=True)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
424 425 426 427 428
    def _shrink(self):
        """Shrink the last file system and update the partition table"""
        self.image.shrink()
        self.shrinked = True

429 430 431 432
    @property
    def ismounted(self):
        return self._mounted

433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449
    def mount(self, readonly=False, silent=False, fatal=True):
        """Returns a context manager for mounting an image"""

        parent = self
        output = lambda msg='', nl=True: None if silent else self.out.output
        success = lambda msg='', nl=True: None if silent else self.out.success
        warn = lambda msg='', nl=True: None if silent else self.out.warn

        class Mount:
            """The Mount context manager"""
            def __enter__(self):
                mount_type = 'read-only' if readonly else 'read-write'
                output("Mounting the media %s ..." % mount_type, False)

                parent._mount_error = ""
                del parent._mount_warnings[:]

450 451 452 453 454 455 456
                try:
                    parent._mounted = parent._do_mount(readonly)
                except:
                    parent.image.g.umount_all()
                    raise

                if not parent.ismounted:
457 458
                    msg = "Unable to mount the media %s. Reason: %s" % \
                        (mount_type, parent._mount_error)
459 460 461 462 463
                    if fatal:
                        raise FatalError(msg)
                    else:
                        warn(msg)

464 465
                for warning in parent._mount_warnings:
                    warn(warning)
466 467 468

                if parent.ismounted:
                    success('done')
469 470 471 472

            def __exit__(self, exc_type, exc_value, traceback):
                output("Umounting the media ...", False)
                parent.image.g.umount_all()
473
                parent._mounted = False
474 475 476
                success('done')

        return Mount()
Nikos Skalkotos's avatar
Nikos Skalkotos committed
477

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494
    def check_version(self, major, minor):
        """Checks the OS version against the one specified by the major, minor
        tuple.

        Returns:
            < 0 if the OS version is smaller than the specified one
            = 0 if they are equal
            > 0 if it is greater
        """
        guestfs = self.image.g
        for a, b in ((guestfs.inspect_get_major_version(self.root), major),
                     (guestfs.inspect_get_minor_version(self.root), minor)):
            if a != b:
                return a - b

        return 0

495
    @add_prefix
Nikos Skalkotos's avatar
Nikos Skalkotos committed
496
    def _ls(self, directory):
497
        """List the name of all files under a directory"""
498
        return self.image.g.ls(directory)
499 500

    @add_prefix
Nikos Skalkotos's avatar
Nikos Skalkotos committed
501
    def _find(self, directory):
502
        """List the name of all files recursively under a directory"""
503
        return self.image.g.find(directory)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
504

Nikos Skalkotos's avatar
Nikos Skalkotos committed
505
    def _foreach_file(self, directory, action, **kargs):
506
        """Perform an action recursively on all files under a directory.
Nikos Skalkotos's avatar
Nikos Skalkotos committed
507

508 509
        The following options are allowed:

510 511
        * maxdepth: If defined, the action will not be performed on files that
          are below this level of directories under the directory parameter.
512

513 514
        * ftype: The action will only be performed on files of this type. For a
          list of all allowed file types, see here:
515 516 517 518
          http://libguestfs.org/guestfs.3.html#guestfs_readdir

        * exclude: Exclude all files that follow this pattern.
        """
519 520 521 522
        if not self.image.g.is_dir(directory):
            self.out.warn("Directory: `%s' does not exist!" % directory)
            return

Nikos Skalkotos's avatar
Nikos Skalkotos committed
523 524 525 526 527 528 529 530 531 532 533 534
        maxdepth = None if 'maxdepth' not in kargs else kargs['maxdepth']
        if maxdepth == 0:
            return

        # maxdepth -= 1
        maxdepth = None if maxdepth is None else maxdepth - 1
        kargs['maxdepth'] = maxdepth

        exclude = None if 'exclude' not in kargs else kargs['exclude']
        ftype = None if 'ftype' not in kargs else kargs['ftype']
        has_ftype = lambda x, y: y is None and True or x['ftyp'] == y

535
        for f in self.image.g.readdir(directory):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
536 537 538 539 540 541 542 543 544
            if f['name'] in ('.', '..'):
                continue

            full_path = "%s/%s" % (directory, f['name'])

            if exclude and re.match(exclude, full_path):
                continue

            if has_ftype(f, 'd'):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
545
                self._foreach_file(full_path, action, **kargs)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
546 547 548

            if has_ftype(f, ftype):
                action(full_path)
549

Nikos Skalkotos's avatar
Nikos Skalkotos committed
550 551
    def _do_inspect(self):
        """helper method for inspect"""
552
        self.out.warn("No inspection method available")
553 554
        pass

Nikos Skalkotos's avatar
Nikos Skalkotos committed
555 556
    def _do_collect_metadata(self):
        """helper method for collect_metadata"""
557 558 559 560 561 562 563 564

        try:
            self.meta['ROOT_PARTITION'] = \
                "%d" % self.image.g.part_to_partnum(self.root)
        except RuntimeError:
            self.out.warn("Unable to identify the partition number from root "
                          "partition: %s" % self.root)

565 566
        self.meta['OSFAMILY'] = self.image.g.inspect_get_type(self.root)
        self.meta['OS'] = self.image.g.inspect_get_distro(self.root)
Nikos Skalkotos's avatar
Nikos Skalkotos committed
567 568
        if self.meta['OS'] == "unknown":
            self.meta['OS'] = self.meta['OSFAMILY']
569 570
        self.meta['DESCRIPTION'] = \
            self.image.g.inspect_get_product_name(self.root)
571 572

    def _do_mount(self, readonly):
Nikos Skalkotos's avatar
Nikos Skalkotos committed
573
        """helper method for mount"""
574
        try:
575 576
            self.image.g.mount_options(
                'ro' if readonly else 'rw', self.root, '/')
577
        except RuntimeError as msg:
578
            self._mount_error = str(msg)
579 580 581 582
            return False

        return True

Nikos Skalkotos's avatar
Nikos Skalkotos committed
583
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :