Merge branch 'x' into master-x-merge

This commit is contained in:
Przemyslaw Kaminski 2015-05-11 09:39:46 +02:00
commit 7417253207
103 changed files with 4331 additions and 967 deletions

3
.gitignore vendored
View File

@ -7,3 +7,6 @@
.vagrant
tmp/
#vim
*.swp

28
Vagrantfile vendored
View File

@ -13,21 +13,24 @@ SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "rustyrobot/deb-jessie-amd64"
config.vm.box = "deb/jessie-amd64"
#rustyrobot/deb-jessie-amd64"
config.vm.define "solar-dev", primary: true do |guest1|
guest1.vm.provision "shell", inline: init_script, privileged: true
guest1.vm.provision "file", source: "~/.vagrant.d/insecure_private_key", destination: "/vagrant/tmp/keys/ssh_private"
guest1.vm.provision "file", source: "ansible.cfg", destination: "/home/vagrant/.ansible.cfg"
guest1.vm.network "private_network", ip: "10.0.0.2"
guest1.vm.host_name = "solar-dev"
guest1.vm.provider :virtualbox do |v|
v.customize ["modifyvm", :id, "--memory", 2048]
v.customize ["modifyvm", :id, "--memory", 256]
v.name = "solar-dev"
end
end
config.vm.define "solar-dev2" do |guest2|
guest2.vm.provision "shell", inline: init_script, privileged: true
guest2.vm.network "private_network", ip: "10.0.0.3"
guest2.vm.host_name = "solar-dev2"
@ -37,4 +40,25 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
end
end
config.vm.define "solar-dev3" do |guest3|
guest3.vm.provision "shell", inline: init_script, privileged: true
guest3.vm.network "private_network", ip: "10.0.0.4"
guest3.vm.host_name = "solar-dev3"
guest3.vm.provider :virtualbox do |v|
v.customize ["modifyvm", :id, "--memory", 1024]
v.name = "solar-dev3"
end
end
config.vm.define "solar-dev4" do |guest4|
guest4.vm.provision "shell", inline: init_script, privileged: true
guest4.vm.network "private_network", ip: "10.0.0.5"
guest4.vm.host_name = "solar-dev4"
guest4.vm.provider :virtualbox do |v|
v.customize ["modifyvm", :id, "--memory", 1024]
v.name = "solar-dev4"
end
end
end

2
ansible.cfg Normal file
View File

@ -0,0 +1,2 @@
[defaults]
host_key_checking = False

182
cli.py Normal file
View File

@ -0,0 +1,182 @@
import click
import json
#import matplotlib
#matplotlib.use('Agg') # don't show windows
#import matplotlib.pyplot as plt
import networkx as nx
import os
import subprocess
from x import actions as xa
from x import deployment as xd
from x import resource as xr
from x import signals as xs
@click.group()
def cli():
pass
def init_cli_resource():
@click.group()
def resource():
pass
cli.add_command(resource)
@click.command()
@click.argument('resource_path')
@click.argument('action_name')
def action(action_name, resource_path):
print 'action', resource_path, action_name
r = xr.load(resource_path)
xa.resource_action(r, action_name)
resource.add_command(action)
@click.command()
@click.argument('name')
@click.argument('base_path')
@click.argument('dest_path')
@click.argument('args')
def create(args, dest_path, base_path, name):
print 'create', name, base_path, dest_path, args
args = json.loads(args)
xr.create(name, base_path, dest_path, args)
resource.add_command(create)
@click.command()
@click.argument('resource_path')
@click.argument('tag_name')
@click.option('--add/--delete', default=True)
def tag(add, tag_name, resource_path):
print 'Tag', resource_path, tag_name, add
r = xr.load(resource_path)
if add:
r.add_tag(tag_name)
else:
r.remove_tag(tag_name)
r.save()
resource.add_command(tag)
@click.command()
@click.argument('path')
@click.option('--all/--one', default=False)
@click.option('--tag', default=None)
def show(tag, all, path):
if all or tag:
for name, resource in xr.load_all(path).items():
show = True
if tag:
if tag not in resource.tags:
show = False
if show:
print resource
print
else:
print xr.load(path)
resource.add_command(show)
@click.command()
@click.argument('path')
@click.argument('args')
def update(args, path):
print 'Update', path, args
args = json.loads(args)
# Need to load all resources for bubbling effect to take place
# TODO: resources can be scattered around, this is a simple
# situation when we assume resources are all in one directory
base_path, name = os.path.split(path)
all = xr.load_all(base_path)
r = all[name]
r.update(args)
resource.add_command(update)
def init_cli_connect():
@click.command()
@click.argument('emitter')
@click.argument('receiver')
@click.option('--mapping', default=None)
def connect(mapping, receiver, emitter):
print 'Connect', emitter, receiver
emitter = xr.load(emitter)
receiver = xr.load(receiver)
print emitter
print receiver
if mapping is not None:
mapping = json.loads(mapping)
xs.connect(emitter, receiver, mapping=mapping)
cli.add_command(connect)
@click.command()
@click.argument('emitter')
@click.argument('receiver')
def disconnect(receiver, emitter):
print 'Disconnect', emitter, receiver
emitter = xr.load(emitter)
receiver = xr.load(receiver)
print emitter
print receiver
xs.disconnect(emitter, receiver)
cli.add_command(disconnect)
def init_cli_connections():
@click.group()
def connections():
pass
cli.add_command(connections)
@click.command()
def show():
print json.dumps(xs.CLIENTS, indent=2)
connections.add_command(show)
# TODO: this requires graphing libraries
@click.command()
def graph():
#g = xs.connection_graph()
g = xs.detailed_connection_graph()
nx.write_dot(g, 'graph.dot')
subprocess.call(['dot', '-Tpng', 'graph.dot', '-o', 'graph.png'])
# Matplotlib
#pos = nx.spring_layout(g)
#nx.draw_networkx_nodes(g, pos)
#nx.draw_networkx_edges(g, pos, arrows=True)
#nx.draw_networkx_labels(g, pos)
#plt.axis('off')
#plt.savefig('graph.png')
connections.add_command(graph)
def init_cli_deployment_config():
@click.command()
@click.argument('filepath')
def deploy(filepath):
print 'Deploying from file {}'.format(filepath)
xd.deploy(filepath)
cli.add_command(deploy)
if __name__ == '__main__':
init_cli_resource()
init_cli_connect()
init_cli_connections()
init_cli_deployment_config()
cli()

View File

@ -1,11 +0,0 @@
---
- hosts: all
sudo: yes
tasks:
- shell: docker-compose --version
register: compose
ignore_errors: true
- shell: curl -L https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
when: compose|failed
- shell: chmod +x /usr/local/bin/docker-compose

1
config.yaml Normal file
View File

@ -0,0 +1 @@
clients-data-file: /vagrant/clients.json

View File

@ -1,67 +0,0 @@
Problem: Different execution strategies
---------------------------------------
We will have different order of execution for different actions
(installation, removal, maintenance)
1. Installation and removal of resources should be done in different order.
2. Running maintenance tasks may require completely different order
of actions, and this order can not be described one time for resources,
it should be described for each action.
IMPORTANT: In such case resources are making very little sense,
because we need to define different dependencies and build different
executions graphs for tasks during lifecycle management
Dependency between resources
-----------------------------
Several options to manage ordering between executables
1. Allow user to specify this order
2. Explicitly set requires/require_for in additional entity like profile
3. Deployment flow should reflect data-dependencies between resources
1st option is pretty clear - and we should provide a way for user
to manage dependencies by himself
(even if they will lead to error during execution)
2nd is similar to what is done in fuel, and allows explicitly set
what is expected to be executed. However we should
not hardcode those deps on resources/actions itself. Because it will lead to
tight-coupling, and some workarounds to skip unwanted resource execution.
3rd option is manage dependencies based on what is provided by different
resources. For example input: some_service
Please note that this format is used only to describe intentions.
::
image:
ref:
namespace: docker
value: base_image
Practically it means that docker resource should be executed before
some_service. And if another_service needs to be connected to some_service
::
connect:
ref:
namespace: some_service
value: port
But what if there is no data-dependencies?
In such case we can add generic way to extend parameters with its
requirements, like:
::
requires:
- ref:
namespace: node
# (dshulyak) How to add backward dependency? (required_for)

View File

@ -1,33 +0,0 @@
1. Discovery (ansible all -m facter)
Read list of ips and store them, and search for different data on those
hosts
2. Create environment ?? with profile, that provides roles (wraps resources)
3. Add nodes to the env and distribute services
Assign roles (partitions of services) from the profiles to the nodes.
Store history of applied resources.
Role only matters as initial template.
4. Change settings provided by resource.
Imporant/Non important settings ??
We need defaults for some settings.
Different templates ?? for different backends of resources ??
5. Start management
Periodicly applying stuff ??
6. Stop management
We need to be able to stop things
7. Run maintenance
Resources should added to history and management graph will be changed
8. Start management

View File

@ -1,60 +0,0 @@
Profile is a global wrapper for all resources in environment.
Profile is versioned and executed by particular driver.
Profile is a container for resources.
Resources can be grouped by roles entities.
::
id: HA
type: profile
version: 0.1
# adapter for any application that satisfies our requirements
driver: ansible
Role is a logical wrapper of resources.
We will provide "opinionated" wrappers, but user should
be able to compose resource in any way.
::
roles:
- id: controller
type: role
resources: []
Resource should have deployment logic for several events:
main deployment, removal of resource, scale up of resource ?
Resource should have list of input parameters that resource provides.
Resources are isolated, and should be executable as long as
required data provided.
::
id: rabbitmq
type: resource
driver: ansible_playbook
actions:
run: $install_rabbitmq_playbook
input:
image: fuel/rabbitmq
port: 5572
# we need to be able to select ip addresses
listen: [{{management.ip}}, {{public.ip}}]
::
id: nova_compute
type: resource
driver: ansible_playbook
actions:
run: $link_to_ansible_playbook
remove: $link_to_another_playbook_that_will_migrate_vms
maintenance: $link_to_playbook_that_will_put_into_maintenance
input:
image: fuel/compute
driver: kvm
rabbitmq_hosts: []

View File

@ -1,18 +0,0 @@
How to approach primary, non-primary resource mangement?
--------------------------------------------------------
It should be possible to avoid storing primary/non-primary flag
for any particular resource.
In ansible there is a way to execute particular task from playbook
only once and on concrete host.
::
- hosts: [mariadb]
tasks:
- debug: msg="Installing first node"
run_once: true
delegate_to: groups['mariadb'][0]
- debug: msg="Installing all other mariadb nodes"
when: inventory_hostname != groups['mariadb'][0]

View File

@ -1,14 +0,0 @@
Inventory mechanism should provide an easy way for user to change any
piece of deployment configuration.
It means several things:
1. When writing modules - developer should take into account possibility
of modification it by user. Development may take a little bit longer, but we
are developing tool that will cover not single particular use case,
but a broad range customized production deployments.
2. Each resource should define what is changeable.
On the stage before deployment we will be able to know what resources
are used on the level of node/cluster and modify them the way we want.

View File

@ -1,8 +0,0 @@
Layers
1. REST API of our CORE service // managing basic information
1.1. Extension API // interface for extensions
2. Orchestration // run tasks, periodic tasks, lifecycle management ??
3. Storage

View File

