diff --git a/fuel_ccp/config/__init__.py b/fuel_ccp/config/__init__.py index 0c3f6466..a2386acd 100644 --- a/fuel_ccp/config/__init__.py +++ b/fuel_ccp/config/__init__.py @@ -9,6 +9,7 @@ from fuel_ccp.config import cli from fuel_ccp.config import images from fuel_ccp.config import kubernetes from fuel_ccp.config import registry +from fuel_ccp.config import replicas from fuel_ccp.config import repositories LOG = logging.getLogger(__name__) @@ -57,7 +58,8 @@ def get_config_defaults(): 'verbose_level': 1, 'log_file': None, }) - for module in [cli, builder, images, kubernetes, registry, repositories]: + for module in [cli, builder, images, kubernetes, registry, replicas, + repositories]: defaults._merge(module.DEFAULTS) return defaults @@ -72,7 +74,8 @@ def get_config_schema(): 'log_file': {'anyOf': [{'type': 'null'}, {'type': 'string'}]}, }, } - for module in [cli, builder, images, kubernetes, registry, repositories]: + for module in [cli, builder, images, kubernetes, registry, replicas, + repositories]: schema['properties'].update(module.SCHEMA) # Don't validate all options used to be added from oslo.log and oslo.config ignore_opts = ['debug', 'verbose', 'log_file'] diff --git a/fuel_ccp/config/replicas.py b/fuel_ccp/config/replicas.py new file mode 100644 index 00000000..e17c1eea --- /dev/null +++ b/fuel_ccp/config/replicas.py @@ -0,0 +1,12 @@ +DEFAULTS = { +} + +SCHEMA = { + 'replicas': { + 'type': 'object', + "additionalProperties": { + "type": "integer", + "minimum": 1, + }, + }, +} diff --git a/fuel_ccp/deploy.py b/fuel_ccp/deploy.py index 4eef5c44..e59c6954 100644 --- a/fuel_ccp/deploy.py +++ b/fuel_ccp/deploy.py @@ -3,6 +3,7 @@ import json import logging import os import re +import six from fuel_ccp.common import jinja_utils from fuel_ccp.common import utils @@ -59,19 +60,21 @@ def _get_service_files_hash(service_dir, files, configs): def parse_role(service_dir, role, config): service = role["service"] - if service["name"] not in config.get("topology", {}): + service_name = service["name"] + + if service_name not in config.get("topology", {}): LOG.info("Service %s not in topology config, skipping deploy", - service["name"]) + service_name) return - LOG.info("Scheduling service %s deployment", service["name"]) + LOG.info("Scheduling service %s deployment", service_name) _expand_files(service, role.get("files")) files_cm = _create_files_configmap( - service_dir, service["name"], role.get("files")) + service_dir, service_name, role.get("files")) meta_cm = _create_meta_configmap(service) workflows = _parse_workflows(service) - workflow_cm = _create_workflow(workflows, service["name"]) + workflow_cm = _create_workflow(workflows, service_name) configmaps = config['configmaps'] + (files_cm, meta_cm, workflow_cm) if CONF.action.dry_run: @@ -91,16 +94,26 @@ def parse_role(service_dir, role, config): cont_spec = templates.serialize_daemon_pod_spec(service) affinity = templates.serialize_affinity(service, config["topology"]) + replicas = config.get("replicas", {}).get(service_name) if service.get("daemonset", False): - obj = templates.serialize_daemonset(service["name"], cont_spec, + if replicas is not None: + LOG.error("Replicas was specified for %s, but it's implemented " + "using Kubernetes DaemonSet that will deploy service on " + "all matching nodes (section 'nodes' in config file)", + service_name) + raise RuntimeError("Replicas couldn't be specified for services " + "implemented using Kubernetes DaemonSet") + + obj = templates.serialize_daemonset(service_name, cont_spec, affinity) else: - obj = templates.serialize_deployment(service["name"], cont_spec, - affinity) + replicas = replicas or 1 + obj = templates.serialize_deployment(service_name, cont_spec, + affinity, replicas) kubernetes.process_object(obj) _create_service(service) - LOG.info("Service %s successfuly scheduled", service["name"]) + LOG.info("Service %s successfuly scheduled", service_name) def _parse_workflows(service): @@ -291,7 +304,7 @@ def _create_meta_configmap(service): return kubernetes.process_object(template) -def _make_topology(nodes, roles): +def _make_topology(nodes, roles, replicas): failed = False # TODO(sreshetniak): move it to validation if not nodes: @@ -303,6 +316,9 @@ def _make_topology(nodes, roles): if failed: raise RuntimeError("Failed to create topology for services") + # Replicas are optional, 1 replica will deployed by default + replicas = replicas or dict() + # TODO(sreshetniak): add validation k8s_nodes = kubernetes.list_k8s_nodes() k8s_node_names = kubernetes.get_object_names(k8s_nodes) @@ -330,6 +346,26 @@ def _make_topology(nodes, roles): service_to_node[svc].extend(roles_to_node[role]) else: LOG.warning("Role '%s' defined, but unused", role) + + replicas = replicas.copy() + for svc, svc_hosts in six.iteritems(service_to_node): + svc_replicas = replicas.pop(svc, None) + + if svc_replicas is None: + continue + + svc_hosts_count = len(svc_hosts) + if svc_replicas > svc_hosts_count: + LOG.error("Requested %s replicas for %s while only %s hosts able " + "to run that service (%s)", svc_replicas, svc, + svc_hosts_count, ", ".join(svc_hosts)) + raise RuntimeError("Replicas doesn't match available hosts.") + + if replicas: + LOG.error("Replicas defined for unspecified service(s): %s", + ", ".join(replicas.keys())) + raise RuntimeError("Replicas defined for unspecified service(s)") + return service_to_node @@ -365,9 +401,11 @@ def deploy_components(components_map, components): if CONF.action.export_dir: os.makedirs(os.path.join(CONF.action.export_dir, 'configmaps')) - config = utils.get_global_parameters("configs", "nodes", "roles") + config = utils.get_global_parameters("configs", "nodes", "roles", + "replicas") config["topology"] = _make_topology(config.get("nodes"), - config.get("roles")) + config.get("roles"), + config.get("replicas")) namespace = CONF.kubernetes.namespace _create_namespace(namespace) diff --git a/fuel_ccp/templates.py b/fuel_ccp/templates.py index 6a0ea867..6a5d44bc 100644 --- a/fuel_ccp/templates.py +++ b/fuel_ccp/templates.py @@ -260,7 +260,7 @@ def serialize_job(name, spec): } -def serialize_deployment(name, spec, affinity): +def serialize_deployment(name, spec, affinity, replicas): return { "apiVersion": "extensions/v1beta1", "kind": "Deployment", @@ -268,7 +268,7 @@ def serialize_deployment(name, spec, affinity): "name": name }, "spec": { - "replicas": 1, + "replicas": replicas, "strategy": { "rollingUpdate": { "maxSurge": 1, diff --git a/fuel_ccp/tests/test_deploy.py b/fuel_ccp/tests/test_deploy.py index e52ac81c..9251bb67 100644 --- a/fuel_ccp/tests/test_deploy.py +++ b/fuel_ccp/tests/test_deploy.py @@ -321,20 +321,11 @@ class TestDeployMakeTopology(base.TestCase): self.useFixture( fixtures.MockPatch("fuel_ccp.kubernetes.list_k8s_nodes")) - def test_make_empty_topology(self): - self.assertRaises(RuntimeError, - deploy._make_topology, None, None) - self.assertRaises(RuntimeError, - deploy._make_topology, None, {"spam": "eggs"}) - self.assertRaises(RuntimeError, - deploy._make_topology, {"spam": "eggs"}, None) - - def test_make_topology(self): node_list = ["node1", "node2", "node3"] self.useFixture(fixtures.MockPatch( "fuel_ccp.kubernetes.get_object_names", return_value=node_list)) - roles = { + self._roles = { "controller": [ "mysql", "keystone" @@ -345,6 +336,15 @@ class TestDeployMakeTopology(base.TestCase): ] } + def test_make_empty_topology(self): + self.assertRaises(RuntimeError, + deploy._make_topology, None, None, None) + self.assertRaises(RuntimeError, + deploy._make_topology, None, {"spam": "eggs"}, None) + self.assertRaises(RuntimeError, + deploy._make_topology, {"spam": "eggs"}, None, None) + + def test_make_topology_without_replicas(self): nodes = { "node1": { "roles": ["controller"] @@ -361,10 +361,10 @@ class TestDeployMakeTopology(base.TestCase): "libvirtd": ["node2", "node3"] } - topology = deploy._make_topology(nodes, roles) + topology = deploy._make_topology(nodes, self._roles, None) self.assertDictEqual(expected_topology, topology) - # check if role is defined but not used + def test_make_topology_without_replicas_unused_role(self): nodes = { "node1": { "roles": ["controller"] @@ -375,11 +375,11 @@ class TestDeployMakeTopology(base.TestCase): "mysql": ["node1"], "keystone": ["node1"] } - topology = deploy._make_topology(nodes, roles) + + topology = deploy._make_topology(nodes, self._roles, None) self.assertDictEqual(expected_topology, topology) - # two ways to define topology that should give the same result - # first + def test_make_topology_without_replicas_twice_used_role(self): nodes = { "node1": { "roles": ["controller", "compute"] @@ -395,10 +395,10 @@ class TestDeployMakeTopology(base.TestCase): "nova-compute": ["node1", "node2", "node3"], "libvirtd": ["node1", "node2", "node3"] } - topology = deploy._make_topology(nodes, roles) + topology = deploy._make_topology(nodes, self._roles, None) self.assertDictEqual(expected_topology, topology) - # second + def test_make_topology_without_replicas_twice_used_node(self): nodes = { "node1": { "roles": ["controller"] @@ -414,5 +414,30 @@ class TestDeployMakeTopology(base.TestCase): "nova-compute": ["node1", "node2", "node3"], "libvirtd": ["node1", "node2", "node3"] } - topology = deploy._make_topology(nodes, roles) + + topology = deploy._make_topology(nodes, self._roles, None) self.assertDictEqual(expected_topology, topology) + + def test_make_topology_replicas_bigger_than_nodes(self): + replicas = { + "keystone": 2 + } + + nodes = { + "node1": { + "roles": ["controller"] + } + } + + self.assertRaises(RuntimeError, + deploy._make_topology, nodes, self._roles, replicas) + + def test_make_topology_unspecified_service_replicas(self): + replicas = { + "foobar": 42 + } + + nodes = {} + + self.assertRaises(RuntimeError, + deploy._make_topology, nodes, self._roles, replicas)