Giuseppe Lavagetto has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/342492 )

Change subject: Initial import
......................................................................


Initial import

Bug: T160178
Change-Id: I6c64268e5a86be13afaa6d8185a6b1533fe71770
---
A .gitignore
A doc/examples/t01_example.py
A setup.py
A switchdc/__init__.py
A switchdc/log.py
A switchdc/menu.py
A switchdc/remote.py
A switchdc/stages/__init__.py
A switchdc/switch.py
A tox.ini
10 files changed, 369 insertions(+), 0 deletions(-)

Approvals:
  Giuseppe Lavagetto: Verified; Looks good to me, approved



diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..83769f1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/.eggs
+/.tox
+/*.egg-info
+/.coverage
+/setup.cfg
+/.venv
+/logs
+*.pyc
diff --git a/doc/examples/t01_example.py b/doc/examples/t01_example.py
new file mode 100644
index 0000000..b0cca69
--- /dev/null
+++ b/doc/examples/t01_example.py
@@ -0,0 +1,23 @@
+from switchdc import remote
+from switchdc.log import logger
+
+
+__title__ = "Example task description"
+
+
+def execute(dc_from, dc_to):
+    """Entry point to execute this task
+
+    Must not raise execeptions, all exceptions must be catched here and 
managed.
+    Returns 0 on success, a positive integer on failure.
+
+    Arguments:
+    dc_from -- the name of the datacenter to switch from
+    dc_to   -- the name of the datacenter to switch to
+    """
+    logger.debug(__name__)
+    print('Executed with {dc_from} - {dc_to}'.format(dc_from=dc_from, 
dc_to=dc_to))
+    rc, worker = remote.run('R:Class = Role::Memcached', 'sync', ['date'],
+                            success_threshold=0.9, batch_size=5, batch_sleep=5)
+
+    return rc
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..6946858
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+"""Package configuration."""
+
+from setuptools import find_packages, setup
+
+setup(
+    author='Riccardo Coccioli',
+    author_email='[email protected]',
+    description='Datacenter switchover automation',
+    entry_points={
+        'console_scripts': [
+            'switchdc = switchdc.switch:main',
+        ],
+    },
+    install_requires=['pyyaml'],
+    name='switchdc',
+    packages=find_packages(),
+    version='0.0.1',
+    zip_safe=False,
+)
diff --git a/switchdc/__init__.py b/switchdc/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/switchdc/__init__.py
diff --git a/switchdc/log.py b/switchdc/log.py
new file mode 100644
index 0000000..db644e6
--- /dev/null
+++ b/switchdc/log.py
@@ -0,0 +1,12 @@
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+_log_formatter = logging.Formatter(
+    fmt='%(asctime)s [%(levelname)s %(filename)s:%(lineno)s in %(funcName)s] 
%(message)s')
+_log_handler = logging.FileHandler('/var/log/switchdc.log')
+_log_handler.setFormatter(_log_formatter)
+logger.addHandler(_log_handler)
+logger.raiseExceptions = False
+logger.setLevel(logging.INFO)
diff --git a/switchdc/menu.py b/switchdc/menu.py
new file mode 100644
index 0000000..9ff04cf
--- /dev/null
+++ b/switchdc/menu.py
@@ -0,0 +1,115 @@
+from switchdc.log import logger
+
+
+class Menu(object):
+    """Menu class."""
+
+    def __init__(self, title, parent=None):
+        """Menu constructor
+
+        Arguments:
+        title  -- the menu title to be show
+        parent -- reference to a parent menu if this is a submenu. [optional, 
default: None]
+        """
+        self.title = title
+        self.parent = parent
+        self.items = []
+
+    @property
+    def status(self):
+        """Getter for the menu status, returns a string representation of the 
status of it's tasks."""
+        completed, total = Menu.calculate_status(self)
+        return '{completed}/{total}'.format(completed=completed, total=total)
+
+    def append(self, item):
+        """Append an item or a submenu to this menu.
+
+        Arguments:
+        item -- the item to append
+        """
+        if type(item) == Menu:
+            item.parent = self
+        self.items.append(item)
+
+    def run(self):
+        """For menu run is equivalent to show."""
+        self.show()
+
+    def show(self):
+        """Print the menu to stdout."""
+        print(self.title)
+        for i, item in enumerate(self.items):
+            print(' {i: >2} [{status}] {title}'.format(i=i + 1, 
title=item.title, status=item.status))
+
+        if self.parent is not None:
+            print('  b - Back to parent menu')
+
+        print('  q - Quit')
+
+    @staticmethod
+    def calculate_status(menu):
+        """Calculate the status of a menu, checking the status of all it's 
tasks recursively.
+
+        Arguments:
+        menu -- the meny for which to calculate the status
+        """
+        completed = 0
+        total = 0
+        for item in menu.items:
+            item_type = type(item)
+            if item_type == Menu:
+                sub_completed, sub_total = Menu.calculate_status(item)
+                completed += sub_completed
+                total += sub_total
+            elif item_type == Item:
+                total += 1
+                if item.status != Item.todo:
+                    completed += 1
+
+        return completed, total
+
+
+class Item(object):
+    """Menu item class."""
+
+    statuses = ('TODO', 'PASS', 'FAIL')  # Status labels
+    todo, success, failed = statuses  # Valid statuses variables
+
+    def __init__(self, name, title, function, args=None, kwargs=None):
+        """Item constructor.
+
+        Arguments
+        name     -- the name of the module for this task
+        title    -- the item's title to be shown in the menu
+        function -- the function to call when the task is run
+        args     -- the list of positional arguments to pass to the function. 
[optional, default: None]
+        kwargs   -- the dictionary of keyword arguments to pass to the 
function. [optional, default: None]
+        """
+        self.name = name
+        self.title = title
+        self.status = self.todo
+        self.function = function
+        if args is not None:
+            self.args = args
+        else:
+            self.args = []
+        if kwargs is not None:
+            self.kwargs = kwargs
+        else:
+            self.kwargs = {}
+
+    def run(self):
+        """Run the item callind the configured function."""
+        try:
+            retval = self.function(*self.args, **self.kwargs)
+        except Exception as e:
+            retval = -1
+            print('FAILED: {msg}'.format(msg=e.message))
+            logger.exception(e)
+
+        if retval == 0:
+            self.status = self.success
+        else:
+            self.status = self.failed
+
+        return retval
diff --git a/switchdc/remote.py b/switchdc/remote.py
new file mode 100644
index 0000000..07c6f4e
--- /dev/null
+++ b/switchdc/remote.py
@@ -0,0 +1,73 @@
+import yaml
+
+from cumin.query import QueryBuilder
+from cumin.transport import Transport
+
+from switchdc.log import logger
+
+
+# Load cumin's configuration
+with open('/etc/cumin/config.yaml', 'r') as f:
+    cumin_config = yaml.safe_load(f)
+
+
+def run(query_string, mode, commands, success_threshold=1.0, batch_size=None, 
batch_sleep=0):
+    """High level Cumin run of commands on hosts matching the query.
+
+    Arguments:
+    query_string      -- the hosts selection query to use with Cumin's 
configured backend
+    mode              -- the Cumin's mode of execution. Accepted values: sync, 
async
+    commands          -- the list of commands to execute on the matching hosts
+    success_threshold -- the threshold to consider the execution still 
successful. A float between 0.0 and 1.0.
+                         [optional, default: 1.0]
+    batch_size        -- the batch size to use in cumin. [optional, default: 
None]
+    batch_sleep       -- the batch sleep in seconds to use in Cumin before 
scheduling the next host.
+                         [optional, default: 0]
+    """
+    hosts = query(query_string)
+    return execute(hosts, mode, commands, success_threshold, batch_size, 
batch_sleep)
+
+
+def query(query_string):
+    """Lower level Cumin's backend query to find matching hosts. Use run() 
when possible.
+
+    Arguments:
+    query_string -- the hosts selection query to use with Cumin's configured 
backend
+    """
+    query = QueryBuilder(query_string, cumin_config, logger).build()
+    return query.execute()
+
+
+def execute(hosts, mode, commands, success_threshold=1.0, batch_size=None, 
batch_sleep=0):
+    """Lower level Cumin's execution of commands on a list o hosts. Use run() 
when possible.
+
+    Arguments:
+    hosts             -- the list of matching hosts to use as a target for 
Cumin's transport
+    mode              -- the Cumin's mode of execution. Accepted values: sync, 
async
+    commands          -- the list of commands to execute on the matching hosts
+    success_threshold -- the threshold to consider the execution still 
successful. A float between 0.0 and 1.0.
+                         [optional, default: 1.0]
+    batch_size        -- the batch size to use in cumin. [optional, default: 
None]
+    batch_sleep       -- the batch sleep in seconds to use in Cumin before 
scheduling the next host.
+                         [optional, default: 0]
+    """
+    worker = Transport.new(cumin_config, logger)
+    worker.hosts = hosts
+    worker.commands = commands
+    worker.handler = mode
+    worker.success_threshold = success_threshold
+    worker.batch_size = batch_size
+    worker.batch_sleep = batch_sleep
+
+    rc = worker.execute()
+
+    return rc, worker
+
+
+def get_puppet_agent_command(noop=False):
+    """Return puppet agent command equivalent to --test without 
--detailed-exitcodes."""
+    command = 'puppet agent -ov --ignorecache --no-daemonize 
--no-usecacheonfailure --no-splay --show_diff'
+    if noop:
+        command += ' --noop'
+
+    return command
diff --git a/switchdc/stages/__init__.py b/switchdc/stages/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/switchdc/stages/__init__.py
diff --git a/switchdc/switch.py b/switchdc/switch.py
new file mode 100644
index 0000000..3fe8099
--- /dev/null
+++ b/switchdc/switch.py
@@ -0,0 +1,115 @@
+import argparse
+import glob
+import importlib
+import os
+
+from switchdc.menu import Item, Menu
+
+
+def run(menu, dc_from, dc_to):
+    """Run the swithcdc interactive menu.
+
+    Arguments:
+    menu    -- the Menu instance to use for the run
+    dc_from -- the name of the datacenter to migrate from
+    dc_to   -- the name of the datacenter to migrate to
+    """
+    while True:
+        print('#--- DATACENTER SWITCHOVER FROM {dc_from} TO {dc_to} 
---#'.format(dc_from=dc_from, dc_to=dc_to))
+        menu.show()
+        try:
+            answer = raw_input('>>> ')
+        except (EOFError, KeyboardInterrupt):
+            print  # Nicer output
+            break  # Ctrl+d or Ctrl+c pressed while waiting for input
+
+        if not answer:
+            continue
+        elif answer == 'q':
+            break
+        elif answer == 'b':
+            menu = menu.parent
+
+        try:
+            index = int(answer)
+        except Exception:
+            print('Invalid answer')
+            continue
+
+        if index > 0 and index <= len(menu.items):
+            item = menu.items[index - 1]
+            if type(item) == Menu:
+                menu = item
+            elif type(item) == Item:
+                item.run()
+        else:
+            print('==> Invalid input <==')
+            continue
+
+
+def parse_args():
+    """Parse command line arguments and return them."""
+    parser = argparse.ArgumentParser(description='Datacenter Switchover for 
Mediawiki')
+    parser.add_argument('-f', '--dc-from', required=True, help='Name of the 
datacenter to migrate **from**')
+    parser.add_argument('-t', '--dc-to', required=True, help='Name of the 
datacenter to migrate **to**')
+    parser.add_argument('--task', help='If specified, run this task only in an 
non-interactive way and exit')
+    parser.add_argument(
+        '--stage', help='If specified, run all the tasks of this stage in an 
non-interactive way and exit')
+
+    return parser.parse_args()
+
+
+def generate_menu(dc_from, dc_to):
+    """Automatically generate the menu with items and submenus based on the 
available modules.
+
+    Arguments:
+    dc_from -- the name of the datacenter to migrate from
+    dc_to   -- the name of the datacenter to migrate to
+    """
+    menu = Menu('Datacenter switchover automation')
+    stage = '00'
+    submenu = None
+
+    for module_file in sorted(glob.glob(os.path.join(
+            os.path.dirname(os.path.abspath(__file__)), 'stages', 
't[0-9][0-9]_*.py'))):
+
+        module_name = os.path.basename(module_file)[:-3]
+        module_stage = module_name[1:3]
+        module = 
importlib.import_module('switchdc.stages.{module_name}'.format(module_name=module_name))
+
+        if module_stage != stage:
+            if submenu is not None:
+                menu.append(submenu)
+            stage = module_stage
+            submenu = Menu('Stage {stage}'.format(stage=stage))
+
+        submenu.append(Item(module.__name__, module.__title__, module.execute, 
args=[dc_from, dc_to]))
+    else:
+        if submenu is not None:
+            menu.append(submenu)
+
+    return menu
+
+
+def main():
+    """Entry point, run the tool."""
+    args = parse_args()
+    menu = generate_menu(args.dc_from, args.dc_to)
+
+    if args.task is not None:
+        # Run a single task in non-interactive mode
+        for item in menu.items[int(args.task[1:3]) - 1].items:
+            if item.name.split('.')[-1] == args.task:
+                item.run()
+                break
+    elif args.stage is not None:
+        # Run all tasks in a stage in non-interactive mode
+        for item in menu.items[int(args.stage) - 1].items:
+            item.run()
+    else:
+        # Run the interactive menu
+        run(menu, args.dc_from, args.dc_to)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..f4d0f4c
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length=120
+statistics = True

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

Gerrit-MessageType: merged
Gerrit-Change-Id: I6c64268e5a86be13afaa6d8185a6b1533fe71770
Gerrit-PatchSet: 4
Gerrit-Project: operations/switchdc
Gerrit-Branch: master
Gerrit-Owner: Volans <[email protected]>
Gerrit-Reviewer: Faidon Liambotis <[email protected]>
Gerrit-Reviewer: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: Volans <[email protected]>

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

Reply via email to