@ -1,81 +0,0 @@
We should make network as separate resource for which we should be
able to add custom handlers.
This resource will actually serialize tasks, and provide inventory
information.
Input:
Different entities in custom database, like networks and nodes, maybe
interfaces and other things.
Another input is parameters, like ovs/linux (it may be parameters or
different tasks)
Output:
List of ansible tasks for orhestrator to execute, like
::
shell: ovs-vsctl add-br {{networks.management.bridge}}
And data to inventory
Networking entities
-----------------------
Network can have a list of subnets that are attached to different node racks.
Each subnets stores l3 parameters, such as cidr/ip ranges.
L2 parameters such as vlans can be stored on network.
Roles should be attached to network, and different subnets can not
be used as different roles per rack.
How it should work:
1. Untagged network created with some l2 parameters like vlan
2. Created subnet for this network with params (10.0.0.0/24)
3. User attaches network to cluster with roles public/management/storage
4. Role can store l2 parameters also (bridge, mtu)
5. User creates rack and uses this subnet
6. IPs assigned for each node in this rack from each subnet
7. During deployment we are creating bridges based on roles.
URIs
-------
/networks/
vlan
mtu
/networks/<network_id>/subnets
cidr
ip ranges
gateway
/clusters/<cluster_id>/networks/
Subset of network attached to cluster
/clusters/<cluster_id>/networks/<network_id>/network_roles
Roles attached to particular network
/network_roles/
bridge
/clusters/<cluster_id>/racks/<rack_id>/subnets
/clusters/<cluster_id>/racks/<rack_id>/nodes

View File

@ -1,94 +0,0 @@
roles:
role-name:
name: ""
description: ""
conflicts:
- another_role
update_required:
- another_role
update_once:
- another_role
has_primary: true
limits:
min: int OR "<<condition dsl>>"
overrides:
- condition: "<<condition dsl>>"
max: 1
- condition: "<<condition dsl>>"
reccomended: 3
message: ""
restrictions:
- condition: "<<condition dsl>>"
message: ""
action: "hide"
fault_tolerance: "2%"
task_groups:
#Stages
- id: stage_name
type: stage
requires: [another_stage]
#Groups
- id: task_group_name
type: group
role: [role_name]
requires: [stage_name_requirement]
required_for: [stage_name_complete_before]
parameters:
strategy:
type: one_by_one
#OR
type: parallel
amount: 6 #Optional concurency limit
tasks:
- id: task_name_puppet
type: puppet
role: '*' #optional role to filter task on, used when in a pre or post deployment stage
groups: [task_group_name]
required_for: [task_name, stage_name]
requires: [task_name, task_group_name, stage_name]
condition: "<<condition dsl>>"
parameters:
puppet_manifest: path_to_manifests
puppet_modules: path_to_modules
timeout: 3600
cwd: /
test_pre:
cmd: bash style exec of command to run
test_post:
cmd: bash style exec of command to run
#all have [roles|groups] and requires /// required_for
- id: task_name_shell
type: shell
parameters:
cmd: bash style exec
timeout: 180
retries: 10
interval: 2
- id: task_name_upload_file
type: upload_file
role: '*'
parameters:
path: /etc/hiera/nodes.yaml
- id: task_name_sync
type: sync
role: '*'
parameters:
src: rsync://{MASTER_IP}:/puppet/version
dst: /etc/puppet
timeout: 180
- id: task_name_copy_files
type: copy_files
role: '*'
parameters:
files:
- src: source_file/{CLUSTER_ID}/
dst: dest/localtion
permissions: '0600'
dir_permissions: '0700'

View File

@ -1,52 +0,0 @@
Entities
------------
We clearly need orchestration entities like:
1. resources/roles/services/profiles
Also we need inventory entities:
2. nodes/networks/ifaces/cluster/release ?
Q: how to allow developer to extend this entities by modules?
Options:
1. Use completely schema-less data model
(i personally more comfortable with sql-like data models)
2. Dont allow anything except standart entities, if developer needs
to manage custom data - he can create its own micro-service and
then integrate it via custom type of resource
(one which perform query to third-part service)
Identities and namespaces
---------------------------
Identities required for several reasons:
- allow reusage of created once entities
- provide clear api to operate with entities
- specify dependencies with identities
Will be root namespace polluted with those entities?
Options:
1. We can create some variable namespace explicitly
2. Or use something like namepsace/entity (example contrail/network)
Multiple options for configuration
----------------------------------
If there will be same parameters defined within different
modules, how this should behave?
1. First option is concatenate several options and make a list of choices.
2. Raise a validation error that certain thing can be enabled with another.
Looks like both should be supported.
Deployment code
----------------
We need to be able to expose all functionality of any
particular deployment tool.
Current challenge: how to specify path to some deployment logic?

92
example.py Normal file
View File

@ -0,0 +1,92 @@
import shutil
import os
from x import resource
from x import signals
signals.Connections.clear()
if os.path.exists('rs'):
shutil.rmtree('rs')
os.mkdir('rs')
node1 = resource.create('node1', 'x/resources/ro_node/', 'rs/', {'ip':'10.0.0.3', 'ssh_key' : '/vagrant/tmp/keys/ssh_private', 'ssh_user':'vagrant'})
node2 = resource.create('node2', 'x/resources/ro_node/', 'rs/', {'ip':'10.0.0.4', 'ssh_key' : '/vagrant/tmp/keys/ssh_private', 'ssh_user':'vagrant'})
node3 = resource.create('node3', 'x/resources/ro_node/', 'rs/', {'ip':'10.0.0.5', 'ssh_key' : '/vagrant/tmp/keys/ssh_private', 'ssh_user':'vagrant'})
mariadb_service1 = resource.create('mariadb_service1', 'x/resources/mariadb_service', 'rs/', {'image':'mariadb', 'root_password' : 'mariadb', 'port' : '3306', 'ip': '', 'ssh_user': '', 'ssh_key': ''})
keystone_db = resource.create('keystone_db', 'x/resources/mariadb_db/', 'rs/', {'db_name':'keystone_db', 'login_password':'', 'login_user':'root', 'login_port': '', 'ip':'', 'ssh_user':'', 'ssh_key':''})
keystone_db_user = resource.create('keystone_db_user', 'x/resources/mariadb_user/', 'rs/', {'new_user_name' : 'keystone', 'new_user_password' : 'keystone', 'db_name':'', 'login_password':'', 'login_user':'root', 'login_port': '', 'ip':'', 'ssh_user':'', 'ssh_key':''})
keystone_config1 = resource.create('keystone_config1', 'x/resources/keystone_config/', 'rs/', {'config_dir' : '/etc/solar/keystone', 'ip':'', 'ssh_user':'', 'ssh_key':'', 'admin_token':'admin', 'db_password':'', 'db_name':'', 'db_user':'', 'db_host':''})
keystone_service1 = resource.create('keystone_service1', 'x/resources/keystone_service/', 'rs/', {'port':'5000', 'admin_port':'35357', 'ip':'', 'ssh_key':'', 'ssh_user':'', 'config_dir':'', 'config_dir':''})
keystone_config2 = resource.create('keystone_config2', 'x/resources/keystone_config/', 'rs/', {'config_dir' : '/etc/solar/keystone', 'ip':'', 'ssh_user':'', 'ssh_key':'', 'admin_token':'admin', 'db_password':'', 'db_name':'', 'db_user':'', 'db_host':''})
keystone_service2 = resource.create('keystone_service2', 'x/resources/keystone_service/', 'rs/', {'port':'5000', 'admin_port':'35357', 'ip':'', 'ssh_key':'', 'ssh_user':'', 'config_dir':'', 'config_dir':''})
haproxy_keystone_config = resource.create('haproxy_keystone1_config', 'x/resources/haproxy_config/', 'rs/', {'name':'keystone_config', 'listen_port':'5000', 'servers':[], 'ports':[]})
haproxy_config = resource.create('haproxy_config', 'x/resources/haproxy', 'rs/', {'ip':'', 'ssh_key':'', 'ssh_user':'', 'configs_names':[], 'configs_ports':[], 'listen_ports':[], 'configs':[]})
haproxy_service = resource.create('haproxy_service', 'x/resources/docker_container/', 'rs/', {'image' : 'tutum/haproxy', 'ports': [], 'host_binds': [], 'volume_binds':[], 'ip':'', 'ssh_key':'', 'ssh_user':''})
####
# connections
####
#mariadb
signals.connect(node1, mariadb_service1)
#keystone db
signals.connect(node1, keystone_db)
signals.connect(mariadb_service1, keystone_db, {'root_password':'login_password', 'port':'login_port'})
# keystone_db_user
signals.connect(node1, keystone_db_user)
signals.connect(mariadb_service1, keystone_db_user, {'root_password':'login_password', 'port':'login_port'})
signals.connect(keystone_db, keystone_db_user, {'db_name':'db_name'})
signals.connect(node1, keystone_config1)
signals.connect(mariadb_service1, keystone_config1, {'ip':'db_host'})
signals.connect(keystone_db_user, keystone_config1, {'db_name':'db_name', 'new_user_name':'db_user', 'new_user_password':'db_password'})
signals.connect(node1, keystone_service1)
signals.connect(keystone_config1, keystone_service1, {'config_dir': 'config_dir'})
signals.connect(node2, keystone_config2)
signals.connect(mariadb_service1, keystone_config2, {'ip':'db_host'})
signals.connect(keystone_db_user, keystone_config2, {'db_name':'db_name', 'new_user_name':'db_user', 'new_user_password':'db_password'})
signals.connect(node2, keystone_service2)
signals.connect(keystone_config2, keystone_service2, {'config_dir': 'config_dir'})
signals.connect(keystone_service1, haproxy_keystone_config, {'ip':'servers', 'port':'ports'})
signals.connect(node1, haproxy_config)
signals.connect(haproxy_keystone_config, haproxy_config, {'listen_port': 'listen_ports', 'name':'configs_names', 'ports' : 'configs_ports', 'servers':'configs'})
signals.connect(node1, haproxy_service)
signals.connect(haproxy_config, haproxy_service, {'listen_ports':'ports', 'config_dir':'host_binds'})
#run
from x import actions
actions.resource_action(mariadb_service1, 'run')
actions.resource_action(keystone_db, 'run')
actions.resource_action(keystone_db_user, 'run')
actions.resource_action(keystone_config1, 'run')
actions.resource_action(keystone_service1, 'run')
actions.resource_action(haproxy_config, 'run')
actions.resource_action(haproxy_service, 'run')
#remove
actions.resource_action(haproxy_service, 'remove')
actions.resource_action(haproxy_config, 'remove')
actions.resource_action(keystone_service1, 'remove')
actions.resource_action(keystone_config1, 'remove')
actions.resource_action(keystone_db_user, 'remove')
actions.resource_action(keystone_db, 'remove')
actions.resource_action(mariadb_service1, 'remove')

View File

@ -1,14 +0,0 @@
docker:
base_image: ubuntu
rabbitmq:
image: tutum/rabbitmq
name: rabbit-test1
user:
name: test_name
password: test_pass
mariadb:
name: maria-test
image: tutum/mariadb

View File

@ -1,6 +0,0 @@
networks:
default:
ip: 10.0.0.2
cidr: 10.0.0.0/24
interface: eth1

View File

@ -1,20 +0,0 @@
first ansible_connection=local ansible_ssh_host=10.0.0.2
second ansible_ssh_host=10.0.0.3
[docker]
first
second
[rabbitmq]
first
[user]
first
[mariadb]
first

View File

@ -1,17 +0,0 @@
- hosts: [service/mariadb]
sudo: yes
tasks:
- shell: echo {{name}} >> /var/lib/solar/containers_list
- shell: docker ps | grep -q {{name}}
ignore_errors: true
register: is_running
- shell: docker run \
-d \
--net="host" \
--privileged \
--name {{name}} \
-e "MARIADB_ROOT_PASSWORD={{root_password}}" \
-e "BIND_ADDRESS={{bind_ip}}" \
{{image}}
when: is_running|failed

