438 lines
13 KiB
Python
438 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import json
|
|
import web
|
|
import ipaddr
|
|
import netaddr
|
|
|
|
from models import Release, Cluster, Node, Role, Network, Vlan
|
|
from settings import settings
|
|
import rpc
|
|
|
|
|
|
def check_client_content_type(handler):
|
|
content_type = web.ctx.env.get("CONTENT_TYPE", "application/json")
|
|
if web.ctx.path.startswith("/api")\
|
|
and not content_type.startswith("application/json"):
|
|
raise web.unsupportedmediatype
|
|
return handler()
|
|
|
|
handlers = {}
|
|
|
|
|
|
class HandlerRegistrator(type):
|
|
def __init__(cls, name, bases, dct):
|
|
super(HandlerRegistrator, cls).__init__(name, bases, dct)
|
|
if hasattr(cls, 'model'):
|
|
key = cls.model.__name__
|
|
if key in handlers:
|
|
raise Exception("Handler for %s already registered" % key)
|
|
handlers[key] = cls
|
|
|
|
|
|
class JSONHandler(object):
|
|
__metaclass__ = HandlerRegistrator
|
|
|
|
fields = []
|
|
|
|
@classmethod
|
|
def render(cls, instance, fields=None):
|
|
json_data = {}
|
|
use_fields = fields if fields else cls.fields
|
|
if not use_fields:
|
|
raise ValueError("No fields for serialize")
|
|
for field in use_fields:
|
|
if isinstance(field, (tuple,)):
|
|
if field[1] == '*':
|
|
subfields = None
|
|
else:
|
|
subfields = field[1:]
|
|
|
|
value = getattr(instance, field[0])
|
|
rel = getattr(
|
|
instance.__class__, field[0]).impl.__class__.__name__
|
|
if value is None:
|
|
pass
|
|
elif rel == 'ScalarObjectAttributeImpl':
|
|
handler = handlers[value.__class__.__name__]
|
|
json_data[field[0]] = handler.render(
|
|
value, fields=subfields
|
|
)
|
|
elif rel == 'CollectionAttributeImpl':
|
|
if not value:
|
|
json_data[field[0]] = []
|
|
else:
|
|
handler = handlers[value[0].__class__.__name__]
|
|
json_data[field[0]] = [
|
|
handler.render(v, fields=subfields) for v in value
|
|
]
|
|
else:
|
|
value = getattr(instance, field)
|
|
if value is None:
|
|
pass
|
|
else:
|
|
f = getattr(instance.__class__, field)
|
|
if hasattr(f, "impl"):
|
|
rel = f.impl.__class__.__name__
|
|
if rel == 'ScalarObjectAttributeImpl':
|
|
json_data[field] = value.id
|
|
elif rel == 'CollectionAttributeImpl':
|
|
json_data[field] = [v.id for v in value]
|
|
else:
|
|
json_data[field] = value
|
|
else:
|
|
json_data[field] = value
|
|
return json_data
|
|
|
|
|
|
class ClusterHandler(JSONHandler):
|
|
fields = (
|
|
"id",
|
|
"name",
|
|
("nodes", "*"),
|
|
("release", "*")
|
|
)
|
|
model = Cluster
|
|
|
|
@classmethod
|
|
def render(cls, instance, fields=None):
|
|
json_data = JSONHandler.render(instance, fields=cls.fields)
|
|
json_data["nodes"] = map(
|
|
NodeHandler.render,
|
|
instance.nodes
|
|
)
|
|
return json_data
|
|
|
|
def GET(self, cluster_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Cluster)
|
|
cluster = q.filter(Cluster.id == cluster_id).first()
|
|
if not cluster:
|
|
return web.notfound()
|
|
return json.dumps(
|
|
self.render(cluster),
|
|
indent=4
|
|
)
|
|
|
|
def PUT(self, cluster_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Cluster).filter(Cluster.id == cluster_id)
|
|
cluster = q.first()
|
|
if not cluster:
|
|
return web.notfound()
|
|
# additional validation needed?
|
|
data = Cluster.validate_json(web.data())
|
|
# /additional validation needed?
|
|
for key, value in data.iteritems():
|
|
if key == "nodes":
|
|
map(cluster.nodes.remove, cluster.nodes)
|
|
nodes = web.ctx.orm.query(Node).filter(
|
|
Node.id.in_(value)
|
|
)
|
|
map(cluster.nodes.append, nodes)
|
|
else:
|
|
setattr(cluster, key, value)
|
|
web.ctx.orm.add(cluster)
|
|
web.ctx.orm.commit()
|
|
return json.dumps(
|
|
self.render(cluster),
|
|
indent=4
|
|
)
|
|
|
|
def DELETE(self, cluster_id):
|
|
cluster = web.ctx.orm.query(Cluster).filter(
|
|
Cluster.id == cluster_id
|
|
).first()
|
|
if not cluster:
|
|
return web.notfound()
|
|
web.ctx.orm.delete(cluster)
|
|
web.ctx.orm.commit()
|
|
raise web.webapi.HTTPError(
|
|
status="204 No Content",
|
|
data=""
|
|
)
|
|
|
|
|
|
class ClusterCollectionHandler(JSONHandler):
|
|
def GET(self):
|
|
web.header('Content-Type', 'application/json')
|
|
return json.dumps(map(
|
|
ClusterHandler.render,
|
|
web.ctx.orm.query(Cluster).all()
|
|
), indent=4)
|
|
|
|
def POST(self):
|
|
web.header('Content-Type', 'application/json')
|
|
data = Cluster.validate(web.data())
|
|
release = web.ctx.orm.query(Release).get(data["release"])
|
|
|
|
cluster = Cluster(
|
|
name=data["name"],
|
|
release=release
|
|
)
|
|
# TODO: discover how to add multiple objects
|
|
if 'nodes' in data and data['nodes']:
|
|
nodes = web.ctx.orm.query(Node).filter(
|
|
Node.id.in_(data['nodes'])
|
|
)
|
|
map(cluster.nodes.append, nodes)
|
|
|
|
web.ctx.orm.add(cluster)
|
|
web.ctx.orm.commit()
|
|
|
|
used_nets = [n.cidr for n in web.ctx.orm.query(Network).all()]
|
|
used_vlans = [v.id for v in web.ctx.orm.query(Vlan).all()]
|
|
|
|
for network in release.networks_metadata:
|
|
new_vlan = sorted(list(set(settings.VLANS) - set(used_vlans)))[0]
|
|
vlan_db = Vlan(id=new_vlan)
|
|
web.ctx.orm.add(vlan_db)
|
|
|
|
pool = settings.NETWORK_POOLS[network['access']]
|
|
nets_free_set = netaddr.IPSet(pool) -\
|
|
netaddr.IPSet(settings.NET_EXCLUDE) -\
|
|
netaddr.IPSet(used_nets)
|
|
|
|
free_cidrs = sorted(list(nets_free_set._cidrs))
|
|
new_net = list(free_cidrs[0].subnet(24, count=1))[0]
|
|
|
|
nw_db = Network(
|
|
release=release.id,
|
|
name=network['name'],
|
|
access=network['access'],
|
|
cidr=str(new_net),
|
|
gateway=str(new_net[1]),
|
|
vlan_id=vlan_db.id
|
|
)
|
|
web.ctx.orm.add(nw_db)
|
|
web.ctx.orm.commit()
|
|
|
|
used_vlans.append(new_vlan)
|
|
used_nets.append(str(new_net))
|
|
|
|
raise web.webapi.created(json.dumps(
|
|
ClusterHandler.render(cluster),
|
|
indent=4
|
|
))
|
|
|
|
|
|
class ClusterChangesHandler(JSONHandler):
|
|
fields = (
|
|
"id",
|
|
"name",
|
|
)
|
|
|
|
def PUT(self, cluster_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Cluster).filter(Cluster.id == cluster_id)
|
|
cluster = q.first()
|
|
if not cluster:
|
|
return web.notfound()
|
|
|
|
message = {"method": "deploy", "args": {"var1": "Hello from nailgun"}}
|
|
rpc.cast('mcollective', message)
|
|
|
|
return json.dumps(
|
|
self.render(cluster),
|
|
indent=4
|
|
)
|
|
|
|
|
|
class ReleaseHandler(JSONHandler):
|
|
fields = (
|
|
"id",
|
|
"name",
|
|
"version",
|
|
"description",
|
|
"networks_metadata",
|
|
("roles", "name")
|
|
)
|
|
model = Release
|
|
|
|
def GET(self, release_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Release)
|
|
release = q.filter(Release.id == release_id).first()
|
|
if not release:
|
|
return web.notfound()
|
|
return json.dumps(
|
|
self.render(release),
|
|
indent=4
|
|
)
|
|
|
|
def PUT(self, release_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Release)
|
|
release = q.filter(Release.id == release_id).first()
|
|
if not release:
|
|
return web.notfound()
|
|
# additional validation needed?
|
|
data = Release.validate_json(web.data())
|
|
# /additional validation needed?
|
|
for key, value in data.iteritems():
|
|
setattr(release, key, value)
|
|
web.ctx.orm.commit()
|
|
return json.dumps(
|
|
self.render(release),
|
|
indent=4
|
|
)
|
|
|
|
def DELETE(self, release_id):
|
|
release = web.ctx.orm.query(Release).filter(
|
|
Release.id == release_id
|
|
).first()
|
|
if not release:
|
|
return web.notfound()
|
|
web.ctx.orm.delete(release)
|
|
web.ctx.orm.commit()
|
|
raise web.webapi.HTTPError(
|
|
status="204 No Content",
|
|
data=""
|
|
)
|
|
|
|
|
|
class ReleaseCollectionHandler(JSONHandler):
|
|
def GET(self):
|
|
web.header('Content-Type', 'application/json')
|
|
return json.dumps(map(
|
|
ReleaseHandler.render,
|
|
web.ctx.orm.query(Release).all()
|
|
), indent=4)
|
|
|
|
def POST(self):
|
|
web.header('Content-Type', 'application/json')
|
|
data = Release.validate(web.data())
|
|
release = Release()
|
|
for key, value in data.iteritems():
|
|
setattr(release, key, value)
|
|
web.ctx.orm.add(release)
|
|
web.ctx.orm.commit()
|
|
raise web.webapi.created(json.dumps(
|
|
ReleaseHandler.render(release),
|
|
indent=4
|
|
))
|
|
|
|
|
|
class NodeHandler(JSONHandler):
|
|
fields = ('id', 'name', 'info', ('roles', '*'), ('new_roles', '*'),
|
|
'status', 'mac', 'fqdn', 'ip', 'manufacturer', 'platform_name',
|
|
'redeployment_needed', 'os_platform')
|
|
model = Node
|
|
|
|
def GET(self, node_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Node)
|
|
node = q.filter(Node.id == node_id).first()
|
|
if not node:
|
|
return web.notfound()
|
|
|
|
return json.dumps(
|
|
self.render(node),
|
|
indent=4
|
|
)
|
|
|
|
def PUT(self, node_id):
|
|
web.header('Content-Type', 'application/json')
|
|
q = web.ctx.orm.query(Node)
|
|
node = q.filter(Node.id == node_id).first()
|
|
if not node:
|
|
return web.notfound()
|
|
# additional validation needed?
|
|
data = Node.validate_update(web.data())
|
|
if not data:
|
|
raise web.badrequest()
|
|
# /additional validation needed?
|
|
for key, value in data.iteritems():
|
|
setattr(node, key, value)
|
|
web.ctx.orm.commit()
|
|
return json.dumps(
|
|
self.render(node),
|
|
indent=4
|
|
)
|
|
|
|
def DELETE(self, node_id):
|
|
node = web.ctx.orm.query(Node).filter(
|
|
Node.id == node_id
|
|
).first()
|
|
if not node:
|
|
return web.notfound()
|
|
web.ctx.orm.delete(node)
|
|
web.ctx.orm.commit()
|
|
raise web.webapi.HTTPError(
|
|
status="204 No Content",
|
|
data=""
|
|
)
|
|
|
|
|
|
class NodeCollectionHandler(JSONHandler):
|
|
|
|
def GET(self):
|
|
web.header('Content-Type', 'application/json')
|
|
return json.dumps(map(
|
|
NodeHandler.render,
|
|
web.ctx.orm.query(Node).all()
|
|
), indent=4)
|
|
|
|
def POST(self):
|
|
web.header('Content-Type', 'application/json')
|
|
data = Node.validate(web.data())
|
|
node = Node()
|
|
for key, value in data.iteritems():
|
|
setattr(node, key, value)
|
|
web.ctx.orm.add(node)
|
|
web.ctx.orm.commit()
|
|
raise web.webapi.created(json.dumps(
|
|
NodeHandler.render(node),
|
|
indent=4
|
|
))
|
|
|
|
|
|
class RoleCollectionHandler(JSONHandler):
|
|
|
|
def GET(self):
|
|
web.header('Content-Type', 'application/json')
|
|
data = web.data() if web.data() else {}
|
|
if data:
|
|
data = Role.validate_json(data)
|
|
if 'release_id' in data:
|
|
return json.dumps(
|
|
map(
|
|
RoleHandler.render,
|
|
web.ctx.orm.query(Role).filter(
|
|
Role.id == data["release_id"]
|
|
)
|
|
), indent=4)
|
|
|
|
roles = web.ctx.orm.query(Role).all()
|
|
if 'node_id' in data:
|
|
result = []
|
|
for role in roles:
|
|
# TODO role filtering
|
|
# use request.form.cleaned_data['node_id'] to filter roles
|
|
if False:
|
|
continue
|
|
# if the role is suitable for the node, set 'available' field
|
|
# to True. If it is not, set it to False and also describe the
|
|
# reason in 'reason' field of rendered_role
|
|
rendered_role = RoleHandler.render(role)
|
|
rendered_role['available'] = True
|
|
result.append(rendered_role)
|
|
return json.dumps(result)
|
|
else:
|
|
return json.dumps(map(RoleHandler.render, roles))
|
|
|
|
|
|
class RoleHandler(JSONHandler):
|
|
fields = ('id', 'name', ('release', 'id', 'name'))
|
|
model = Role
|
|
|
|
def GET(self, role_id):
|
|
q = web.ctx.orm.query(Role)
|
|
role = q.filter(Role.id == role_id).first()
|
|
if not role:
|
|
return web.notfound()
|
|
return json.dumps(
|
|
self.render(role),
|
|
indent=4
|
|
)
|