From: Roman Grigoryev Date: Fri, 26 Jul 2019 14:41:55 +0000 (+0200) Subject: provision: add Pelagos support X-Git-Tag: 1.1.0~149^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=d5c545308a54fdec7b8903f6f66f8277caac49a7;p=teuthology.git provision: add Pelagos support 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 --- diff --git a/docs/siteconfig.rst b/docs/siteconfig.rst index 55b2e288a..1a66bd0e6 100644 --- a/docs/siteconfig.rst +++ b/docs/siteconfig.rst @@ -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'] diff --git a/teuthology/lock/cli.py b/teuthology/lock/cli.py index b6f6ed000..b5adbb5a1 100644 --- a/teuthology/lock/cli.py +++ b/teuthology/lock/cli.py @@ -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) diff --git a/teuthology/lock/ops.py b/teuthology/lock/ops.py index 3515cbf98..8ba872999 100644 --- a/teuthology/lock/ops.py +++ b/teuthology/lock/ops.py @@ -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) diff --git a/teuthology/nuke/__init__.py b/teuthology/nuke/__init__.py index 8370e4d94..e6d141ed8 100644 --- a/teuthology/nuke/__init__.py +++ b/teuthology/nuke/__init__.py @@ -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: diff --git a/teuthology/provision/__init__.py b/teuthology/provision/__init__.py index 555b7fdbe..325f2c34b 100644 --- a/teuthology/provision/__init__.py +++ b/teuthology/provision/__init__.py @@ -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 index 000000000..b0a275d85 --- /dev/null +++ b/teuthology/provision/pelagos.py @@ -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 index 000000000..390385037 --- /dev/null +++ b/teuthology/provision/test/test_init_provision.py @@ -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 index 000000000..a8969d4b4 --- /dev/null +++ b/teuthology/provision/test/test_pelagos.py @@ -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 +