]> git-server-git.apps.pok.os.sepia.ceph.com Git - teuthology.git/commitdiff
provision: add Pelagos support 1372/head
authorRoman Grigoryev <Roman_Grigoryev@seagate.com>
Fri, 26 Jul 2019 14:41:55 +0000 (16:41 +0200)
committerRoman Grigoryev <rgrigorev@suse.de>
Mon, 2 Mar 2020 09:06:17 +0000 (10:06 +0100)
Add provisioning support via Pelagos provisioner
https://github.com/SUSE/pelagos/

Pelagos is pxe boot and provisioning system which created especially
for connecting bare metal nodes to ceph/teuthology testing system.
Integration tests here pelagos/test_pelagos_teuthology/test_pelagos.py
because depends on executed Pelagios service

For enabling pelagos you need add section to teuthology configuration:
    pelagos:
      endpoint: http://your.server.host:5000/
      machine_types: ['type1', 'type2', 'type3']

provision/pelagos.py: added support of Pelagos provisioning project,
                      interface is compatible with FOG provisioner
provision/__init.py: added processing of pelagos section in teuthology
                     configuration and provisioner instantiation
lock/*: added Pelagos provisioner instantiation
nuke/__init__.py: added call to pelagos module for nodes, which are
                   controlled by pelagos, for booting to live images.

Signed-off-by: Roman Grigorev <rgrigorev@suse.de>
docs/siteconfig.rst
teuthology/lock/cli.py
teuthology/lock/ops.py
teuthology/nuke/__init__.py
teuthology/provision/__init__.py
teuthology/provision/pelagos.py [new file with mode: 0644]
teuthology/provision/test/test_init_provision.py [new file with mode: 0644]
teuthology/provision/test/test_pelagos.py [new file with mode: 0644]

index 55b2e288aab68219c88261f23c940bef7488a028..1a66bd0e684988fdd1b73b66dadef0806ca9238d 100644 (file)
@@ -236,3 +236,9 @@ Here is a sample configuration with many of the options set and documented::
       api_token: your_api_token
       user_token: your_user_token
       machine_types: ['mira', 'smithi']
+
+    # FOG provisioner is default and switching to Pelgas
+    # should be made explicitly
+    pelagos:
+      endpoint: http://head.ses.suse.de:5000/
+      machine_types: ['type1', 'type2', 'type3']
index b6f6ed00057cdf1dfd2bcef75318c418b130cf31..b5adbb5a1541a89e605db9ef5508af10517420e3 100644 (file)
@@ -148,14 +148,16 @@ def main(ctx):
                 ctx.machine_type, ctx.os_type, ctx.os_version):
             log.error('Invalid os-type or version detected -- lock failed')
             return 1
-        reimage_types = teuthology.provision.fog.get_types()
+        reimage_types = teuthology.provision.get_reimage_types()
         reimage_machines = list()
         updatekeys_machines = list()
+        machine_types = dict()
         for machine in machines:
             resp = ops.lock_one(machine, user, ctx.desc)
             if resp.ok:
                 machine_status = resp.json()
                 machine_type = machine_status['machine_type']
+                machine_types[machine] = machine_type
             if not resp.ok:
                 ret = 1
                 if not ctx.f:
@@ -176,7 +178,7 @@ def main(ctx):
         with teuthology.parallel.parallel() as p:
             ops.update_nodes(reimage_machines, True)
             for machine in reimage_machines:
-                p.spawn(teuthology.provision.reimage, ctx, machine)
+                p.spawn(teuthology.provision.reimage, ctx, machine, machine_types[machine])
         for machine in updatekeys_machines:
             ops.do_update_keys([machine])
         ops.update_nodes(reimage_machines + machines_to_update)
index 3515cbf988b6ccb755d5cb2b94a14d894f2e976d..8ba87299955ce43e8f433f26161efda2686be6eb 100644 (file)
@@ -96,7 +96,7 @@ def lock_many(ctx, num, machine_type, user=None, description=None,
         # Only query for os_type/os_version if non-vps and non-libcloud, since
         # in that case we just create them.
         vm_types = ['vps'] + teuthology.provision.cloud.get_types()
-        reimage_types = teuthology.provision.fog.get_types()
+        reimage_types = teuthology.provision.get_reimage_types()
         if machine_type not in vm_types + reimage_types:
             if os_type:
                 data['os_type'] = os_type
@@ -140,7 +140,8 @@ def lock_many(ctx, num, machine_type, user=None, description=None,
                     update_nodes(reimaged, True)
                     with teuthology.parallel.parallel() as p:
                         for machine in machines:
-                            p.spawn(teuthology.provision.reimage, ctx, machine)
+                            p.spawn(teuthology.provision.reimage, ctx,
+                                    machine, machine_type)
                             reimaged[machine] = machines[machine]
                 reimaged = do_update_keys(reimaged.keys())[1]
                 update_nodes(reimaged)
index 8370e4d94fde5a489c47829fa81aadc642c8b928..e6d141ed83fcf557449189187eb58965259200c6 100644 (file)
@@ -318,6 +318,10 @@ def nuke_helper(ctx, should_unlock):
         remote = Remote(host)
         remote.console.power_off()
         return
+    elif status['machine_type'] in provision.pelagos.get_types():
+        provision.pelagos.park_node(host)
+        return
+
     if (not ctx.noipmi and 'ipmi_user' in config and
             'vpm' not in shortname):
         try:
index 555b7fdbe1526bcfd94889015eb10b35a6bd206d..325f2c34bf3eb7fc838d933a04b63fbddd780e39 100644 (file)
@@ -7,9 +7,9 @@ from teuthology.provision import cloud
 from teuthology.provision import downburst
 from teuthology.provision import fog
 from teuthology.provision import openstack
+from teuthology.provision import pelagos
 import os
 
-
 log = logging.getLogger(__name__)
 
 
@@ -18,12 +18,25 @@ def _logfile(ctx, shortname):
         return os.path.join(ctx.config['archive_path'],
                             shortname + '.downburst.log')
 
+def get_reimage_types():
+    return pelagos.get_types() + fog.get_types()
 
-def reimage(ctx, machine_name):
+def reimage(ctx, machine_name, machine_type):
     os_type = get_distro(ctx)
     os_version = get_distro_version(ctx)
-    fog_obj = fog.FOG(machine_name, os_type, os_version)
-    return fog_obj.create()
+
+    pelagos_types = pelagos.get_types()
+    fog_types = fog.get_types()
+    if machine_type in pelagos_types and machine_type in fog_types:
+        raise Exception('machine_type can be used with one provisioner only')
+    elif machine_type in pelagos_types:
+        obj = pelagos.Pelagos(machine_name, os_type, os_version)
+    elif machine_type in fog_types:
+        obj = fog.FOG(machine_name, os_type, os_version)
+    else:
+        raise Exception("The machine_type '%s' is not known to any "
+                        "of configured provisioners" % machine_type)
+    return obj.create()
 
 
 def create_if_vm(ctx, machine_name, _downburst=None):
diff --git a/teuthology/provision/pelagos.py b/teuthology/provision/pelagos.py
new file mode 100644 (file)
index 0000000..b0a275d
--- /dev/null
@@ -0,0 +1,162 @@
+
+import logging
+import requests
+import re
+import time
+
+from teuthology.config import config
+from teuthology.contextutil import safe_while
+from teuthology.util.compat import  HTTPError
+
+log = logging.getLogger(__name__)
+config_section = 'pelagos'
+
+# Provisioner configuration section description see in
+# docs/siteconfig.rst
+
+def enabled(warn=False):
+    """
+    Check for required Pelagos settings
+
+    :param warn: Whether or not to log a message containing unset parameters
+    :returns: True if they are present; False if they are not
+    """
+    conf = config.get(config_section, dict())
+    params = ['endpoint', 'machine_types']
+    unset = [_ for _ in params if not conf.get(_)]
+    if unset and warn:
+        log.warn(
+            "Pelagos is disabled; set the following config options to enable: %s",
+            ' '.join(unset),
+        )
+    return (unset == [])
+
+
+def get_types():
+    """
+    Fetch and parse config.pelagos['machine_types']
+
+    :returns: The list of Pelagos-configured machine types. An empty list if Pelagos is
+              not configured.
+    """
+    if not enabled():
+        return []
+    conf = config.get(config_section, dict())
+    types = conf.get('machine_types', '')
+    if not isinstance(types, list):
+        types = [_ for _ in types.split(',') if _]
+    return [_ for _ in types if _]
+
+def park_node(name):
+        p = Pelagos(name, "maintenance_image")
+        p.create()
+
+
+class Pelagos(object):
+
+    def __init__(self, name, os_type, os_version=""):
+        #for service should be a hostname, not a user@host
+        split_uri = re.search(r'(\w*)@(.+)', name)
+        if split_uri is not None:
+            self.name = split_uri.groups()[1]
+        else:
+            self.name = name
+
+        self.os_type = os_type
+        self.os_version = os_version
+        if os_version:
+            self.os_name = os_type + "-" + os_version
+        else:
+            self.os_name = os_type
+        self.log = log.getChild(self.name)
+
+    def create(self):
+        """
+        Initiate deployment via REST requests and wait until completion
+
+        """
+        if not enabled():
+            raise RuntimeError("Pelagos is not configured!")
+        location = None
+        try:
+            response = self.do_request('node/provision',
+                                      data={'os': self.os_name,
+                                            'node': self.name},
+                                      method='POST')
+            location = response.headers.get('Location')
+            self.log.info("Waiting for deploy to finish")
+            self.log.info("Observe location: '%s'", location)
+            time.sleep(2)
+            with safe_while(sleep=15, tries=60) as proceed:
+                while proceed():
+                    if not self.is_task_active(location):
+                        break
+        except Exception as e:
+            if location:
+                self.cancel_deploy_task(location)
+            else:
+                self.log.error("No task started")
+            raise e
+        self.log.info("Deploy completed")
+        if self.task_status_response.status_code != 200:
+            raise Exception("Provisioning failed")
+        return self.task_status_response
+
+    def cancel_deploy_task(self,  task_id):
+        # TODO implement it
+        return
+
+    def is_task_active(self, task_url):
+        try:
+            status_response = self.do_request('', url=task_url, verify=False)
+        except HTTPError as err:
+            self.log.error("Task fail reason: '%s'", err.reason)
+            if err.status_code == 404:
+                self.log.error(err.reason)
+                self.task_status_response = 'failed'
+                return False
+            else:
+                raise HTTPError(err.code, err.reason)
+            self.log.info("Response code '%s'", str(status_response.status_code))
+        self.task_status_response = status_response
+        if status_response.status_code == 202:
+            self.log.info("Status response: '%s'", status_response.headers['status'])
+            if status_response.headers['status'] == 'not completed':
+                return True
+        return False
+
+    def do_request(self, url_suffix, url="" , data=None, method='GET', verify=True):
+        """
+        A convenience method to submit a request to the Pelagos server
+        :param url_suffix: The portion of the URL to append to the endpoint,
+                           e.g.  '/system/info'
+        :param data: Optional JSON data to submit with the request
+        :param method: The HTTP method to use for the request (default: 'GET')
+        :param verify: Whether or not to raise an exception if the request is
+                       unsuccessful (default: True)
+        :returns: A requests.models.Response object
+        """
+        prepared_url = config.pelagos['endpoint'] + url_suffix
+        if url != '':
+            prepared_url = url
+        self.log.info("Connect to: '%s'", prepared_url)
+        if data is not None:
+            self.log.info("Send data: '%s'", str(data))
+        req = requests.Request(
+            method,
+            prepared_url,
+            data=data
+        )
+        prepared = req.prepare()
+        resp = requests.Session().send(prepared)
+        self.log.debug("do_request code %s text %s", resp.status_code, resp.text)
+        if not resp.ok and resp.text:
+            self.log.error("%s: %s", resp.status_code, resp.text)
+        if verify:
+            resp.raise_for_status()
+        return resp
+
+    def destroy(self):
+        """A no-op; we just leave idle nodes as-is"""
+        pass
+
diff --git a/teuthology/provision/test/test_init_provision.py b/teuthology/provision/test/test_init_provision.py
new file mode 100644 (file)
index 0000000..3903850
--- /dev/null
@@ -0,0 +1,46 @@
+from copy import deepcopy
+from pytest import raises
+from teuthology.config import config
+
+import teuthology.provision
+
+test_config = dict(
+    pelagos=dict(
+        endpoint='http://pelagos.example:5000/',
+        machine_types='ptype1,ptype2,common_type',
+    ),
+    fog=dict(
+        endpoint='http://fog.example.com/fog',
+        api_token='API_TOKEN',
+        user_token='USER_TOKEN',
+        machine_types='ftype1,ftype2,common_type',
+    )
+)
+
+class TestInitProvision(object):
+
+    def setup(self):
+        config.load(deepcopy(test_config))
+
+    def test_get_reimage_types(self):
+        reimage_types = teuthology.provision.get_reimage_types()
+        assert reimage_types == ["ptype1", "ptype2", "common_type",
+                                 "ftype1", "ftype2", "common_type"]
+
+    def test_reimage(self):
+        class context:
+            pass
+        ctx = context()
+        ctx.os_type = 'sle'
+        ctx.os_version = '15.1'
+        with raises(Exception) as e_info:
+            teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'not-defined-type')
+        e_str = str(e_info)
+        print("Caught exception: " +  e_str)
+        assert e_str.find("configured\sprovisioners") == -1
+
+        with raises(Exception) as e_info:
+            teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'common_type')
+        e_str = str(e_info)
+        print("Caught exception: " +  e_str)
+        assert e_str.find("used\swith\sone\sprovisioner\sonly") == -1
diff --git a/teuthology/provision/test/test_pelagos.py b/teuthology/provision/test/test_pelagos.py
new file mode 100644 (file)
index 0000000..a8969d4
--- /dev/null
@@ -0,0 +1,46 @@
+from copy import deepcopy
+from pytest import raises
+from teuthology.config import config
+from teuthology.provision import pelagos
+
+import teuthology.provision
+
+
+test_config = dict(
+    pelagos=dict(
+        endpoint='http://pelagos.example:5000/',
+        machine_types='ptype1,ptype2',
+    ),
+)
+
+class TestPelagos(object):
+
+    def setup(self):
+        config.load(deepcopy(test_config))
+
+    def teardown(self):
+        pass
+
+    def test_get_types(self):
+        #klass = pelagos.Pelagos
+        types = pelagos.get_types()
+        assert types == ["ptype1", "ptype2"]
+
+    def test_disabled(self):
+        config.pelagos['endpoint'] = None
+        enabled = pelagos.enabled()
+        assert enabled == False
+
+    def test_pelagos(self):
+            class context:
+                pass
+
+            ctx = context()
+            ctx.os_type ='sle'
+            ctx.os_version = '15.1'
+            with raises(Exception) as e_info:
+                teuthology.provision.reimage(ctx, 'f.q.d.n.org', 'ptype1')
+            e_str = str(e_info)
+            print("Caught exception: " +  e_str)
+            assert e_str.find("Name\sor\sservice\snot\sknown") == -1
+