View File

@ -1,6 +0,0 @@
- hosts: [rabbitmq]
sudo: yes
tasks:
- shell: docker stop {{ rabbitmq.name }}
- shell: docker rm {{ rabbitmq.name }}

View File

@ -1,6 +0,0 @@
- hosts: [rabbitmq]
sudo: yes
tasks:
- shell: docker run --net="host" --privileged \
--name {{ rabbitmq.name }} -d {{ rabbitmq.image }}

View File

@ -1,5 +0,0 @@
- include: user/remove.yml
- include: rabbitmq/remove.yml
- include: mariadb/remove.yml
- include: docker/remove.yml

View File

@ -1,5 +0,0 @@
- include: docker/run.yml
- include: rabbitmq/run.yml
- include: mariadb/run.yml
- include: user/run.yml

View File

@ -1,12 +0,0 @@
- hosts: [rabbitmq]
sudo: yes
tasks:
- shell: docker exec -i {{rabbitmq.name}} /usr/sbin/rabbitmqctl delete_user {{user.name}}
run_once: true
- hosts: [mariadb]
sudo: yes
tasks:
- command: docker exec -t {{mariadb.name}} \
mysql -uroot -e "DROP USER '{{user.name}}'"

View File

@ -1,6 +0,0 @@
- hosts: [rabbitmq]
sudo: yes
tasks:
- command: docker exec -t {{rabbitmq.name}} /usr/sbin/rabbitmqctl add_user {{user.name}} {{user.password}}
run_once: true

46
haproxy.cfg Normal file
View File

@ -0,0 +1,46 @@
global
log 127.0.0.1 local0
log 127.0.0.1 local1 notice
maxconn 4096
tune.ssl.default-dh-param 2048
pidfile /var/run/haproxy.pid
user haproxy
group haproxy
daemon
stats socket /var/run/haproxy.stats level admin
ssl-default-bind-options no-sslv3
defaults
log global
mode http
option redispatch
option httplog
option dontlognull
option forwardfor
timeout connect 5000
timeout client 50000
timeout server 50000
#frontend default_frontend
# bind 0.0.0.0:80
# default_backend default_service
#backend default_service
# balance roundrobin
{% for service in haproxy_services %}
listen {{ service['name'] }} 0.0.0.0:{{ service['listen_port'] }}
mode http
stats enable
stats uri /haproxy?stats
stats realm Strictly\ Private
stats auth A_Username:YourPassword
stats auth Another_User:passwd
balance roundrobin
option httpclose
option forwardfor
{% for server in service['servers'] %}
server {{ server['name'] }} {{ server['ip'] }}:{{ server['port'] }} check
{% endfor %}
{% endfor %}

View File

@ -0,0 +1 @@
__author__ = 'przemek'

