diff --git a/bareon_dynamic_allocator/allocators.py b/bareon_dynamic_allocator/allocators.py new file mode 100644 index 0000000..cddbf3b --- /dev/null +++ b/bareon_dynamic_allocator/allocators.py @@ -0,0 +1,170 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See then +# License for the specific language governing permissions and limitations +# under the License. + +import six + +from scipy.optimize import linprog +from scipy.ndimage.interpolation import shift + +import numpy as np +from numpy import array +from numpy import zeros +from numpy import append +from numpy import vstack +from numpy import concatenate +from numpy import put +from numpy import roll + + +def shift(arr, steps, val=0): + res_arr = roll(arr, steps) + put(res_arr, range(steps), val) + + return res_arr + + +class Disk(object): + + def __init__(self, **kwargs): + for k, v in six.iteritems(kwargs): + setattr(self, k, v) + + +class Space(object): + + def __init__(self, **kwargs): + for k, v in six.iteritems(kwargs): + setattr(self, k, v) + + +class DynamicAllocator(object): + + def __init__(self, hw_info, schema): + self.disks = [Disk(**disk) for disk in hw_info['disks']] + self.spaces = [Space(**space) for space in schema] + + # Add fake volume Unallocated, in order to be able + # to have only volumes with minimal size, without + # additional space allocation + self.lp = DynamicAllocationLinearProgram(self.disks, self.spaces) + + def generate_static(self): + sizes = self.lp.solve() + + return sizes + + +class DynamicAllocationLinearProgram(object): + """Use Linear Programming method [0] (the method itself has nothing to do + with computer-programming) in order to formulate and solve the problem + of spaces allocation on disks, with the best outcome. + + In this implementation scipy is being used since it already implements + simplex algorithm to find the best feasible solution. + + [0] https://en.wikipedia.org/wiki/Linear_programming + [1] http://docs.scipy.org/doc/scipy-0.16.0/reference/generated + /scipy.optimize.linprog.html + [2] https://en.wikipedia.org/wiki/Simplex_algorithm + """ + + def __init__(self, disks, spaces): + # Coefficients of the linear objective minimization function. + # During iteration over vertexes the function is used to identify + # if current solution (vertex) satisfies the equation more, than + # previous one. + # Example of equation: c[0]*x1 + c[1]*x2 + self.objective_function_coefficients = [] + + # A matrix which, gives the values of the equality constraints at x, + # when multipled by x. + self.equality_constraint_matrix = [] + + # An array of values representing right side of equation, + # left side is represented by row of `equality_constraint_matrix` + # matrix + self.equality_constraint_vector = array([]) + + # Specify boundaries of each x in the next format (min, max). Use + # None for one of min or max when there is no bound. + self.bounds = array([]) + + self._initialize_equation(disks, spaces) + + print '*' * 30 + print self.equality_constraint_matrix + print self.objective_function_coefficients + + def solve(self): + solution = linprog( + self.objective_function_coefficients, + A_eq=self.equality_constraint_matrix, + b_eq=self.equality_constraint_vector, + bounds=self.bounds, + options={"disp": True}) + + return solution.x + + def _initialize_equation(self, disks, spaces): + for d in disks: + # Initialize constraints, each row in the matrix should + # be equal to size of the disk + self.equality_constraint_vector = append(self.equality_constraint_vector, d.size) + + # Initialize the matrix + # In case of 2 spaces and 3 disks the result should be: + # [[1, 1, 0, 0, 0, 0], + # [0, 0, 1, 1, 0, 0], + # [0, 0, 0, 0, 1, 1]] + # + # Explanation of the first row + # [1, - x1 multiplier, size of space 1 on the first disk + # 1, - x2 multiplier, size of space 2 on the first disk + # 0, - x3 multiplier, size of space 1 on 2nd disk, 0 for the first + # 0, - x4 multiplier, size of space 2 on 2nd disk, 0 for the first + # 0, - x5 multiplier, size of space 1 on 3rd disk, 0 for the first + # 0] - x6 multiplier, size of space 2 on 3rd disk, 0 for the first + + # For each space x (size of the space) is represented + # for each disk as separate variable, so for each + # disk we have len(spaces) * len(disks) sizes + equality_matrix_row = zeros(len(spaces) * len(disks)) + self._init_objective_function_coefficient(len(spaces) * len(disks)) + + # Set first len(spaces) elements to 1 + equality_matrix_row = shift(equality_matrix_row, len(spaces), val=1) + + for _ in range(len(disks)): + self.equality_constraint_matrix.append(equality_matrix_row) + equality_matrix_row = shift(equality_matrix_row, len(spaces), val=0) + + def _add_disk(self): + pass + + def _add_space(self): + pass + + def _add_objective_function_coefficient(self): + # By default the algorithm tries to minimize the solution + # we should invert sign, in order to make it as a maximization + # function, we want disks to be maximally allocated. + # Coefficient for space per disk is 1, because all spaces + # are equal and should not be adjusted. + self.objective_function_coefficients.append(-1) + + def _init_objective_function_coefficient(self, size): + self.objective_function_coefficients = [-1] * size + + def _add_bound(self, min_, max_): + append(self.bounds.append, (min_, max_)) diff --git a/bareon_dynamic_allocator/cmd.py b/bareon_dynamic_allocator/cmd.py new file mode 100644 index 0000000..c3cb030 --- /dev/null +++ b/bareon_dynamic_allocator/cmd.py @@ -0,0 +1,101 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See then +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +from oslo_config import cfg +from oslo_log import log + +from bareon_dynamic_allocator import utils +from bareon_dynamic_allocator.allocators import DynamicAllocator + + +cli_opts = [ + cfg.StrOpt( + 'schema', + required=True, + help='Input file, path to a file with dynamic partitioning schema' + ), + cfg.StrOpt( + 'file', + required=True, + help='Output path to a file with static partitioning schema' + ), + cfg.StrOpt( + 'hw-info', + required=True, + help='Hardware information' + ) +] + + +def make_config(): + conf = cfg.ConfigOpts() + + conf.register_cli_opts(cli_opts) + log.register_options(conf) + + return conf + + +def parse_args(conf, args=None): + project = 'bareon_dynamic_allocator' + version = '1.0.0' + conf(args=args if args else sys.argv[1:], + project=project, + version=version) + log.setup(conf, + project, + version=version) + + +CONF = make_config() +parse_args(CONF) +LOG = log.getLogger() + + +def parse_configs(conf): + hw_info = utils.parse_yaml(conf.hw_info) + schema = utils.parse_yaml(conf.schema) + + return (hw_info, schema) + + +def save_result(data, output_file): + print data + + +def validate_schema(schema): + # TODO should be implemented + return schema + + +def validate_hw_info(hw_info): + # TODO should be implemented + return hw_info + + +def allocator(): + LOG.debug('hi') + conf = parse_configs(CONF) + validate_schema(conf[0]) + validate_hw_info(conf[1]) + + schema = DynamicAllocator(*conf).generate_static() + + save_result(schema, CONF.file) + + +if __name__ == '__main__': + allocator() diff --git a/bareon_dynamic_allocator/utils.py b/bareon_dynamic_allocator/utils.py new file mode 100644 index 0000000..a2d91f2 --- /dev/null +++ b/bareon_dynamic_allocator/utils.py @@ -0,0 +1,23 @@ +# Copyright 2015 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See then +# License for the specific language governing permissions and limitations +# under the License. + +import yaml + + +def parse_yaml(path): + """Parses yaml file + :param str path: path to the file + :returns: dict or list + """ + return yaml.load(open(path))