From ce5851c073308f4ccc0fe488a226008518a5f401 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Wed, 27 May 2015 15:24:08 +0200 Subject: [PATCH 01/10] Add entities for commited/staging pipeline - remove deployment module --- cli.py | 2 +- config.yaml | 4 ++ solar/solar/state.py | 91 ++++++++++++++++++++++++++++++++++++++++++++ state/commit_log | 0 state/commited_data | 0 state/inprogress_log | 0 state/stage_log | 0 state/staged_data | 0 8 files changed, 96 insertions(+), 1 deletion(-) mode change 100644 => 100755 cli.py create mode 100644 solar/solar/state.py create mode 100644 state/commit_log create mode 100644 state/commited_data create mode 100644 state/inprogress_log create mode 100644 state/stage_log create mode 100644 state/staged_data diff --git a/cli.py b/cli.py old mode 100644 new mode 100755 index ccc30c8b..9f8b5a6a --- a/cli.py +++ b/cli.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python import click import json #import matplotlib @@ -8,7 +9,6 @@ import os import subprocess from solar.core import actions as xa -from solar.core import deployment as xd from solar.core import resource as xr from solar.core import signals as xs diff --git a/config.yaml b/config.yaml index d52c879b..3af564ca 100644 --- a/config.yaml +++ b/config.yaml @@ -10,5 +10,9 @@ file-system-db: storage-path: /vagrant/tmp/storage template-dir: /vagrant/templates + resources-files-mask: /vagrant/resources/*/*.yaml node_resource_template: /vagrant/resources/ro_node/ + +state: /vagrant/state/ + diff --git a/solar/solar/state.py b/solar/solar/state.py new file mode 100644 index 00000000..148509ec --- /dev/null +++ b/solar/solar/state.py @@ -0,0 +1,91 @@ +# 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 the +# License for the specific language governing permissions and limitations +# under the License. + +import collections +from collections import deque + +from solar import utils + +import dictdiffer as dd + + +def create(resource, staged_data, commited_data): + """ + :param resource: Resource obj + :param staged_data: dict + :param commited_data: dict + """ + diff = dd.diff( + staged_data[resource.uid], commited_data[resource.uid]) + return LogItem(utils.generate_uuid(), resource.uid, diff) + + +class LogItem(object): + + def __init__(self, uid, res_uid, diff): + self.uid = uid + self.res = res_uid + self.diff = diff + + def to_yaml(self): + return utils.yaml_dump(self.to_dict()) + + def to_dict(self): + return {'uid': self.uid, + 'res': self.res_uid, + 'diff': self.diff} + + def __str__(self): + return self.to_yaml() + + def __repr__(self): + return self.to_yaml() + + +class Log(object): + + def __init__(self, path): + self.path = path + self.items = deque([LogItem(**l) for + l in utils.yaml_load(path)]) + + def add(self, logitem): + self.items.append(logitem) + utils.yaml_dump_to(self.items, path) + + def popleft(self): + item = self.items.popleft() + utils.yaml_dump_to(self.items, path) + return item + + def __repr__(self): + return 'Log({0})'.format(self.path) + + +class Data(collections.MutableMapping): + + def __init__(self, path): + self.path = path + self.store = utils.yaml_load(path) + + def __getitem__(self, key): + return self.store[key] + + def __setitem__(self, key, value): + self.store[key] = value + utils.yaml_dump_to(self.store, path) + + def __delitem__(self, key): + self.store.pop(key) + utils.yaml_dump_to(self.store, path) diff --git a/state/commit_log b/state/commit_log new file mode 100644 index 00000000..e69de29b diff --git a/state/commited_data b/state/commited_data new file mode 100644 index 00000000..e69de29b diff --git a/state/inprogress_log b/state/inprogress_log new file mode 100644 index 00000000..e69de29b diff --git a/state/stage_log b/state/stage_log new file mode 100644 index 00000000..e69de29b diff --git a/state/staged_data b/state/staged_data new file mode 100644 index 00000000..e69de29b From 3b56aab14214d77e9bfdae98ef73fcca6cef18c9 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Fri, 29 May 2015 10:07:41 +0200 Subject: [PATCH 02/10] Add staging procedure --- solar/solar/core/signals.py | 3 +-- solar/solar/operations.py | 50 +++++++++++++++++++++++++++++++++++++ solar/solar/state.py | 15 ----------- 3 files changed, 51 insertions(+), 17 deletions(-) create mode 100644 solar/solar/operations.py diff --git a/solar/solar/core/signals.py b/solar/solar/core/signals.py index 79bff180..5afbb72a 100644 --- a/solar/solar/core/signals.py +++ b/solar/solar/core/signals.py @@ -93,8 +93,7 @@ def guess_mapping(emitter, receiver): def connect(emitter, receiver, mapping=None): - guessed = guess_mapping(emitter, receiver) - mapping = mapping or guessed + mapping = mapping or guess_mapping(emitter, receiver) for src, dst in mapping.items(): # Disconnect all receiver inputs diff --git a/solar/solar/operations.py b/solar/solar/operations.py new file mode 100644 index 00000000..0cfba81c --- /dev/null +++ b/solar/solar/operations.py @@ -0,0 +1,50 @@ + + +from solar import state +from solar.core import signals +from solar.core import resource + +from dictdiffer import diff +import networkx as nx + + +def connections(res, graph): + + for pred in graph.predecessors(res.name): + edge = graph.get_edge_edge(pred, res.name) + if ':' in edge['label']: + parent, child = edge['label'].split(':') + yield pred, res.name, {parent: child} + else: + yield pred, res.name, {edge['label']: edge['label']} + + +def to_dict(resource, graph): + return {'uid': resource.name, + 'path': resource.dest_path, + 'meta': resource.metadata, + 'tags': resource.tags, + 'args': resource.args_dict(), + 'connections': connections(resource, graph)} + + +def stage_changes(): + resources = resource.load_all() + conn_graph = signals.detailed_connection_graph() + + commited = state.CD() + log = state.SL() + + for res_uid in nx.topological_sort(conn_graph): + commited_data = commited.get(res_uid, {}) + staged_data = to_dict(resources[res_uid], conn_graph) + df = diff(commited_data, staged_data) + + if df: + log_item = state.LogItem( + utils.generate_uuid(), + res_uid, + df) + log.add(log_item) + return log + diff --git a/solar/solar/state.py b/solar/solar/state.py index 148509ec..8614ebb9 100644 --- a/solar/solar/state.py +++ b/solar/solar/state.py @@ -15,21 +15,6 @@ import collections from collections import deque -from solar import utils - -import dictdiffer as dd - - -def create(resource, staged_data, commited_data): - """ - :param resource: Resource obj - :param staged_data: dict - :param commited_data: dict - """ - diff = dd.diff( - staged_data[resource.uid], commited_data[resource.uid]) - return LogItem(utils.generate_uuid(), resource.uid, diff) - class LogItem(object): From 36e3b240f9c9923e9ce80799b70ea0cb1d502e60 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Fri, 29 May 2015 12:19:58 +0200 Subject: [PATCH 03/10] Shortcut for commiting log items --- .gitignore | 4 +++ cli.py | 24 ++++++++++++++ requirements.txt | 2 ++ solar/solar/core/resource.py | 5 ++- solar/solar/operations.py | 41 +++++++++++++++++------- solar/solar/state.py | 62 ++++++++++++++++++++++++++++++------ 6 files changed, 115 insertions(+), 23 deletions(-) diff --git a/.gitignore b/.gitignore index 043c504a..596446eb 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ tmp/ #vim *.swp + +state/ +clients.json +rs/ diff --git a/cli.py b/cli.py index 9f8b5a6a..1ad50520 100755 --- a/cli.py +++ b/cli.py @@ -11,6 +11,7 @@ import subprocess from solar.core import actions as xa from solar.core import resource as xr from solar.core import signals as xs +from solar import operations @click.group() @@ -137,6 +138,28 @@ def init_cli_connect(): cli.add_command(disconnect) +def init_changes(): + @click.group() + def changes(): + pass + + cli.add_command(changes) + + @click.command() + @click.argument('path') + def stage(path): + log = operations.stage_changes(path) + print log.show() + + changes.add_command(stage) + + @click.command() + def commit(): + operations.commit_changes() + + changes.add_command(commit) + + def init_cli_connections(): @click.group() def connections(): @@ -185,5 +208,6 @@ if __name__ == '__main__': init_cli_connect() init_cli_connections() init_cli_deployment_config() + init_changes() cli() diff --git a/requirements.txt b/requirements.txt index a13f9914..de631f81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ PyYAML==3.11 jsonschema==2.4.0 requests==2.7.0 mock +dictdiffer==0.4.0 +enum34==1.0.4 diff --git a/solar/solar/core/resource.py b/solar/solar/core/resource.py index 70215007..68d2a0aa 100644 --- a/solar/solar/core/resource.py +++ b/solar/solar/core/resource.py @@ -4,6 +4,7 @@ import os from copy import deepcopy +<<<<<<< HEAD import yaml import solar @@ -118,6 +119,8 @@ class Resource(object): metadata['input'][k]['value'] = v db.add_resource(self.name, metadata) + meta_file = os.path.join(self.base_dir, 'meta.yaml') + utils.yaml_dump_to(metadata, meta_file) def create(name, base_path, args, tags=[], connections={}): @@ -127,7 +130,7 @@ def create(name, base_path, args, tags=[], connections={}): base_meta_file = os.path.join(base_path, 'meta.yaml') actions_path = os.path.join(base_path, 'actions') - meta = yaml.load(open(base_meta_file).read()) + meta = utils.yaml_load(base_meta_file) meta['id'] = name meta['version'] = '1.0.0' meta['actions'] = {} diff --git a/solar/solar/operations.py b/solar/solar/operations.py index 0cfba81c..078b0b81 100644 --- a/solar/solar/operations.py +++ b/solar/solar/operations.py @@ -3,33 +3,38 @@ from solar import state from solar.core import signals from solar.core import resource +from solar import utils -from dictdiffer import diff +from dictdiffer import diff, patch import networkx as nx def connections(res, graph): - + result = [] for pred in graph.predecessors(res.name): - edge = graph.get_edge_edge(pred, res.name) - if ':' in edge['label']: - parent, child = edge['label'].split(':') - yield pred, res.name, {parent: child} + edge = graph.get_edge_data(pred, res.name) + if 'label' in edge: + if ':' in edge['label']: + parent, child = edge['label'].split(':') + mapping = {parent: child} + else: + mapping = {edge['label']: edge['label']} else: - yield pred, res.name, {edge['label']: edge['label']} + mapping = None + result.append((pred, res.name, mapping)) + return result def to_dict(resource, graph): return {'uid': resource.name, - 'path': resource.dest_path, - 'meta': resource.metadata, + 'path': resource.base_dir, 'tags': resource.tags, 'args': resource.args_dict(), 'connections': connections(resource, graph)} -def stage_changes(): - resources = resource.load_all() +def stage_changes(path): + resources = resource.load_all(path) conn_graph = signals.detailed_connection_graph() commited = state.CD() @@ -38,7 +43,7 @@ def stage_changes(): for res_uid in nx.topological_sort(conn_graph): commited_data = commited.get(res_uid, {}) staged_data = to_dict(resources[res_uid], conn_graph) - df = diff(commited_data, staged_data) + df = list(diff(commited_data, staged_data)) if df: log_item = state.LogItem( @@ -48,3 +53,15 @@ def stage_changes(): log.add(log_item) return log + +def commit_changes(): + # just shortcut to test stuff + commited = state.CD() + history = state.CL() + staged = state.SL() + + while staged.items: + l = staged.popleft() + commited[l.res] = patch(commited.get(l.res, {}), l.diff) + l.state = state.states.success + history.add(l) diff --git a/solar/solar/state.py b/solar/solar/state.py index 8614ebb9..66ca6617 100644 --- a/solar/solar/state.py +++ b/solar/solar/state.py @@ -12,24 +12,50 @@ # License for the specific language governing permissions and limitations # under the License. +import os import collections from collections import deque +from functools import partial + +from solar import utils + +from enum import Enum + + +states = Enum('States', 'pending inprogress error success') + + +def state_file(filename): + filepath = os.path.join(utils.read_config()['state'], filename) + if 'log' in filename: + return Log(filepath) + elif 'data' in filename: + return Data(filepath) + + +CD = partial(state_file, 'commited_data') +SD = partial(state_file, 'staged_data') +SL = partial(state_file, 'stage_log') +IL = partial(state_file, 'inprogress_log') +CL = partial(state_file, 'commit_log') class LogItem(object): - def __init__(self, uid, res_uid, diff): + def __init__(self, uid, res_uid, diff, state=None): self.uid = uid self.res = res_uid self.diff = diff + self.state = state or states.pending def to_yaml(self): return utils.yaml_dump(self.to_dict()) def to_dict(self): return {'uid': self.uid, - 'res': self.res_uid, - 'diff': self.diff} + 'res': self.res, + 'diff': self.diff, + 'state': self.state.name} def __str__(self): return self.to_yaml() @@ -42,18 +68,27 @@ class Log(object): def __init__(self, path): self.path = path - self.items = deque([LogItem(**l) for - l in utils.yaml_load(path)]) + items = utils.yaml_load(path) or [] + self.items = deque([LogItem( + l['uid'], l['res'], + l['diff'], getattr(states, l['state'])) for l in items]) + + def sync(self): + utils.yaml_dump_to([i.to_dict() for i in self.items], self.path) def add(self, logitem): self.items.append(logitem) - utils.yaml_dump_to(self.items, path) + self.sync() def popleft(self): item = self.items.popleft() - utils.yaml_dump_to(self.items, path) + self.sync() return item + def show(self, verbose=False): + return ['L(uuid={0}, res={1})'.format(l.uid, l.res) + for l in self.items] + def __repr__(self): return 'Log({0})'.format(self.path) @@ -62,15 +97,22 @@ class Data(collections.MutableMapping): def __init__(self, path): self.path = path - self.store = utils.yaml_load(path) + self.store = utils.yaml_load(path) or {} def __getitem__(self, key): return self.store[key] def __setitem__(self, key, value): self.store[key] = value - utils.yaml_dump_to(self.store, path) + utils.yaml_dump_to(self.store, self.path) def __delitem__(self, key): self.store.pop(key) - utils.yaml_dump_to(self.store, path) + utils.yaml_dump_to(self.store, self.path) + + def __iter__(self): + return iter(self.store) + + def __len__(self): + return len(self.store) + From b190f780bdd8da24e8a84ee47363046e39b91175 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Fri, 29 May 2015 12:48:36 +0200 Subject: [PATCH 04/10] Several stylistic changes --- solar/solar/core/resource.py | 1 - solar/solar/operations.py | 5 +++-- solar/solar/state.py | 12 +++++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/solar/solar/core/resource.py b/solar/solar/core/resource.py index 68d2a0aa..8d3f7e5e 100644 --- a/solar/solar/core/resource.py +++ b/solar/solar/core/resource.py @@ -4,7 +4,6 @@ import os from copy import deepcopy -<<<<<<< HEAD import yaml import solar diff --git a/solar/solar/operations.py b/solar/solar/operations.py index 078b0b81..f3fad368 100644 --- a/solar/solar/operations.py +++ b/solar/solar/operations.py @@ -60,8 +60,9 @@ def commit_changes(): history = state.CL() staged = state.SL() - while staged.items: + while staged: l = staged.popleft() + commited[l.res] = patch(commited.get(l.res, {}), l.diff) - l.state = state.states.success + l.state = state.STATES.success history.add(l) diff --git a/solar/solar/state.py b/solar/solar/state.py index 66ca6617..9a0d06b6 100644 --- a/solar/solar/state.py +++ b/solar/solar/state.py @@ -22,7 +22,7 @@ from solar import utils from enum import Enum -states = Enum('States', 'pending inprogress error success') +STATES = Enum('States', 'pending inprogress error success') def state_file(filename): @@ -46,7 +46,7 @@ class LogItem(object): self.uid = uid self.res = res_uid self.diff = diff - self.state = state or states.pending + self.state = state or STATES.pending def to_yaml(self): return utils.yaml_dump(self.to_dict()) @@ -71,7 +71,7 @@ class Log(object): items = utils.yaml_load(path) or [] self.items = deque([LogItem( l['uid'], l['res'], - l['diff'], getattr(states, l['state'])) for l in items]) + l['diff'], getattr(STATES, l['state'])) for l in items]) def sync(self): utils.yaml_dump_to([i.to_dict() for i in self.items], self.path) @@ -92,6 +92,12 @@ class Log(object): def __repr__(self): return 'Log({0})'.format(self.path) + def __iter__(self): + return iter(self.items) + + def __nonzero__(self): + return bool(self.items) + class Data(collections.MutableMapping): From 75766f132e67884ea9976ab56fba3b695e5eef32 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Fri, 29 May 2015 17:52:32 +0200 Subject: [PATCH 05/10] Rollback support by uid and last one --- cli.py | 24 +++++++++-- solar/solar/core/resource.py | 2 - solar/solar/operations.py | 79 ++++++++++++++++++++++++++++-------- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/cli.py b/cli.py index 1ad50520..afea1f66 100755 --- a/cli.py +++ b/cli.py @@ -12,6 +12,7 @@ from solar.core import actions as xa from solar.core import resource as xr from solar.core import signals as xs from solar import operations +from solar import state @click.group() @@ -146,9 +147,8 @@ def init_changes(): cli.add_command(changes) @click.command() - @click.argument('path') - def stage(path): - log = operations.stage_changes(path) + def stage(): + log = operations.stage_changes() print log.show() changes.add_command(stage) @@ -159,6 +159,24 @@ def init_changes(): changes.add_command(commit) + @click.command() + @click.option('--limit', default=5) + def history(limit): + print state.CL().show() + + changes.add_command(history) + + @click.command() + @click.option('--last', is_flag=True, default=False) + @click.option('--uid', default=None) + def rollback(last, uid): + if last: + print operations.rollback_last() + elif uid: + print operations.rollback_uid(uid) + + changes.add_command(rollback) + def init_cli_connections(): @click.group() diff --git a/solar/solar/core/resource.py b/solar/solar/core/resource.py index 8d3f7e5e..46fbd0d1 100644 --- a/solar/solar/core/resource.py +++ b/solar/solar/core/resource.py @@ -118,8 +118,6 @@ class Resource(object): metadata['input'][k]['value'] = v db.add_resource(self.name, metadata) - meta_file = os.path.join(self.base_dir, 'meta.yaml') - utils.yaml_dump_to(metadata, meta_file) def create(name, base_path, args, tags=[], connections={}): diff --git a/solar/solar/operations.py b/solar/solar/operations.py index f3fad368..32511d62 100644 --- a/solar/solar/operations.py +++ b/solar/solar/operations.py @@ -4,37 +4,40 @@ from solar import state from solar.core import signals from solar.core import resource from solar import utils +from solar.interfaces.db import get_db -from dictdiffer import diff, patch +db = get_db() + +from dictdiffer import diff, patch, revert import networkx as nx + def connections(res, graph): result = [] for pred in graph.predecessors(res.name): - edge = graph.get_edge_data(pred, res.name) - if 'label' in edge: - if ':' in edge['label']: - parent, child = edge['label'].split(':') - mapping = {parent: child} + for num, edge in graph.get_edge_data(pred, res.name).items(): + if 'label' in edge: + if ':' in edge['label']: + parent, child = edge['label'].split(':') + mapping = [parent, child] + else: + mapping = [edge['label'], edge['label']] else: - mapping = {edge['label']: edge['label']} - else: - mapping = None - result.append((pred, res.name, mapping)) + mapping = None + result.append([pred, res.name, mapping]) return result def to_dict(resource, graph): return {'uid': resource.name, - 'path': resource.base_dir, 'tags': resource.tags, 'args': resource.args_dict(), 'connections': connections(resource, graph)} -def stage_changes(path): - resources = resource.load_all(path) +def stage_changes(): + resources = resource.load_all() conn_graph = signals.detailed_connection_graph() commited = state.CD() @@ -43,8 +46,15 @@ def stage_changes(path): for res_uid in nx.topological_sort(conn_graph): commited_data = commited.get(res_uid, {}) staged_data = to_dict(resources[res_uid], conn_graph) - df = list(diff(commited_data, staged_data)) + if 'connections' in commited_data: + commited_data['connections'].sort() + staged_data['connections'].sort() + if 'tags' in commited_data: + commited_data['tags'].sort() + staged_data['tags'].sort() + + df = list(diff(commited_data, staged_data)) if df: log_item = state.LogItem( utils.generate_uuid(), @@ -62,7 +72,44 @@ def commit_changes(): while staged: l = staged.popleft() - - commited[l.res] = patch(commited.get(l.res, {}), l.diff) + commited[l.res] = patch(l.diff, commited.get(l.res, {})) l.state = state.STATES.success history.add(l) + + +def rollback(log_item): + log = state.SL() + + resources = resource.load_all() + commited = state.CD()[log_item.res] + + staged = revert(log_item.diff, commited) + + for e, r, mapping in commited['connections']: + signals.disconnect(resources[e], resources[r]) + + for e, r, mapping in staged['connections']: + signals.connect(resources[e], resources[r], dict([mapping])) + + df = list(diff(commited, staged)) + + log_item = state.LogItem(utils.generate_uuid(), log_item.res, df) + log.add(log_item) + + res = resource.wrap_resource(db.get_resource(log_item.res)) + res.update(staged['args']) + res.save() + + return log + + +def rollback_uid(uid): + item = next(l for l in state.CL() if l.uuid == uid) + return rollback(item) + + +def rollback_last(): + l = state.CL().items[-1] + return rollback(l) + + From 04542c9949eb9fcca2e04e8a331b1d39152e5d70 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Mon, 1 Jun 2015 11:32:23 +0200 Subject: [PATCH 06/10] Add information about action to commit log --- solar/solar/operations.py | 21 +++++++++++++++++---- solar/solar/state.py | 13 ++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/solar/solar/operations.py b/solar/solar/operations.py index 32511d62..d9081033 100644 --- a/solar/solar/operations.py +++ b/solar/solar/operations.py @@ -12,6 +12,15 @@ from dictdiffer import diff, patch, revert import networkx as nx +def guess_action(from_, to): + # TODO(dshulyak) it should be more flexible + if not from_: + return 'run' + elif not to: + return 'remove' + else: + return 'update' + def connections(res, graph): result = [] @@ -42,6 +51,7 @@ def stage_changes(): commited = state.CD() log = state.SL() + action = None for res_uid in nx.topological_sort(conn_graph): commited_data = commited.get(res_uid, {}) @@ -59,7 +69,8 @@ def stage_changes(): log_item = state.LogItem( utils.generate_uuid(), res_uid, - df) + df, + guess_action(commited_data, staged_data)) log.add(log_item) return log @@ -88,16 +99,18 @@ def rollback(log_item): for e, r, mapping in commited['connections']: signals.disconnect(resources[e], resources[r]) - for e, r, mapping in staged['connections']: + for e, r, mapping in staged.get('connections', ()): signals.connect(resources[e], resources[r], dict([mapping])) df = list(diff(commited, staged)) - log_item = state.LogItem(utils.generate_uuid(), log_item.res, df) + log_item = state.LogItem( + utils.generate_uuid(), + log_item.res, df, guess_action(commited, staged)) log.add(log_item) res = resource.wrap_resource(db.get_resource(log_item.res)) - res.update(staged['args']) + res.update(staged.get('args', {})) res.save() return log diff --git a/solar/solar/state.py b/solar/solar/state.py index 9a0d06b6..6fbcc29f 100644 --- a/solar/solar/state.py +++ b/solar/solar/state.py @@ -42,11 +42,12 @@ CL = partial(state_file, 'commit_log') class LogItem(object): - def __init__(self, uid, res_uid, diff, state=None): + def __init__(self, uid, res_uid, diff, action, state=None): self.uid = uid self.res = res_uid self.diff = diff self.state = state or STATES.pending + self.action = action def to_yaml(self): return utils.yaml_dump(self.to_dict()) @@ -55,7 +56,8 @@ class LogItem(object): return {'uid': self.uid, 'res': self.res, 'diff': self.diff, - 'state': self.state.name} + 'state': self.state.name, + 'action': self.action} def __str__(self): return self.to_yaml() @@ -71,7 +73,8 @@ class Log(object): items = utils.yaml_load(path) or [] self.items = deque([LogItem( l['uid'], l['res'], - l['diff'], getattr(STATES, l['state'])) for l in items]) + l['diff'], l['action'], + getattr(STATES, l['state'])) for l in items]) def sync(self): utils.yaml_dump_to([i.to_dict() for i in self.items], self.path) @@ -86,8 +89,8 @@ class Log(object): return item def show(self, verbose=False): - return ['L(uuid={0}, res={1})'.format(l.uid, l.res) - for l in self.items] + return ['L(uuid={0}, res={1}, aciton={2})'.format( + l.uid, l.res, l.action) for l in self.items] def __repr__(self): return 'Log({0})'.format(self.path) From 253e3e5c21778237ebd528dbd5bbd251c22ad8d9 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Mon, 1 Jun 2015 11:47:19 +0200 Subject: [PATCH 07/10] Add execution of action on commiting changes --- solar/solar/operations.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/solar/solar/operations.py b/solar/solar/operations.py index d9081033..54c7761b 100644 --- a/solar/solar/operations.py +++ b/solar/solar/operations.py @@ -5,6 +5,7 @@ from solar.core import signals from solar.core import resource from solar import utils from solar.interfaces.db import get_db +from solar.core import actions db = get_db() @@ -19,7 +20,8 @@ def guess_action(from_, to): elif not to: return 'remove' else: - return 'update' + # it should be update + return 'run' def connections(res, graph): @@ -80,9 +82,14 @@ def commit_changes(): commited = state.CD() history = state.CL() staged = state.SL() + resources = resource.load_all() while staged: l = staged.popleft() + wrapper = resources[l.res] + + actions.resource_action(wrapper, l.action) + commited[l.res] = patch(l.diff, commited.get(l.res, {})) l.state = state.STATES.success history.add(l) From 66dae0f26733c024c231064e5e4f150d8c9ef519 Mon Sep 17 00:00:00 2001 From: Dmitry Shulyak Date: Mon, 1 Jun 2015 16:37:45 +0200 Subject: [PATCH 08/10] Change update to use remove with context and run with new --- cli.py | 5 ++++- solar/solar/operations.py | 27 +++++++++++++++++++++++---- solar/solar/state.py | 5 +++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/cli.py b/cli.py index afea1f66..37e3e117 100755 --- a/cli.py +++ b/cli.py @@ -168,10 +168,13 @@ def init_changes(): @click.command() @click.option('--last', is_flag=True, default=False) + @click.option('--all', is_flag=True, default=False) @click.option('--uid', default=None) - def rollback(last, uid): + def rollback(last, all, uid): if last: print operations.rollback_last() + elif all: + print operations.rollback_all() elif uid: print operations.rollback_uid(uid) diff --git a/solar/solar/operations.py b/solar/solar/operations.py index 54c7761b..e1c43842 100644 --- a/solar/solar/operations.py +++ b/solar/solar/operations.py @@ -21,7 +21,7 @@ def guess_action(from_, to): return 'remove' else: # it should be update - return 'run' + return 'update' def connections(res, graph): @@ -68,6 +68,7 @@ def stage_changes(): df = list(diff(commited_data, staged_data)) if df: + log_item = state.LogItem( utils.generate_uuid(), res_uid, @@ -88,9 +89,20 @@ def commit_changes(): l = staged.popleft() wrapper = resources[l.res] - actions.resource_action(wrapper, l.action) + staged_data = patch(l.diff, commited.get(l.res, {})) - commited[l.res] = patch(l.diff, commited.get(l.res, {})) + # TODO(dshulyak) think about this hack for update + if l.action == 'update': + commited_args = commited[l.res]['args'] + wrapper.update(commited_args) + actions.resource_action(wrapper, 'remove') + + wrapper.update(staged_data.get('args', {})) + actions.resource_action(wrapper, 'run') + else: + actions.resource_action(wrapper, l.action) + + commited[l.res] = staged_data l.state = state.STATES.success history.add(l) @@ -103,7 +115,7 @@ def rollback(log_item): staged = revert(log_item.diff, commited) - for e, r, mapping in commited['connections']: + for e, r, mapping in commited.get('connections', ()): signals.disconnect(resources[e], resources[r]) for e, r, mapping in staged.get('connections', ()): @@ -133,3 +145,10 @@ def rollback_last(): return rollback(l) +def rollback_all(): + cl = state.CL() + + while cl: + rollback(cl.pop()) + + diff --git a/solar/solar/state.py b/solar/solar/state.py index 6fbcc29f..a997e011 100644 --- a/solar/solar/state.py +++ b/solar/solar/state.py @@ -88,6 +88,11 @@ class Log(object): self.sync() return item + def pop(self): + item = self.items.pop() + self.sync() + return item + def show(self, verbose=False): return ['L(uuid={0}, res={1}, aciton={2})'.format( l.uid, l.res, l.action) for l in self.items] From 5df0e3ccc9730811d60e4b55647574b2c6e31c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Ole=C5=9B?= Date: Mon, 1 Jun 2015 14:58:05 +0000 Subject: [PATCH 09/10] Wait until rabbitmq is ready --- resources/rabbitmq_service/actions/run.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resources/rabbitmq_service/actions/run.yml b/resources/rabbitmq_service/actions/run.yml index f928c291..66d36c77 100644 --- a/resources/rabbitmq_service/actions/run.yml +++ b/resources/rabbitmq_service/actions/run.yml @@ -15,3 +15,8 @@ - {{ management_port }}:15672 env: RABBITMQ_NODENAME: {{container_name}} + - shell: docker exec -t {{ name }} rabbitmqctl list_users + register: result + until: result.rc == 0 + retries: 20 + delay: 0.5 From db1c56eb94d78564d6adecb8352456000af0afba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Ole=C5=9B?= Date: Mon, 1 Jun 2015 14:58:39 +0000 Subject: [PATCH 10/10] Simulate removing keystone services Ansible modules does not implemenet removing yet --- resources/keystone_role/actions/remove.yml | 3 ++- resources/keystone_service_endpoint/actions/remove.yaml | 7 ++++++- resources/keystone_user/actions/remove.yml | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/resources/keystone_role/actions/remove.yml b/resources/keystone_role/actions/remove.yml index 386d38ea..2a719f43 100644 --- a/resources/keystone_role/actions/remove.yml +++ b/resources/keystone_role/actions/remove.yml @@ -2,4 +2,5 @@ sudo: yes tasks: - name: keystone role - keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} user={{user_name}} tenant={{tenant_name}} role={{role_name}} state=absent + #TODO: not implemented in module + pause: seconds=1 diff --git a/resources/keystone_service_endpoint/actions/remove.yaml b/resources/keystone_service_endpoint/actions/remove.yaml index b15fe775..67218324 100644 --- a/resources/keystone_service_endpoint/actions/remove.yaml +++ b/resources/keystone_service_endpoint/actions/remove.yaml @@ -1 +1,6 @@ -#todo +- hosts: [{{ ip }}] + sudo: yes + tasks: + - name: keystone service and endpoint + #TODO: not implemented in module + pause: seconds=1 diff --git a/resources/keystone_user/actions/remove.yml b/resources/keystone_user/actions/remove.yml index a56289db..8ab1004f 100644 --- a/resources/keystone_user/actions/remove.yml +++ b/resources/keystone_user/actions/remove.yml @@ -2,4 +2,5 @@ sudo: yes tasks: - name: keystone user - keystone_user: endpoint=http://{{keystone_host}}:{{keystone_port}}/v2.0/ token={{admin_token}} user={{user_name}} password={{user_password}} tenant={{tenant_name}} state=absent + #TODO: not implemented in module + pause: seconds=1