solar/solar/cli/resource.py

265 lines
8.1 KiB
Python

# 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 sys
import os
import json
import yaml
import tabulate
import click
from solar.core import actions
from solar.core import resource as sresource
from solar.core.resource import virtual_resource as vr
from solar.core.log import log
from solar import errors
from solar import utils
from solar.cli import executors
@click.group()
def resource():
pass
@resource.command()
@click.argument('action')
@click.argument('resource')
@click.option('-d', '--dry-run', default=False, is_flag=True)
@click.option('-m', '--dry-run-mapping', default='{}')
def action(dry_run_mapping, dry_run, action, resource):
if dry_run:
dry_run_executor = executors.DryRunExecutor(mapping=json.loads(dry_run_mapping))
click.echo(
'action {} for resource {}'.format(action, resource)
)
r = sresource.load(resource)
try:
actions.resource_action(r, action)
except errors.SolarError as e:
log.debug(e)
sys.exit(1)
if dry_run:
click.echo('EXECUTED:')
for key in dry_run_executor.executed:
click.echo('{}: {}'.format(
click.style(dry_run_executor.compute_hash(key), fg='green'),
str(key)
))
def backtrack_single(i):
def format_input(i):
return '{}::{}'.format(i.resource.name, i.name)
if isinstance(i, list):
return [backtrack_single(bi) for bi in i]
if isinstance(i, dict):
return {
k: backtrack_single(bi) for k, bi in i.items()
}
bi = i.backtrack_value_emitter(level=1)
if isinstance(i, orm.DBResourceInput) and isinstance(bi, orm.DBResourceInput) and i == bi:
return (format_input(i), )
return (format_input(i), backtrack_single(bi))
@resource.command()
@click.option('-v', '--values', default=False, is_flag=True)
@click.option('-r', '--real_values', default=False, is_flag=True)
@click.option('-i', '--input', default=None)
@click.argument('resource')
def backtrack_inputs(resource, input, values, real_values):
r = sresource.load(resource)
db_obj = r.db_obj
def single(resource, name, get_val=False):
db_obj = sresource.load(resource).db_obj
se = db_obj.inputs._single_edge(name)
se = tuple(se)
if not se:
if get_val:
return dict(resource=resource, name=name, value=db_obj.inputs[name])
else:
return dict(resource=resource, name=name)
l = []
for (rname, rinput), _, meta in se:
l.append(dict(resource=resource, name=name))
val = single(rname, rinput, get_val)
if meta and isinstance(val, dict):
val['meta'] = meta
l.append(val)
return l
inps = {}
if input:
inps[input] = single(resource, input, values)
else:
for _inp in db_obj.inputs:
inps[_inp] = single(resource, _inp, values)
for name, values in inps.iteritems():
click.echo(yaml.safe_dump({name: values}, default_flow_style=False))
if real_values:
click.echo('! Real value: %r' % sresource.load(resource).db_obj.inputs[name] , nl=True)
@resource.command()
def compile_all():
from solar.core.resource import compiler
destination_path = utils.read_config()['resources-compiled-file']
if os.path.exists(destination_path):
os.remove(destination_path)
for path in utils.find_by_mask(utils.read_config()['resources-files-mask']):
meta = utils.yaml_load(path)
meta['base_path'] = os.path.dirname(path)
compiler.compile(meta)
@resource.command()
def clear_all():
from solar.dblayer.model import ModelMeta
click.echo('Clearing all resources and connections')
ModelMeta.remove_all()
@resource.command()
@click.argument('name')
@click.argument(
'base_path', type=click.Path(exists=True, resolve_path=True))
@click.argument('args', nargs=-1)
def create(args, base_path, name):
args_parsed = {}
click.echo('create {} {} {}'.format(name, base_path, args))
for arg in args:
try:
args_parsed.update(json.loads(arg))
except ValueError:
k, v = arg.split('=')
args_parsed.update({k: v})
resources = vr.create(name, base_path, args=args_parsed)
for res in resources:
click.echo(res.color_repr())
@resource.command()
@click.option('--name', '-n', default=None)
@click.option('--tag', '-t', multiple=True)
@click.option('--as_json', default=False, is_flag=True)
@click.option('--color', default=True, is_flag=True)
def show(name, tag, as_json, color):
echo = click.echo_via_pager
if name:
resources = [sresource.load(name)]
echo = click.echo
elif tag:
resources = sresource.load_by_tags(set(tag))
else:
resources = sresource.load_all()
if as_json:
output = json.dumps([r.to_dict(inputs=True) for r in resources], indent=2)
echo = click.echo
else:
if color:
formatter = lambda r: r.color_repr(inputs=True)
else:
formatter = lambda r: unicode(r)
output = '\n'.join(formatter(r) for r in resources)
if output:
echo(output)
@resource.command()
@click.argument('resource_name')
@click.argument('tags', nargs=-1)
@click.option('--add/--delete', default=True)
def tag(add, tags, resource_name):
r = sresource.load(resource_name)
if add:
r.add_tags(*tags)
click.echo('Tag(s) {} added to {}'.format(tags, resource_name))
else:
r.remove_tags(*tags)
click.echo('Tag(s) {} removed from {}'.format(tags, resource_name))
@resource.command()
@click.argument('name')
@click.argument('args', nargs=-1)
def update(name, args):
args_parsed = {}
for arg in args:
try:
args_parsed.update(json.loads(arg))
except ValueError:
k, v = arg.split('=')
args_parsed.update({k: v})
click.echo('Updating resource {} with args {}'.format(name, args_parsed))
res = sresource.load(name)
res.update(args_parsed)
@resource.command()
@click.option('--check-missing-connections', default=False, is_flag=True)
def validate(check_missing_connections):
errors = sresource.validate_resources()
for r, error in errors:
click.echo('ERROR: %s: %s' % (r.name, error))
if check_missing_connections:
missing_connections = vr.find_missing_connections()
if missing_connections:
click.echo(
'The following resources have inputs of the same value '
'but are not connected:'
)
click.echo(
tabulate.tabulate([
['%s::%s' % (r1, i1), '%s::%s' % (r2, i2)]
for r1, i1, r2, i2 in missing_connections
])
)
@resource.command()
@click.argument('path', type=click.Path(exists=True, dir_okay=False))
def get_inputs(path):
with open(path) as f:
content = f.read()
click.echo(vr.get_inputs(content))
@resource.command()
@click.option('--name', '-n', default=None)
@click.option('--tag', '-t', multiple=True)
@click.option('-f', default=False, is_flag=True, help='force removal from database')
def remove(name, tag, f):
if name:
resources = [sresource.load(name)]
elif tag:
resources = sresource.load_by_tags(set(tag))
else:
resources = sresource.load_all()
for res in resources:
res.remove(force=f)
if f:
click.echo('Resource %s removed from database' % res.name)
else:
click.echo('Resource %s will be removed after commiting changes.' % res.name)