Thcipriani has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/235385

Change subject: Add config deployment
......................................................................

Add config deployment

Adds a section inside the local scap/scap.cfg for configuration files to
be deployed, formatted like:

    [config_files]
    /path/to/dest_config.file:
       template: file_in_the_template_dir.yaml.j2
       vars_file: /path/to/vars_file.yaml

The deploy command renders a json file in a known location on tin that
is later fetched by each deploy target.

The deploy target loops through the config files and grabs the
`template` and renders it using the `vars_file` (which is put into place
by puppet on the target and contains puppet private repo information)

Current limitations:

  - Files created are created by the ssh_user specified in the config
    and therefore must be created in locations to which that user has
    read access

Change-Id: I64aa464039c0471f156795a3042ea44d48c8622a
---
A bin/deploy-local-config
M scap/__init__.py
M scap/config.py
M scap/main.py
M scap/tasks.py
A scap/template.py
M scap/utils.py
7 files changed, 227 insertions(+), 22 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/tools/scap 
refs/changes/85/235385/1

diff --git a/bin/deploy-local-config b/bin/deploy-local-config
new file mode 100755
index 0000000..f555f4b
--- /dev/null
+++ b/bin/deploy-local-config
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Deploy service to localhost.
+#
+# Copyright © 2015 Wikimedia Foundation and contributors
+
+import os
+import sys
+
+# Add scap package to search path
+script = os.path.realpath(sys.argv[0])
+scap_src = os.path.dirname(os.path.dirname(script))
+sys.path.append(scap_src)
+
+import scap
+scap.DeployLocalConfig.run()
diff --git a/scap/__init__.py b/scap/__init__.py
index 70baa76..4d68bc9 100644
--- a/scap/__init__.py
+++ b/scap/__init__.py
@@ -9,6 +9,7 @@
 from .main import (
     CompileWikiversions,
     DeployLocal,
+    DeployLocalConfig,
     Deploy,
     HHVMGracefulAll,
     MWVersionsInUse,
@@ -28,6 +29,7 @@
 __all__ = (
     'CompileWikiversions',
     'DeployLocal',
+    'DeployLocalConfig',
     'Deploy',
     'HHVMGracefulAll',
     'MWVersionsInUse',
@@ -47,6 +49,7 @@
 any((
     CompileWikiversions,
     DeployLocal,
+    DeployLocalConfig,
     Deploy,
     HHVMGracefulAll,
     MWVersionsInUse,
diff --git a/scap/config.py b/scap/config.py
index 3ef9463..340e7bb 100644
--- a/scap/config.py
+++ b/scap/config.py
@@ -9,6 +9,9 @@
 import getpass
 import os
 import socket
+import yaml
+
+from . import utils
 
 
 DEFAULT_CONFIG = {
@@ -35,6 +38,7 @@
     'git_server': 'tin.eqiad.wmnet',
     'git_scheme': 'http',
     'git_submodules': False,
+    'config_deploy': False,
 }
 
 
@@ -63,6 +67,8 @@
     :param overrides: Dict of configuration values
     :returns: dict of configuration values
     """
+    local_cfg = os.path.join(os.getcwd(), 'scap')
+
     parser = ConfigParser.SafeConfigParser()
     if cfg_file:
         try:
@@ -77,7 +83,7 @@
             os.path.join(os.path.dirname(__file__), '..', 'scap.cfg'),
             '/srv/scap/scap.cfg',
             '/etc/scap.cfg',
-            os.path.join(os.getcwd(), 'scap.cfg'),
+            os.path.join(local_cfg, 'scap.cfg'),
         ])
 
     fqdn = socket.getfqdn().split('.')
@@ -95,4 +101,23 @@
     if overrides:
         config.update(overrides)
 
+    # Grab the config file
+    # config file section example:
+    # [config_files]
+    # /path/to/dest_config.file:
+    #    template: file_in_the_template_dir.yaml.j2
+    #    vars_file: /path/to/vars_file.yaml
+    if parser.has_section('config_files'):
+        config['config_files'] = []
+        for config_file in parser.items('config_files', True):
+            name = config_file[0]
+            info = yaml.load(config_file[1])
+            template = utils.get_realm_specific_filename(
+                os.path.join(local_cfg, 'templates', info['template']),
+                config['wmf_realm'],
+                config['datacenter']
+            )
+            info['template'] = template
+            config['config_files'].append({'name': name, 'info': info})
+
     return config
diff --git a/scap/main.py b/scap/main.py
index fb6e37d..e6f4f86 100644
--- a/scap/main.py
+++ b/scap/main.py
@@ -11,12 +11,14 @@
 import netifaces
 import os
 import psutil
+import requests
 import subprocess
 
 from . import cli
 from . import log
 from . import ssh
 from . import tasks
+from . import template
 from . import utils
 
 
@@ -575,8 +577,51 @@
         return exit_code
 
 
+class DeployLocalConfig(cli.Application):
+    """Creates config files
+    #. Pull config_files.json (generated during deploy on tin)
+    #. Loop through config files and render template from variables in
+       final location
+    """
+
+    def main(self, *extra_args):
+        """Looks only in the scap directory for the repo being deployed
+        """
+        repo = self.config['git_repo']
+        server = self.config['git_server']
+        deploy_dir = self.config['git_deploy_dir']
+
+        location = os.path.normpath("{0}/{1}/scap".format(server, repo))
+        http_fmt = 'http://{}'
+        scap_config_url = http_fmt.format(location)
+
+        r = requests.get('{}/config_files.json'.format(scap_config_url))
+        config_files = r.json()
+        for config_file in config_files:
+            info = config_file['info']
+
+            name = info['template']
+            if os.path.commonprefix([name, deploy_dir]) == deploy_dir:
+                name = os.path.relpath(name, deploy_dir)
+
+            tmpl = template.Template(
+                name=name,
+                location=http_fmt.format(server),
+                var_file=info['vars_file']
+            )
+
+            with open(config_file['name'], 'w') as f:
+                f.write(tmpl.render())
+
+
 class DeployLocal(cli.Application):
-    """Deploy service code via git"""
+    """Deploy service locally
+
+    #. Deploy service configuration if specified
+    #. Deploy service code
+    #. Restart service
+    #. Check service port
+    """
 
     def main(self, *extra_args):
         repo = self.config['git_repo']
@@ -584,6 +629,7 @@
         server = self.config['git_server']
         rev = self.config['git_rev']
         has_submodules = self.config['git_submodules']
+        config_deploy = self.config['config_deploy']
 
         # service_name/service_port only set for deploys requiring restart
         service_name = self.config.get('service_name', None)
@@ -595,9 +641,17 @@
         # only supports http from tin for the moment
         scheme = 'http'
 
-        url = os.path.normpath('{0}/{1}'.format(server, repo))
-        url = "{0}://{1}/.git".format(scheme, url)
+        base_url = os.path.normpath('{0}/{1}'.format(server, repo))
+        url = "{0}://{1}/.git".format(scheme, base_url)
         self.get_logger().debug('Fetching from: {}'.format(url))
+
+        deploy_local_config = ' '.join(utils.pass_config(
+            self.get_script_path('deploy-local-config'),
+            self.config,
+        ))
+
+        if config_deploy:
+            utils.sudo_check_call(repo_user, deploy_local_config)
 
         tasks.git_fetch(location, url, repo_user)
         tasks.git_checkout(location, rev, has_submodules, repo_user)
@@ -655,25 +709,20 @@
                 # apache server
                 tasks.git_update_server_info(self.config['git_submodules'])
 
-                deploy_conf = [
-                    'git_deploy_dir',
-                    'git_repo_user',
-                    'git_server',
-                    'git_scheme',
-                    'git_repo',
-                    'git_rev',
-                    'git_submodules',
-                    'service_name',
-                    'service_port',
-                ]
+                if self.config['config_deploy']:
+                    config_file_info = os.path.join(os.getcwd(),
+                                                    'scap',
+                                                    'config_files.json')
 
-                deploy_local_cmd = [self.get_script_path('deploy-local')]
+                    tasks.config_deploy_setup(
+                        cfg=self.config,
+                        tmp_file=config_file_info
+                    )
 
-                deploy_local_cmd.extend([
-                    "-D '{}:{}'".format(x, self.config.get(x))
-                    for x in deploy_conf
-                    if self.config.get(x) is not None
-                ])
+                deploy_local_cmd = utils.pass_config(
+                    self.get_script_path('deploy-local'),
+                    self.config
+                )
 
                 logger.debug('Running cmd {}'.format(deploy_local_cmd))
                 deploy_rev = ssh.Job(
diff --git a/scap/tasks.py b/scap/tasks.py
index a7fee9d..3c9c741 100644
--- a/scap/tasks.py
+++ b/scap/tasks.py
@@ -17,6 +17,8 @@
 import subprocess
 import time
 
+import requests
+
 from . import cdblib
 from . import log
 from . import ssh
@@ -678,6 +680,19 @@
         subprocess.check_call(cmd, shell=True)
 
 
+def config_deploy_setup(cfg, tmp_file):
+    """Creates json files that are used by deploy targets to compile and
+    write config files
+    """
+    logger = logging.getLogger('config_deploy_setup')
+    if not cfg['config_deploy'] or not cfg.get('config_files'):
+        return
+
+    logger.debug('Building config files')
+    with open(tmp_file, 'w') as f:
+        json.dump(cfg['config_files'], f)
+
+
 def restart_service(service, user='mwdeploy'):
     logger = logging.getLogger('service_restart')
 
diff --git a/scap/template.py b/scap/template.py
new file mode 100644
index 0000000..e2169e4
--- /dev/null
+++ b/scap/template.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+"""
+    scap.tmpl
+    ~~~~~~~~~~
+    Module for working with file templates
+"""
+
+import jinja2
+import requests
+import yaml
+
+
+class ScapTemplateLoader(jinja2.BaseLoader):
+    """Overrides the jinja2 baseloader with one that will fetch template
+    files from tin
+    """
+    def __init__(self, location):
+        self.location = location
+
+    def get_source(self, environment, template):
+        """Grab a template file from the staging server"""
+        uri = '{}/{}'.format(self.location, template)
+        template = self._get_remote_file(uri)
+        if not template:
+            raise jinja2.TemplateNotFound(template)
+
+        source, etag = template
+        uptodate = self._uptodate(uri, etag)
+        return source, uri, uptodate
+
+    def _get_remote_file(self, uri):
+        """Helper method to return etag and response body"""
+        r = requests.get(uri)
+        if r.status_code != requests.codes.ok:
+            return None
+        return r.text, r.headers.get('etag', '')
+
+    def _uptodate(self, uri, etag):
+        """Returns a functions that takes no arguments that will check if
+        a given template is up-to-date
+        """
+        def uptodate():
+            import requests
+            r = requests.get(uri)
+            if r.status_code == requests.codes.ok:
+                return etag == r.headers.get('etag', '')
+            # If we got a non-200 response, assume the template we have is fine
+            return True
+        return uptodate
+
+
+class Template(object):
+    """Adapter class that wraps jinja2 templates
+    """
+    def __init__(self, name, location, var_file):
+        loader = ScapTemplateLoader(location)
+        self._env = jinja2.Environment(loader=loader)
+        self._template = self._env.get_template(name)
+
+        self.var_file = var_file
+
+    def render(self):
+        """Renders the templates specified by `self.name` using the
+        variables sourced from the import yaml file specified by
+        `self.var_file`
+        """
+        with open(self.var_file, 'r') as variables:
+            template_vars = yaml.load(variables.read())
+            return self._template.render(template_vars)
diff --git a/scap/utils.py b/scap/utils.py
index 6da0376..644d98b 100644
--- a/scap/utils.py
+++ b/scap/utils.py
@@ -543,7 +543,7 @@
         return subprocess.check_output(cmd, shell=True).strip()
 
 
-def generate_json_tag(user=get_real_username(), location=os.getcwd()):
+def generate_json_tag(location, user=get_real_username()):
     """Generates a json object"""
     timestamp = datetime.utcnow().strftime('%Y%m%d-%H%M%S')
     tag = '{0}-{1}-{2}'.format(location, 'sync', timestamp)
@@ -554,3 +554,30 @@
     }
 
     return (tag, tag_info)
+
+
+def pass_config(cmd, cfg={}):
+    """Takes command and returns array with service_deploy config vars set
+    """
+    cmd = [cmd]
+
+    deploy_conf = [
+        'git_deploy_dir',
+        'git_repo_user',
+        'git_server',
+        'git_scheme',
+        'git_repo',
+        'git_rev',
+        'git_submodules',
+        'service_name',
+        'service_port',
+        'config_deploy',
+    ]
+
+    cmd.extend([
+        "-D '{}:{}'".format(x, cfg.get(x))
+        for x in deploy_conf
+        if cfg.get(x)
+    ])
+
+    return cmd

-- 
To view, visit https://gerrit.wikimedia.org/r/235385
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I64aa464039c0471f156795a3042ea44d48c8622a
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/tools/scap
Gerrit-Branch: master
Gerrit-Owner: Thcipriani <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to