View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# HAProxy deployment with Keystone and Nova
set -e
cd /vagrant
rm clients.json
rm -Rf rs/*
# Create resources
python cli.py resource create node1 x/resources/ro_node/ rs/ '{"ip":"10.0.0.3", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create node2 x/resources/ro_node/ rs/ '{"ip":"10.0.0.4", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create node3 x/resources/ro_node/ rs/ '{"ip":"10.0.0.5", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create node4 x/resources/ro_node/ rs/ '{"ip":"10.0.0.6", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create node5 x/resources/ro_node/ rs/ '{"ip":"10.0.0.7", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create mariadb_keystone1_data x/resources/data_container/ rs/ '{"image": "mariadb", "export_volumes" : ["/var/lib/mysql"], "ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create mariadb_keystone2_data x/resources/data_container/ rs/ '{"image": "mariadb", "export_volumes" : ["/var/lib/mysql"], "ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create keystone1 x/resources/keystone/ rs/ '{"ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create keystone2 x/resources/keystone/ rs/ '{"ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create haproxy_keystone_config x/resources/haproxy_config/ rs/ '{"servers": {}, "ssh_user": "", "ssh_key": ""}'
python cli.py resource create mariadb_nova1_data x/resources/data_container/ rs/ '{"image" : "mariadb", "export_volumes" : ["/var/lib/mysql"], "ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create mariadb_nova2_data x/resources/data_container/ rs/ '{"image" : "mariadb", "export_volumes" : ["/var/lib/mysql"], "ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create nova1 x/resources/nova/ rs/ '{"ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create nova2 x/resources/nova/ rs/ '{"ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create haproxy_nova_config x/resources/haproxy_config/ rs/ '{"ip": "", "servers": {}, "ssh_user": "", "ssh_key": ""}'
python cli.py resource create haproxy x/resources/haproxy/ rs/ '{"ip": "", "configs": {}, "ssh_user": "", "ssh_key": ""}'
# Connect resources
python cli.py connect rs/node1 rs/mariadb_keystone1_data
python cli.py connect rs/node2 rs/mariadb_keystone2_data
python cli.py connect rs/mariadb_keystone1_data rs/keystone1
python cli.py connect rs/mariadb_keystone2_data rs/keystone2
python cli.py connect rs/keystone1 rs/haproxy_keystone_config --mapping '{"ip": "servers"}'
python cli.py connect rs/keystone2 rs/haproxy_keystone_config --mapping '{"ip": "servers"}'
python cli.py connect rs/node3 rs/mariadb_nova1_data
python cli.py connect rs/node4 rs/mariadb_nova2_data
python cli.py connect rs/mariadb_nova1_data rs/nova1
python cli.py connect rs/mariadb_nova2_data rs/nova2
python cli.py connect rs/nova1 rs/haproxy_nova_config --mapping '{"ip": "servers"}'
python cli.py connect rs/nova2 rs/haproxy_nova_config --mapping '{"ip": "servers"}'
python cli.py connect rs/node5 rs/haproxy
python cli.py connect rs/haproxy_keystone_config rs/haproxy --mapping '{"server": "configs"}'
python cli.py connect rs/haproxy_nova_config rs/haproxy --mapping '{"server": "configs"}'

View File

@ -0,0 +1,216 @@
# HAProxy deployment with MariaDB, Keystone and Nova
workdir: /vagrant
resource-save-path: rs/
test-suite: haproxy_deployment.haproxy_deployment
resources:
- name: node1
model: x/resources/ro_node/
args:
ip: 10.0.0.3
ssh_key: /vagrant/.vagrant/machines/solar-dev2/virtualbox/private_key
ssh_user: vagrant
- name: node2
model: x/resources/ro_node/
args:
ip: 10.0.0.4
ssh_key: /vagrant/.vagrant/machines/solar-dev3/virtualbox/private_key
ssh_user: vagrant
- name: node3
model: x/resources/ro_node/
args:
ip: 10.0.0.5
ssh_key: /vagrant/.vagrant/machines/solar-dev4/virtualbox/private_key
ssh_user: vagrant
- name: node4
model: x/resources/ro_node/
args:
ip: 10.0.0.6
ssh_key: /vagrant/.vagrant/machines/solar-dev5/virtualbox/private_key
ssh_user: vagrant
- name: node5
model: x/resources/ro_node/
args:
ip: 10.0.0.7
ssh_key: /vagrant/.vagrant/machines/solar-dev6/virtualbox/private_key
ssh_user: vagrant
- name: mariadb_keystone1_data
model: x/resources/data_container/
args:
image: mariadb
export_volumes:
- /var/lib/mysql
ip:
ssh_user:
ssh_key:
- name: mariadb_keystone2_data
model: x/resources/data_container/
args:
image: mariadb
export_volumes:
- /var/lib/mysql
ip:
ssh_user:
ssh_key:
- name: keystone1
model: x/resources/keystone/
args:
admin_port: 35357
port: 5000
image: TEST
config_dir: /etc/solar/keystone1
ip:
ssh_user:
ssh_key:
- name: keystone2
model: x/resources/keystone/
args:
admin_port: 35357
port: 5000
config_dir: /etc/solar/keystone2
image: TEST
ip:
ssh_user:
ssh_key:
- name: haproxy_keystone_config
model: x/resources/haproxy_config/
args:
name: keystone
servers: []
listen_port: 5000
ports: []
ssh_user:
ssh_key:
- name: mariadb_nova1_data
model: x/resources/data_container/
args:
image: mariadb
export_volumes:
- /var/lib/mysql
ip:
ssh_user:
ssh_key:
- name: mariadb_nova2_data
model: x/resources/data_container/
args:
image: mariadb
export_volumes:
- /var/lib/mysql
ip:
ssh_user:
ssh_key:
- name: nova1
model: x/resources/nova/
args:
ip:
image: TEST
ssh_user:
ssh_key:
- name: nova2
model: x/resources/nova/
args:
ip:
image: TEST
ssh_user:
ssh_key:
- name: haproxy_nova_config
model: x/resources/haproxy_config/
args:
name: nova
servers: []
listen_port: 8774
ports: []
ssh_user:
ssh_key:
- name: haproxy-config
model: x/resources/haproxy/
args:
ip:
listen_ports: []
configs: []
configs_names: []
configs_ports: []
ssh_user:
ssh_key:
- name: haproxy
model: x/resources/docker_container
args:
ip:
image: tutum/haproxy
ports: []
ssh_user:
ssh_key:
host_binds: []
volume_binds: []
connections:
- emitter: node1
receiver: mariadb_keystone1_data
- emitter: node2
receiver: mariadb_keystone2_data
- emitter: mariadb_keystone1_data
receiver: keystone1
- emitter: mariadb_keystone2_data
receiver: keystone2
- emitter: keystone1
receiver: haproxy_keystone_config
mapping:
ip: servers
port: ports
- emitter: keystone2
receiver: haproxy_keystone_config
mapping:
ip: servers
port: ports
- emitter: node3
receiver: mariadb_nova1_data
- emitter: node4
receiver: mariadb_nova2_data
- emitter: mariadb_nova1_data
receiver: nova1
- emitter: mariadb_nova2_data
receiver: nova2
- emitter: nova1
receiver: haproxy_nova_config
mapping:
ip: servers
port: ports
- emitter: nova2
receiver: haproxy_nova_config
mapping:
ip: servers
port: ports
# HAProxy config container
- emitter: node5
receiver: haproxy-config
- emitter: haproxy_keystone_config
receiver: haproxy-config
mapping:
listen_port: listen_ports
name: configs_names
ports: configs_ports
servers: configs
- emitter: haproxy_nova_config
receiver: haproxy-config
mapping:
listen_port: listen_ports
name: configs_names
ports: configs_ports
servers: configs
- emitter: haproxy-config
receiver: haproxy
mapping:
ip: ip
listen_ports: ports
ssh_user: ssh_user
ssh_key: ssh_key
config_dir: host_binds

View File

@ -0,0 +1,111 @@
import unittest
from x import db
class TestHAProxyDeployment(unittest.TestCase):
def test_keystone_config(self):
node1 = db.get_resource('node1')
node2 = db.get_resource('node2')
keystone1 = db.get_resource('keystone1')
keystone2 = db.get_resource('keystone2')
self.assertEqual(keystone1.args['ip'], node1.args['ip'])
self.assertEqual(keystone2.args['ip'], node2.args['ip'])
def test_haproxy_keystone_config(self):
keystone1 = db.get_resource('keystone1')
keystone2 = db.get_resource('keystone2')
haproxy_keystone_config = db.get_resource('haproxy_keystone_config')
self.assertEqual(
[ip['value'] for ip in haproxy_keystone_config.args['servers'].value],
[
keystone1.args['ip'],
keystone2.args['ip'],
]
)
self.assertEqual(
[p['value'] for p in haproxy_keystone_config.args['ports'].value],
[
keystone1.args['port'],
keystone2.args['port'],
]
)
def test_nova_config(self):
node3 = db.get_resource('node3')
node4 = db.get_resource('node4')
nova1 = db.get_resource('nova1')
nova2 = db.get_resource('nova2')
self.assertEqual(nova1.args['ip'], node3.args['ip'])
self.assertEqual(nova2.args['ip'], node4.args['ip'])
def test_haproxy_nova_config(self):
nova1 = db.get_resource('nova1')
nova2 = db.get_resource('nova2')
haproxy_nova_config = db.get_resource('haproxy_nova_config')
self.assertEqual(
[ip['value'] for ip in haproxy_nova_config.args['servers'].value],
[
nova1.args['ip'],
nova2.args['ip'],
]
)
self.assertEqual(
[p['value'] for p in haproxy_nova_config.args['ports'].value],
[
nova1.args['port'],
nova2.args['port'],
]
)
def test_haproxy(self):
node5 = db.get_resource('node5')
haproxy_keystone_config = db.get_resource('haproxy_keystone_config')
haproxy_nova_config = db.get_resource('haproxy_nova_config')
haproxy = db.get_resource('haproxy')
haproxy_config = db.get_resource('haproxy-config')
self.assertEqual(node5.args['ip'], haproxy.args['ip'])
self.assertEqual(node5.args['ssh_key'], haproxy.args['ssh_key'])
self.assertEqual(node5.args['ssh_user'], haproxy.args['ssh_user'])
self.assertEqual(
[c['value'] for c in haproxy_config.args['configs'].value],
[
haproxy_keystone_config.args['servers'],
haproxy_nova_config.args['servers'],
]
)
self.assertEqual(
[cp['value'] for cp in haproxy_config.args['configs_ports'].value],
[
haproxy_keystone_config.args['ports'],
haproxy_nova_config.args['ports'],
]
)
self.assertEqual(
[lp['value'] for lp in haproxy_config.args['listen_ports'].value],
[
haproxy_keystone_config.args['listen_port'],
haproxy_nova_config.args['listen_port'],
]
)
self.assertEqual(
[
haproxy_config.args['config_dir'],
],
[hb['value'] for hb in haproxy.args['host_binds'].value]
)
self.assertEqual(
haproxy.args['ports'],
haproxy_config.args['listen_ports'],
)
def main():
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(TestHAProxyDeployment)
unittest.TextTestRunner().run(suite)

View File

@ -11,5 +11,16 @@
- apt: name=virtualenvwrapper state=present
- apt: name=ipython state=present
- apt: name=python-pudb state=present
- apt: name=python-pip state=present
- apt: name=python-mysqldb state=present
- shell: pip install docker-py==1.1.0
# requirements
- shell: pip install -r /vagrant/requirements.txt
# Graph drawing
#- apt: name=python-matplotlib state=present
- apt: name=python-pygraphviz state=present
# Setup development env for solar
- shell: python setup.py develop chdir=/vagrant/solar
#- shell: python setup.py develop chdir=/vagrant/solar

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
click==4.0
jinja2==2.7.3
networkx==1.9.1
PyYAML==3.11

43
simple-deployment.yaml Executable file
View File

@ -0,0 +1,43 @@
# HAProxy deployment with MariaDB, Keystone and Nova
workdir: /vagrant
resource-save-path: rs/
#test-suite: haproxy_deployment.haproxy_deployment
resources:
- name: node1
model: x/resources/ro_node/
args:
ip: 10.0.0.3
ssh_key: /vagrant/.vagrant/machines/solar-dev2/virtualbox/private_key
ssh_user: vagrant
- name: keystone1
model: x/resources/keystone/
args:
ip:
image: TEST
ssh_user:
ssh_key:
- name: haproxy_keystone_config
model: x/resources/haproxy_config/
args:
listen_port: 5000
ports: {}
servers: {}
connections:
- emitter: node1
receiver: keystone1
# Multiple subscription test
- emitter: node1
receiver: keystone1
- emitter: keystone1
receiver: haproxy_keystone_config
mapping:
ip: servers
port: ports

View File

@ -1,2 +0,0 @@
include *.txt
recursive-include solar/ *

View File

@ -1,50 +0,0 @@
# 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 os
from setuptools import find_packages
from setuptools import setup
def find_requires():
prj_root = os.path.dirname(os.path.realpath(__file__))
requirements = []
with open(u'{0}/requirements.txt'.format(prj_root), 'r') as reqs:
requirements = reqs.readlines()
return requirements
setup(
name='solar',
version='0.0.1',
description='Deployment tool',
long_description="""Deployment tool""",
classifiers=[
"Development Status :: 1 - Beta",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Topic :: System :: Software Distribution"],
author='Mirantis Inc.',
author_email='product@mirantis.com',
url='http://mirantis.com',
keywords='deployment',
packages=find_packages(),
zip_safe=False,
install_requires=find_requires(),
include_package_data=True,
entry_points={
'console_scripts': [
'solar = solar.cli:main']})

View File

@ -1,17 +0,0 @@
from solar import extensions
from solar import errors
class ExtensionsManager(object):
def __init__(self, profile):
self.profile = profile
def get_data(self, key):
"""Finds data by extensions provider"""
providers = filter(lambda e: key in e.PROVIDES, extensions.get_all_extensions())
if not providers:
raise errors.CannotFindExtension('Cannot find extension which provides "{0}"'.format(key))
return getattr(providers[0](self.profile), key)()

View File

@ -1,10 +0,0 @@
class Profile(object):
def __init__(self, profile):
self._profile = profile
self.tags = set(profile['tags'])
self.extensions = profile.get('extensions', [])
def get(self, key):
return self._profile.get(key, None)

View File

@ -1,10 +0,0 @@
class SolarError(Exception):
pass
class CannotFindID(SolarError):
pass
class CannotFindExtension(SolarError):
pass

View File

@ -1,28 +0,0 @@
from solar.interfaces.db import get_db
class BaseExtension(object):
ID = None
NAME = None
PROVIDES = []
def __init__(self, profile, core_manager=None, config=None):
self.config = config or {}
self.uid = self.ID
self.db = get_db()
self.profile = profile
from solar.core.extensions_manager import ExtensionsManager
self.core = core_manager or ExtensionsManager(self.profile)
def prepare(self):
"""Make some changes in database state."""
@property
def input(self):
return self.config.get('input', {})
@property
def output(self):
return self.config.get('output', {})

View File

@ -1,9 +0,0 @@
from solar.interfaces.db.file_system_db import FileSystemDB
mapping = {
'file_system': FileSystemDB
}
def get_db():
# Should be retrieved from config
return mapping['file_system']()

View File

View File

@ -1,303 +0,0 @@
# -*- test-case-name: twisted.test.test_dirdbm -*-
#
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
DBM-style interface to a directory.
Each key is stored as a single file. This is not expected to be very fast or
efficient, but it's good for easy debugging.
DirDBMs are *not* thread-safe, they should only be accessed by one thread at
a time.
No files should be placed in the working directory of a DirDBM save those
created by the DirDBM itself!
Maintainer: Itamar Shtull-Trauring
"""
import os
import types
import base64
import glob
try:
import cPickle as pickle
except ImportError:
import pickle
try:
_open
except NameError:
_open = open
class DirDBM(object):
"""A directory with a DBM interface.
This class presents a hash-like interface to a directory of small,
flat files. It can only use strings as keys or values.
"""
def __init__(self, name):
"""
@type name: str
@param name: Base path to use for the directory storage.
"""
self.dname = os.path.abspath(name)
if not os.path.isdir(self.dname):
os.mkdir(self.dname)
else:
# Run recovery, in case we crashed. we delete all files ending
# with ".new". Then we find all files who end with ".rpl". If a
# corresponding file exists without ".rpl", we assume the write
# failed and delete the ".rpl" file. If only a ".rpl" exist we
# assume the program crashed right after deleting the old entry
# but before renaming the replacement entry.
#
# NOTE: '.' is NOT in the base64 alphabet!
for f in glob.glob(os.path.join(self.dname, "*.new")):
os.remove(f)
replacements = glob.glob(os.path.join(self.dname, "*.rpl"))
for f in replacements:
old = f[:-4]
if os.path.exists(old):
os.remove(f)
else:
os.rename(f, old)
def _encode(self, k):
"""Encode a key so it can be used as a filename.
"""
# NOTE: '_' is NOT in the base64 alphabet!
return base64.encodestring(k).replace('\n', '_').replace("/", "-")
def _decode(self, k):
"""Decode a filename to get the key.
"""
return base64.decodestring(k.replace('_', '\n').replace("-", "/"))
def _readFile(self, path):
"""Read in the contents of a file.
Override in subclasses to e.g. provide transparently encrypted dirdbm.
"""
f = _open(path, "rb")
s = f.read()
f.close()
return s
def _writeFile(self, path, data):
"""Write data to a file.
Override in subclasses to e.g. provide transparently encrypted dirdbm.
"""
f = _open(path, "wb")
f.write(data)
f.flush()
f.close()
def __len__(self):
"""
@return: The number of key/value pairs in this Shelf
"""
return len(os.listdir(self.dname))
def __setitem__(self, k, v):
"""
C{dirdbm[k] = v}
Create or modify a textfile in this directory
@type k: str
@param k: key to set
@type v: str
@param v: value to associate with C{k}
"""
assert type(k) == types.StringType, "DirDBM key must be a string"
# NOTE: Can be not a string if _writeFile in the child is redefined
# assert type(v) == types.StringType, "DirDBM value must be a string"
k = self._encode(k)
# we create a new file with extension .new, write the data to it, and
# if the write succeeds delete the old file and rename the new one.
old = os.path.join(self.dname, k)
if os.path.exists(old):
new = old + ".rpl" # replacement entry
else:
new = old + ".new" # new entry
try:
self._writeFile(new, v)
except:
os.remove(new)
raise
else:
if os.path.exists(old): os.remove(old)
os.rename(new, old)
def __getitem__(self, k):
"""
C{dirdbm[k]}
Get the contents of a file in this directory as a string.
@type k: str
@param k: key to lookup
@return: The value associated with C{k}
@raise KeyError: Raised when there is no such key
"""
assert type(k) == types.StringType, "DirDBM key must be a string"
path = os.path.join(self.dname, self._encode(k))
try:
return self._readFile(path)
except:
raise KeyError, k
def __delitem__(self, k):
"""
C{del dirdbm[foo]}
Delete a file in this directory.
@type k: str
@param k: key to delete
@raise KeyError: Raised when there is no such key
"""
assert type(k) == types.StringType, "DirDBM key must be a string"
k = self._encode(k)
try: os.remove(os.path.join(self.dname, k))
except (OSError, IOError): raise KeyError(self._decode(k))
def keys(self):
"""
@return: a C{list} of filenames (keys).
"""
return map(self._decode, os.listdir(self.dname))
def values(self):
"""
@return: a C{list} of file-contents (values).
"""
vals = []
keys = self.keys()
for key in keys:
vals.append(self[key])
return vals
def items(self):
"""
@return: a C{list} of 2-tuples containing key/value pairs.
"""
items = []
keys = self.keys()
for key in keys:
items.append((key, self[key]))
return items
def has_key(self, key):
"""
@type key: str
@param key: The key to test
@return: A true value if this dirdbm has the specified key, a faluse
value otherwise.
"""
assert type(key) == types.StringType, "DirDBM key must be a string"
key = self._encode(key)
return os.path.isfile(os.path.join(self.dname, key))
def setdefault(self, key, value):
"""
@type key: str
@param key: The key to lookup
@param value: The value to associate with key if key is not already
associated with a value.
"""
if not self.has_key(key):
self[key] = value
return value
return self[key]
def get(self, key, default = None):
"""
@type key: str
@param key: The key to lookup
@param default: The value to return if the given key does not exist
@return: The value associated with C{key} or C{default} if not
C{self.has_key(key)}
"""
if self.has_key(key):
return self[key]
else:
return default
def __contains__(self, key):
"""
C{key in dirdbm}
@type key: str
@param key: The key to test
@return: A true value if C{self.has_key(key)}, a false value otherwise.
"""
assert type(key) == types.StringType, "DirDBM key must be a string"
key = self._encode(key)
return os.path.isfile(os.path.join(self.dname, key))
def update(self, dict):
"""
Add all the key/value pairs in C{dict} to this dirdbm. Any conflicting
keys will be overwritten with the values from C{dict}.
@type dict: mapping
@param dict: A mapping of key/value pairs to add to this dirdbm.
"""
for key, val in dict.items():
self[key]=val
def copyTo(self, path):
"""
Copy the contents of this dirdbm to the dirdbm at C{path}.
@type path: C{str}
@param path: The path of the dirdbm to copy to. If a dirdbm
exists at the destination path, it is cleared first.
@rtype: C{DirDBM}
@return: The dirdbm this dirdbm was copied to.
"""
path = os.path.abspath(path)
assert path != self.dname
d = self.__class__(path)
d.clear()
for k in self.keys():
d[k] = self[k]
return d
def clear(self):
"""
Delete all key/value pairs in this dirdbm.
"""
for k in self.keys():
del self[k]
def close(self):
"""
Close this dbm: no-op, for dbm-style interface compliance.
"""
def getModificationTime(self, key):
"""
Returns modification time of an entry.
@return: Last modification date (seconds since epoch) of entry C{key}
@raise KeyError: Raised when there is no such key
"""
assert type(key) == types.StringType, "DirDBM key must be a string"
path = os.path.join(self.dname, self._encode(key))
if os.path.isfile(path):
return os.path.getmtime(path)
else:
raise KeyError, key

118
x/README.md Normal file
View File

@ -0,0 +1,118 @@
# x
## HAProxy deployment
```
cd /vagrant
python cli.py deploy haproxy_deployment/haproxy-deployment.yaml
```
or from Python shell:
```
from x import deployment
deployment.deploy('/vagrant/haproxy_deployment/haproxy-deployment.yaml')
```
## Usage:
Creating resources:
```
from x import resource
node1 = resource.create('node1', 'x/resources/ro_node/', 'rs/', {'ip':'10.0.0.3', 'ssh_key' : '/vagrant/tmp/keys/ssh_private', 'ssh_user':'vagrant'})
node2 = resource.create('node2', 'x/resources/ro_node/', 'rs/', {'ip':'10.0.0.4', 'ssh_key' : '/vagrant/tmp/keys/ssh_private', 'ssh_user':'vagrant'})
keystone_db_data = resource.create('mariadb_keystone_data', 'x/resources/data_container/', 'rs/', {'image' : 'mariadb', 'export_volumes' : ['/var/lib/mysql'], 'ip': '', 'ssh_user': '', 'ssh_key': ''}, connections={'ip' : 'node2.ip', 'ssh_key':'node2.ssh_key', 'ssh_user':'node2.ssh_user'})
nova_db_data = resource.create('mariadb_nova_data', 'x/resources/data_container/', 'rs/', {'image' : 'mariadb', 'export_volumes' : ['/var/lib/mysql'], 'ip': '', 'ssh_user': '', 'ssh_key': ''}, connections={'ip' : 'node1.ip', 'ssh_key':'node1.ssh_key', 'ssh_user':'node1.ssh_user'})
```
to make connection after resource is created use `signal.connect`
To test notifications:
```
keystone_db_data.args # displays node2 IP
node2.update({'ip': '10.0.0.5'})
keystone_db_data.args # updated IP
```
If you close the Python shell you can load the resources like this:
```
from x import resource
node1 = resource.load('rs/node1')
node2 = resource.load('rs/node2')
keystone_db_data = resource.load('rs/mariadn_keystone_data')
nova_db_data = resource.load('rs/mariadb_nova_data')
```
Connections are loaded automatically.
You can also load all resources at once:
```
from x import resource
all_resources = resource.load_all('rs')
```
## CLI
You can do the above from the command-line client:
```
cd /vagrant
python cli.py resource create node1 x/resources/ro_node/ rs/ '{"ip":"10.0.0.3", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create node2 x/resources/ro_node/ rs/ '{"ip":"10.0.0.4", "ssh_key" : "/vagrant/tmp/keys/ssh_private", "ssh_user":"vagrant"}'
python cli.py resource create mariadb_keystone_data x/resources/data_container/ rs/ '{"image": "mariadb", "export_volumes" : ["/var/lib/mysql"], "ip": "", "ssh_user": "", "ssh_key": ""}'
python cli.py resource create mariadb_nova_data x/resources/data_container/ rs/ '{"image" : "mariadb", "export_volumes" : ["/var/lib/mysql"], "ip": "", "ssh_user": "", "ssh_key": ""}'
# View resources
python cli.py resource show rs/mariadb_keystone_data
# Show all resources at location rs/
python cli.py resource show rs/ --all
# Show resources with specific tag
python cli.py resources show rs/ --tag test
# Connect resources
python cli.py connect rs/node2 rs/mariadb_keystone_data
python cli.py connect rs/node1 rs/mariadb_nova_data
# Test update
python cli.py update rs/node2 '{"ip": "1.1.1.1"}'
python cli.py resource show rs/mariadb_keystone_data # --> IP is 1.1.1.1
# View connections
python cli.py connections show
# Outputs graph to 'graph.png' file, please note that arrows don't have "normal" pointers, but just the line is thicker
# please see http://networkx.lanl.gov/_modules/networkx/drawing/nx_pylab.html
python cli.py connections graph
# Disconnect
python cli.py disconnect rs/mariadb_nova_data rs/node1
# Tag a resource:
python cli.py resource tag rs/node1 test-tag
# Remove tag
python cli.py resource tag rs/node1 test-tag --delete
```

18
x/TODO.md Normal file
View File

@ -0,0 +1,18 @@
# TODO
- store all resource configurations somewhere globally (this is required to
correctly perform an update on one resource and bubble down to all others)
- config templates
- Handler also can require some data, for example ansible: ip, ssh_key, ssh_user
- tag-filtered graph generation
- separate resource for docker image -- this is e.g. to make automatic image removal
when some image is unused to conserve space
# DONE
- Deploy HAProxy, Keystone and MariaDB
- ansible handler (loles)
- tags are kept in resource mata file (pkaminski)
- add 'list' connection type (pkaminski)
- connections are made automaticly(pkaminski)
- graph is build from CLIENT dict, clients are stored in JSON file (pkaminski)
- cli (pkaminski)

13
x/actions.py Normal file
View File

@ -0,0 +1,13 @@
# -*- coding: UTF-8 -*-
import handlers
def resource_action(resource, action):
handler = resource.metadata['handler']
with handlers.get(handler)([resource]) as h:
h.action(resource, action)
def tag_action(tag, action):
#TODO
pass

19
x/db.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: UTF-8 -*-
RESOURCE_DB = {}
def resource_add(key, value):
if key in RESOURCE_DB:
raise Exception('Key `{0}` already exists'.format(key))
RESOURCE_DB[key] = value
def get_resource(key):
return RESOURCE_DB.get(key, None)
def clear():
global RESOURCE_DB
RESOURCE_DB = {}

45
x/deployment.py Normal file
View File

@ -0,0 +1,45 @@
# Deploying stuff from YAML definition
import os
import shutil
import yaml
from x import db
from x import resource as xr
from x import signals as xs
def deploy(filename):
with open(filename) as f:
config = yaml.load(f)
workdir = config['workdir']
resource_save_path = os.path.join(workdir, config['resource-save-path'])
# Clean stuff first
db.clear()
xs.Connections.clear()
shutil.rmtree(resource_save_path, ignore_errors=True)
os.makedirs(resource_save_path)
# Create resources first
for resource_definition in config['resources']:
name = resource_definition['name']
model = os.path.join(workdir, resource_definition['model'])
args = resource_definition.get('args', {})
print 'Creating ', name, model, resource_save_path, args
xr.create(name, model, resource_save_path, args=args)
# Create resource connections
for connection in config['connections']:
emitter = db.get_resource(connection['emitter'])
receiver = db.get_resource(connection['receiver'])
mapping = connection.get('mapping')
print 'Connecting ', emitter.name, receiver.name, mapping
xs.connect(emitter, receiver, mapping=mapping)
# Run all tests
if 'test-suite' in config:
print 'Running tests from {}'.format(config['test-suite'])
test_suite = __import__(config['test-suite'], {}, {}, ['main'])
test_suite.main()

15
x/handlers/__init__.py Normal file
View File

@ -0,0 +1,15 @@
# -*- coding: UTF-8 -*-
from x.handlers.ansible import Ansible
from x.handlers.base import Empty
from x.handlers.shell import Shell
HANDLERS = {'ansible': Ansible,
'shell': Shell,
'none': Empty}
def get(handler_name):
handler = HANDLERS.get(handler_name, None)
if handler:
return handler
raise Exception('Handler {0} does not exist'.format(handler_name))

39
x/handlers/ansible.py Normal file
View File

@ -0,0 +1,39 @@
# -*- coding: UTF-8 -*-
import os
import subprocess
import yaml
from x.handlers.base import BaseHandler
class Ansible(BaseHandler):
def action(self, resource, action_name):
inventory_file = self._create_inventory(resource)
playbook_file = self._create_playbook(resource, action_name)
print 'inventory_file', inventory_file
print 'playbook_file', playbook_file
call_args = ['ansible-playbook', '-i', inventory_file, playbook_file]
print 'EXECUTING: ', ' '.join(call_args)
subprocess.call(call_args)
#def _get_connection(self, resource):
# return {'ssh_user': '',
# 'ssh_key': '',
# 'host': ''}
def _create_inventory(self, r):
inventory = '{0} ansible_ssh_host={1} ansible_connection=ssh ansible_ssh_user={2} ansible_ssh_private_key_file={3}'
host, user, ssh_key = r.args['ip'].value, r.args['ssh_user'].value, r.args['ssh_key'].value
print host
print user
print ssh_key
inventory = inventory.format(host, host, user, ssh_key)
print inventory
directory = self.dirs[r.name]
inventory_path = os.path.join(directory, 'inventory')
with open(inventory_path, 'w') as inv:
inv.write(inventory)
return inventory_path
def _create_playbook(self, resource, action):
return self._compile_action_file(resource, action)

53
x/handlers/base.py Normal file
View File

@ -0,0 +1,53 @@
# -*- coding: UTF-8 -*-
import os
import shutil
import tempfile
from jinja2 import Template
class BaseHandler(object):
def __init__(self, resources):
self.dst = tempfile.mkdtemp()
self.resources = resources
def __enter__(self):
self.dirs = {}
for resource in self.resources:
resource_dir = tempfile.mkdtemp(suffix=resource.name, dir=self.dst)
self.dirs[resource.name] = resource_dir
return self
def __exit__(self, type, value, traceback):
print self.dst
return
shutil.rmtree(self.dst)
def _compile_action_file(self, resource, action):
action_file = resource.metadata['actions'][action]
action_file = os.path.join(resource.base_dir, 'actions', action_file)
dir_path = self.dirs[resource.name]
dest_file = tempfile.mkstemp(text=True, prefix=action, dir=dir_path)[1]
args = self._make_args(resource)
self._compile_file(action_file, dest_file, args)
return dest_file
def _compile_file(self, template, dest_file, args):
print 'Rendering', template, args
with open(template) as f:
tpl = Template(f.read())
tpl = tpl.render(args, zip=zip)
with open(dest_file, 'w') as g:
g.write(tpl)
def _make_args(self, resource):
args = {'name': resource.name}
args['resource_dir'] = resource.base_dir
args.update(resource.args)
return args
class Empty(BaseHandler):
def action(self, resource, action):
pass

10
x/handlers/shell.py Normal file
View File

@ -0,0 +1,10 @@
# -*- coding: UTF-8 -*-
import subprocess
from x.handlers.base import BaseHandler
class Shell(BaseHandler):
def action(self, resource, action_name):
action_file = self._compile_action_file(resource, action_name)
subprocess.call(['bash', action_file])

190
x/observer.py Normal file
View File

@ -0,0 +1,190 @@
from x import signals
class BaseObserver(object):
type_ = None
def __init__(self, attached_to, name, value):
"""
:param attached_to: resource.Resource
:param name:
:param value:
:return:
"""
self.attached_to = attached_to
self.name = name
self.value = value
self.receivers = []
def log(self, msg):
print '{} {}'.format(self, msg)
def __repr__(self):
return '[{}:{}] {}'.format(self.attached_to.name, self.name, self.value)
def __unicode__(self):
return self.value
def __eq__(self, other):
if isinstance(other, BaseObserver):
return self.value == other.value
return self.value == other
def notify(self, emitter):
"""
:param emitter: Observer
:return:
"""
raise NotImplementedError
def update(self, value):
"""
:param value:
:return:
"""
raise NotImplementedError
def find_receiver(self, receiver):
fltr = [r for r in self.receivers
if r.attached_to == receiver.attached_to
and r.name == receiver.name]
if fltr:
return fltr[0]
def subscribe(self, receiver):
"""
:param receiver: Observer
:return:
"""
self.log('Subscribe {}'.format(receiver))
# No multiple subscriptions
if self.find_receiver(receiver):
self.log('No multiple subscriptions from {}'.format(receiver))
return
self.receivers.append(receiver)
receiver.subscribed(self)
signals.Connections.add(
self.attached_to,
self.name,
receiver.attached_to,
receiver.name
)
receiver.notify(self)
def subscribed(self, emitter):
self.log('Subscribed {}'.format(emitter))
def unsubscribe(self, receiver):
"""
:param receiver: Observer
:return:
"""
self.log('Unsubscribe {}'.format(receiver))
if self.find_receiver(receiver):
self.receivers.remove(receiver)
receiver.unsubscribed(self)
signals.Connections.remove(
self.attached_to,
self.name,
receiver.attached_to,
receiver.name
)
# TODO: ?
#receiver.notify(self)
def unsubscribed(self, emitter):
self.log('Unsubscribed {}'.format(emitter))
class Observer(BaseObserver):
type_ = 'simple'
def __init__(self, *args, **kwargs):
super(Observer, self).__init__(*args, **kwargs)
self.emitter = None
def notify(self, emitter):
self.log('Notify from {} value {}'.format(emitter, emitter.value))
# Copy emitter's values to receiver
self.value = emitter.value
for receiver in self.receivers:
receiver.notify(self)
self.attached_to.save()
def update(self, value):
self.log('Updating to value {}'.format(value))
self.value = value
for receiver in self.receivers:
receiver.notify(self)
self.attached_to.save()
def subscribed(self, emitter):
super(Observer, self).subscribed(emitter)
# Simple observer can be attached to at most one emitter
if self.emitter is not None:
self.emitter.unsubscribe(self)
self.emitter = emitter
def unsubscribed(self, emitter):
super(Observer, self).unsubscribed(emitter)
self.emitter = None
class ListObserver(BaseObserver):
type_ = 'list'
def __unicode__(self):
return unicode(self.value)
@staticmethod
def _format_value(emitter):
return {
'emitter': emitter.name,
'emitter_attached_to': emitter.attached_to.name,
'value': emitter.value,
}
def notify(self, emitter):
self.log('Notify from {} value {}'.format(emitter, emitter.value))
# Copy emitter's values to receiver
#self.value[emitter.attached_to.name] = emitter.value
idx = self._emitter_idx(emitter)
self.value[idx] = self._format_value(emitter)
for receiver in self.receivers:
receiver.notify(self)
self.attached_to.save()
def subscribed(self, emitter):
super(ListObserver, self).subscribed(emitter)
idx = self._emitter_idx(emitter)
if idx is None:
self.value.append(self._format_value(emitter))
def unsubscribed(self, emitter):
"""
:param receiver: Observer
:return:
"""
self.log('Unsubscribed emitter {}'.format(emitter))
idx = self._emitter_idx(emitter)
self.value.pop(idx)
def _emitter_idx(self, emitter):
try:
return [i for i, e in enumerate(self.value)
if e['emitter_attached_to'] == emitter.attached_to.name
][0]
except IndexError:
return
def create(type_, *args, **kwargs):
for klass in BaseObserver.__subclasses__():
if klass.type_ == type_:
return klass(*args, **kwargs)
raise NotImplementedError('No handling class for type {}'.format(type_))

176
x/resource.py Normal file
View File

@ -0,0 +1,176 @@
# -*- coding: UTF-8 -*-
import copy
import json
import os
import shutil
import yaml
from x import actions
from x import db
from x import observer
from x import signals
from x import utils
class Resource(object):
def __init__(self, name, metadata, args, base_dir, tags=None):
self.name = name
self.base_dir = base_dir
self.metadata = metadata
self.actions = metadata['actions'].keys() if metadata['actions'] else None
self.requires = metadata['input'].keys()
self._validate_args(args, metadata['input'])
self.args = {}
for arg_name, arg_value in args.items():
type_ = metadata.get('input-types', {}).get(arg_name) or 'simple'
self.args[arg_name] = observer.create(type_, self, arg_name, arg_value)
self.metadata['input'] = args
self.input_types = metadata.get('input-types', {})
self.changed = []
self.tags = tags or []
def __repr__(self):
return ("Resource(name='{0}', metadata={1}, args={2}, "
"base_dir='{3}', tags={4})").format(self.name,
json.dumps(self.metadata),
json.dumps(self.args_show()),
self.base_dir,
self.tags)
def args_show(self):
def formatter(v):
if isinstance(v, observer.ListObserver):
return v.value
elif isinstance(v, observer.Observer):
return {
'emitter': v.emitter.attached_to.name if v.emitter else None,
'value': v.value,
}
return v
return {k: formatter(v) for k, v in self.args.items()}
def args_dict(self):
return {k: v.value for k, v in self.args.items()}
def add_tag(self, tag):
if tag not in self.tags:
self.tags.append(tag)
def remove_tag(self, tag):
try:
self.tags.remove(tag)
except ValueError:
pass
def notify(self, emitter):
"""Update resource's args from emitter's args.
:param emitter: Resource
:return:
"""
for key, value in emitter.args.iteritems():
self.args[key].notify(value)
def update(self, args):
"""This method updates resource's args with a simple dict.
:param args:
:return:
"""
# Update will be blocked if this resource is listening
# on some input that is to be updated -- we should only listen
# to the emitter and not be able to change the input's value
for key, value in args.iteritems():
self.args[key].update(value)
def action(self, action):
if action in self.actions:
actions.resource_action(self, action)
else:
raise Exception('Uuups, action is not available')
def _validate_args(self, args, inputs):
for req in self.requires:
if req not in args:
# If metadata input is filled with a value, use it as default
# and don't report an error
if inputs.get(req):
args[req] = inputs[req]
else:
raise Exception('Requirement `{0}` is missing in args'.format(req))
# TODO: versioning
def save(self):
metadata = copy.deepcopy(self.metadata)
metadata['tags'] = self.tags
metadata['input'] = self.args_dict()
meta_file = os.path.join(self.base_dir, 'meta.yaml')
with open(meta_file, 'w') as f:
f.write(yaml.dump(metadata))
f.write(yaml.dump(metadata, default_flow_style=False))
def create(name, base_path, dest_path, args, connections={}):
if not os.path.exists(base_path):
raise Exception('Base resource does not exist: {0}'.format(base_path))
if not os.path.exists(dest_path):
raise Exception('Dest dir does not exist: {0}'.format(dest_path))
if not os.path.isdir(dest_path):
raise Exception('Dest path is not a directory: {0}'.format(dest_path))
dest_path = os.path.abspath(os.path.join(dest_path, name))
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['id'] = name
meta['version'] = '1.0.0'
meta['actions'] = {}
meta['tags'] = []
if os.path.exists(actions_path):
for f in os.listdir(actions_path):
meta['actions'][os.path.splitext(f)[0]] = f
resource = Resource(name, meta, args, dest_path)
signals.assign_connections(resource, connections)
# save
shutil.copytree(base_path, dest_path)
resource.save()
db.resource_add(name, resource)
return resource
def load(dest_path):
meta_file = os.path.join(dest_path, 'meta.yaml')
meta = utils.load_file(meta_file)
name = meta['id']
args = meta['input']
tags = meta.get('tags', [])
resource = Resource(name, meta, args, dest_path, tags=tags)
db.resource_add(name, resource)
return resource
def load_all(dest_path):
ret = {}
for name in os.listdir(dest_path):
resource_path = os.path.join(dest_path, name)
resource = load(resource_path)
ret[resource.name] = resource
signals.Connections.reconnect_all()
return ret

View File

@ -0,0 +1,5 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- shell: echo `/sbin/ifconfig`

View File

@ -0,0 +1,6 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- shell: docker stop {{ name }}
- shell: docker rm {{ name }}

View File

@ -0,0 +1,20 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- docker:
name: {{ name }}
image: {{ image }}
state: running
net: host
ports:
{% for port in ports.value %}
- {{ port['value'] }}:{{ port['value'] }}
{% endfor %}
volumes:
# TODO: host_binds might need more work
# Currently it's not that trivial to pass custom src: dst here
# (when a config variable is passed here from other resource)
# so we mount it to the same directory as on host
{% for bind in host_binds.value %}
- {{ bind['value']['src'] }}:{{ bind['value']['dst'] }}:{{ bind['value'].get('mode', 'ro') }}
{% endfor %}

View File

@ -0,0 +1,7 @@
id: data_container
handler: ansible
version: 1.0.0
input:
ip:
image:
export_volumes:

View File

@ -0,0 +1,6 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- shell: docker stop {{ name }}
- shell: docker rm {{ name }}

View File

@ -0,0 +1,21 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- docker:
name: {{ name }}
image: {{ image }}
state: running
net: host
ports:
{% for port in ports.value %}
- {{ port['value'] }}:{{ port['value'] }}
{% endfor %}
volumes:
# TODO: host_binds might need more work
# Currently it's not that trivial to pass custom src: dst here
# (when a config variable is passed here from other resource)
# so we mount it to the same directory as on host
{% for bind in host_binds.value %}
- {{ bind['value']['src'] }}:{{ bind['value']['dst'] }}:{{ bind['value'].get('mode', 'ro') }}
{% endfor %}

View File

@ -0,0 +1,15 @@
id: container
handler: ansible
version: 1.0.0
input:
ip:
image:
ports:
host_binds:
volume_binds:
ssh_user:
ssh_key:
input-types:
ports:
host_binds: list
volume_binds: list

View File

@ -0,0 +1,3 @@
#!/bin/bash
rm {{ path }}

View File

@ -0,0 +1,3 @@
#!/bin/bash
touch {{ path }}

View File

@ -0,0 +1,5 @@
id: file
handler: shell
version: 1.0.0
input:
path: /tmp/test_file

View File

@ -0,0 +1,5 @@
# TODO
- hosts: [{{ ip }}]
sudo: yes
tasks:
- file: path={{ config_dir.value['src'] }} state=absent

View File

@ -0,0 +1,21 @@
# TODO
- hosts: [{{ ip }}]
sudo: yes
vars:
config_dir: {src: {{ config_dir.value['src'] }}, dst: {{ config_dir.value['dst'] }}}
haproxy_ip: {{ ip }}
haproxy_services:
{% for service, ports, listen_port in zip(configs.value, configs_ports.value, listen_ports.value) %}
- name: {{ service['emitter_attached_to'] }}
listen_port: {{ listen_port['value'] }}
servers:
{% for server_ip, server_port in zip(service['value'], ports['value']) %}
- name: {{ server_ip['emitter_attached_to'] }}
ip: {{ server_ip['value'] }}
port: {{ server_port['value'] }}
{% endfor %}
{% endfor %}
tasks:
- file: path={{ config_dir.value['src'] }}/ state=directory
- file: path={{ config_dir.value['src'] }}/haproxy.cfg state=touch
- template: src=/vagrant/haproxy.cfg dest={{ config_dir.value['src'] }}/haproxy.cfg

View File

@ -0,0 +1,17 @@
id: haproxy
handler: ansible
version: 1.0.0
input:
ip:
config_dir: {src: /etc/solar/haproxy, dst: /etc/haproxy}
listen_ports:
configs:
configs_names:
configs_ports:
ssh_user:
ssh_key:
input-types:
listen_ports: list
configs: list
configs_names: list
configs_ports: list

View File

@ -0,0 +1,11 @@
id: haproxy_config
handler: none
version: 1.0.0
input:
name:
listen_port:
ports:
servers:
input-types:
ports: list
servers: list

View File

@ -0,0 +1,4 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- file: path={{config_dir}} state=absent

View File

@ -0,0 +1,14 @@
- hosts: [{{ ip }}]
sudo: yes
vars:
admin_token: {{admin_token}}
db_user: {{db_user}}
db_password: {{db_password}}
db_host: {{db_host}}
db_name: {{db_name}}
tasks:
- file: path={{config_dir}} state=directory
- template: src={{resource_dir}}/templates/keystone.conf dest={{config_dir}}/keystone.conf
- template: src={{resource_dir}}/templates/default_catalog.templates dest={{config_dir}}/default_catalog.templates
- template: src={{resource_dir}}/templates/logging.conf dest={{config_dir}}/logging.conf
- template: src={{resource_dir}}/templates/policy.json dest={{config_dir}}/policy.json

View File

@ -0,0 +1,13 @@
id: keystone_config
handler: ansible
version: 1.0.0
input:
config_dir:
admin_token:
db_user:
db_password:
db_host:
db_name:
ip:
ssh_key:
ssh_user:

View File

@ -0,0 +1,27 @@
# config for templated.Catalog, using camelCase because I don't want to do
# translations for keystone compat
catalog.RegionOne.identity.publicURL = http://localhost:$(public_port)s/v2.0
catalog.RegionOne.identity.adminURL = http://localhost:$(admin_port)s/v2.0
catalog.RegionOne.identity.internalURL = http://localhost:$(public_port)s/v2.0
catalog.RegionOne.identity.name = Identity Service
# fake compute service for now to help novaclient tests work
catalog.RegionOne.compute.publicURL = http://localhost:8774/v1.1/$(tenant_id)s
catalog.RegionOne.compute.adminURL = http://localhost:8774/v1.1/$(tenant_id)s
catalog.RegionOne.compute.internalURL = http://localhost:8774/v1.1/$(tenant_id)s
catalog.RegionOne.compute.name = Compute Service
catalog.RegionOne.volume.publicURL = http://localhost:8776/v1/$(tenant_id)s
catalog.RegionOne.volume.adminURL = http://localhost:8776/v1/$(tenant_id)s
catalog.RegionOne.volume.internalURL = http://localhost:8776/v1/$(tenant_id)s
catalog.RegionOne.volume.name = Volume Service
catalog.RegionOne.ec2.publicURL = http://localhost:8773/services/Cloud
catalog.RegionOne.ec2.adminURL = http://localhost:8773/services/Admin
catalog.RegionOne.ec2.internalURL = http://localhost:8773/services/Cloud
catalog.RegionOne.ec2.name = EC2 Service
catalog.RegionOne.image.publicURL = http://localhost:9292/v1
catalog.RegionOne.image.adminURL = http://localhost:9292/v1
catalog.RegionOne.image.internalURL = http://localhost:9292/v1
catalog.RegionOne.image.name = Image Service

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
[loggers]
keys=root,access
[handlers]
keys=production,file,access_file,devel
[formatters]
keys=minimal,normal,debug
###########
# Loggers #
###########
[logger_root]
level=WARNING
handlers=file
[logger_access]
level=INFO
qualname=access
handlers=access_file
################
# Log Handlers #
################
[handler_production]
class=handlers.SysLogHandler
level=ERROR
formatter=normal
args=(('localhost', handlers.SYSLOG_UDP_PORT), handlers.SysLogHandler.LOG_USER)
[handler_file]
class=handlers.WatchedFileHandler
level=WARNING
formatter=normal
args=('error.log',)
[handler_access_file]
class=handlers.WatchedFileHandler
level=INFO
formatter=minimal
args=('access.log',)
[handler_devel]
class=StreamHandler
level=NOTSET
formatter=debug
args=(sys.stdout,)
##################
# Log Formatters #
##################
[formatter_minimal]
format=%(message)s
[formatter_normal]
format=(%(name)s): %(asctime)s %(levelname)s %(message)s
[formatter_debug]
format=(%(name)s): %(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s

View File

@ -0,0 +1,171 @@
{
"admin_required": "role:admin or is_admin:1",
"service_role": "role:service",
"service_or_admin": "rule:admin_required or rule:service_role",
"owner" : "user_id:%(user_id)s",
"admin_or_owner": "rule:admin_required or rule:owner",
"default": "rule:admin_required",
"identity:get_region": "",
"identity:list_regions": "",
"identity:create_region": "rule:admin_required",
"identity:update_region": "rule:admin_required",
"identity:delete_region": "rule:admin_required",
"identity:get_service": "rule:admin_required",
"identity:list_services": "rule:admin_required",
"identity:create_service": "rule:admin_required",
"identity:update_service": "rule:admin_required",
"identity:delete_service": "rule:admin_required",
"identity:get_endpoint": "rule:admin_required",
"identity:list_endpoints": "rule:admin_required",
"identity:create_endpoint": "rule:admin_required",
"identity:update_endpoint": "rule:admin_required",
"identity:delete_endpoint": "rule:admin_required",
"identity:get_domain": "rule:admin_required",
"identity:list_domains": "rule:admin_required",
"identity:create_domain": "rule:admin_required",
"identity:update_domain": "rule:admin_required",
"identity:delete_domain": "rule:admin_required",
"identity:get_project": "rule:admin_required",
"identity:list_projects": "rule:admin_required",
"identity:list_user_projects": "rule:admin_or_owner",
"identity:create_project": "rule:admin_required",
"identity:update_project": "rule:admin_required",
"identity:delete_project": "rule:admin_required",
"identity:get_user": "rule:admin_required",
"identity:list_users": "rule:admin_required",
"identity:create_user": "rule:admin_required",
"identity:update_user": "rule:admin_required",
"identity:delete_user": "rule:admin_required",
"identity:change_password": "rule:admin_or_owner",
"identity:get_group": "rule:admin_required",
"identity:list_groups": "rule:admin_required",
"identity:list_groups_for_user": "rule:admin_or_owner",
"identity:create_group": "rule:admin_required",
"identity:update_group": "rule:admin_required",
"identity:delete_group": "rule:admin_required",
"identity:list_users_in_group": "rule:admin_required",
"identity:remove_user_from_group": "rule:admin_required",
"identity:check_user_in_group": "rule:admin_required",
"identity:add_user_to_group": "rule:admin_required",
"identity:get_credential": "rule:admin_required",
"identity:list_credentials": "rule:admin_required",
"identity:create_credential": "rule:admin_required",
"identity:update_credential": "rule:admin_required",
"identity:delete_credential": "rule:admin_required",
"identity:ec2_get_credential": "rule:admin_or_owner",
"identity:ec2_list_credentials": "rule:admin_or_owner",
"identity:ec2_create_credential": "rule:admin_or_owner",
"identity:ec2_delete_credential": "rule:admin_required or (rule:owner and user_id:%(target.credential.user_id)s)",
"identity:get_role": "rule:admin_required",
"identity:list_roles": "rule:admin_required",
"identity:create_role": "rule:admin_required",
"identity:update_role": "rule:admin_required",
"identity:delete_role": "rule:admin_required",
"identity:check_grant": "rule:admin_required",
"identity:list_grants": "rule:admin_required",
"identity:create_grant": "rule:admin_required",
"identity:revoke_grant": "rule:admin_required",
"identity:list_role_assignments": "rule:admin_required",
"identity:get_policy": "rule:admin_required",
"identity:list_policies": "rule:admin_required",
"identity:create_policy": "rule:admin_required",
"identity:update_policy": "rule:admin_required",
"identity:delete_policy": "rule:admin_required",
"identity:check_token": "rule:admin_required",
"identity:validate_token": "rule:service_or_admin",
"identity:validate_token_head": "rule:service_or_admin",
"identity:revocation_list": "rule:service_or_admin",
"identity:revoke_token": "rule:admin_or_owner",
"identity:create_trust": "user_id:%(trust.trustor_user_id)s",
"identity:get_trust": "rule:admin_or_owner",
"identity:list_trusts": "",
"identity:list_roles_for_trust": "",
"identity:check_role_for_trust": "",
"identity:get_role_for_trust": "",
"identity:delete_trust": "",
"identity:create_consumer": "rule:admin_required",
"identity:get_consumer": "rule:admin_required",
"identity:list_consumers": "rule:admin_required",
"identity:delete_consumer": "rule:admin_required",
"identity:update_consumer": "rule:admin_required",
"identity:authorize_request_token": "rule:admin_required",
"identity:list_access_token_roles": "rule:admin_required",
"identity:get_access_token_role": "rule:admin_required",
"identity:list_access_tokens": "rule:admin_required",
"identity:get_access_token": "rule:admin_required",
"identity:delete_access_token": "rule:admin_required",
"identity:list_projects_for_endpoint": "rule:admin_required",
"identity:add_endpoint_to_project": "rule:admin_required",
"identity:check_endpoint_in_project": "rule:admin_required",
"identity:list_endpoints_for_project": "rule:admin_required",
"identity:remove_endpoint_from_project": "rule:admin_required",
"identity:create_endpoint_group": "rule:admin_required",
"identity:list_endpoint_groups": "rule:admin_required",
"identity:get_endpoint_group": "rule:admin_required",
"identity:update_endpoint_group": "rule:admin_required",
"identity:delete_endpoint_group": "rule:admin_required",
"identity:list_projects_associated_with_endpoint_group": "rule:admin_required",
"identity:list_endpoints_associated_with_endpoint_group": "rule:admin_required",
"identity:list_endpoint_groups_for_project": "rule:admin_required",
"identity:add_endpoint_group_to_project": "rule:admin_required",
"identity:remove_endpoint_group_from_project": "rule:admin_required",
"identity:create_identity_provider": "rule:admin_required",
"identity:list_identity_providers": "rule:admin_required",
"identity:get_identity_providers": "rule:admin_required",
"identity:update_identity_provider": "rule:admin_required",
"identity:delete_identity_provider": "rule:admin_required",
"identity:create_protocol": "rule:admin_required",
"identity:update_protocol": "rule:admin_required",
"identity:get_protocol": "rule:admin_required",
"identity:list_protocols": "rule:admin_required",
"identity:delete_protocol": "rule:admin_required",
"identity:create_mapping": "rule:admin_required",
"identity:get_mapping": "rule:admin_required",
"identity:list_mappings": "rule:admin_required",
"identity:delete_mapping": "rule:admin_required",
"identity:update_mapping": "rule:admin_required",
"identity:get_auth_catalog": "",
"identity:get_auth_projects": "",
"identity:get_auth_domains": "",
"identity:list_projects_for_groups": "",
"identity:list_domains_for_groups": "",
"identity:list_revoke_events": "",
"identity:create_policy_association_for_endpoint": "rule:admin_required",
"identity:check_policy_association_for_endpoint": "rule:admin_required",
"identity:delete_policy_association_for_endpoint": "rule:admin_required",
"identity:create_policy_association_for_service": "rule:admin_required",
"identity:check_policy_association_for_service": "rule:admin_required",
"identity:delete_policy_association_for_service": "rule:admin_required",
"identity:create_policy_association_for_region_and_service": "rule:admin_required",
"identity:check_policy_association_for_region_and_service": "rule:admin_required",
"identity:delete_policy_association_for_region_and_service": "rule:admin_required",
"identity:get_policy_for_endpoint": "rule:admin_required",
"identity:list_endpoints_for_policy": "rule:admin_required"
}

View File

@ -0,0 +1,6 @@
# TODO
- hosts: [{{ ip }}]
sudo: yes
tasks:
- shell: docker stop {{ name }}
- shell: docker rm {{ name }}

View File

@ -0,0 +1,17 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: keystone container
docker:
command: /bin/bash -c "keystone-manage db_sync && /usr/bin/keystone-all"
name: {{ name }}
image: {{ image }}
state: running
expose:
- 5000
- 35357
ports:
- {{ port }}:5000
- {{ admin_port }}:35357
volumes:
- {{ config_dir }}:/etc/keystone

View File

@ -0,0 +1,11 @@
id: keystone
handler: ansible
version: 1.0.0
input:
image: kollaglue/centos-rdo-keystone
config_dir:
port:
admin_port:
ip:
ssh_key:
ssh_user:

View File

@ -0,0 +1,6 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: keystone user
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ user={{user_name}} tenant={{tenant_name}} state=absent
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ tenant={{tenant_name}} state=absent

View File

@ -0,0 +1,6 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: keystone user
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ tenant={{tenant_name}} state=present
- keystone_user: endpoint=http://{keystone_host}}:{{keystone_port}}/v2.0/ user={{user_name}} password={{user_password}} tenant={{tenant_name}} state=present

View File

@ -0,0 +1,14 @@
id: keystone_user
handler: ansible
version: 1.0.0
input:
keystone_host:
keystone_port:
login_user:
login_token:
user_name:
user_password:
tenant_name:
ip:
ssh_key:
ssh_user:

View File

@ -0,0 +1,11 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: mariadb db
mysql_db:
name: {{db_name}}
state: absent
login_user: root
login_password: {{login_password}}
login_port: {{login_port}}
login_host: 127.0.0.1

View File

@ -0,0 +1,11 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: mariadb db
mysql_db:
name: {{db_name}}
state: present
login_user: root
login_password: {{login_password}}
login_port: {{login_port}}
login_host: 127.0.0.1

View File

@ -0,0 +1,14 @@
id: mariadb_table
handler: ansible
version: 1.0.0
actions:
run: run.yml
remove: remove.yml
input:
db_name:
login_password:
login_port:
login_user:
ip:
ssh_key:
ssh_user:

View File

@ -0,0 +1,8 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: mariadb container
docker:
name: {{ name }}
image: {{ image }}
state: absent

View File

@ -0,0 +1,12 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: mariadb container
docker:
name: {{ name }}
image: {{ image }}
state: running
ports:
- {{ port }}:3306
env:
MYSQL_ROOT_PASSWORD: {{ root_password }}

View File

@ -0,0 +1,10 @@
id: mariadb
handler: ansible
version: 1.0.0
input:
image:
root_password:
port:
ip:
ssh_key:
ssh_user:

View File

@ -0,0 +1,11 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: mariadb user
mysql_user:
name: {{new_user_name}}
state: absent
login_user: root
login_password: {{login_password}}
login_port: {{login_port}}
login_host: 127.0.0.1

View File

@ -0,0 +1,14 @@
- hosts: [{{ ip }}]
sudo: yes
tasks:
- name: mariadb user
mysql_user:
name: {{new_user_name}}
password: {{new_user_password}}
priv: {{db_name}}.*:ALL
host: '%'
state: present
login_user: root
login_password: {{login_password}}
login_port: {{login_port}}
login_host: 127.0.0.1

View File

@ -0,0 +1,16 @@
id: mariadb_user
handler: ansible
version: 1.0.0
actions:
run: run.yml
remove: remove.yml
input:
new_user_password:
new_user_name:
db_name:
login_password:
login_port:
login_user:
ip:
ssh_key:
ssh_user:

View File

@ -0,0 +1,6 @@
# TODO
- hosts: [{{ ip }}]
sudo: yes
tasks:
- shell: docker stop {{ name }}
- shell: docker rm {{ name }}

View File

@ -0,0 +1,6 @@
# TODO
- hosts: [{{ ip }}]
sudo: yes
tasks:
- shell: docker run -d --net="host" --privileged \
--name {{ name }} {{ image }}

View File

@ -0,0 +1,7 @@
id: nova
handler: ansible
version: 1.0.0
input:
ip:
port: 8774
image: # TODO

View File

@ -0,0 +1,8 @@
id: mariadb
handler: none
version: 1.0.0
actions:
input:
ip:
ssh_key:
ssh_user:

201
x/signals.py Normal file
View File

@ -0,0 +1,201 @@
# -*- coding: UTF-8 -*-
from collections import defaultdict
import itertools
import networkx as nx
import os
import db
from x import utils
CLIENTS_CONFIG_KEY = 'clients-data-file'
CLIENTS = utils.read_config_file(CLIENTS_CONFIG_KEY)
class Connections(object):
@staticmethod
def add(emitter, src, receiver, dst):
if src not in emitter.args:
return
# TODO: implement general circular detection, this one is simple
if [emitter.name, src] in CLIENTS.get(receiver.name, {}).get(dst, []):
raise Exception('Attempted to create cycle in dependencies. Not nice.')
CLIENTS.setdefault(emitter.name, {})
CLIENTS[emitter.name].setdefault(src, [])
if [receiver.name, dst] not in CLIENTS[emitter.name][src]:
CLIENTS[emitter.name][src].append([receiver.name, dst])
utils.save_to_config_file(CLIENTS_CONFIG_KEY, CLIENTS)
@staticmethod
def remove(emitter, src, receiver, dst):
CLIENTS[emitter.name][src] = [
destination for destination in CLIENTS[emitter.name][src]
if destination != [receiver.name, dst]
]
utils.save_to_config_file(CLIENTS_CONFIG_KEY, CLIENTS)
@staticmethod
def reconnect_all():
"""Reconstruct connections for resource inputs from CLIENTS.
:return:
"""
for emitter_name, dest_dict in CLIENTS.items():
emitter = db.get_resource(emitter_name)
for emitter_input, destinations in dest_dict.items():
for receiver_name, receiver_input in destinations:
receiver = db.get_resource(receiver_name)
emitter.args[emitter_input].subscribe(
receiver.args[receiver_input])
@staticmethod
def clear():
global CLIENTS
CLIENTS = {}
path = utils.read_config()[CLIENTS_CONFIG_KEY]
if os.path.exists(path):
os.remove(path)
def guess_mapping(emitter, receiver):
"""Guess connection mapping between emitter and receiver.
Suppose emitter and receiver have common inputs:
ip, ssh_key, ssh_user
Then we return a connection mapping like this:
{
'ip': '<receiver>.ip',
'ssh_key': '<receiver>.ssh_key',
'ssh_user': '<receiver>.ssh_user'
}
:param emitter:
:param receiver:
:return:
"""
guessed = {}
for key in emitter.requires:
if key in receiver.requires:
guessed[key] = key
return guessed
def connect(emitter, receiver, mapping=None):
guessed = guess_mapping(emitter, receiver)
mapping = mapping or guessed
for src, dst in mapping.items():
# Disconnect all receiver inputs
# Check if receiver input is of list type first
if receiver.args[dst].type_ != 'list':
disconnect_receiver_by_input(receiver, dst)
emitter.args[src].subscribe(receiver.args[dst])
receiver.save()
def disconnect(emitter, receiver):
for src, destinations in CLIENTS[emitter.name].items():
disconnect_by_src(emitter, src, receiver)
for destination in destinations:
receiver_input = destination[1]
if receiver.args[receiver_input].type_ != 'list':
print 'Removing input {} from {}'.format(receiver_input, receiver.name)
emitter.args[src].unsubscribe(receiver.args[receiver_input])
def disconnect_receiver_by_input(receiver, input):
"""Find receiver connection by input and disconnect it.
:param receiver:
:param input:
:return:
"""
for emitter_name, inputs in CLIENTS.items():
emitter = db.get_resource(emitter_name)
disconnect_by_src(emitter, input, receiver)
def disconnect_by_src(emitter, src, receiver):
if src in CLIENTS[emitter.name]:
CLIENTS[emitter.name][src] = [
destination for destination in CLIENTS[emitter.name][src]
if destination[0] != receiver.name
]
utils.save_to_config_file(CLIENTS_CONFIG_KEY, CLIENTS)
def notify(source, key, value):
CLIENTS.setdefault(source.name, {})
print 'Notify', source.name, key, value, CLIENTS[source.name]
if key in CLIENTS[source.name]:
for client, r_key in CLIENTS[source.name][key]:
resource = db.get_resource(client)
print 'Resource found', client
if resource:
resource.update({r_key: value}, emitter=source)
else:
print 'Resource {} deleted?'.format(client)
pass
def assign_connections(receiver, connections):
mappings = defaultdict(list)
for key, dest in connections.iteritems():
resource, r_key = dest.split('.')
mappings[resource].append([r_key, key])
for resource, r_mappings in mappings.iteritems():
connect(resource, receiver, r_mappings)
def connection_graph():
resource_dependencies = {}
for source, destination_values in CLIENTS.items():
resource_dependencies.setdefault(source, set())
for src, destinations in destination_values.items():
resource_dependencies[source].update([
destination[0] for destination in destinations
])
g = nx.DiGraph()
# TODO: tags as graph node attributes
for source, destinations in resource_dependencies.items():
g.add_node(source)
g.add_nodes_from(destinations)
g.add_edges_from(
itertools.izip(
itertools.repeat(source),
destinations
)
)
return g
def detailed_connection_graph():
g = nx.MultiDiGraph()
for emitter_name, destination_values in CLIENTS.items():
for emitter_input, receivers in CLIENTS[emitter_name].items():
for receiver_name, receiver_input in receivers:
label = emitter_input
if emitter_input != receiver_input:
label = '{}:{}'.format(emitter_input, receiver_input)
g.add_edge(emitter_name, receiver_name, label=label)
return g

1
x/test/__init__.py Normal file
View File

@ -0,0 +1 @@
__author__ = 'przemek'

Some files were not shown because too many files have changed in this diff Show More