123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- # -*- coding: utf-8 -*-
- # 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.
- import os
- import stat
- import uuid
- import time
- import logging
- import tarfile
- import tempfile
- from .. import utils
- class SshTarballTransferMixin(object):
- """
- Transfers resources and recipes by packing them to tar.gz and
- copying it via ssh.
- """
- def _transfer(self, pack_path, pack_dest, res_dir):
- node = self.node
- args = locals()
- # copy and extract tarball
- script = utils.ScriptRunner()
- script.append("scp %(pack_path)s root@%(node)s:%(pack_dest)s"
- % args)
- script.append("ssh -o StrictHostKeyChecking=no "
- "-o UserKnownHostsFile=/dev/null root@%(node)s "
- "tar -C %(res_dir)s -xpzf %(pack_dest)s" % args)
- try:
- script.execute()
- except ScriptRuntimeError as ex:
- # TO-DO: change to appropriate exception
- raise RuntimeError('Failed to copy resources to node %s. '
- 'Reason: %s' % (node, ex))
- def _pack_resources(self):
- randpart = uuid.uuid4().hex[:8]
- pack_path = os.path.join(self.local_tmpdir,
- 'res-%s.tar.gz' % randpart)
- pack = tarfile.open(pack_path, mode='w:gz')
- os.chmod(pack_path, stat.S_IRUSR | stat.S_IWUSR)
- for path, dest in self._resources:
- if not dest:
- dest = os.path.basename(path)
- pack.add(path,
- arcname=os.path.join(dest, os.path.basename(path)))
- pack.close()
- return pack_path
- def _copy_resources(self):
- pack_path = self._pack_resources()
- pack_dest = os.path.join(self.remote_tmpdir,
- os.path.basename(pack_path))
- self._transfer(pack_path, pack_dest, self.resource_dir)
- def _pack_recipes(self):
- randpart = uuid.uuid4().hex[:8]
- pack_path = os.path.join(self.local_tmpdir,
- 'rec-%s.tar.gz' % randpart)
- pack = tarfile.open(pack_path, mode='w:gz')
- os.chmod(pack_path, stat.S_IRUSR | stat.S_IWUSR)
- if self.recipe_dir.startswith(self.resource_dir):
- dest = self.recipe_dir[len(self.resource_dir):].lstrip('/')
- else:
- dest = ''
- for marker, recipes in self._recipes.iteritems():
- for path in recipes:
- _dest = os.path.join(dest, os.path.basename(path))
- pack.add(path, arcname=_dest)
- pack.close()
- return pack_path
- def _copy_recipes(self):
- pack_path = self._pack_recipes()
- pack_dest = os.path.join(self.remote_tmpdir,
- os.path.basename(pack_path))
- if self.recipe_dir.startswith(self.resource_dir):
- extr_dest = self.resource_dir
- else:
- extr_dest = self.recipe_dir
- self._transfer(pack_path, pack_dest, extr_dest)
- class DroneObserver(object):
- """
- Base class for listening messages from drones.
- """
- def applying(self, drone, recipe):
- """
- Drone is calling this method when it starts applying recipe.
- """
- # subclass must implement this method
- raise NotImplementedError()
- def checking(self, drone, recipe):
- """
- Drone is calling this method when it starts checking if recipe
- has been applied.
- """
- # subclass must implement this method
- raise NotImplementedError()
- def finished(self, drone, recipe):
- """
- Drone is calling this method when it's finished with recipe
- application.
- """
- # subclass must implement this method
- raise NotImplementedError()
- class Drone(object):
- """
- Base class used to apply installation recipes to nodes.
- """
- def __init__(self, node, resource_dir=None, recipe_dir=None,
- local_tmpdir=None, remote_tmpdir=None):
- self._recipes = utils.SortedDict()
- self._resources = []
- self._applied = set()
- self._running = set()
- self._observer = None
- # remote host IP or hostname
- self.node = node
- # working directories on remote host
- self.resource_dir = (resource_dir or
- '/tmp/drone%s' % uuid.uuid4().hex[:8])
- self.recipe_dir = (recipe_dir or
- os.path.join(self.resource_dir, 'recipes'))
- # temporary directories
- self.remote_tmpdir = (remote_tmpdir or
- '/tmp/drone%s' % uuid.uuid4().hex[:8])
- self.local_tmpdir = (local_tmpdir or
- tempfile.mkdtemp(prefix='drone'))
- def init_node(self):
- """
- Initializes node for manipulation.
- """
- created = []
- server = utils.ScriptRunner(self.node)
- for i in (self.resource_dir, self.recipe_dir,
- self.remote_tmpdir):
- server.append('mkdir -p %s' % os.path.dirname(i))
- server.append('mkdir --mode 0700 %s' % i)
- created.append('%s:%s' % (self.node, i))
- server.execute()
- # TO-DO: complete logger name when logging will be setup correctly
- logger = logging.getLogger()
- logger.debug('Created directories: %s' % ','.join(created))
- @property
- def recipes(self):
- for i in self._recipes.itervalues():
- for y in i:
- yield y
- @property
- def resources(self):
- for i in self._resources:
- yield i[0]
- def add_recipe(self, path, marker=None):
- """
- Registers recipe for application on node. Recipes will be
- applied in order they where added to drone. Multiple recipes can
- be applied in paralel if they have same marker.
- """
- marker = marker or uuid.uuid4().hex[:8]
- self._recipes.setdefault(marker, []).append(path)
- def add_resource(self, path, destination=None):
- """
- Registers resource. Destination will be relative from resource
- directory on node.
- """
- dest = destination or ''
- self._resources.append((path, dest))
- def _copy_resources(self):
- """
- Copies all local files registered in self._resources to their
- appropriate destination on self.node. If tmpdir is given this
- method can operate only in this directory.
- """
- # subclass must implement this method
- raise NotImplementedError()
- def _copy_recipes(self):
- """
- Copies all local files registered in self._recipes to their
- appropriate destination on self.node. If tmpdir is given this
- method can operate only in this directory.
- """
- # subclass must implement this method
- raise NotImplementedError()
- def prepare_node(self):
- """
- Copies all local resources and recipes to self.node.
- """
- # TO-DO: complete logger name when logging will be setup correctly
- logger = logging.getLogger()
- logger.debug('Copying drone resources to node %s: %s'
- % (self.node, self.resources))
- self._copy_resources()
- logger.debug('Copying drone recipes to node %s: %s'
- % (self.node, [i[0] for i in self.recipes]))
- self._copy_recipes()
- def _apply(self, recipe):
- """
- Starts application of single recipe given as path to the recipe
- file in self.node. This method should not wait until recipe is
- applied.
- """
- # subclass must implement this method
- raise NotImplementedError()
- def _finished(self, recipe):
- """
- Returns True if given recipe is applied, otherwise returns False
- """
- # subclass must implement this method
- raise NotImplementedError()
- def _wait(self):
- """
- Waits until all started applications of recipes will be finished
- """
- while self._running:
- _run = list(self._running)
- for recipe in _run:
- if self._observer:
- self._observer.checking(self, recipe)
- if self._finished(recipe):
- self._applied.add(recipe)
- self._running.remove(recipe)
- if self._observer:
- self._observer.finished(self, recipe)
- else:
- time.sleep(3)
- continue
- def set_observer(self, observer):
- """
- Registers an observer. Given object should be subclass of class
- DroneObserver.
- """
- for attr in ('applying', 'checking', 'finished'):
- if not hasattr(observer, attr):
- raise ValueError('Observer object should be a subclass '
- 'of class DroneObserver.')
- self._observer = observer
- def apply(self, marker=None, name=None, skip=None):
- """
- Applies recipes on node. If marker is specified, only recipes
- with given marker are applied. If name is specified only recipe
- with given name is applied. Skips recipes with names given
- in list parameter skip.
- """
- # TO-DO: complete logger name when logging will be setup correctly
- logger = logging.getLogger()
- skip = skip or []
- lastmarker = None
- for mark, recipelist in self._recipes.iteritems():
- if marker and marker != mark:
- logger.debug('Skipping marker %s for node %s.' %
- (mark, self.node))
- continue
- for recipe in recipelist:
- base = os.path.basename(recipe)
- if (name and name != base) or base in skip:
- logger.debug('Skipping recipe %s for node %s.' %
- (recipe, self.node))
- continue
- # if the marker has changed then we don't want to
- # proceed until all of the previous puppet runs have
- # finished
- if lastmarker and lastmarker != mark:
- self._wait()
- lastmarker = mark
- logger.debug('Applying recipe %s to node %s.' %
- (base, self.node))
- rpath = os.path.join(self.recipe_dir, base)
- if self._observer:
- self._observer.applying(self, recipe)
- self._running.add(rpath)
- self._apply(rpath)
- self._wait()
- def cleanup(self, resource_dir=True, recipe_dir=True):
- """
- Removes all directories created by this drone.
- """
- shutil.rmtree(self.local_tmpdir, ignore_errors=True)
- server = utils.ScriptRunner(self.node)
- server.append('rm -fr %s' % self.remote_tmpdir)
- if recipe_dir:
- server.append('rm -fr %s' % self.recipe_dir)
- if resource_dir:
- server.append('rm -fr %s' % self.resource_dir)
- server.execute()
- class PackstackDrone(SshTarballTransferMixin, Drone):
- """
- This drone uses Puppet and it's manifests to manipulate node.
- """
- # XXX: Since this implementation is Packstack specific (_apply
- # method), it should be moved out of installer when
- # Controller and plugin system will be refactored and installer
- # will support projects.
- def __init__(self, *args, **kwargs):
- kwargs['resource_dir'] = ('/var/tmp/packstack/drone%s'
- % uuid.uuid4().hex[:8])
- kwargs['recipe_dir'] = '%s/manifests' % kwargs['resource_dir']
- kwargs['remote_tmpdir'] = '%s/temp' % kwargs['resource_dir']
- super(PackstackDrone, self).__init__(*args, **kwargs)
- self.module_dir = os.path.join(self.resource_dir, 'modules')
- self.fact_dir = os.path.join(self.resource_dir, 'facts')
- def init_node(self):
- """
- Initializes node for manipulation.
- """
- super(PackstackDrone, self).init_node()
- server = utils.ScriptRunner(self.node)
- for pkg in ("puppet", "openssh-clients", "tar"):
- server.append("rpm -q --whatprovides %(pkg)s || "
- "yum install -y %(pkg)s" % locals())
- server.execute()
- def add_resource(self, path, resource_type=None):
- """
- Resource type should be module, fact or resource.
- """
- resource_type = resource_type or 'resource'
- dest = '%ss' % resource_type
- super(PackstackDrone, self).add_resource(path, destination=dest)
- def _finished(self, recipe):
- recipe_base = os.path.basename(recipe)
- log = os.path.join(self.recipe_dir,
- recipe_base.replace(".finished", ".log"))
- local = utils.ScriptRunner()
- local.append('scp -o StrictHostKeyChecking=no '
- '-o UserKnownHostsFile=/dev/null '
- 'root@%s:%s %s' % (self.node, recipe, log))
- try:
- # once a remote puppet run has finished, we retrieve
- # the log file and check it for errors
- local.execute(log=False)
- # if we got to this point the puppet apply has finished
- return True
- except utils.ScriptRuntimeError as e:
- # the test raises an exception if the file doesn't exist yet
- return False
- def _apply(self, recipe):
- running = "%s.running" % recipe
- finished = "%s.finished" % recipe
- server = utils.ScriptRunner(self.node)
- server.append("touch %s" % running)
- server.append("chmod 600 %s" % running)
- # XXX: This is terrible hack, but unfortunatelly the apache
- # puppet module doesn't work if we set FACTERLIB
- # https://github.com/puppetlabs/puppetlabs-apache/pull/138
- for bad_word in ('horizon', 'nagios', 'apache'):
- if bad_word in recipe:
- break
- else:
- server.append("export FACTERLIB=$FACTERLIB:%s" %
- self.fact_dir)
- server.append("export PACKSTACK_VAR_DIR=%s" % self.resource_dir)
- # TO-DO: complete logger name when logging will be setup correctly
- logger = logging.getLogger()
- loglevel = logger.level <= logging.DEBUG and '--debug' or ''
- rdir = self.resource_dir
- mdir = self._module_dir
- server.append(
- "( flock %(rdir)s/ps.lock "
- "puppet apply %(loglevel)s --modulepath %(mdir)s "
- "%(recipe)s > %(running)s 2>&1 < /dev/null; "
- "mv %(running)s %(finished)s ) "
- "> /dev/null 2>&1 < /dev/null &" % locals())
- server.execute()
|