nailgun reborn
20
nailgun/api/fields.py
Normal file
@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
import sqlalchemy.types as types
|
||||
|
||||
|
||||
class JSON(types.TypeDecorator):
|
||||
|
||||
impl = types.Text
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
349
nailgun/api/handlers.py
Normal file
@ -0,0 +1,349 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
import web
|
||||
import ipaddr
|
||||
|
||||
import settings
|
||||
from helpers.vlan import VlanManager
|
||||
from api.models import Release, Cluster, Node, Role, Network
|
||||
|
||||
|
||||
def check_client_content_type(handler):
|
||||
content_type = web.ctx.env.get("CONTENT_TYPE", "application/json")
|
||||
if content_type != "application/json" \
|
||||
and web.ctx.path.startswith("/api"):
|
||||
raise web.unsupportedmediatype
|
||||
return handler()
|
||||
|
||||
|
||||
class JSONHandler(object):
|
||||
fields = []
|
||||
|
||||
@classmethod
|
||||
def render(cls, instance, fields=None):
|
||||
json_data = {}
|
||||
use_fields = fields if fields else cls.fields
|
||||
for field in use_fields:
|
||||
json_data[field] = getattr(instance, field)
|
||||
return json_data
|
||||
|
||||
|
||||
class ClusterHandler(JSONHandler):
|
||||
fields = (
|
||||
"id",
|
||||
"name",
|
||||
"release_id"
|
||||
)
|
||||
|
||||
@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":
|
||||
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()
|
||||
|
||||
network_objects = web.ctx.orm.query(Network)
|
||||
for network in release.networks_metadata:
|
||||
for nw_pool in settings.NETWORK_POOLS[network['access']]:
|
||||
nw_ip = ipaddr.IPv4Network(nw_pool)
|
||||
new_network = None
|
||||
for net in nw_ip.iter_subnets(new_prefix=24):
|
||||
nw_exist = network_objects.filter(
|
||||
Network.network == str(net)
|
||||
).first()
|
||||
if not nw_exist:
|
||||
new_network = net
|
||||
break
|
||||
if new_network:
|
||||
break
|
||||
|
||||
nw = Network(
|
||||
release=release.id,
|
||||
name=network['name'],
|
||||
access=network['access'],
|
||||
network=str(new_network),
|
||||
gateway=str(new_network[1]),
|
||||
range_l=str(new_network[3]),
|
||||
range_h=str(new_network[-1]),
|
||||
vlan_id=VlanManager.generate_id(network['name'])
|
||||
)
|
||||
web.ctx.orm.add(nw)
|
||||
web.ctx.orm.commit()
|
||||
|
||||
raise web.webapi.created(json.dumps(
|
||||
ClusterHandler.render(cluster),
|
||||
indent=4
|
||||
))
|
||||
|
||||
|
||||
class ReleaseHandler(JSONHandler):
|
||||
fields = (
|
||||
"name",
|
||||
"version",
|
||||
"description",
|
||||
"networks_metadata"
|
||||
)
|
||||
|
||||
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', 'roles', 'status', 'mac', 'fqdn', 'ip',
|
||||
'manufacturer', 'platform_name', 'redeployment_needed',
|
||||
'os_platform')
|
||||
|
||||
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 = Role.validate_json(web.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')
|
||||
|
||||
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
|
||||
)
|
178
nailgun/api/models.py
Normal file
@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
|
||||
import web
|
||||
import ipaddr
|
||||
from sqlalchemy import Column, UniqueConstraint, Table
|
||||
from sqlalchemy import Integer, String, Unicode, Boolean, ForeignKey, Enum
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
import settings
|
||||
from api.fields import JSON
|
||||
from api.validators import BasicValidator
|
||||
|
||||
engine = create_engine(settings.DATABASE_ENGINE)
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Release(Base, BasicValidator):
|
||||
__tablename__ = 'releases'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('name', 'version'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(Unicode(100), nullable=False)
|
||||
version = Column(String(30), nullable=False)
|
||||
description = Column(Unicode)
|
||||
networks_metadata = Column(JSON)
|
||||
roles = relationship("Role", backref="release")
|
||||
clusters = relationship("Cluster", backref="release")
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
d = cls.validate_json(data)
|
||||
if not "name" in d:
|
||||
raise web.webapi.badrequest(
|
||||
message="No release name specified"
|
||||
)
|
||||
if not "version" in d:
|
||||
raise web.webapi.badrequest(
|
||||
message="No release version specified"
|
||||
)
|
||||
if web.ctx.orm.query(Release).filter(
|
||||
Release.name == d["name"] \
|
||||
and Release.version == d["version"]
|
||||
).first():
|
||||
raise web.webapi.conflict
|
||||
if "networks_metadata" in d:
|
||||
for network in d["networks_metadata"]:
|
||||
if not "name" in network or not "access" in network:
|
||||
raise web.webapi.badrequest(
|
||||
message="Invalid network data: %s" % str(network)
|
||||
)
|
||||
if network["access"] not in settings.NETWORK_POOLS:
|
||||
raise web.webapi.badrequest(
|
||||
message="Invalid access mode for network"
|
||||
)
|
||||
else:
|
||||
d["networks_metadata"] = []
|
||||
return d
|
||||
|
||||
|
||||
class Role(Base):
|
||||
__tablename__ = 'roles'
|
||||
__table_args__ = (
|
||||
UniqueConstraint('name', 'release_id'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(Unicode(100), nullable=False)
|
||||
release_id = Column(Integer, ForeignKey('releases.id'), nullable=False)
|
||||
|
||||
|
||||
class Cluster(Base, BasicValidator):
|
||||
__tablename__ = 'clusters'
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(Unicode(100), unique=True, nullable=False)
|
||||
release_id = Column(Integer, ForeignKey('releases.id'), nullable=False)
|
||||
nodes = relationship("Node", backref="cluster")
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
d = cls.validate_json(data)
|
||||
if web.ctx.orm.query(Cluster).filter(
|
||||
Cluster.name == d["name"]
|
||||
).first():
|
||||
raise web.webapi.conflict
|
||||
if d["release"]:
|
||||
release = web.ctx.orm.query(Release).get(d["release"])
|
||||
if not release:
|
||||
raise web.webapi.badrequest(message="Invalid release id")
|
||||
return d
|
||||
|
||||
|
||||
nodes_roles = Table('nodes_roles', Base.metadata,
|
||||
Column('node', Integer, ForeignKey('nodes.id')),
|
||||
Column('role', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
nodes_new_roles = Table('nodes_new_roles', Base.metadata,
|
||||
Column('node', Integer, ForeignKey('nodes.id')),
|
||||
Column('role', Integer, ForeignKey('roles.id'))
|
||||
)
|
||||
|
||||
|
||||
class Node(Base, BasicValidator):
|
||||
__tablename__ = 'nodes'
|
||||
NODE_STATUSES = (
|
||||
'offline',
|
||||
'ready',
|
||||
'discover',
|
||||
'deploying',
|
||||
'error'
|
||||
)
|
||||
id = Column(Integer, primary_key=True)
|
||||
cluster_id = Column(Integer, ForeignKey('clusters.id'))
|
||||
name = Column(Unicode(100))
|
||||
status = Column(Enum(*NODE_STATUSES), nullable=False, default='ready')
|
||||
meta = Column(JSON)
|
||||
mac = Column(String(17), nullable=False)
|
||||
ip = Column(String(15))
|
||||
fqdn = Column(String(255))
|
||||
manufacturer = Column(Unicode(50))
|
||||
platform_name = Column(String(150))
|
||||
os_platform = Column(String(150))
|
||||
roles = relationship("Role",
|
||||
secondary=nodes_roles,
|
||||
backref="nodes")
|
||||
new_roles = relationship("Role",
|
||||
secondary=nodes_new_roles)
|
||||
redeployment_needed = Column(Boolean, default=False)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
d = cls.validate_json(data)
|
||||
if not "mac" in d:
|
||||
raise web.webapi.badrequest(message="No mac address specified")
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
def validate_update(cls, data):
|
||||
d = cls.validate_json(data)
|
||||
if "status" in d and d["status"] not in cls.NODE_STATUSES:
|
||||
raise web.webapi.badrequest(message="Invalid status for node")
|
||||
return d
|
||||
|
||||
|
||||
class IPAddr(Base):
|
||||
__tablename__ = 'ip_addrs'
|
||||
id = Column(Integer, primary_key=True)
|
||||
network = Column(Integer, ForeignKey('networks.id'))
|
||||
node = Column(Integer, ForeignKey('nodes.id'))
|
||||
ip_addr = Column(String(25))
|
||||
|
||||
|
||||
class Network(Base, BasicValidator):
|
||||
__tablename__ = 'networks'
|
||||
id = Column(Integer, primary_key=True)
|
||||
release = Column(Integer, ForeignKey('releases.id'), nullable=False)
|
||||
name = Column(Unicode(20), nullable=False)
|
||||
access = Column(String(20), nullable=False)
|
||||
vlan_id = Column(Integer)
|
||||
network = Column(String(25), nullable=False)
|
||||
range_l = Column(String(25))
|
||||
range_h = Column(String(25))
|
||||
gateway = Column(String(25))
|
||||
nodes = relationship("Node",
|
||||
secondary=IPAddr.__table__,
|
||||
backref="networks")
|
||||
|
||||
@property
|
||||
def netmask(self):
|
||||
return str(ipaddr.IPv4Network(self.network).netmask)
|
||||
|
||||
@property
|
||||
def broadcast(self):
|
||||
return str(ipaddr.IPv4Network(self.network).broadcast)
|
18
nailgun/api/urls.py
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import web
|
||||
|
||||
from api.handlers import ClusterHandler, ClusterCollectionHandler
|
||||
from api.handlers import ReleaseHandler, ReleaseCollectionHandler
|
||||
from api.handlers import NodeHandler, NodeCollectionHandler
|
||||
|
||||
urls = (
|
||||
r'/releases/?$', 'ReleaseCollectionHandler',
|
||||
r'/releases/(?P<release_id>\d+)/?$', 'ReleaseHandler',
|
||||
r'/clusters/?$', 'ClusterCollectionHandler',
|
||||
r'/clusters/(?P<cluster_id>\d+)/?$', 'ClusterHandler',
|
||||
r'/nodes/?$', 'NodeCollectionHandler',
|
||||
r'/nodes/(?P<node_id>\d+)/?$', 'NodeHandler',
|
||||
)
|
||||
|
||||
api_app = web.application(urls, locals())
|
22
nailgun/api/validators.py
Normal file
@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
import web
|
||||
|
||||
|
||||
class BasicValidator(object):
|
||||
@classmethod
|
||||
def validate_json(cls, data):
|
||||
if data:
|
||||
try:
|
||||
res = json.loads(data)
|
||||
except:
|
||||
raise web.webapi.badrequest(
|
||||
message="Invalid json format!"
|
||||
)
|
||||
return res
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data):
|
||||
raise NotImplementedError("You should override this method")
|
37
nailgun/db.py
Normal file
@ -0,0 +1,37 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import web
|
||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||
from api.models import engine, Release
|
||||
|
||||
|
||||
def load_db_driver(handler):
|
||||
web.ctx.orm = scoped_session(sessionmaker(bind=engine))
|
||||
try:
|
||||
return handler()
|
||||
except web.HTTPError:
|
||||
web.ctx.orm.commit()
|
||||
raise
|
||||
except:
|
||||
web.ctx.orm.rollback()
|
||||
raise
|
||||
finally:
|
||||
web.ctx.orm.commit()
|
||||
|
||||
|
||||
def syncdb():
|
||||
from api.models import Base
|
||||
Base.metadata.create_all(engine)
|
||||
|
||||
|
||||
def dropdb():
|
||||
from api.models import Base
|
||||
Base.metadata.drop_all(engine)
|
||||
|
||||
|
||||
def flush():
|
||||
from api.models import Base
|
||||
session = scoped_session(sessionmaker(bind=engine))
|
||||
for table in reversed(Base.metadata.sorted_tables):
|
||||
session.execute(table.delete())
|
||||
session.commit()
|
18
nailgun/helpers/vlan.py
Normal file
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
class VlanManager(object):
|
||||
"""
|
||||
A stub for some real logic in the future
|
||||
"""
|
||||
vlan_ids = {
|
||||
'storage': 200,
|
||||
'public': 300,
|
||||
'floating': 400,
|
||||
'fixed': 500,
|
||||
'admin': 100
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def generate_id(cls, name):
|
||||
return cls.vlan_ids[name]
|
@ -1,11 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
# sys.path.insert(0, os.getcwd())
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nailgun.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
@ -1,7 +0,0 @@
|
||||
|
||||
test-unit: test-unit-nailgun
|
||||
|
||||
.PHONY: test-unit-nailgun
|
||||
test-unit-nailgun:
|
||||
cd nailgun && ./run_tests.sh
|
||||
|
@ -1,120 +0,0 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import threading
|
||||
import atexit
|
||||
import Queue
|
||||
|
||||
_interval = 1.0
|
||||
_times = {}
|
||||
_files = []
|
||||
|
||||
_running = False
|
||||
_queue = Queue.Queue()
|
||||
_lock = threading.Lock()
|
||||
|
||||
|
||||
def _restart(path):
|
||||
_queue.put(True)
|
||||
prefix = 'monitor (pid=%d):' % os.getpid()
|
||||
print >> sys.stderr, '%s Change detected to \'%s\'.' % (prefix, path)
|
||||
print >> sys.stderr, '%s Triggering process restart.' % prefix
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
|
||||
def _modified(path):
|
||||
try:
|
||||
# If path doesn't denote a file and were previously
|
||||
# tracking it, then it has been removed or the file type
|
||||
# has changed so force a restart. If not previously
|
||||
# tracking the file then we can ignore it as probably
|
||||
# pseudo reference such as when file extracted from a
|
||||
# collection of modules contained in a zip file.
|
||||
|
||||
if not os.path.isfile(path):
|
||||
return path in _times
|
||||
|
||||
# Check for when file last modified.
|
||||
|
||||
mtime = os.stat(path).st_mtime
|
||||
if path not in _times:
|
||||
_times[path] = mtime
|
||||
|
||||
# Force restart when modification time has changed, even
|
||||
# if time now older, as that could indicate older file
|
||||
# has been restored.
|
||||
|
||||
if mtime != _times[path]:
|
||||
return True
|
||||
except:
|
||||
# If any exception occured, likely that file has been
|
||||
# been removed just before stat(), so force a restart.
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _monitor():
|
||||
while 1:
|
||||
# Check modification times on all files in sys.modules.
|
||||
|
||||
for module in sys.modules.values():
|
||||
if not hasattr(module, '__file__'):
|
||||
continue
|
||||
path = getattr(module, '__file__')
|
||||
if not path:
|
||||
continue
|
||||
if os.path.splitext(path)[1] in ['.pyc', '.pyo', '.pyd']:
|
||||
path = path[:-1]
|
||||
if _modified(path):
|
||||
return _restart(path)
|
||||
|
||||
# Check modification times on files which have
|
||||
# specifically been registered for monitoring.
|
||||
|
||||
for path in _files:
|
||||
if _modified(path):
|
||||
return _restart(path)
|
||||
|
||||
# Go to sleep for specified interval.
|
||||
|
||||
try:
|
||||
return _queue.get(timeout=_interval)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
_thread = threading.Thread(target=_monitor)
|
||||
_thread.setDaemon(True)
|
||||
|
||||
|
||||
def _exiting():
|
||||
try:
|
||||
_queue.put(True)
|
||||
except:
|
||||
pass
|
||||
_thread.join()
|
||||
|
||||
atexit.register(_exiting)
|
||||
|
||||
|
||||
def track(path):
|
||||
if not path in _files:
|
||||
_files.append(path)
|
||||
|
||||
|
||||
def start(interval=1.0):
|
||||
global _interval
|
||||
if interval < _interval:
|
||||
_interval = interval
|
||||
|
||||
global _running
|
||||
_lock.acquire()
|
||||
if not _running:
|
||||
prefix = 'monitor (pid=%d):' % os.getpid()
|
||||
print >> sys.stderr, '%s Starting change monitor.' % prefix
|
||||
_running = True
|
||||
_thread.start()
|
||||
_lock.release()
|
64
nailgun/nailgun.py
Executable file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
import web
|
||||
|
||||
import db
|
||||
from api.handlers import check_client_content_type
|
||||
from unit_test import TestRunner
|
||||
from urls import urls
|
||||
|
||||
logging.basicConfig(level="DEBUG")
|
||||
|
||||
app = web.application(urls, locals())
|
||||
app.add_processor(db.load_db_driver)
|
||||
app.add_processor(check_client_content_type)
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(
|
||||
dest="action", help='actions'
|
||||
)
|
||||
run_parser = subparsers.add_parser(
|
||||
'run', help='run application locally'
|
||||
)
|
||||
runwsgi_parser = subparsers.add_parser(
|
||||
'runwsgi', help='run WSGI application'
|
||||
)
|
||||
test_parser = subparsers.add_parser(
|
||||
'test', help='run unit tests'
|
||||
)
|
||||
syncdb_parser = subparsers.add_parser(
|
||||
'syncdb', help='sync application database'
|
||||
)
|
||||
params, other_params = parser.parse_known_args()
|
||||
sys.argv.pop(1)
|
||||
|
||||
if params.action == "syncdb":
|
||||
logging.info("Syncing database...")
|
||||
db.syncdb()
|
||||
logging.info("Done")
|
||||
elif params.action == "test":
|
||||
logging.info("Running tests...")
|
||||
TestRunner.run()
|
||||
logging.info("Done")
|
||||
elif params.action == "run":
|
||||
app.run()
|
||||
elif params.action == "runwsgi":
|
||||
logging.info("Running WSGI app...")
|
||||
server = web.httpserver.WSGIServer(
|
||||
("0.0.0.0", 8080),
|
||||
app.wsgifunc()
|
||||
)
|
||||
try:
|
||||
server.start()
|
||||
except KeyboardInterrupt:
|
||||
logging.info("Stopping WSGI app...")
|
||||
server.stop()
|
||||
logging.info("Done")
|
||||
else:
|
||||
parser.print_help()
|
BIN
nailgun/nailgun.sqlite~
Normal file
@ -1,31 +0,0 @@
|
||||
import json
|
||||
import urllib
|
||||
import httplib
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
def query_api(url, method='GET', params={}):
|
||||
if method not in ('GET', 'POST', 'PUT', 'DELETE'):
|
||||
raise ValueError("Invalid method %s" % method)
|
||||
|
||||
parsed_url = urlparse(url)
|
||||
|
||||
body = None
|
||||
path = parsed_url.path
|
||||
if method in ('POST', 'PUT'):
|
||||
body = urllib.urlencode(params)
|
||||
elif params:
|
||||
path = "%s?%s" % (path, urllib.urlencode(params))
|
||||
|
||||
conn = httplib.HTTPConnection(parsed_url.netloc)
|
||||
conn.request(method, path, body)
|
||||
response = conn.getresponse()
|
||||
raw_data = response.read()
|
||||
|
||||
data = None
|
||||
try:
|
||||
data = json.loads(raw_data)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return (response.status, data)
|
@ -1,3 +0,0 @@
|
||||
import re
|
||||
from django.db import models
|
||||
from django import forms
|
@ -1,160 +0,0 @@
|
||||
import re
|
||||
|
||||
import simplejson as json
|
||||
from django.core.exceptions import ValidationError
|
||||
from django import forms
|
||||
from django.forms.fields import Field, IntegerField, CharField, ChoiceField, \
|
||||
BooleanField
|
||||
from django.core.validators import RegexValidator
|
||||
|
||||
from nailgun.models import Cluster
|
||||
from nailgun.models import Node
|
||||
from nailgun.models import Role
|
||||
from nailgun.models import Release
|
||||
from nailgun.models import Network
|
||||
from nailgun.models import Point
|
||||
from nailgun.models import Com
|
||||
|
||||
import nailgun.api.validators as vld
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger('forms')
|
||||
|
||||
|
||||
class RoleFilterForm(forms.Form):
|
||||
node_id = Field(required=False, validators=[vld.validate_node_id])
|
||||
release_id = Field(required=False, validators=[])
|
||||
|
||||
|
||||
class RoleCreateForm(forms.ModelForm):
|
||||
components = Field(validators=[], required=False)
|
||||
|
||||
def clean_components(self):
|
||||
|
||||
return [c.name for c in Com.objects.filter(
|
||||
name__in=self.data['components'],
|
||||
release=Release.objects.get(id=self.data['release'])
|
||||
)]
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
|
||||
|
||||
class PointFilterForm(forms.Form):
|
||||
release = IntegerField(required=False)
|
||||
|
||||
|
||||
class PointUpdateForm(forms.ModelForm):
|
||||
scheme = Field(validators=[])
|
||||
|
||||
class Meta:
|
||||
model = Point
|
||||
exclude = ('name', 'release', 'provided_by', 'required_by')
|
||||
|
||||
|
||||
class PointCreateForm(forms.ModelForm):
|
||||
scheme = Field(required=False, validators=[])
|
||||
|
||||
class Meta:
|
||||
model = Point
|
||||
exclude = ('provided_by', 'required_by')
|
||||
|
||||
|
||||
class ComFilterForm(forms.Form):
|
||||
release = IntegerField(required=False)
|
||||
|
||||
|
||||
class ComCreateForm(forms.ModelForm):
|
||||
deploy = Field(validators=[])
|
||||
requires = Field(validators=[], required=False)
|
||||
provides = Field(validators=[], required=False)
|
||||
|
||||
def clean_requires(self):
|
||||
|
||||
return [p.name for p in Point.objects.filter(
|
||||
name__in=self.data['requires'],
|
||||
release=Release.objects.get(id=self.data['release'])
|
||||
)]
|
||||
|
||||
def clean_provides(self):
|
||||
|
||||
return [p.name for p in Point.objects.filter(
|
||||
name__in=self.data['provides'],
|
||||
release=Release.objects.get(id=self.data['release'])
|
||||
)]
|
||||
|
||||
class Meta:
|
||||
model = Com
|
||||
exclude = ('roles')
|
||||
|
||||
|
||||
class ClusterForm(forms.Form):
|
||||
name = CharField(max_length=100, required=False)
|
||||
nodes = Field(required=False, validators=[vld.validate_node_ids])
|
||||
task = Field(required=False, validators=[vld.forbid_modifying_tasks])
|
||||
|
||||
|
||||
class ClusterCreationForm(forms.ModelForm):
|
||||
nodes = Field(required=False, validators=[vld.validate_node_ids])
|
||||
task = Field(required=False, validators=[vld.forbid_modifying_tasks])
|
||||
|
||||
class Meta:
|
||||
model = Cluster
|
||||
|
||||
|
||||
class NodeForm(forms.Form):
|
||||
metadata = Field(required=False, validators=[vld.validate_node_metadata])
|
||||
status = ChoiceField(required=False, choices=Node.NODE_STATUSES)
|
||||
name = CharField(max_length=100, required=False)
|
||||
fqdn = CharField(max_length=255, required=False)
|
||||
ip = CharField(max_length=15, required=False)
|
||||
mac = CharField(max_length=17, required=False)
|
||||
manufacturer = CharField(max_length=50, required=False)
|
||||
platform_name = CharField(max_length=150, required=False)
|
||||
os_platform = CharField(max_length=150, required=False)
|
||||
roles = Field(required=False, validators=[vld.forbid_modifying_roles])
|
||||
new_roles = Field(required=False, validators=[vld.validate_node_roles])
|
||||
redeployment_needed = BooleanField(required=False)
|
||||
|
||||
|
||||
class NodeCreationForm(NodeForm):
|
||||
id = CharField(validators=[vld.validate_node_id])
|
||||
|
||||
|
||||
class NodeFilterForm(forms.Form):
|
||||
cluster_id = IntegerField(required=False)
|
||||
|
||||
|
||||
class ReleaseCreationForm(forms.ModelForm):
|
||||
networks_metadata = Field(validators=[vld.validate_networks_metadata])
|
||||
|
||||
class Meta:
|
||||
model = Release
|
||||
|
||||
def clean(self):
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class NetworkCreationForm(forms.ModelForm):
|
||||
release = CharField()
|
||||
network = CharField(validators=[vld.validate_network])
|
||||
range_l = CharField(validators=[vld.validate_ip])
|
||||
range_h = CharField(validators=[vld.validate_ip])
|
||||
gateway = CharField(validators=[vld.validate_ip])
|
||||
|
||||
class Meta:
|
||||
model = Network
|
||||
|
||||
def clean_release(self):
|
||||
release_id = self.cleaned_data["release"]
|
||||
if not release_id:
|
||||
raise ValidationError("Release id not specified!")
|
||||
try:
|
||||
r = Release.objects.get(id=release_id)
|
||||
except Release.DoesNotExist:
|
||||
raise ValidationError("Invalid release id!")
|
||||
|
||||
#self.instance.release = r
|
||||
return r
|
@ -1,803 +0,0 @@
|
||||
import os
|
||||
import copy
|
||||
import re
|
||||
import celery
|
||||
import ipaddr
|
||||
import json
|
||||
|
||||
from piston.handler import BaseHandler, HandlerMetaClass
|
||||
from piston.utils import rc, validate
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from nailgun.models import Cluster
|
||||
from nailgun.models import Release
|
||||
from nailgun.models import Role
|
||||
from nailgun.models import Com
|
||||
from nailgun.models import Point
|
||||
from nailgun.models import EndPoint
|
||||
from nailgun.models import Network
|
||||
from nailgun.models import Node
|
||||
from nailgun.models import Task
|
||||
|
||||
from nailgun.deployment_types import deployment_types
|
||||
from nailgun.api.validators import validate_json, validate_json_list
|
||||
from nailgun.api.forms import ClusterForm
|
||||
from nailgun.api.forms import ClusterCreationForm
|
||||
from nailgun.api.forms import RoleFilterForm
|
||||
from nailgun.api.forms import RoleCreateForm
|
||||
from nailgun.api.forms import PointFilterForm
|
||||
from nailgun.api.forms import PointUpdateForm
|
||||
from nailgun.api.forms import PointCreateForm
|
||||
from nailgun.api.forms import ComFilterForm
|
||||
from nailgun.api.forms import ComCreateForm
|
||||
from nailgun.api.forms import NodeCreationForm
|
||||
from nailgun.api.forms import NodeFilterForm
|
||||
from nailgun.api.forms import NodeForm
|
||||
from nailgun.api.forms import ReleaseCreationForm
|
||||
from nailgun.api.forms import NetworkCreationForm
|
||||
|
||||
from nailgun import tasks
|
||||
import nailgun.api.validators as vld
|
||||
|
||||
from nailgun.helpers import DeployManager
|
||||
from nailgun.helpers import DeployDriver
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
handlers = {}
|
||||
|
||||
|
||||
class HandlerRegistrator(HandlerMetaClass):
|
||||
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(BaseHandler):
|
||||
"""
|
||||
Basic JSON handler
|
||||
"""
|
||||
__metaclass__ = HandlerRegistrator
|
||||
|
||||
fields = None
|
||||
|
||||
@classmethod
|
||||
def render(cls, item, 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,)):
|
||||
|
||||
logger.debug("rendering: field is a tuple: %s" % str(field))
|
||||
if field[1] == '*':
|
||||
subfields = None
|
||||
else:
|
||||
subfields = field[1:]
|
||||
|
||||
value = getattr(item, field[0])
|
||||
if value is None:
|
||||
pass
|
||||
elif value.__class__.__name__ in ('ManyRelatedManager',
|
||||
'RelatedManager'):
|
||||
try:
|
||||
handler = handlers[value.model.__name__]
|
||||
json_data[field[0]] = [
|
||||
handler.render(o, fields=subfields) \
|
||||
for o in value.all()]
|
||||
except KeyError:
|
||||
raise Exception("No handler for %s" % \
|
||||
value.model.__name__)
|
||||
|
||||
elif value.__class__.__name__ in handlers:
|
||||
handler = handlers[value.__class__.__name__]
|
||||
json_data[field[0]] = handler.render(value,
|
||||
fields=subfields)
|
||||
else:
|
||||
json_data[field[0]] = value.id
|
||||
|
||||
else:
|
||||
value = getattr(item, field)
|
||||
|
||||
if value is None:
|
||||
pass
|
||||
elif value.__class__.__name__ in ('ManyRelatedManager',
|
||||
'RelatedManager',):
|
||||
json_data[field] = [getattr(o, 'id') \
|
||||
for o in value.all()]
|
||||
elif value.__class__.__name__ in handlers:
|
||||
json_data[field] = value.id
|
||||
else:
|
||||
json_data[field] = value
|
||||
|
||||
return json_data
|
||||
|
||||
|
||||
class TaskHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET',)
|
||||
model = Task
|
||||
|
||||
@classmethod
|
||||
def render(cls, task, fields=None):
|
||||
result = {
|
||||
'task_id': task.pk,
|
||||
'name': task.name,
|
||||
'ready': task.ready,
|
||||
}
|
||||
errors = task.errors
|
||||
if len(errors):
|
||||
result['error'] = '; '.join(map(lambda e: e.__str__(), errors))
|
||||
|
||||
return result
|
||||
|
||||
def read(self, request, task_id):
|
||||
try:
|
||||
task = Task.objects.get(id=task_id)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
return TaskHandler.render(task)
|
||||
|
||||
|
||||
class ClusterChangesHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('PUT', 'DELETE')
|
||||
|
||||
def update(self, request, cluster_id):
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
logger.debug("Cluster changes: Checking if another task is running")
|
||||
if cluster.task:
|
||||
if cluster.task.ready:
|
||||
cluster.task.delete()
|
||||
else:
|
||||
response = rc.DUPLICATE_ENTRY
|
||||
response.content = "Another task is running"
|
||||
return response
|
||||
|
||||
logger.debug("Cluster changes: Updating node roles")
|
||||
for node in cluster.nodes.filter(redeployment_needed=True):
|
||||
node.roles = node.new_roles.all()
|
||||
node.new_roles.clear()
|
||||
node.redeployment_needed = False
|
||||
node.save()
|
||||
|
||||
logger.debug("Cluster changes: Updating node networks")
|
||||
for nw in cluster.release.networks.all():
|
||||
for node in cluster.nodes.all():
|
||||
nw.update_node_network_info(node)
|
||||
|
||||
logger.debug("Cluster changes: Trying to instantiate cluster")
|
||||
|
||||
dm = DeployManager(cluster_id)
|
||||
dm.clean_cluster()
|
||||
dm.instantiate_cluster()
|
||||
|
||||
logger.debug("Cluster changes: Trying to deploy cluster")
|
||||
task = Task(task_name='deploy_cluster', cluster=cluster)
|
||||
task.run(cluster_id)
|
||||
|
||||
response = rc.ACCEPTED
|
||||
response.content = TaskHandler.render(task)
|
||||
return response
|
||||
|
||||
def delete(self, request, cluster_id):
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
for node in cluster.nodes.filter(redeployment_needed=True):
|
||||
node.new_roles.clear()
|
||||
node.redeployment_needed = False
|
||||
node.save()
|
||||
|
||||
return rc.DELETED
|
||||
|
||||
|
||||
class DeploymentTypeCollectionHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('GET',)
|
||||
|
||||
def read(self, request, cluster_id):
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
return map(DeploymentTypeHandler.render, deployment_types.values())
|
||||
|
||||
|
||||
class DeploymentTypeHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('PUT',)
|
||||
fields = ('id', 'name', 'description')
|
||||
|
||||
def update(self, request, cluster_id, deployment_type_id):
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
deployment_type = deployment_types[deployment_type_id]
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
deployment_type.assign_roles(cluster)
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
class EndPointCollectionHandler(BaseHandler):
|
||||
allowed_methods = ('GET',)
|
||||
|
||||
def read(self, request, node_id=None, component_name=None):
|
||||
if not node_id or not component_name:
|
||||
return map(EndPointHandler.render,
|
||||
EndPoint.objects.all())
|
||||
|
||||
try:
|
||||
node = Node.objects.get(id=node_id)
|
||||
component = Com.objects.get(
|
||||
name=component_name,
|
||||
release=node.cluster.release
|
||||
)
|
||||
dd = DeployDriver(node, component)
|
||||
return dd.deploy_data()
|
||||
except:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class EndPointHandler(JSONHandler):
|
||||
model = EndPoint
|
||||
|
||||
@classmethod
|
||||
def render(cls, endpoint):
|
||||
return endpoint.data
|
||||
|
||||
|
||||
class ClusterCollectionHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
def read(self, request):
|
||||
json_data = map(
|
||||
ClusterHandler.render,
|
||||
Cluster.objects.all()
|
||||
)
|
||||
return json_data
|
||||
|
||||
@validate_json(ClusterCreationForm)
|
||||
def create(self, request):
|
||||
data = request.form.cleaned_data
|
||||
|
||||
try:
|
||||
cluster = Cluster.objects.get(
|
||||
name=data['name']
|
||||
)
|
||||
return rc.DUPLICATE_ENTRY
|
||||
except Cluster.DoesNotExist:
|
||||
pass
|
||||
|
||||
cluster = Cluster()
|
||||
for key, value in request.form.cleaned_data.items():
|
||||
if key in request.form.data:
|
||||
if key != 'nodes':
|
||||
setattr(cluster, key, value)
|
||||
|
||||
cluster.save()
|
||||
|
||||
# TODO: solve vlan issues
|
||||
vlan_ids = {
|
||||
'storage': 200,
|
||||
'public': 300,
|
||||
'floating': 400,
|
||||
'fixed': 500,
|
||||
'admin': 100
|
||||
}
|
||||
|
||||
for network in cluster.release.networks_metadata:
|
||||
access = network['access']
|
||||
if access not in settings.NETWORK_POOLS:
|
||||
raise Exception("Incorrect access mode for network")
|
||||
|
||||
for nw_pool in settings.NETWORK_POOLS[access]:
|
||||
nw_ip = ipaddr.IPv4Network(nw_pool)
|
||||
new_network = None
|
||||
for net in nw_ip.iter_subnets(new_prefix=24):
|
||||
try:
|
||||
nw_exist = Network.objects.get(network=net)
|
||||
except Network.DoesNotExist:
|
||||
new_network = net
|
||||
break
|
||||
|
||||
if new_network:
|
||||
break
|
||||
|
||||
nw = Network(
|
||||
release=cluster.release,
|
||||
name=network['name'],
|
||||
access=access,
|
||||
network=str(new_network),
|
||||
gateway=str(new_network[1]),
|
||||
range_l=str(new_network[3]),
|
||||
range_h=str(new_network[-1]),
|
||||
vlan_id=vlan_ids[network['name']]
|
||||
)
|
||||
nw.save()
|
||||
|
||||
if 'nodes' in request.form.data:
|
||||
nodes = Node.objects.filter(
|
||||
id__in=request.form.cleaned_data['nodes']
|
||||
)
|
||||
cluster.nodes = nodes
|
||||
|
||||
return ClusterHandler.render(cluster)
|
||||
|
||||
|
||||
class ClusterHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = Cluster
|
||||
fields = ('id', 'name',
|
||||
('nodes', '*'),
|
||||
('release', '*'), 'task')
|
||||
|
||||
def read(self, request, cluster_id):
|
||||
logger.debug("Cluster reading: id: %s" % cluster_id)
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
return ClusterHandler.render(cluster)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
@validate_json(ClusterForm)
|
||||
def update(self, request, cluster_id):
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
for key, value in request.form.cleaned_data.items():
|
||||
if key in request.form.data:
|
||||
if key == 'nodes':
|
||||
new_nodes = Node.objects.filter(id__in=value)
|
||||
cluster.nodes = new_nodes
|
||||
elif key == 'task':
|
||||
cluster.task.delete()
|
||||
else:
|
||||
setattr(cluster, key, value)
|
||||
|
||||
cluster.save()
|
||||
return ClusterHandler.render(cluster)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
def delete(self, request, cluster_id):
|
||||
try:
|
||||
cluster = Cluster.objects.get(id=cluster_id)
|
||||
cluster.delete()
|
||||
return rc.DELETED
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class NodeCollectionHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
@validate(NodeFilterForm, 'GET')
|
||||
def read(self, request):
|
||||
nodes = Node.objects.all()
|
||||
if 'cluster_id' in request.form.data:
|
||||
nodes = nodes.filter(
|
||||
cluster_id=request.form.cleaned_data['cluster_id'])
|
||||
return map(NodeHandler.render, nodes)
|
||||
|
||||
@validate_json(NodeCreationForm)
|
||||
def create(self, request):
|
||||
node = Node()
|
||||
for key, value in request.form.cleaned_data.items():
|
||||
if key in request.form.data:
|
||||
if key != 'new_roles':
|
||||
setattr(node, key, value)
|
||||
|
||||
node.save()
|
||||
return NodeHandler.render(node)
|
||||
|
||||
|
||||
class NodeHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET', 'PUT', 'DELETE')
|
||||
model = Node
|
||||
fields = ('id', 'name', 'info', 'status', 'mac', 'fqdn', 'ip',
|
||||
'manufacturer', 'platform_name', 'redeployment_needed',
|
||||
('roles', '*'), ('new_roles', '*'), 'os_platform')
|
||||
|
||||
def read(self, request, node_id):
|
||||
try:
|
||||
node = Node.objects.get(id=node_id)
|
||||
return NodeHandler.render(node)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
@validate_json(NodeForm)
|
||||
def update(self, request, node_id):
|
||||
node, is_created = Node.objects.get_or_create(id=node_id)
|
||||
for key, value in request.form.cleaned_data.items():
|
||||
if key in request.form.data:
|
||||
if key == 'new_roles':
|
||||
new_roles = Role.objects.filter(id__in=value)
|
||||
node.new_roles = new_roles
|
||||
else:
|
||||
setattr(node, key, value)
|
||||
|
||||
node.save()
|
||||
return NodeHandler.render(node)
|
||||
|
||||
def delete(self, request, node_id):
|
||||
try:
|
||||
node = Node.objects.get(id=node_id)
|
||||
node.delete()
|
||||
return rc.DELETED
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class PointCollectionHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
@validate(PointFilterForm, 'GET')
|
||||
def read(self, request):
|
||||
logger.debug("Getting points from data: %s" % \
|
||||
str(request.form.data))
|
||||
if 'release' in request.form.data:
|
||||
points = Point.objects.filter(
|
||||
release__id=request.form.cleaned_data['release']
|
||||
)
|
||||
else:
|
||||
points = Point.objects.all()
|
||||
return map(PointHandler.render, points)
|
||||
|
||||
@validate_json(PointCreateForm)
|
||||
def create(self, request):
|
||||
data = request.form.cleaned_data
|
||||
logger.debug("Creating Point from data: %s" % str(data))
|
||||
|
||||
try:
|
||||
point = Point.objects.get(
|
||||
name=data['name'],
|
||||
release=data['release']
|
||||
)
|
||||
return rc.DUPLICATE_ENTRY
|
||||
except Point.DoesNotExist:
|
||||
pass
|
||||
|
||||
point = Point(
|
||||
name=data['name'],
|
||||
release=data['release']
|
||||
)
|
||||
|
||||
if 'scheme' in data:
|
||||
point.scheme = data['scheme']
|
||||
else:
|
||||
point.scheme = {}
|
||||
point.save()
|
||||
|
||||
return PointHandler.render(point)
|
||||
|
||||
|
||||
class PointHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET', 'PUT')
|
||||
model = Point
|
||||
|
||||
fields = ('id', 'name', 'scheme', ('release', 'name'),
|
||||
('required_by', 'name'),
|
||||
('provided_by', 'name'))
|
||||
|
||||
def read(self, request, point_id):
|
||||
try:
|
||||
return PointHandler.render(Point.objects.get(id=point_id))
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
@validate_json(PointUpdateForm)
|
||||
def update(self, request, point_id):
|
||||
data = request.form.cleaned_data
|
||||
logger.debug("Updating Point from data: %s" % str(data))
|
||||
|
||||
try:
|
||||
point = Point.objects.get(id=point_id)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
if data.get('scheme', None):
|
||||
point.scheme = data['scheme']
|
||||
|
||||
point.save()
|
||||
return PointHandler.render(point)
|
||||
|
||||
|
||||
class ComCollectionHandler(BaseHandler):
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
@validate(ComFilterForm, 'GET')
|
||||
def read(self, request):
|
||||
logger.debug("Getting components from data: %s" % \
|
||||
str(request.form.data))
|
||||
if 'release' in request.form.data:
|
||||
components = Com.objects.filter(
|
||||
release__id=request.form.cleaned_data['release']
|
||||
)
|
||||
else:
|
||||
components = Com.objects.all()
|
||||
return map(ComHandler.render, components)
|
||||
|
||||
@validate_json(ComCreateForm)
|
||||
def create(self, request):
|
||||
data = request.form.cleaned_data
|
||||
logger.debug("Creating Com from data: %s" % str(data))
|
||||
|
||||
try:
|
||||
component = Com.objects.get(
|
||||
name=data['name'],
|
||||
release=data['release']
|
||||
)
|
||||
return rc.DUPLICATE_ENTRY
|
||||
except Com.DoesNotExist:
|
||||
pass
|
||||
|
||||
component = Com(
|
||||
name=data['name'],
|
||||
release=data['release']
|
||||
)
|
||||
|
||||
component.deploy = data['deploy']
|
||||
component.save()
|
||||
|
||||
if data.get('requires', None):
|
||||
for point_name in data['requires']:
|
||||
try:
|
||||
point = Point.objects.get(
|
||||
name=point_name,
|
||||
release=data['release']
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
else:
|
||||
component.requires.add(point)
|
||||
|
||||
if data.get('provides', None):
|
||||
for point_name in data['provides']:
|
||||
try:
|
||||
point = Point.objects.get(
|
||||
name=point_name,
|
||||
release=data['release']
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
else:
|
||||
component.provides.add(point)
|
||||
|
||||
component.save()
|
||||
return ComHandler.render(component)
|
||||
|
||||
|
||||
class ComHandler(JSONHandler):
|
||||
allowed_methods = ('GET',)
|
||||
model = Com
|
||||
|
||||
fields = ('id', 'name', 'deploy', ('release', 'name'),
|
||||
('requires', 'name'), ('provides', 'name'),
|
||||
('roles', 'name'))
|
||||
|
||||
def read(self, request, component_id):
|
||||
try:
|
||||
return ComHandler.render(Com.objects.get(id=component_id))
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class RoleCollectionHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
@validate(RoleFilterForm, 'GET')
|
||||
def read(self, request):
|
||||
if 'release_id' in request.form.data:
|
||||
return map(
|
||||
RoleHandler.render,
|
||||
Role.objects.filter(
|
||||
release__id=request.form.data['release_id']
|
||||
)
|
||||
)
|
||||
|
||||
roles = Role.objects.all()
|
||||
if 'node_id' in request.form.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 result
|
||||
else:
|
||||
return map(RoleHandler.render, roles)
|
||||
|
||||
@validate_json(RoleCreateForm)
|
||||
def create(self, request):
|
||||
data = request.form.cleaned_data
|
||||
logger.debug("Creating Role from data: %s" % str(data))
|
||||
|
||||
try:
|
||||
role = Role.objects.get(
|
||||
name=data['name'],
|
||||
release=data['release']
|
||||
)
|
||||
return rc.DUPLICATE_ENTRY
|
||||
except Role.DoesNotExist:
|
||||
pass
|
||||
|
||||
role = Role(
|
||||
name=data['name'],
|
||||
release=data['release']
|
||||
)
|
||||
|
||||
role.save()
|
||||
|
||||
if data.get('components', None):
|
||||
for component_name in data['components']:
|
||||
try:
|
||||
component = Com.objects.get(
|
||||
name=component_name,
|
||||
release=data['release']
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
else:
|
||||
role.components.add(component)
|
||||
|
||||
role.save()
|
||||
return RoleHandler.render(role)
|
||||
|
||||
|
||||
class RoleHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET',)
|
||||
model = Role
|
||||
fields = ('id', 'name', ('release', 'id', 'name'),
|
||||
('components', 'name'))
|
||||
|
||||
def read(self, request, role_id):
|
||||
try:
|
||||
return RoleHandler.render(Role.objects.get(id=role_id))
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class ReleaseCollectionHandler(BaseHandler):
|
||||
|
||||
logger.warning("Trying to add release")
|
||||
|
||||
allowed_methods = ('GET', 'POST')
|
||||
model = Release
|
||||
|
||||
def read(self, request):
|
||||
return map(ReleaseHandler.render, Release.objects.all())
|
||||
|
||||
@validate_json(ReleaseCreationForm)
|
||||
def create(self, request):
|
||||
data = request.form.cleaned_data
|
||||
logger.debug("Creating release from data: %s" % str(data))
|
||||
try:
|
||||
release = Release.objects.get(
|
||||
name=data['name'],
|
||||
version=data['version']
|
||||
)
|
||||
return rc.DUPLICATE_ENTRY
|
||||
except Release.DoesNotExist:
|
||||
pass
|
||||
|
||||
release = Release(
|
||||
name=data["name"],
|
||||
version=data["version"],
|
||||
description=data["description"],
|
||||
networks_metadata=data["networks_metadata"]
|
||||
)
|
||||
release.save()
|
||||
|
||||
return ReleaseHandler.render(release)
|
||||
|
||||
|
||||
class ReleaseHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET', 'DELETE')
|
||||
model = Release
|
||||
fields = ('id', 'name', 'version', 'description', 'networks_metadata',
|
||||
('roles', 'name'), ('components', 'name'),
|
||||
('points', 'name'))
|
||||
|
||||
def read(self, request, release_id):
|
||||
try:
|
||||
release = Release.objects.get(id=release_id)
|
||||
return ReleaseHandler.render(release)
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
def delete(self, request, release_id):
|
||||
try:
|
||||
release = Release.objects.get(id=release_id)
|
||||
release.delete()
|
||||
return rc.DELETED
|
||||
except ObjectDoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class NetworkHandler(JSONHandler):
|
||||
|
||||
allowed_methods = ('GET',)
|
||||
model = Network
|
||||
fields = ('id', 'network', 'name', 'access',
|
||||
'vlan_id', 'range_l', 'range_h', 'gateway',
|
||||
'release', 'nodes',
|
||||
'release_id')
|
||||
|
||||
def read(self, request, network_id):
|
||||
try:
|
||||
network = Network.objects.get(id=network_id)
|
||||
return NetworkHandler.render(network)
|
||||
except Network.DoesNotExist:
|
||||
return rc.NOT_FOUND
|
||||
|
||||
|
||||
class NetworkCollectionHandler(BaseHandler):
|
||||
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
def read(self, request):
|
||||
return map(NetworkHandler.render, Network.objects.all())
|
||||
|
||||
@validate_json(NetworkCreationForm)
|
||||
def create(self, request):
|
||||
data = request.form.cleaned_data
|
||||
|
||||
try:
|
||||
release = Network.objects.get(
|
||||
name=data['name'],
|
||||
network=data['network']
|
||||
)
|
||||
return rc.DUPLICATE_ENTRY
|
||||
except Network.DoesNotExist:
|
||||
pass
|
||||
|
||||
nw = Network(
|
||||
name=data['name'],
|
||||
network=data['network'],
|
||||
release=data['release'],
|
||||
access=data['access'],
|
||||
range_l=data['range_l'],
|
||||
range_h=data['range_h'],
|
||||
gateway=data['gateway'],
|
||||
vlan_id=data['vlan_id']
|
||||
)
|
||||
nw.save()
|
||||
|
||||
return NetworkHandler.render(nw)
|
@ -1,87 +0,0 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from piston.resource import Resource
|
||||
|
||||
from nailgun.api.handlers import ClusterCollectionHandler, ClusterHandler, \
|
||||
NodeCollectionHandler, NodeHandler, \
|
||||
NetworkHandler, NetworkCollectionHandler, \
|
||||
RoleCollectionHandler, RoleHandler, \
|
||||
ReleaseCollectionHandler, ReleaseHandler, \
|
||||
ClusterChangesHandler, \
|
||||
DeploymentTypeCollectionHandler, \
|
||||
DeploymentTypeHandler, \
|
||||
TaskHandler
|
||||
from nailgun.api.handlers import ComCollectionHandler
|
||||
from nailgun.api.handlers import ComHandler
|
||||
from nailgun.api.handlers import PointCollectionHandler
|
||||
from nailgun.api.handlers import PointHandler
|
||||
from nailgun.api.handlers import EndPointCollectionHandler
|
||||
|
||||
|
||||
class JsonResource(Resource):
|
||||
def determine_emitter(self, request, *args, **kwargs):
|
||||
return 'json'
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^clusters/?$',
|
||||
JsonResource(ClusterCollectionHandler),
|
||||
name='cluster_collection_handler'),
|
||||
url(r'^clusters/(?P<cluster_id>\d+)/?$',
|
||||
JsonResource(ClusterHandler),
|
||||
name='cluster_handler'),
|
||||
url(r'^nodes/?$',
|
||||
JsonResource(NodeCollectionHandler),
|
||||
name='node_collection_handler'),
|
||||
url(r'^nodes/(?P<node_id>[\dA-F]{12})/?$',
|
||||
JsonResource(NodeHandler),
|
||||
name='node_handler'),
|
||||
url(r'^networks/?$',
|
||||
JsonResource(NetworkCollectionHandler),
|
||||
name='network_collection_handler'),
|
||||
url(r'^networks/(?P<network_id>\d+)/?$',
|
||||
JsonResource(NetworkHandler),
|
||||
name='network_handler'),
|
||||
url(r'^clusters/(?P<cluster_id>\d+)/changes/?$',
|
||||
JsonResource(ClusterChangesHandler),
|
||||
name='cluster_changes_handler'),
|
||||
url(r'^tasks/(?P<task_id>[\da-f\-]{36})/?$',
|
||||
JsonResource(TaskHandler),
|
||||
name='task_handler'),
|
||||
url(r'^roles/?$',
|
||||
JsonResource(RoleCollectionHandler),
|
||||
name='role_collection_handler'),
|
||||
url(r'^roles/(?P<role_id>\d+)/?$',
|
||||
JsonResource(RoleHandler),
|
||||
name='role_handler'),
|
||||
url(r'^coms/?$',
|
||||
JsonResource(ComCollectionHandler),
|
||||
name='com_collection_handler'),
|
||||
url(r'^coms/(?P<component_id>\d+)/?$',
|
||||
JsonResource(ComHandler),
|
||||
name='com_handler'),
|
||||
url(r'^points/?$',
|
||||
JsonResource(PointCollectionHandler),
|
||||
name='point_collection_handler'),
|
||||
url(r'^points/(?P<point_id>\d+)/?$',
|
||||
JsonResource(PointHandler),
|
||||
name='point_handler'),
|
||||
url(r'^endpoints/(?P<node_id>[\dA-F]{12})/(?P<component_name>\w+)/?$',
|
||||
JsonResource(EndPointCollectionHandler),
|
||||
name='endpoint_handler'),
|
||||
url(r'^endpoints/?$',
|
||||
JsonResource(EndPointCollectionHandler),
|
||||
name='endpoint_collection_handler'),
|
||||
url(r'^releases/?$',
|
||||
JsonResource(ReleaseCollectionHandler),
|
||||
name='release_collection_handler'),
|
||||
url(r'^releases/(?P<release_id>\d+)/?$',
|
||||
JsonResource(ReleaseHandler),
|
||||
name='release_handler'),
|
||||
url(r'^clusters/(?P<cluster_id>\d+)/deployment_types/?$',
|
||||
JsonResource(DeploymentTypeCollectionHandler),
|
||||
name='deployment_type_collection_handler'),
|
||||
url(r'^clusters/(?P<cluster_id>\d+)/deployment_types/' \
|
||||
r'(?P<deployment_type_id>\w+)/?$',
|
||||
JsonResource(DeploymentTypeHandler),
|
||||
name='deployment_type_handler'),
|
||||
)
|
@ -1,183 +0,0 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
import ipaddr
|
||||
from piston.utils import FormValidationError, HttpStatusCode, rc
|
||||
from piston.decorator import decorator
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from nailgun.models import Cluster
|
||||
from nailgun.models import Node
|
||||
from nailgun.models import Role
|
||||
from nailgun.models import Release
|
||||
from nailgun.models import Network
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger("validators")
|
||||
|
||||
|
||||
# Handler decorator for JSON validation using forms
|
||||
def validate_json(v_form):
|
||||
@decorator
|
||||
def wrap(f, self, request, *a, **kwa):
|
||||
logger.debug("Validation json: trying to find out content_type")
|
||||
content_type = request.content_type.split(';')[0]
|
||||
logger.debug("Validation json: content_type: %s" % content_type)
|
||||
if content_type != "application/json":
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "Invalid content type, must be application/json"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
try:
|
||||
parsed_body = json.loads(request.body)
|
||||
logger.debug("Validation json: body: %s" % str(parsed_body))
|
||||
except:
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "Invalid JSON object"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
if not isinstance(parsed_body, dict):
|
||||
logger.debug("Validation json: parsed_body is not dict")
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "Dictionary expected"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
logger.debug("Validation json: trying to construct form from v_form")
|
||||
try:
|
||||
form = v_form(parsed_body, request.FILES)
|
||||
except Exception as e:
|
||||
logger.debug("Validation json: error: %s" % str(e.message))
|
||||
logger.debug("Validation json: form: %s" % str(form))
|
||||
if form.is_valid():
|
||||
setattr(request, 'form', form)
|
||||
return f(self, request, *a, **kwa)
|
||||
else:
|
||||
raise FormValidationError(form)
|
||||
return wrap
|
||||
|
||||
|
||||
def validate_json_list(v_form):
|
||||
@decorator
|
||||
def wrap(f, self, request, *a, **kwa):
|
||||
content_type = request.content_type.split(';')[0]
|
||||
if content_type != "application/json":
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "Invalid content type, must be application/json"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
try:
|
||||
parsed_body = json.loads(request.body)
|
||||
except:
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "Invalid JSON object"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
if not isinstance(parsed_body, list):
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "List expected"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
if not len(parsed_body):
|
||||
response = rc.BAD_REQUEST
|
||||
response.content = "No entries to update"
|
||||
raise HttpStatusCode(response)
|
||||
|
||||
forms = []
|
||||
for entry in parsed_body:
|
||||
form = v_form(entry, request.FILES)
|
||||
if form.is_valid():
|
||||
forms.append(form)
|
||||
else:
|
||||
raise FormValidationError(form)
|
||||
setattr(request, 'forms', forms)
|
||||
return f(self, request, *a, **kwa)
|
||||
return wrap
|
||||
|
||||
|
||||
"""
|
||||
FORM DATA VALIDATORS
|
||||
"""
|
||||
|
||||
validate_node_id = RegexValidator(regex=re.compile('^[\dA-F]{12}$'))
|
||||
|
||||
|
||||
def validate_node_ids(value):
|
||||
if isinstance(value, list):
|
||||
for node_id in value:
|
||||
validate_node_id(node_id)
|
||||
else:
|
||||
raise ValidationError('Node list must be a list of node IDs')
|
||||
|
||||
|
||||
def validate_node_metadata(value):
|
||||
if value is not None:
|
||||
if isinstance(value, dict):
|
||||
for field in ('block_device', 'interfaces', 'cpu', 'memory'):
|
||||
# TODO(mihgen): We need more comprehensive checks here
|
||||
# For example, now, it's possible to store value[field] = []
|
||||
if not field in value or value[field] == "":
|
||||
raise ValidationError("Node metadata '%s' \
|
||||
field is required" % field)
|
||||
else:
|
||||
raise ValidationError('Node metadata must be a dictionary')
|
||||
|
||||
|
||||
def validate_node_roles(value):
|
||||
if not isinstance(value, list) or \
|
||||
not all(map(lambda i: isinstance(i, int), value)):
|
||||
raise ValidationError('Role list must be a list of integers')
|
||||
|
||||
|
||||
def validate_release_node_roles(data):
|
||||
if not data or not isinstance(data, list):
|
||||
raise ValidationError('Invalid roles list')
|
||||
if not all(map(lambda i: 'name' in i, data)):
|
||||
raise ValidationError('Role name is empty')
|
||||
for role in data:
|
||||
if 'components' not in role or not role['components']:
|
||||
raise ValidationError('Components list for role "%s" \
|
||||
should not be empty' % role['name'])
|
||||
|
||||
|
||||
def validate_release_points(data):
|
||||
if not data or not isinstance(data, list):
|
||||
raise ValidationError('Invalid points list')
|
||||
if not all(map(lambda i: 'name' in i, data)):
|
||||
raise ValidationError('Point name is empty')
|
||||
|
||||
|
||||
def validate_release_components(data):
|
||||
if not data or not isinstance(data, list):
|
||||
raise ValidationError('Invalid components list')
|
||||
if not all(map(lambda i: 'name' in i, data)):
|
||||
raise ValidationError('Component name is empty')
|
||||
|
||||
|
||||
def forbid_modifying_roles(value):
|
||||
raise ValidationError('Role list cannot be modified directly')
|
||||
|
||||
|
||||
def validate_networks_metadata(data):
|
||||
if not isinstance(data, list):
|
||||
raise ValidationError("There should be a list of network names")
|
||||
|
||||
|
||||
def validate_network(data):
|
||||
try:
|
||||
a = ipaddr.IPv4Network(data)
|
||||
except:
|
||||
raise ValidationError("Invalid network format!")
|
||||
|
||||
|
||||
def validate_ip(data):
|
||||
try:
|
||||
a = ipaddr.IPv4Address(data)
|
||||
except:
|
||||
raise ValidationError("Invalid IP address format!")
|
||||
|
||||
|
||||
def forbid_modifying_tasks(value):
|
||||
raise ValidationError("Tasks cannot be modified directly")
|
@ -1,51 +0,0 @@
|
||||
import itertools
|
||||
|
||||
deployment_types = {}
|
||||
|
||||
|
||||
class TypeRegistrator(type):
|
||||
def __init__(cls, name, bases, dct):
|
||||
super(TypeRegistrator, cls).__init__(name, bases, dct)
|
||||
if hasattr(cls, 'id'):
|
||||
deployment_types[cls.id] = cls
|
||||
|
||||
|
||||
class BaseDeploymentType(object):
|
||||
__metaclass__ = TypeRegistrator
|
||||
|
||||
|
||||
class SimpleDeploymentType(BaseDeploymentType):
|
||||
id = 'simple'
|
||||
name = 'Simple Deployment'
|
||||
description = 'No redundancy. Best suited for non-critical ' \
|
||||
'OpenStack installations (e.g. dev, staging, QA)'
|
||||
|
||||
@classmethod
|
||||
def assign_roles(cls, cluster):
|
||||
roles = cluster.release.roles.all()
|
||||
nodes = itertools.cycle(cluster.nodes.all())
|
||||
new_roles = {}
|
||||
for role in roles:
|
||||
node = nodes.next()
|
||||
node.new_roles.add(role)
|
||||
node.redeployment_needed = True
|
||||
node.save()
|
||||
|
||||
|
||||
class HighAvailabilityDeploymentType(BaseDeploymentType):
|
||||
id = 'ha'
|
||||
name = 'HA Deployment'
|
||||
description = 'Built-in redundancy for OpenStack components ' \
|
||||
'(database, rabbitmq, nova, swift). ' \
|
||||
'Ideal for production deployments'
|
||||
|
||||
@classmethod
|
||||
def assign_roles(cls, cluster):
|
||||
roles = cluster.release.roles.all()
|
||||
nodes = itertools.cycle(cluster.nodes.all())
|
||||
new_roles = {}
|
||||
for role in roles:
|
||||
node = nodes.next()
|
||||
node.new_roles.add(role)
|
||||
node.redeployment_needed = True
|
||||
node.save()
|
@ -1,17 +0,0 @@
|
||||
# TODO(enchantner): create exceptions for handling different situations
|
||||
|
||||
|
||||
class EmptyListError(LookupError):
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(LookupError):
|
||||
pass
|
||||
|
||||
|
||||
class SSHError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DeployError(Exception):
|
||||
pass
|
@ -1,21 +0,0 @@
|
||||
import os
|
||||
import os.path
|
||||
|
||||
LOGPATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")
|
||||
LOGFILE = os.path.join(LOGPATH, "nailgun.log")
|
||||
LOGLEVEL = "DEBUG"
|
||||
CELERYLOGFILE = os.path.join(LOGPATH, "celery.log")
|
||||
CELERYLOGLEVEL = "DEBUG"
|
||||
CHEF_CONF_FOLDER = LOGPATH # For testing purposes
|
||||
|
||||
home = os.getenv("HOME")
|
||||
PATH_TO_SSH_KEY = home and os.path.join(home, ".ssh", "id_rsa") or None
|
||||
PATH_TO_BOOTSTRAP_SSH_KEY = home and \
|
||||
os.path.join(home, ".ssh", "bootstrap.rsa") or None
|
||||
|
||||
COBBLER_URL = "http://localhost/cobbler_api"
|
||||
COBBLER_USER = "cobbler"
|
||||
COBBLER_PASSWORD = "cobbler"
|
||||
COBBLER_PROFILE = "centos-6.3-x86_64"
|
||||
|
||||
REPO_ADDRESS = "127.0.0.1"
|
@ -1,165 +0,0 @@
|
||||
[
|
||||
{
|
||||
"model": "nailgun.release",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Default Release",
|
||||
"version": "0.1.0",
|
||||
"networks_metadata": [
|
||||
{"name": "floating", "access": "public"},
|
||||
{"name": "fixed", "access": "private"},
|
||||
{"name": "admin", "access": "private"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.cluster",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Default Cluster",
|
||||
"release": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.point",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "point0",
|
||||
"release": 1,
|
||||
"scheme": {
|
||||
"attr0": {
|
||||
"generator": "generator_ip",
|
||||
"generator_args": "floating",
|
||||
"attribute": "attr.path0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.point",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "point1",
|
||||
"release": 1,
|
||||
"scheme": {
|
||||
"attr1": {
|
||||
"generator": "generator_ip",
|
||||
"generator_args": "floating",
|
||||
"attribute": "attr.path1"
|
||||
},
|
||||
"attr2": {
|
||||
"generator": "generator_ip",
|
||||
"generator_args": "floating",
|
||||
"attribute": "attr.path2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.com",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "component0",
|
||||
"release": 1,
|
||||
"deploy": {
|
||||
"driver": "chef-solo",
|
||||
"driver_args": {
|
||||
"run_list": [
|
||||
"recipe[cookbook0::recipe0@0.1.0]"
|
||||
]
|
||||
}
|
||||
},
|
||||
"provides": [1],
|
||||
"requires": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.com",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "component1",
|
||||
"release": 1,
|
||||
"deploy": {
|
||||
"driver": "chef-solo",
|
||||
"driver_args": {
|
||||
"run_list": [
|
||||
"recipe[cookbook0::recipe1@0.1.0]",
|
||||
"recipe[cookbook0::recipe2@0.1.0]"
|
||||
]
|
||||
}
|
||||
},
|
||||
"provides": [2],
|
||||
"requires": [1]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.com",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "component2",
|
||||
"release": 1,
|
||||
"deploy": {
|
||||
"driver": "chef-solo",
|
||||
"driver_args": {
|
||||
"run_list": [
|
||||
"recipe[cookbook1::recipe0@0.1.0]",
|
||||
"recipe[cookbook2::recipe0@0.1.0]"
|
||||
]
|
||||
}
|
||||
},
|
||||
"provides": [2],
|
||||
"provides": [],
|
||||
"requires": [1, 2]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.role",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "role1",
|
||||
"release": 1,
|
||||
"components": [1,2]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.role",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "role2",
|
||||
"release": 1,
|
||||
"components": [3]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.node",
|
||||
"pk": "080000000001",
|
||||
"fields": {
|
||||
"name": "test.example.com",
|
||||
"ip": "127.0.0.1",
|
||||
"metadata": {
|
||||
"block_device": {},
|
||||
"interfaces": {},
|
||||
"cpu": {},
|
||||
"memory": {}
|
||||
},
|
||||
"cluster": 1,
|
||||
"roles": [1]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.node",
|
||||
"pk": "080000000002",
|
||||
"fields": {
|
||||
"name": "test2.example.com",
|
||||
"ip": "127.0.0.2",
|
||||
"metadata": {
|
||||
"block_device": {},
|
||||
"interfaces": {},
|
||||
"cpu": {},
|
||||
"memory": {}
|
||||
},
|
||||
"cluster": 1,
|
||||
"roles": [2]
|
||||
}
|
||||
}
|
||||
]
|
@ -1,384 +0,0 @@
|
||||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "nailgun.release",
|
||||
"fields": {
|
||||
"name": "Essex",
|
||||
"version": "1.2.3",
|
||||
"description": "Essex release description",
|
||||
"networks_metadata": [
|
||||
{"name": "floating", "access": "public"},
|
||||
{"name": "fixed", "access": "private"},
|
||||
{"name": "admin", "access": "private"}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "nailgun.cluster",
|
||||
"fields": {
|
||||
"name": "Production",
|
||||
"release": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "nailgun.cluster",
|
||||
"fields": {
|
||||
"name": "Staging",
|
||||
"release": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.point",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "point0",
|
||||
"release": 1,
|
||||
"scheme": {
|
||||
"attr0": {
|
||||
"generator": "generator_ip",
|
||||
"generator_args": "floating",
|
||||
"attribute": "attr.path0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.point",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "point1",
|
||||
"release": 1,
|
||||
"scheme": {
|
||||
"attr1": {
|
||||
"generator": "generator_ip",
|
||||
"generator_args": "floating",
|
||||
"attribute": "attr.path1"
|
||||
},
|
||||
"attr2": {
|
||||
"generator": "generator_ip",
|
||||
"generator_args": "floating",
|
||||
"attribute": "attr.path2"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.com",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "component0",
|
||||
"release": 1,
|
||||
"deploy": {
|
||||
"driver": "chef-solo",
|
||||
"driver_args": {
|
||||
"run_list": [
|
||||
"recipe[cookbook0::recipe0@0.1.0]"
|
||||
]
|
||||
}
|
||||
},
|
||||
"provides": [1],
|
||||
"requires": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.com",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "component1",
|
||||
"release": 1,
|
||||
"deploy": {
|
||||
"driver": "chef-solo",
|
||||
"driver_args": {
|
||||
"run_list": [
|
||||
"recipe[cookbook0::recipe1@0.1.0]",
|
||||
"recipe[cookbook0::recipe2@0.1.0]"
|
||||
]
|
||||
}
|
||||
},
|
||||
"provides": [2],
|
||||
"requires": [1]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.com",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"name": "component2",
|
||||
"release": 1,
|
||||
"deploy": {
|
||||
"driver": "chef-solo",
|
||||
"driver_args": {
|
||||
"run_list": [
|
||||
"recipe[cookbook1::recipe0@0.1.0]",
|
||||
"recipe[cookbook2::recipe0@0.1.0]"
|
||||
]
|
||||
}
|
||||
},
|
||||
"provides": [2],
|
||||
"provides": [],
|
||||
"requires": [1, 2]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.role",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "Controller",
|
||||
"release": 1,
|
||||
"components": [1,2]
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "nailgun.role",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Compute",
|
||||
"release": 1,
|
||||
"components": [3]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "111111111111",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "ready",
|
||||
"name": "",
|
||||
"manufacturer": "Dell",
|
||||
"platform_name": "Model-1",
|
||||
"roles": [
|
||||
1
|
||||
],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": 1,
|
||||
"mac": "C0:8D:DF:52:76:F1",
|
||||
"metadata": {
|
||||
"block_device": {
|
||||
"ram0": {
|
||||
"removable": "0",
|
||||
"size": "1228800"
|
||||
},
|
||||
"sda": {
|
||||
"vendor": "ATA",
|
||||
"removable": "0",
|
||||
"rev": "0.14",
|
||||
"state": "running",
|
||||
"timeout": "30",
|
||||
"model": "QEMU HARDDISK",
|
||||
"size": "16777216"
|
||||
}
|
||||
},
|
||||
"interfaces": [
|
||||
{
|
||||
"addresses": {
|
||||
"fe80::5054:ff:fe28:16c3": {
|
||||
"prefixlen": "64",
|
||||
"scope": "Link",
|
||||
"family": "inet6"
|
||||
},
|
||||
"52:54:00:28:16:C3": {
|
||||
"family": "lladdr"
|
||||
},
|
||||
"10.20.0.229": {
|
||||
"prefixlen": "24",
|
||||
"scope": "Global",
|
||||
"netmask": "255.255.255.0",
|
||||
"broadcast": "10.20.0.255",
|
||||
"family": "inet"
|
||||
}
|
||||
},
|
||||
"name": "eth0"
|
||||
},
|
||||
{
|
||||
"default_interface": "eth0"
|
||||
},
|
||||
{
|
||||
"default_gateway": "10.20.0.2"
|
||||
}
|
||||
],
|
||||
"cpu": {
|
||||
"real": 0,
|
||||
"0": {
|
||||
"family": "6",
|
||||
"vendor_id": "GenuineIntel",
|
||||
"mhz": "3192.766",
|
||||
"stepping": "3",
|
||||
"cache_size": "4096 KB",
|
||||
"flags": [
|
||||
"fpu",
|
||||
"lahf_lm"
|
||||
],
|
||||
"model": "2",
|
||||
"model_name": "QEMU Virtual CPU version 0.14.1"
|
||||
},
|
||||
"total": 1
|
||||
},
|
||||
"memory": {
|
||||
"anon_pages": "16420kB",
|
||||
"vmalloc_total": "34359738367kB",
|
||||
"bounce": "0kB",
|
||||
"active": "28576kB",
|
||||
"inactive": "20460kB",
|
||||
"nfs_unstable": "0kB",
|
||||
"vmalloc_used": "7160kB",
|
||||
"total": "1019548kB",
|
||||
"slab": "16260kB",
|
||||
"buffers": "4888kB",
|
||||
"slab_unreclaim": "7180kB",
|
||||
"swap": {
|
||||
"cached": "0kB",
|
||||
"total": "0kB",
|
||||
"free": "0kB"
|
||||
},
|
||||
"dirty": "84kB",
|
||||
"writeback": "0kB",
|
||||
"vmalloc_chunk": "34359729156kB",
|
||||
"free": "322008kB",
|
||||
"page_tables": "1328kB",
|
||||
"cached": "27728kB",
|
||||
"commit_limit": "509772kB",
|
||||
"committed_as": "54864kB",
|
||||
"mapped": "5380kB",
|
||||
"slab_reclaimable": "9080kB"
|
||||
},
|
||||
"serial": "Unknown",
|
||||
"networks": {
|
||||
"floating": {
|
||||
"access": "public",
|
||||
"device": "eth0",
|
||||
"netmask": "255.255.255.0",
|
||||
"vlan_id": 300,
|
||||
"address": "172.18.0.2"
|
||||
},
|
||||
"admin": {
|
||||
"access": "private",
|
||||
"device": "eth0",
|
||||
"netmask": "255.255.255.0",
|
||||
"vlan_id": 100,
|
||||
"address": "10.0.0.2"
|
||||
},
|
||||
"storage": {
|
||||
"access": "private",
|
||||
"device": "eth0",
|
||||
"netmask": "255.255.255.0",
|
||||
"vlan_id": 200,
|
||||
"address": "10.0.1.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "222222222222",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "error",
|
||||
"name": "",
|
||||
"manufacturer": "HP",
|
||||
"platform_name": "Model-2",
|
||||
"roles": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": 1,
|
||||
"mac": "46:FC:5A:0C:F9:51"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "333333333333",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "deploying",
|
||||
"name": "",
|
||||
"manufacturer": "OpenVZ",
|
||||
"platform_name": "Model-3",
|
||||
"roles": [
|
||||
2
|
||||
],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": 1,
|
||||
"mac": "2E:04:78:86:69:1F"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "444444444444",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "offline",
|
||||
"name": "",
|
||||
"manufacturer": "",
|
||||
"platform_name": "No-Manufacturer",
|
||||
"roles": [],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": 1,
|
||||
"mac": "BC:10:A1:44:94:A0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "555555555555",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "ready",
|
||||
"name": "Node with name",
|
||||
"manufacturer": "VMWare",
|
||||
"platform_name": "",
|
||||
"roles": [],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": 1,
|
||||
"mac": "B6:17:54:39:27:EA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "000000000000",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "ready",
|
||||
"name": "Node without cluster",
|
||||
"manufacturer": "",
|
||||
"platform_name": "",
|
||||
"roles": [],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": null,
|
||||
"mac": "47:33:22:46:9B:92"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "000000000001",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "ready",
|
||||
"name": "Another node without cluster",
|
||||
"manufacturer": "QEMU",
|
||||
"platform_name": "",
|
||||
"roles": [],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": null,
|
||||
"mac": "84:67:BA:CA:69:95"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "000000000001",
|
||||
"model": "nailgun.node",
|
||||
"fields": {
|
||||
"status": "ready",
|
||||
"name": "node virtualbox",
|
||||
"manufacturer": "virtualbox",
|
||||
"platform_name": "",
|
||||
"roles": [],
|
||||
"ip": "",
|
||||
"fqdn": "",
|
||||
"cluster": null,
|
||||
"mac": "3A:10:EC:04:9A:DE"
|
||||
}
|
||||
}
|
||||
|
||||
]
|
@ -1,469 +0,0 @@
|
||||
import logging
|
||||
import socket
|
||||
import paramiko
|
||||
import copy
|
||||
import string
|
||||
import logging
|
||||
from random import choice
|
||||
import re
|
||||
import time
|
||||
import socket
|
||||
import pprint
|
||||
|
||||
from nailgun import models
|
||||
from nailgun import settings
|
||||
|
||||
from nailgun.exceptions import EmptyListError, NotFound
|
||||
|
||||
|
||||
logger = logging.getLogger("helpers")
|
||||
|
||||
|
||||
class SshConnect(object):
|
||||
|
||||
def __init__(self, host, user, keyfile=None, password=None):
|
||||
try:
|
||||
self.host = host
|
||||
self.t = paramiko.Transport((host, 22))
|
||||
if password:
|
||||
self.t.connect(username=user, password=password)
|
||||
elif keyfile:
|
||||
self.t.connect(username=user,
|
||||
pkey=paramiko.RSAKey.from_private_key_file(keyfile))
|
||||
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def run(self, cmd, timeout=30):
|
||||
logger.debug("[%s] Running command: %s", self.host, cmd)
|
||||
chan = self.t.open_session()
|
||||
chan.settimeout(timeout)
|
||||
chan.exec_command(cmd)
|
||||
return chan.recv_exit_status() == 0
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self.t:
|
||||
self.t.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class EndPointDataDriver:
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
|
||||
def node_ip(self, network_name):
|
||||
for ip_addr in models.IPAddr.objects.filter(node__id=self.node.id):
|
||||
network = models.Network.objects.get(id=ip_addr.network.id)
|
||||
if network.name == network_name:
|
||||
return ip_addr.ip_addr
|
||||
|
||||
def node_netmask(self, network_name):
|
||||
release = self.node.cluster.release
|
||||
network = models.Network.objects.get(name=network_name,
|
||||
release=release)
|
||||
return network.netmask
|
||||
|
||||
def node_vlan(self, network_name):
|
||||
release = self.node.cluster.release
|
||||
network = models.Network.objects.get(name=network_name,
|
||||
release=release)
|
||||
return network.vlan_id
|
||||
|
||||
|
||||
class EndPointManager:
|
||||
def __init__(self, data_driver, name, scheme):
|
||||
|
||||
self.data_driver = data_driver
|
||||
self.name = name
|
||||
self.scheme = scheme
|
||||
self.data = {}
|
||||
|
||||
def generator_ip_repo(self, args):
|
||||
return settings.REPO_ADDRESS
|
||||
|
||||
def generator_ip(self, network_name):
|
||||
network_name = str(network_name)
|
||||
ip = self.data_driver.node_ip(network_name)
|
||||
logger.debug("EndPointManager: generator_ip: %s" % ip)
|
||||
return ip
|
||||
|
||||
def generator_netmask(self, network_name):
|
||||
network_name = str(network_name)
|
||||
netmask = self.data_driver.node_netmask(network_name)
|
||||
logger.debug("EndPointManager: generator_netmask: %s" % netmask)
|
||||
return netmask
|
||||
|
||||
def generator_vlan(self, network_name):
|
||||
network_name = str(network_name)
|
||||
vlan_id = self.data_driver.node_vlan(network_name)
|
||||
logger.debug("EndPointManager: generator_vlan: %s" % vlan_id)
|
||||
return vlan_id
|
||||
|
||||
def generator_url(self, url_args):
|
||||
url_args = dict(url_args)
|
||||
ip = self.data_driver.node_ip(url_args['network'])
|
||||
url = "%s://%s:%s%s" % (url_args['protocol'],
|
||||
ip,
|
||||
url_args['port'],
|
||||
url_args.get('url', ''))
|
||||
logger.debug("EndPointManager: generator_url: %s" % url)
|
||||
return url
|
||||
|
||||
def generator_transparent(self, args):
|
||||
logger.debug("EndPointManager: generator_transparent: %s" % \
|
||||
args)
|
||||
return args
|
||||
|
||||
def generator_password(self, length=8):
|
||||
length = int(length)
|
||||
password = ''.join(
|
||||
choice(
|
||||
''.join((string.ascii_letters, string.digits))
|
||||
) for _ in xrange(length)
|
||||
)
|
||||
logger.debug("EndPointManager: generator_password: %s" % \
|
||||
password)
|
||||
return password
|
||||
|
||||
@classmethod
|
||||
def merge_dictionary(cls, dst, src):
|
||||
"""
|
||||
'True' way of merging two dictionaries
|
||||
(python dict.update() updates just top-level keys and items)
|
||||
"""
|
||||
stack = [(dst, src)]
|
||||
while stack:
|
||||
current_dst, current_src = stack.pop()
|
||||
for key in current_src:
|
||||
if key not in current_dst:
|
||||
current_dst[key] = current_src[key]
|
||||
else:
|
||||
if isinstance(current_src[key], dict) \
|
||||
and isinstance(current_dst[key], dict):
|
||||
stack.append((current_dst[key], current_src[key]))
|
||||
else:
|
||||
current_dst[key] = current_src[key]
|
||||
return dst
|
||||
|
||||
@classmethod
|
||||
def list2dict(cls, d, k):
|
||||
"""
|
||||
Creating a nested dictionary:
|
||||
['a', 'b', 'c', 'd'] => {'a': {'b': {'c': 'd'}}}
|
||||
Merging it with the main dict updates the single key
|
||||
"""
|
||||
_d = copy.deepcopy(d)
|
||||
if len(k) > 1:
|
||||
_k = k.pop(0)
|
||||
_d[_k] = cls.list2dict(d, k)
|
||||
return _d
|
||||
return k.pop(0)
|
||||
|
||||
def instantiate(self):
|
||||
for k in self.scheme:
|
||||
logger.debug("EndPointManager: generating %s" % k)
|
||||
generator = getattr(self, self.scheme[k]["generator"])
|
||||
generator_args = self.scheme[k]["generator_args"]
|
||||
generated = generator(generator_args)
|
||||
|
||||
attributes = self.scheme[k]["attribute"]
|
||||
"""
|
||||
example of attribute:
|
||||
["service.mysql.user", "service.postgresql.user"]
|
||||
"""
|
||||
if not isinstance(attributes, (list, tuple)):
|
||||
attributes = [attributes]
|
||||
|
||||
for attribute in attributes:
|
||||
attribute_keys = re.split(ur'\.', attribute)
|
||||
logger.debug("EndPointManager: attribute_keys: %s" % \
|
||||
str(attribute_keys))
|
||||
|
||||
attribute_keys.append(generated)
|
||||
logger.debug("EndPointManager: attribute_keys: %s" % \
|
||||
str(attribute_keys))
|
||||
attribute_dict = self.list2dict({}, attribute_keys)
|
||||
logger.debug("EndPointManager: attribute_dict: %s" % \
|
||||
str(attribute_dict))
|
||||
|
||||
self.merge_dictionary(self.data, attribute_dict)
|
||||
|
||||
def get_data(self):
|
||||
logger.debug("EndPointManager: data: %s" % \
|
||||
str(self.data))
|
||||
return self.data
|
||||
|
||||
|
||||
class DeployManager:
|
||||
def __init__(self, cluster_id):
|
||||
self.cluster_id = cluster_id
|
||||
self.cluster_component_ids = [
|
||||
c.id for n, r, c in self._cluster_iterator()
|
||||
]
|
||||
self.release_id = models.Cluster.objects.get(id=cluster_id).release.id
|
||||
|
||||
def sorted_components(self):
|
||||
graph = {}
|
||||
for component in models.Com.objects.filter(
|
||||
id__in=self.cluster_component_ids
|
||||
):
|
||||
self._resolve_cluster_deps(graph, component)
|
||||
|
||||
try:
|
||||
sorted_components = self._topol_sort(graph)
|
||||
except KeyError:
|
||||
raise Exception("Cluster dependencies cannot be resolved")
|
||||
|
||||
logger.debug("sorted_components: %s" % \
|
||||
pprint.pformat(sorted_components))
|
||||
return sorted_components
|
||||
|
||||
def _cluster_iterator(self):
|
||||
for node in models.Node.objects.filter(cluster__id=self.cluster_id):
|
||||
for role in node.roles.all():
|
||||
for component in role.components.all():
|
||||
yield [node, role, component]
|
||||
|
||||
def _resolve_cluster_deps(self, graph, component):
|
||||
if component.name not in graph:
|
||||
graph[component.name] = []
|
||||
requires = component.requires.all()
|
||||
logger.debug("Resolving cluster: component %s requires: %s" % \
|
||||
(component.name,
|
||||
str([p.name for p in requires])))
|
||||
|
||||
for provided_by in models.Com.objects.filter(
|
||||
id__in=self.cluster_component_ids,
|
||||
provides__in=requires
|
||||
):
|
||||
graph[component.name].append(provided_by.name)
|
||||
self._resolve_cluster_deps(graph, provided_by)
|
||||
|
||||
def _topol_sort(self, graph):
|
||||
""" Depth First Traversal algorithm for sorting DAG graph.
|
||||
|
||||
Example graph: 1 depends on 4; 3 depends on 2 and 6; etc.
|
||||
Example code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> graph = {1: [4], 2: [], 3: [2,6], 4:[2,3], 5: [], 6: [2]}
|
||||
>>> topol_sort(graph)
|
||||
[2, 6, 3, 4, 1, 5]
|
||||
|
||||
Exception is raised if there is a cycle:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> graph = {1: [4], 2: [], 3: [2,6], 4:[2,3,1], 5: [], 6: [2]}
|
||||
>>> topol_sort(graph)
|
||||
...
|
||||
Exception: Graph contains cycles, processed 4 depends on 1
|
||||
|
||||
"""
|
||||
|
||||
def dfs(v):
|
||||
color[v] = "gray"
|
||||
for w in graph[v]:
|
||||
if color[w] == "black":
|
||||
continue
|
||||
elif color[w] == "gray":
|
||||
raise Exception(
|
||||
"Graph contains cycles, processed %s depends on %s" % \
|
||||
(v, w))
|
||||
dfs(w)
|
||||
color[v] = "black"
|
||||
_sorted.append(v)
|
||||
|
||||
_sorted = []
|
||||
color = {}
|
||||
for j in graph:
|
||||
color[j] = "white"
|
||||
for i in graph:
|
||||
if color[i] == "white":
|
||||
dfs(i)
|
||||
|
||||
return _sorted
|
||||
|
||||
def clean_cluster(self):
|
||||
models.EndPoint.objects.filter(
|
||||
node__in=models.Node.objects.filter(cluster__id=self.cluster_id)
|
||||
).delete()
|
||||
|
||||
def instantiate_cluster(self):
|
||||
|
||||
for node in models.Node.objects.filter(cluster__id=self.cluster_id):
|
||||
|
||||
"""
|
||||
it is needed to be checked if node have only one component
|
||||
assignment of given component and only one given point
|
||||
"""
|
||||
components_used = []
|
||||
points_used = []
|
||||
|
||||
data_driver = EndPointDataDriver(node)
|
||||
|
||||
roles = node.roles.all()
|
||||
for role in roles:
|
||||
components = role.components.all()
|
||||
for component in components:
|
||||
if component.name in components_used:
|
||||
raise Exception(
|
||||
"Duplicated component: node: %s com: %s" % \
|
||||
(node.id, component.name))
|
||||
components_used.append(component.name)
|
||||
|
||||
provides = list(component.provides.all())
|
||||
|
||||
logger.debug("Com %s provides %s" % \
|
||||
(component.name,
|
||||
str([p.name for p in provides])))
|
||||
|
||||
for point in provides:
|
||||
if point.name in points_used:
|
||||
raise Exception(
|
||||
"Duplicated point: node: %s point: %s" % \
|
||||
(node.id, point.name))
|
||||
points_used.append(point.name)
|
||||
|
||||
logger.debug("Instantiating point: %s" % point.name)
|
||||
manager = EndPointManager(
|
||||
data_driver,
|
||||
point.name,
|
||||
point.scheme
|
||||
)
|
||||
manager.instantiate()
|
||||
|
||||
end_point = models.EndPoint(
|
||||
point=point,
|
||||
node=node,
|
||||
data=manager.get_data()
|
||||
)
|
||||
|
||||
end_point.save()
|
||||
|
||||
|
||||
class DeployDriver:
|
||||
def __init__(self, node, component):
|
||||
self.node = node
|
||||
self.component = component
|
||||
|
||||
@classmethod
|
||||
def merge_dictionary(cls, dst, src):
|
||||
"""
|
||||
'True' way of merging two dictionaries
|
||||
(python dict.update() updates just top-level keys and items)
|
||||
"""
|
||||
stack = [(dst, src)]
|
||||
while stack:
|
||||
current_dst, current_src = stack.pop()
|
||||
for key in current_src:
|
||||
if key not in current_dst:
|
||||
current_dst[key] = current_src[key]
|
||||
else:
|
||||
if isinstance(current_src[key], dict) \
|
||||
and isinstance(current_dst[key], dict):
|
||||
stack.append((current_dst[key], current_src[key]))
|
||||
else:
|
||||
current_dst[key] = current_src[key]
|
||||
return dst
|
||||
|
||||
def endpoint_iterator(self, node, component):
|
||||
logger.debug("endpoint_iterator: node: %s component: %s" % \
|
||||
(node.id, component.name))
|
||||
for point in component.provides.all():
|
||||
logger.debug("endpoint_iterator: component: %s provides: %s" % \
|
||||
(component.name, point.name))
|
||||
try:
|
||||
logger.debug("endpoint_iterator: looking for provided "\
|
||||
"endpoint point: %s node: %s" % \
|
||||
(point.name, node.id))
|
||||
ep = models.EndPoint.objects.get(point=point, node=node)
|
||||
except ObjectDoesNotExist as e:
|
||||
logger.debug("endpoint_iterator: provided endpoint "\
|
||||
"is not found point: %s node: %s" % \
|
||||
(point.name, node.id))
|
||||
raise Exception("Provided point %s instance is not found" % \
|
||||
point.name)
|
||||
except Exception as e:
|
||||
logger.debug("Exception: %s" % str(e))
|
||||
raise e
|
||||
else:
|
||||
logger.debug("endpoint_iterator: provided endpoint found " \
|
||||
"point: %s node: %s endpoint: %s" % \
|
||||
(point.name, node.id, ep.id))
|
||||
yield ep
|
||||
|
||||
for point in component.requires.all():
|
||||
"""
|
||||
FOR THE START WE TRY TO FIND ENDPOINT INSTANCE
|
||||
BOUND TO THIS NODE. IT IT FAILS THEN WE LOOK FOR
|
||||
ENDPOINT INSTANCES BOUND TO OTHER NODES IN CLUSTER
|
||||
"""
|
||||
try:
|
||||
ep = models.EndPoint.objects.get(
|
||||
point=point,
|
||||
node=node
|
||||
)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
yield ep
|
||||
|
||||
eps = models.EndPoint.objects.filter(point=point)
|
||||
|
||||
if eps:
|
||||
"""
|
||||
FIXME
|
||||
WE NEED MORE INTELLIGENT ALGORITHM TO CHOOSE
|
||||
WHICH ENDPOINT INSTANCE IS A MOST SUITABLE
|
||||
ONE FOR THIS COMPONENT. AT THE MOMENT WE
|
||||
SIMPLY RETURN FIRST FOUND INSTANCE
|
||||
"""
|
||||
ep = eps[0]
|
||||
logger.debug("endpoint_iterator: required endpoint found " \
|
||||
"point: %s node: %s" % \
|
||||
(point.name, ep.node.id))
|
||||
|
||||
yield ep
|
||||
else:
|
||||
raise Exception("Required point %s instance is not found" % \
|
||||
point.name)
|
||||
|
||||
def deploy_data(self):
|
||||
self.data = {}
|
||||
try:
|
||||
for endpoint in self.endpoint_iterator(self.node, self.component):
|
||||
logger.error("Found endpoint id: %s for n=%s c=%s" % \
|
||||
(endpoint.id, self.node.id,
|
||||
self.component.name))
|
||||
self.merge_dictionary(self.data, endpoint.data)
|
||||
except:
|
||||
logger.error("Error while getting endpoints for n=%s c=%s" % \
|
||||
(self.node.id, self.component.name))
|
||||
raise Exception("Getting endpoints failed: node=%s com=%s" % \
|
||||
(self.node.id, self.component.name))
|
||||
|
||||
logger.debug("Node: %s com: %s data: %s" % \
|
||||
(self.node.id, self.component.name, str(self.data)))
|
||||
return {
|
||||
"chef-solo": self.chef_solo_data,
|
||||
"puppet": self.puppet_data,
|
||||
}[self.component.deploy["driver"]]()
|
||||
|
||||
def chef_solo_data(self):
|
||||
chef_data = {
|
||||
"run_list": self.component.deploy["driver_args"]["run_list"]
|
||||
}
|
||||
if self.component.deploy["driver_args"].get("cooks", None) is not None:
|
||||
chef_data["cooks"] = \
|
||||
self.component.deploy["driver_args"]["cooks"]
|
||||
logger.debug("Chef-data: %s" % str(chef_data))
|
||||
self.merge_dictionary(chef_data, self.data)
|
||||
return chef_data
|
||||
|
||||
def puppet_data(self):
|
||||
return self.data
|
@ -1,6 +0,0 @@
|
||||
import traceback
|
||||
|
||||
|
||||
class ExceptionLoggingMiddleware(object):
|
||||
def process_exception(self, request, exception):
|
||||
print traceback.format_exc()
|
@ -1,243 +0,0 @@
|
||||
import re
|
||||
import ipaddr
|
||||
import celery
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from jsonfield import JSONField
|
||||
|
||||
|
||||
class EndPoint(models.Model):
|
||||
point = models.ForeignKey('Point', related_name='endpoints')
|
||||
node = models.ForeignKey('Node', related_name='endpoints')
|
||||
data = JSONField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("point", "node")
|
||||
|
||||
|
||||
class Point(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
release = models.ForeignKey('Release', related_name='points')
|
||||
scheme = JSONField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "release")
|
||||
|
||||
|
||||
class Com(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
release = models.ForeignKey('Release', related_name='components')
|
||||
requires = models.ManyToManyField(Point, related_name='required_by')
|
||||
provides = models.ManyToManyField(Point, related_name='provided_by')
|
||||
deploy = JSONField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "release")
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
release = models.ForeignKey('Release', related_name='roles')
|
||||
components = models.ManyToManyField(Com, related_name="roles")
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "release")
|
||||
|
||||
|
||||
class Release(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
version = models.CharField(max_length=30)
|
||||
description = models.TextField(null=True, blank=True)
|
||||
networks_metadata = JSONField()
|
||||
|
||||
class Meta:
|
||||
unique_together = ("name", "version")
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
cluster = models.OneToOneField('Cluster', related_name='+')
|
||||
task_name = models.CharField(max_length=100)
|
||||
|
||||
def _get_celery_task(self):
|
||||
from nailgun import tasks
|
||||
return getattr(tasks, self.task_name)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._get_celery_task().name
|
||||
|
||||
def run(self, *args):
|
||||
task_result = self._get_celery_task().delay(*args)
|
||||
self.id = task_result.task_id
|
||||
self.save()
|
||||
return task_result
|
||||
|
||||
@property
|
||||
def celery_task_result(self):
|
||||
return celery.result.AsyncResult(self.id)
|
||||
|
||||
def _flatten_celery_subtasks(self, task=None):
|
||||
if task is None:
|
||||
task = self.celery_task_result
|
||||
result = [task]
|
||||
if isinstance(task.result, celery.result.ResultSet):
|
||||
result += reduce(list.__add__, \
|
||||
map(self._flatten_celery_subtasks, task.result.results))
|
||||
elif isinstance(task.result, celery.result.AsyncResult):
|
||||
result += self._flatten_celery_subtasks(task.result)
|
||||
return result
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
tasks = self._flatten_celery_subtasks()
|
||||
return all(map(lambda t: t.ready(), tasks))
|
||||
|
||||
@property
|
||||
def errors(self):
|
||||
tasks = self._flatten_celery_subtasks()
|
||||
errors = []
|
||||
for task in tasks:
|
||||
if isinstance(task.result, Exception):
|
||||
errors.append(task.result)
|
||||
return errors
|
||||
|
||||
|
||||
class Cluster(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
release = models.ForeignKey(Release, related_name='clusters')
|
||||
|
||||
# working around Django issue #10227
|
||||
@property
|
||||
def task(self):
|
||||
try:
|
||||
return Task.objects.get(cluster=self)
|
||||
except ObjectDoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class Node(models.Model):
|
||||
NODE_STATUSES = (
|
||||
('offline', 'offline'),
|
||||
('ready', 'ready'),
|
||||
('discover', 'discover'),
|
||||
('deploying', 'deploying'),
|
||||
('error', 'error'),
|
||||
)
|
||||
id = models.CharField(max_length=12, primary_key=True)
|
||||
cluster = models.ForeignKey(Cluster, related_name='nodes',
|
||||
null=True, blank=True, on_delete=models.SET_NULL)
|
||||
name = models.CharField(max_length=100, blank=True)
|
||||
status = models.CharField(max_length=30, choices=NODE_STATUSES,
|
||||
default='ready')
|
||||
metadata = JSONField()
|
||||
mac = models.CharField(max_length=17)
|
||||
ip = models.CharField(max_length=15)
|
||||
fqdn = models.CharField(max_length=255)
|
||||
manufacturer = models.CharField(max_length=50, blank=True)
|
||||
platform_name = models.CharField(max_length=150, blank=True)
|
||||
os_platform = models.CharField(max_length=150, blank=True)
|
||||
|
||||
roles = models.ManyToManyField(Role, related_name='nodes')
|
||||
new_roles = models.ManyToManyField(Role, related_name='+')
|
||||
redeployment_needed = models.BooleanField(default=False)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
""" Safely aggregate metadata to provide short info for UI """
|
||||
result = {}
|
||||
|
||||
try:
|
||||
kilobytes = int(self.metadata['memory']['total'][:-2])
|
||||
gigabytes = kilobytes / 1024.0 ** 2
|
||||
result['ram'] = gigabytes
|
||||
except Exception:
|
||||
result['ram'] = None
|
||||
|
||||
try:
|
||||
result['cpu'] = self.metadata['cpu']['real']
|
||||
result['cores'] = self.metadata['cpu']['total']
|
||||
except Exception:
|
||||
result['cpu'] = None
|
||||
result['cores'] = None
|
||||
|
||||
# FIXME: disk space calculating may be wrong
|
||||
try:
|
||||
result['hdd'] = 0
|
||||
for name, info in self.metadata['block_device'].iteritems():
|
||||
if re.match(r'^sd.$', name):
|
||||
bytes = int(info['size']) * 512
|
||||
terabytes = bytes / 1024.0 ** 4
|
||||
result['hdd'] += terabytes
|
||||
except Exception:
|
||||
result['hdd'] = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class IPAddr(models.Model):
|
||||
network = models.ForeignKey('Network')
|
||||
node = models.ForeignKey(Node)
|
||||
ip_addr = models.CharField(max_length=25)
|
||||
|
||||
|
||||
class Network(models.Model):
|
||||
release = models.ForeignKey(Release, related_name="networks")
|
||||
name = models.CharField(max_length=20)
|
||||
access = models.CharField(max_length=20)
|
||||
vlan_id = models.PositiveIntegerField()
|
||||
network = models.CharField(max_length=25)
|
||||
range_l = models.CharField(max_length=25)
|
||||
range_h = models.CharField(max_length=25)
|
||||
gateway = models.CharField(max_length=25)
|
||||
nodes = models.ManyToManyField(Node, through=IPAddr, null=True, blank=True)
|
||||
|
||||
@property
|
||||
def netmask(self):
|
||||
return str(ipaddr.IPv4Network(self.network).netmask)
|
||||
|
||||
@property
|
||||
def broadcast(self):
|
||||
return str(ipaddr.IPv4Network(self.network).broadcast)
|
||||
|
||||
def update_node_network_info(self, node):
|
||||
nw = ipaddr.IPv4Network(self.network)
|
||||
range_l = ipaddr.IPv4Address(self.range_l)
|
||||
range_h = ipaddr.IPv4Address(self.range_h)
|
||||
new_ip = None
|
||||
for host in nw.iterhosts():
|
||||
if range_l <= ipaddr.IPv4Address(host) <= range_h:
|
||||
try:
|
||||
IPAddr.objects.get(network=self, ip_addr=host)
|
||||
except IPAddr.DoesNotExist:
|
||||
new_ip = host
|
||||
break
|
||||
|
||||
if not new_ip:
|
||||
raise Exception("There is no free IP for node %s" % node.id)
|
||||
|
||||
new_ip_obj = IPAddr(network=self, ip_addr=new_ip, node=node)
|
||||
new_ip_obj.save()
|
||||
|
||||
if not "networks" in node.metadata:
|
||||
node.metadata["networks"] = {}
|
||||
|
||||
# FIXME: populate real value
|
||||
if 'default_interface' in node.metadata['interfaces']:
|
||||
device = node.metadata['interfaces']['default_interface']
|
||||
else:
|
||||
device = 'eth0'
|
||||
|
||||
node.metadata["networks"][self.name] = {
|
||||
"access": self.access,
|
||||
"device": device,
|
||||
"vlan_id": self.vlan_id,
|
||||
"address": str(new_ip),
|
||||
"netmask": self.netmask,
|
||||
# FIXME: do we need those?
|
||||
# "broascast": self.broadcast,
|
||||
# "gateway": self.gateway,
|
||||
}
|
||||
|
||||
node.save()
|
@ -1,48 +0,0 @@
|
||||
import re
|
||||
|
||||
|
||||
class ProvisionException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ProvisionAlreadyExists(ProvisionException):
|
||||
pass
|
||||
|
||||
|
||||
class ProvisionDoesNotExist(ProvisionException):
|
||||
pass
|
||||
|
||||
|
||||
class ProvisionConfig:
|
||||
cn = 'nailgun.provision.driver.cobbler.Cobbler'
|
||||
|
||||
|
||||
class Provision:
|
||||
def __init__(self):
|
||||
raise NotImplementedError(
|
||||
"Try to use ProvisionFactory.getInstance() method."
|
||||
)
|
||||
|
||||
def save_profile(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def save_node(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ProvisionFactory:
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls, config=ProvisionConfig()):
|
||||
name = config.cn
|
||||
module_name = '.'.join(re.split(ur'\.', name)[:-1])
|
||||
class_name = re.split(ur'\.', name)[-1]
|
||||
return getattr(
|
||||
__import__(
|
||||
module_name,
|
||||
globals(),
|
||||
locals(),
|
||||
[class_name],
|
||||
-1),
|
||||
class_name
|
||||
)(config)
|
@ -1,367 +0,0 @@
|
||||
from nailgun.provision import ProvisionException
|
||||
from nailgun.provision import ProvisionAlreadyExists, ProvisionDoesNotExist
|
||||
from nailgun.provision import Provision
|
||||
import logging
|
||||
import xmlrpclib
|
||||
|
||||
|
||||
class Cobbler(Provision):
|
||||
def __init__(self, config):
|
||||
self.logger = logging.getLogger('provision.cobbler')
|
||||
try:
|
||||
self.url = config.url
|
||||
self.user = config.user
|
||||
self.password = config.password
|
||||
except AttributeError as e:
|
||||
self.logger.error(
|
||||
'Provision configuration error.' \
|
||||
' Not all necessary attributes are set properly.'
|
||||
)
|
||||
raise e
|
||||
|
||||
self.logger.debug(
|
||||
'Cobbler config: url="%s", user="%s", password="%s"' \
|
||||
% (self.url, self.user, self.password)
|
||||
)
|
||||
|
||||
try:
|
||||
self.server = xmlrpclib.Server(self.url)
|
||||
self.token = self.server.login(self.user, self.password)
|
||||
except ProvisionException as e:
|
||||
self.logger.error(
|
||||
'Error occured while connecting to provision server.'
|
||||
)
|
||||
raise e
|
||||
|
||||
def _get_any_profile(self):
|
||||
profiles = self.server.get_profiles(self.token)
|
||||
if profiles:
|
||||
return profiles[0]
|
||||
raise ProvisionException("There is no available profiles")
|
||||
|
||||
def system_by_name(self, name):
|
||||
systems = self.server.find_system({'name': name}, self.token)
|
||||
if systems:
|
||||
if len(systems) > 1:
|
||||
self.logger.error(
|
||||
"There are more than one system found by pattern: %s" \
|
||||
% name
|
||||
)
|
||||
raise ProvisionException(
|
||||
"There are more than one system found by pattern: %s" \
|
||||
% name
|
||||
)
|
||||
return systems[0]
|
||||
return None
|
||||
|
||||
# FIXME
|
||||
# IT NEEDED TO BE IMPLEMENTED AS ONLY METHOD FOR ADD AND EDIT
|
||||
def add_system(self, name, mac, power, profile, kopts=""):
|
||||
if self.system_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to add system that already exists: %s" \
|
||||
% name
|
||||
)
|
||||
raise ProvisionAlreadyExists(
|
||||
"System with name %s already exists. Try to edit it." \
|
||||
% name
|
||||
)
|
||||
system_id = self.server.new_system(self.token)
|
||||
self.server.modify_system(
|
||||
system_id, 'name', name, self.token
|
||||
)
|
||||
self.server.modify_system(
|
||||
system_id, 'profile', profile.name, self.token
|
||||
)
|
||||
self.server.modify_system(
|
||||
system_id, 'kopts', kopts, self.token
|
||||
)
|
||||
self.server.modify_system(
|
||||
system_id, 'modify_interface', {
|
||||
"macaddress-eth0": mac,
|
||||
}, self.token
|
||||
)
|
||||
self.server.modify_system(
|
||||
system_id, 'power_type', power.power_type, self.token
|
||||
)
|
||||
if power.power_user:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_user', power.power_user, self.token
|
||||
)
|
||||
if power.power_pass:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_pass', power.power_pass, self.token
|
||||
)
|
||||
if power.power_id:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_id', power.power_id, self.token
|
||||
)
|
||||
if power.power_address:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_address', power.power_address, self.token
|
||||
)
|
||||
self.server.save_system(system_id, self.token)
|
||||
return self.system_by_name(name)
|
||||
|
||||
def edit_system(self, name, mac, power, profile, kopts=""):
|
||||
if not self.system_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to edit system that does not exist: %s" \
|
||||
% name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"System with name %s does not exist. Try to edit it." \
|
||||
% name
|
||||
)
|
||||
system_id = self.server.get_system_handle(name, self.token)
|
||||
self.server.modify_system(
|
||||
system_id, 'profile', profile.name, self.token
|
||||
)
|
||||
self.server.modify_system(
|
||||
system_id, 'kopts', kopts, self.token
|
||||
)
|
||||
self.server.modify_system(
|
||||
system_id, 'modify_interface',
|
||||
{
|
||||
"macaddress-eth0": mac,
|
||||
}, self.token
|
||||
)
|
||||
|
||||
self.server.modify_system(
|
||||
system_id, 'power_type', power.power_type, self.token
|
||||
)
|
||||
if power.power_user:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_user', power.power_user, self.token
|
||||
)
|
||||
if power.power_pass:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_pass', power.power_pass, self.token
|
||||
)
|
||||
if power.power_id:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_id', power.power_id, self.token
|
||||
)
|
||||
if power.power_address:
|
||||
self.server.modify_system(
|
||||
system_id, 'power_address', power.power_address, self.token
|
||||
)
|
||||
self.server.save_system(system_id, self.token)
|
||||
return self.system_by_name(name)
|
||||
|
||||
def power_system(self, name, power):
|
||||
if not self.system_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to power system that does not exist: %s" % name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"System with name %s does not exist. Try to edit it." % name
|
||||
)
|
||||
if power not in ('on', 'off', 'reboot', 'status'):
|
||||
raise ValueError("Power has invalid value")
|
||||
system_id = self.server.get_system_handle(name, self.token)
|
||||
self.server.power_system(system_id, power, self.token)
|
||||
return self.system_by_name(name)
|
||||
|
||||
def handle_system(self, name, mac, power, profile, kopts=""):
|
||||
try:
|
||||
self.edit_system(name, mac, power, profile, kopts)
|
||||
self.logger.info("Edited system: %s" % name)
|
||||
except ProvisionDoesNotExist:
|
||||
self.add_system(name, mac, power, profile, kopts)
|
||||
self.logger.info("Added system: %s" % name)
|
||||
|
||||
def del_system(self, name):
|
||||
system = self.system_by_name(name)
|
||||
if not system:
|
||||
self.logger.error(
|
||||
"Trying to remove system that does not exist: %s" % name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"There is no system with name %s" % name
|
||||
)
|
||||
self.server.remove_system(name, self.token)
|
||||
self.logger.info("Removed system %s" % name)
|
||||
|
||||
def profile_by_name(self, name):
|
||||
profiles = self.server.find_profile({'name': name}, self.token)
|
||||
if profiles:
|
||||
if len(profiles) > 1:
|
||||
self.logger.error(
|
||||
"There are more than one profile found by pattern: %s" \
|
||||
% name
|
||||
)
|
||||
raise ProvisionException(
|
||||
"There are more than one profile found by pattern: %s" \
|
||||
% name
|
||||
)
|
||||
return profiles[0]
|
||||
return None
|
||||
|
||||
# FIXME
|
||||
# IT NEEDED TO BE IMPLEMENTED AS ONLY METHOD FOR ADD AND EDIT
|
||||
def add_profile(self, name, distro, kickstart):
|
||||
if self.profile_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to add profile that already exists: %s" % name
|
||||
)
|
||||
raise ProvisionAlreadyExists(
|
||||
"Profile with name %s already exists. Try to edit it." \
|
||||
% name
|
||||
)
|
||||
profile_id = self.server.new_profile(self.token)
|
||||
self.server.modify_profile(profile_id, 'name', name, self.token)
|
||||
self.server.modify_profile(profile_id, 'distro', distro, self.token)
|
||||
self.server.modify_profile(
|
||||
profile_id, 'kickstart', kickstart, self.token
|
||||
)
|
||||
self.server.save_profile(profile_id, self.token)
|
||||
return self.profile_by_name(name)
|
||||
|
||||
def edit_profile(self, name, distro, kickstart):
|
||||
if not self.profile_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to edit profile that does not exist: %s" % name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"Profile with name %s does not exist. Try to add it." % name
|
||||
)
|
||||
profile_id = self.server.get_profile_handle(name, self.token)
|
||||
self.server.modify_profile(profile_id, 'distro', distro, self.token)
|
||||
self.server.modify_profile(
|
||||
profile_id, 'kickstart', kickstart, self.token
|
||||
)
|
||||
self.server.save_profile(profile_id, self.token)
|
||||
return self.profile_by_name(name)
|
||||
|
||||
def handle_profile(self, name, distro, seed):
|
||||
try:
|
||||
self.edit_profile(name, distro, seed)
|
||||
self.logger.info("Edited profile: %s" % name)
|
||||
except ProvisionDoesNotExist:
|
||||
self.add_profile(name, distro, seed)
|
||||
self.logger.info("Added profile: %s" % name)
|
||||
|
||||
def del_profile(self, name):
|
||||
profile = self.profile_by_name(name)
|
||||
if not profile:
|
||||
self.logger.error(
|
||||
"Trying to remove profile that does not exist: %s" % name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"There is no profile with name %s" % name
|
||||
)
|
||||
self.server.remove_profile(name, self.token)
|
||||
self.logger.info("Removed profile: %s" % name)
|
||||
|
||||
def distro_by_name(self, name):
|
||||
distros = self.server.find_distro({'name': name}, self.token)
|
||||
if distros:
|
||||
if len(distros) > 1:
|
||||
self.logger.error(
|
||||
"There are more than one distro found by pattern: %s" \
|
||||
% name
|
||||
)
|
||||
raise ProvisionException(
|
||||
"There are more than one distro found by pattern %s" \
|
||||
% name
|
||||
)
|
||||
return distros[0]
|
||||
return None
|
||||
|
||||
# FIXME
|
||||
# IT NEEDED TO BE IMPLEMENTED AS ONLY METHOD FOR ADD AND EDIT
|
||||
def add_distro(self, name, kernel, initrd, arch, breed, osversion):
|
||||
if self.distro_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to add distro that already exists: %s" \
|
||||
% name
|
||||
)
|
||||
raise ProvisionAlreadyExists(
|
||||
"Distro with name %s already exists. Try to edit it." \
|
||||
% name
|
||||
)
|
||||
distro_id = self.server.new_distro(self.token)
|
||||
self.server.modify_distro(distro_id, 'name', name, self.token)
|
||||
self.server.modify_distro(distro_id, 'kernel', kernel, self.token)
|
||||
self.server.modify_distro(distro_id, 'initrd', initrd, self.token)
|
||||
self.server.modify_distro(distro_id, 'arch', arch, self.token)
|
||||
self.server.modify_distro(distro_id, 'breed', breed, self.token)
|
||||
self.server.modify_distro(
|
||||
distro_id, 'os_version', osversion, self.token
|
||||
)
|
||||
self.server.save_distro(distro_id, self.token)
|
||||
return self.distro_by_name(name)
|
||||
|
||||
def edit_distro(self, name, kernel, initrd, arch, breed, osversion):
|
||||
if not self.distro_by_name(name):
|
||||
self.logger.error(
|
||||
"Trying to edit distro that does not exist: %s" % name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"Distro with name %s does not exist. Try to add it." \
|
||||
% name
|
||||
)
|
||||
distro_id = self.server.get_distro_handle(name, self.token)
|
||||
self.server.modify_distro(distro_id, 'kernel', kernel, self.token)
|
||||
self.server.modify_distro(distro_id, 'initrd', initrd, self.token)
|
||||
self.server.modify_distro(distro_id, 'arch', arch, self.token)
|
||||
self.server.modify_distro(distro_id, 'breed', breed, self.token)
|
||||
self.server.modify_distro(
|
||||
distro_id, 'os_version', osversion, self.token
|
||||
)
|
||||
self.server.save_distro(distro_id, self.token)
|
||||
return self.distro_by_name(name)
|
||||
|
||||
def handle_distro(self, name, kernel, initrd, arch, os, osversion):
|
||||
try:
|
||||
self.edit_distro(name, kernel, initrd, arch, os, osversion)
|
||||
self.logger.info("Edited distro: %s" % name)
|
||||
except ProvisionDoesNotExist:
|
||||
self.add_distro(name, kernel, initrd, arch, os, osversion)
|
||||
self.logger.info("Added distro: %s" % name)
|
||||
|
||||
def del_distro(self, name):
|
||||
distro = self.distro_by_name(name)
|
||||
if not distro:
|
||||
self.logger.error(
|
||||
"Trying to remove distro that does not exist: %s" % name
|
||||
)
|
||||
raise ProvisionDoesNotExist(
|
||||
"There is no distro with name %s" % name
|
||||
)
|
||||
self.server.remove_distro(name, self.token)
|
||||
self.logger.info("Removed distro %s" % name)
|
||||
|
||||
# API
|
||||
|
||||
def save_profile(self, profile):
|
||||
self.handle_distro(profile.name,
|
||||
profile.kernel,
|
||||
profile.initrd,
|
||||
profile.arch,
|
||||
profile.os,
|
||||
profile.osversion)
|
||||
self.handle_profile(profile.name,
|
||||
profile.name,
|
||||
profile.seed)
|
||||
|
||||
def save_node(self, node):
|
||||
self.handle_system(node.name,
|
||||
node.mac,
|
||||
node.power,
|
||||
node.profile,
|
||||
node.kopts,
|
||||
)
|
||||
|
||||
def power_on(self, node):
|
||||
self.power_system(node.name, 'on')
|
||||
|
||||
def power_off(self, node):
|
||||
self.power_system(node.name, 'off')
|
||||
|
||||
def power_reboot(self, node):
|
||||
self.power_system(node.name, 'reboot')
|
||||
|
||||
def power_status(self, node):
|
||||
raise NotImplementedError
|
@ -1,81 +0,0 @@
|
||||
import re
|
||||
from nailgun.provision import ProvisionException
|
||||
import logging
|
||||
|
||||
|
||||
class ModelObject(object):
|
||||
_driver = None
|
||||
|
||||
@property
|
||||
def driver(self):
|
||||
if self._driver is None:
|
||||
raise ProvisionException("Driver is not set properly.")
|
||||
return self._driver
|
||||
|
||||
@driver.setter
|
||||
def driver(self, driver):
|
||||
self._driver = driver
|
||||
|
||||
|
||||
class Validator:
|
||||
_supported_os = (
|
||||
"ubuntu",
|
||||
"redhat",
|
||||
)
|
||||
|
||||
_supported_osversion = (
|
||||
"precise",
|
||||
"rhel6",
|
||||
)
|
||||
|
||||
_supported_arch = (
|
||||
"x86_64",
|
||||
)
|
||||
|
||||
_supported_platform = (
|
||||
("ubuntu", "precise", "x86_64"),
|
||||
("redhat", "rhel6", "x86_64"),
|
||||
)
|
||||
|
||||
_supported_powertypes = (
|
||||
"virsh",
|
||||
"ssh",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def is_mac_valid(cls, mac):
|
||||
rex = re.compile(ur'^([0-9abcdef]{2}:){5}[0-9abcdef]{2}$', re.I)
|
||||
return rex.match(mac)
|
||||
|
||||
@classmethod
|
||||
def is_os_valid(cls, os):
|
||||
return os in cls._supported_os
|
||||
|
||||
@classmethod
|
||||
def is_osversion_valid(cls, osversion):
|
||||
return osversion in cls._supported_osversion
|
||||
|
||||
@classmethod
|
||||
def is_arch_valid(cls, arch):
|
||||
return arch in cls._supported_arch
|
||||
|
||||
@classmethod
|
||||
def is_platform_valid(cls, os, osversion, arch):
|
||||
return (os, osversion, arch) in cls._supported_platform
|
||||
|
||||
# FIXME
|
||||
# IT IS NEEDED TO BE CHECKED IF PROVISION ALREADY HAS THAT PROFILE
|
||||
# IF NOT THEN PROFILE IS OBVIOUSLY INVALID
|
||||
@classmethod
|
||||
def is_profile_valid(cls, profile):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_powertype_valid(cls, powertype):
|
||||
return powertype in cls._supported_powertypes
|
||||
|
||||
# FIXME
|
||||
# IT IS NEEDED TO BE CHECKED IF POWER IS VALID
|
||||
@classmethod
|
||||
def is_power_valid(cls, power):
|
||||
return True
|
@ -1,89 +0,0 @@
|
||||
import logging
|
||||
from nailgun.provision import ProvisionException
|
||||
from . import ModelObject, Validator
|
||||
|
||||
|
||||
class Node(ModelObject):
|
||||
_mac = None
|
||||
_profile = None
|
||||
_kopts = ""
|
||||
_pxe = False
|
||||
_power = None
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.logger = logging.getLogger('provision.model.node')
|
||||
|
||||
def save(self):
|
||||
self.driver.save_node(self)
|
||||
|
||||
@property
|
||||
def mac(self):
|
||||
if not self._mac:
|
||||
raise ProvisionException("Mac is not set properly")
|
||||
return self._mac
|
||||
|
||||
@mac.setter
|
||||
def mac(self, mac):
|
||||
if not Validator.is_mac_valid(mac):
|
||||
raise ProvisionException("Mac is not valid")
|
||||
self._mac = mac
|
||||
|
||||
@property
|
||||
def profile(self):
|
||||
if not self._profile:
|
||||
raise ProvisionException("Profile is not set properly")
|
||||
return self._profile
|
||||
|
||||
@profile.setter
|
||||
def profile(self, profile):
|
||||
if not Validator.is_profile_valid(profile):
|
||||
raise ProvisionException("Profile is not valid")
|
||||
self._profile = profile
|
||||
|
||||
@property
|
||||
def kopts(self):
|
||||
self.logger.debug("Node kopts getter: %s" % self._kopts)
|
||||
return self._kopts
|
||||
|
||||
@kopts.setter
|
||||
def kopts(self, kopts):
|
||||
self.logger.debug("Node kopts setter: %s" % kopts)
|
||||
self._kopts = kopts
|
||||
|
||||
@property
|
||||
def pxe(self):
|
||||
self.logger.debug("Node pxe getter: %s" % str(self._pxe))
|
||||
return self._pxe
|
||||
|
||||
@pxe.setter
|
||||
def pxe(self, pxe):
|
||||
self.logger.debug("Node pxe setter: %s" % str(pxe))
|
||||
if pxe:
|
||||
self._pxe = True
|
||||
else:
|
||||
self._pxe = False
|
||||
|
||||
@property
|
||||
def power(self):
|
||||
if not self._power:
|
||||
raise ProvisionException("Power is not set properly")
|
||||
return self._power
|
||||
|
||||
@power.setter
|
||||
def power(self, power):
|
||||
if not Validator.is_power_valid(power):
|
||||
raise ProvisionException("Power is not valid")
|
||||
self._power = power
|
||||
|
||||
def power_on(self):
|
||||
self.driver.power_on(self)
|
||||
|
||||
def power_off(self):
|
||||
self.driver.power_off(self)
|
||||
|
||||
def power_reboot(self):
|
||||
self.driver.power_reboot(self)
|
||||
|
||||
def power_status(self):
|
||||
self.driver.power_status(self)
|
@ -1,52 +0,0 @@
|
||||
import logging
|
||||
from nailgun.provision import ProvisionException
|
||||
from . import Validator
|
||||
|
||||
|
||||
class Power:
|
||||
_power_user = None
|
||||
_power_pass = None
|
||||
_power_address = None
|
||||
_power_id = None
|
||||
|
||||
def __init__(self, power_type):
|
||||
if Validator.is_powertype_valid(power_type):
|
||||
self._power_type = power_type
|
||||
else:
|
||||
raise ProvisionException("Power type is not valid")
|
||||
|
||||
@property
|
||||
def power_type(self):
|
||||
return self._power_type
|
||||
|
||||
@property
|
||||
def power_user(self):
|
||||
return self._power_user
|
||||
|
||||
@power_user.setter
|
||||
def power_user(self, power_user):
|
||||
self._power_user = power_user
|
||||
|
||||
@property
|
||||
def power_pass(self):
|
||||
return self._power_pass
|
||||
|
||||
@power_pass.setter
|
||||
def power_pass(self, power_pass):
|
||||
self._power_pass = power_pass
|
||||
|
||||
@property
|
||||
def power_address(self):
|
||||
return self._power_address
|
||||
|
||||
@power_address.setter
|
||||
def power_address(self, power_address):
|
||||
self._power_address = power_address
|
||||
|
||||
@property
|
||||
def power_id(self):
|
||||
return self._power_id
|
||||
|
||||
@power_id.setter
|
||||
def power_id(self, power_id):
|
||||
self._power_id = power_id
|
@ -1,102 +0,0 @@
|
||||
import logging
|
||||
from . import ModelObject, Validator
|
||||
from nailgun.provision import ProvisionException
|
||||
|
||||
|
||||
class Profile(ModelObject):
|
||||
_arch = None
|
||||
_kernel = None
|
||||
_initrd = None
|
||||
_os = None
|
||||
_osversion = None
|
||||
_seed = None
|
||||
_kopts = ""
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.logger = logging.getLogger('provision.model.profile')
|
||||
|
||||
def save(self):
|
||||
if not Validator.is_platform_valid(
|
||||
self._os, self._osversion, self._arch
|
||||
):
|
||||
raise ProvisionException("Platform is not valid")
|
||||
self.driver.save_profile(self)
|
||||
|
||||
@property
|
||||
def arch(self):
|
||||
if not self._arch:
|
||||
raise ProvisionException("Arch is not set properly")
|
||||
return self._arch
|
||||
|
||||
@arch.setter
|
||||
def arch(self, arch):
|
||||
if not Validator.is_arch_valid(arch):
|
||||
raise ProvisionException("Arch is not valid")
|
||||
self._arch = arch
|
||||
|
||||
@property
|
||||
def kernel(self):
|
||||
if not self._kernel:
|
||||
raise ProvisionException("Kernel is not set properly")
|
||||
return self._kernel
|
||||
|
||||
@kernel.setter
|
||||
def kernel(self, kernel):
|
||||
self._kernel = kernel
|
||||
|
||||
@property
|
||||
def initrd(self):
|
||||
if not self._initrd:
|
||||
raise ProvisionException("Initrd is not set properly")
|
||||
return self._initrd
|
||||
|
||||
@initrd.setter
|
||||
def initrd(self, initrd):
|
||||
self._initrd = initrd
|
||||
|
||||
@property
|
||||
def os(self):
|
||||
if not self._os:
|
||||
raise ProvisionException("Os is not set properly")
|
||||
return self._os
|
||||
|
||||
@os.setter
|
||||
def os(self, os):
|
||||
if not Validator.is_os_valid(os):
|
||||
raise ProvisionException("Os is not valid")
|
||||
self._os = os
|
||||
|
||||
@property
|
||||
def osversion(self):
|
||||
if not self._osversion:
|
||||
raise ProvisionException("Osversion is not set properly")
|
||||
return self._osversion
|
||||
|
||||
@osversion.setter
|
||||
def osversion(self, osversion):
|
||||
if not Validator.is_osversion_valid(osversion):
|
||||
raise ProvisionException("Osversion is not valid")
|
||||
self._osversion = osversion
|
||||
|
||||
@property
|
||||
def seed(self):
|
||||
if not self._seed:
|
||||
raise ProvisionException("Seed is not set properly")
|
||||
self.logger.debug("Profile seed getter: %s" % self._seed)
|
||||
return self._seed
|
||||
|
||||
@seed.setter
|
||||
def seed(self, seed):
|
||||
self.logger.debug("Profile seed setter: %s" % seed)
|
||||
self._seed = seed
|
||||
|
||||
@property
|
||||
def kopts(self):
|
||||
self.logger.debug("Profile kopts getter: %s" % self._kopts)
|
||||
return self._kopts
|
||||
|
||||
@kopts.setter
|
||||
def kopts(self, kopts):
|
||||
self.logger.debug("Profile kopts setter: %s" % kopts)
|
||||
self._kopts = kopts
|
@ -1,128 +0,0 @@
|
||||
from model import Validator
|
||||
from model.profile import Profile
|
||||
from model.node import Node
|
||||
from model.power import Power
|
||||
from nose.tools import eq_
|
||||
|
||||
|
||||
class TestValidator:
|
||||
def setUp(self):
|
||||
self.mac = "c8:0a:a9:a6:ff:28"
|
||||
self.platform = ("ubuntu", "precise", "x86_64")
|
||||
self.os = "ubuntu"
|
||||
self.osversion = "precise"
|
||||
self.arch = "x86_64"
|
||||
|
||||
def test_is_mac_valid(self):
|
||||
assert Validator.is_mac_valid(self.mac)
|
||||
|
||||
def test_is_platform_valid(self):
|
||||
assert Validator.is_platform_valid(
|
||||
self.platform[0],
|
||||
self.platform[1],
|
||||
self.platform[2]
|
||||
)
|
||||
|
||||
def test_is_os_valid(self):
|
||||
assert Validator.is_os_valid(self.os)
|
||||
|
||||
def test_is_osversion_valid(self):
|
||||
assert Validator.is_osversion_valid(self.osversion)
|
||||
|
||||
def test_is_arch_valid(self):
|
||||
assert Validator.is_arch_valid(self.arch)
|
||||
|
||||
|
||||
class TestProfile:
|
||||
def setUp(self):
|
||||
self.profile = Profile('profile')
|
||||
self.arch = "x86_64"
|
||||
self.os = "ubuntu"
|
||||
self.osversion = "precise"
|
||||
self.kernel = "kernel"
|
||||
self.initrd = "initrd"
|
||||
self.seed = "seed"
|
||||
self.kopts = "kopts"
|
||||
|
||||
def test_arch(self):
|
||||
self.profile.arch = self.arch
|
||||
eq_(self.profile.arch, self.arch)
|
||||
|
||||
def test_os(self):
|
||||
self.profile.os = self.os
|
||||
eq_(self.profile.os, self.os)
|
||||
|
||||
def test_osversion(self):
|
||||
self.profile.osversion = self.osversion
|
||||
eq_(self.profile.osversion, self.osversion)
|
||||
|
||||
def test_kernel(self):
|
||||
self.profile.kernel = self.kernel
|
||||
eq_(self.profile.kernel, self.kernel)
|
||||
|
||||
def test_initrd(self):
|
||||
self.profile.initrd = self.initrd
|
||||
eq_(self.profile.initrd, self.initrd)
|
||||
|
||||
def test_seed(self):
|
||||
self.profile.seed = self.seed
|
||||
eq_(self.profile.seed, self.seed)
|
||||
|
||||
def test_kopts(self):
|
||||
self.profile.kopts = self.kopts
|
||||
eq_(self.profile.kopts, self.kopts)
|
||||
|
||||
|
||||
class TestNode:
|
||||
def setUp(self):
|
||||
self.node = Node('node')
|
||||
self.mac = "c8:0a:a9:a6:ff:28"
|
||||
self.profile = Profile('profile')
|
||||
self.kopts = "kopts"
|
||||
self.pxe = True
|
||||
self.power = Power('ssh')
|
||||
|
||||
def test_mac(self):
|
||||
self.node.mac = self.mac
|
||||
eq_(self.node.mac, self.mac)
|
||||
|
||||
def test_profile(self):
|
||||
self.node.profile = self.profile
|
||||
eq_(self.node.profile, self.profile)
|
||||
|
||||
def test_kopts(self):
|
||||
self.node.kopts = self.kopts
|
||||
eq_(self.node.kopts, self.kopts)
|
||||
|
||||
def test_pxe(self):
|
||||
self.node.pxe = self.pxe
|
||||
eq_(self.node.pxe, self.pxe)
|
||||
|
||||
def test_power(self):
|
||||
self.node.power = self.power
|
||||
eq_(self.node.power, self.power)
|
||||
|
||||
|
||||
class TestPower:
|
||||
def setUp(self):
|
||||
self.power = Power('ssh')
|
||||
self.power_user = "user"
|
||||
self.power_pass = "pass"
|
||||
self.power_address = "localhost"
|
||||
self.power_id = "localhost"
|
||||
|
||||
def test_power_user(self):
|
||||
self.power.power_user = self.power_user
|
||||
eq_(self.power.power_user, self.power_user)
|
||||
|
||||
def test_power_pass(self):
|
||||
self.power.power_pass = self.power_pass
|
||||
eq_(self.power.power_pass, self.power_pass)
|
||||
|
||||
def test_power_address(self):
|
||||
self.power.power_address = self.power_address
|
||||
eq_(self.power.power_address, self.power_address)
|
||||
|
||||
def test_power_id(self):
|
||||
self.power.power_id = self.power_id
|
||||
eq_(self.power.power_id, self.power_id)
|
@ -1,174 +0,0 @@
|
||||
import os
|
||||
|
||||
from nailgun.extrasettings import *
|
||||
|
||||
SITE_ROOT = os.path.dirname(os.path.realpath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(SITE_ROOT)
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@example.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(PROJECT_ROOT, 'nailgun.sqlite'),
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '',
|
||||
'PORT': '',
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# On Unix systems, a value of None will cause Django to use the same
|
||||
# timezone as the operating system.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'America/Chicago'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# If you set this to False, Django will not format dates, numbers and
|
||||
# calendars according to the current locale.
|
||||
USE_L10N = True
|
||||
|
||||
# If you set this to False, Django will not use timezone-aware datetimes.
|
||||
USE_TZ = True
|
||||
|
||||
# Absolute filesystem path to the directory that will hold user-uploaded files.
|
||||
# Example: "/home/media/media.lawrence.com/media/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
|
||||
MEDIA_URL = ''
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = ''
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
STATIC_DOC_ROOT = os.path.abspath(os.path.join(SITE_ROOT, 'static'))
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = (
|
||||
STATIC_DOC_ROOT,
|
||||
)
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
)
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'tqn)wkzzoisx7kl4l&4wjr!w0o7nr_eg0+oho0$x4dp5y$gr71'
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
if DEBUG:
|
||||
MIDDLEWARE_CLASSES += ('nailgun.middleware.ExceptionLoggingMiddleware',)
|
||||
|
||||
ROOT_URLCONF = 'nailgun.urls'
|
||||
|
||||
# Python dotted path to the WSGI application used by Django's runserver.
|
||||
WSGI_APPLICATION = 'nailgun.wsgi.application'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.abspath(os.path.join(SITE_ROOT, 'templates')),
|
||||
)
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'djcelery',
|
||||
'nailgun',
|
||||
'nailgun.api',
|
||||
'nailgun.webui',
|
||||
'django_nose',
|
||||
# Uncomment the next line to enable the admin:
|
||||
# 'django.contrib.admin',
|
||||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
)
|
||||
|
||||
TEST_RUNNER = 'nailgun.testrunner.MyRunner'
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'simple': {
|
||||
'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'file': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': LOGFILE,
|
||||
'formatter': 'simple',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'level': LOGLEVEL,
|
||||
'handlers': ['file'],
|
||||
},
|
||||
}
|
||||
|
||||
# Celery settings
|
||||
import djcelery
|
||||
|
||||
djcelery.setup_loader()
|
||||
|
||||
BROKER_URL = "redis://localhost:6379/0"
|
||||
CELERY_RESULT_BACKEND = "redis"
|
||||
CELERY_IMPORTS = ("nailgun.tasks",)
|
||||
CELERY_DISABLE_RATE_LIMITS = True
|
||||
CELERY_EAGER_PROPAGATES_EXCEPTIONS = False
|
||||
|
||||
CHEF_NODES_DATABAG_NAME = "nodes"
|
||||
|
||||
PISTON_IGNORE_DUPE_MODELS = True
|
||||
|
||||
NETWORK_POOLS = {
|
||||
'public': ['172.18.0.0/16'],
|
||||
'private': ['10.1.0.0/16']
|
||||
}
|
@ -1,142 +0,0 @@
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
from celery.task import task, chord, TaskSet
|
||||
from nailgun.models import Cluster, Node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def topol_sort(graph):
|
||||
""" Depth First Traversal algorithm for sorting DAG graph.
|
||||
|
||||
Example graph: 1 depends on 4; 3 depends on 2 and 6; etc.
|
||||
Example code:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> graph = {1: [4], 2: [], 3: [2,6], 4:[2,3], 5: [], 6: [2]}
|
||||
>>> topol_sort(graph)
|
||||
[2, 6, 3, 4, 1, 5]
|
||||
|
||||
Exception is raised if there is a cycle:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
>>> graph = {1: [4], 2: [], 3: [2,6], 4:[2,3,1], 5: [], 6: [2]}
|
||||
>>> topol_sort(graph)
|
||||
...
|
||||
Exception: Graph contains cycles, processed 4 depends on 1
|
||||
|
||||
"""
|
||||
|
||||
def dfs(v):
|
||||
color[v] = "gray"
|
||||
for w in graph[v]:
|
||||
if color[w] == "black":
|
||||
continue
|
||||
elif color[w] == "gray":
|
||||
raise Exception(
|
||||
"Graph contains cycles, processed %s depends on %s" % \
|
||||
(v, w))
|
||||
dfs(w)
|
||||
color[v] = "black"
|
||||
_sorted.append(v)
|
||||
|
||||
_sorted = []
|
||||
color = {}
|
||||
for j in graph:
|
||||
color[j] = "white"
|
||||
for i in graph:
|
||||
if color[i] == "white":
|
||||
dfs(i)
|
||||
|
||||
return _sorted
|
||||
|
||||
|
||||
# This code is inspired by
|
||||
# https://github.com/NetAngels/celery-tasktree/blob/master/celery_tasktree.py
|
||||
def task_with_callbacks(func=None, **options):
|
||||
""" decorator "task with callbacks"
|
||||
|
||||
Callback or list of callbacks which go to function in "callbacks" kwarg,
|
||||
will be executed after the function, regardless of the subtask's return
|
||||
status.
|
||||
|
||||
If subtask (function) result is an object, then a property named
|
||||
"async_result" will be added to that object so that it will be possible to
|
||||
join() for that result.
|
||||
"""
|
||||
def _decorate(func):
|
||||
return task(run_with_callbacks(func), **options)
|
||||
if func:
|
||||
return _decorate(func)
|
||||
else:
|
||||
return _decorate
|
||||
|
||||
|
||||
def run_with_callbacks(func):
|
||||
"""Decorator "run with callbacks"
|
||||
|
||||
Function is useful as decorator for :meth:`run` method of tasks which are
|
||||
subclasses of generic :class:`celery.task.Task` and are expected to be used
|
||||
with callbacks.
|
||||
"""
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
callback = kwargs.pop('callback', None)
|
||||
retval = func(*args, **kwargs)
|
||||
if callback:
|
||||
retval = callback.apply_async()
|
||||
return retval
|
||||
return wrapper
|
||||
|
||||
|
||||
class TaskPool(object):
|
||||
|
||||
def __init__(self):
|
||||
self.pool = []
|
||||
|
||||
def push_task(self, func, args=None, kwargs={}):
|
||||
task = {'func': func, 'args': args, 'kwargs': kwargs}
|
||||
# TODO(mihgen): check that list of func has correct args
|
||||
self.pool.append(task)
|
||||
|
||||
@task_with_callbacks
|
||||
def _chord_task(*args):
|
||||
|
||||
if len(args) == 3:
|
||||
taskset, clbk = args[1], args[2]
|
||||
else:
|
||||
taskset, clbk = args[0], args[1]
|
||||
|
||||
logger.error("TaskPool._chord_task: args: %s" % str(args))
|
||||
logger.error("TaskPool._chord_task: args length: %s" % len(args))
|
||||
logger.error("TaskPool._chord_task: taskset: %s" % str(taskset))
|
||||
logger.error("TaskPool._chord_task: clbk: %s" % str(clbk))
|
||||
|
||||
# We have to create separate subtask that contains chord expression
|
||||
# because otherwise chord functions get applied synchronously
|
||||
return chord([
|
||||
tsk['func'].subtask(args=tsk['args'], kwargs=tsk['kwargs']) \
|
||||
for tsk in taskset])(clbk)
|
||||
|
||||
def _get_head_task(self):
|
||||
prev_task = None
|
||||
for t in reversed(self.pool):
|
||||
if isinstance(t['func'], list):
|
||||
task = self._chord_task.subtask((t['func'], prev_task))
|
||||
else:
|
||||
kwargs = t['kwargs'] or {}
|
||||
if prev_task:
|
||||
kwargs['callback'] = prev_task
|
||||
task = t['func'].subtask(args=t['args'], kwargs=kwargs)
|
||||
prev_task = task
|
||||
print "Returning head task: %s" % task
|
||||
return task
|
||||
|
||||
def apply_async(self):
|
||||
# We need only head task. When it's execution is done,
|
||||
# run_with_callbacks will call it's subtask
|
||||
async_result = self._get_head_task().apply_async()
|
||||
return async_result
|
@ -1,256 +0,0 @@
|
||||
import os
|
||||
import os.path
|
||||
import copy
|
||||
import string
|
||||
import logging
|
||||
from random import choice
|
||||
import re
|
||||
import time
|
||||
import socket
|
||||
|
||||
import json
|
||||
import paramiko
|
||||
import tarfile
|
||||
import shutil
|
||||
from django.conf import settings
|
||||
|
||||
from nailgun.models import Cluster, Node, Role, Com
|
||||
from nailgun.helpers import SshConnect, DeployManager
|
||||
from nailgun.task_helpers import task_with_callbacks, TaskPool, topol_sort
|
||||
from nailgun.exceptions import SSHError, EmptyListError, DeployError
|
||||
from nailgun.provision import ProvisionConfig
|
||||
from nailgun.provision import ProvisionFactory
|
||||
from nailgun.provision.model.profile import Profile as ProvisionProfile
|
||||
from nailgun.provision.model.node import Node as ProvisionNode
|
||||
from nailgun.provision.model.power import Power as ProvisionPower
|
||||
|
||||
from celery import current_app
|
||||
from celery.utils import LOG_LEVELS
|
||||
from celery.log import Logging
|
||||
|
||||
|
||||
current_app.conf.CELERYD_LOG_LEVEL = LOG_LEVELS[settings.CELERYLOGLEVEL]
|
||||
celery_logging = Logging(current_app)
|
||||
celery_logging.setup_logger(logfile=settings.CELERYLOGFILE)
|
||||
logger = celery_logging.get_default_logger()
|
||||
|
||||
|
||||
@task_with_callbacks
|
||||
def update_cluster_status(*args):
|
||||
# FIXME(mihgen):
|
||||
# We have to do this ugly trick because chord precedes first argument
|
||||
if isinstance(args[0], list):
|
||||
args = args[1:]
|
||||
cluster_id = args[0]
|
||||
|
||||
return cluster_id
|
||||
|
||||
|
||||
def node_set_error_status(node_id):
|
||||
node = Node.objects.get(id=node_id)
|
||||
node.status = "error"
|
||||
node.save()
|
||||
|
||||
|
||||
@task_with_callbacks
|
||||
def deploy_cluster(cluster_id):
|
||||
|
||||
deploy_manager = DeployManager(cluster_id)
|
||||
release = Cluster.objects.get(id=cluster_id).release
|
||||
logger.debug("deploy_cluster: Cluster release: %s" % release.id)
|
||||
|
||||
tree = TaskPool()
|
||||
# first element in sorted_recipes is the first recipe we have to apply
|
||||
installed = []
|
||||
logger.debug("deploy_cluster: sorted_components: %s" % \
|
||||
deploy_manager.sorted_components())
|
||||
|
||||
for component_name in deploy_manager.sorted_components():
|
||||
logger.debug("deploy_cluster: Com: %s" % component_name)
|
||||
component = Com.objects.get(
|
||||
release=release,
|
||||
name=component_name)
|
||||
roles = component.roles.all()
|
||||
nodes = Node.objects.filter(roles__in=roles, cluster__id=cluster_id)
|
||||
|
||||
taskset = []
|
||||
for node in nodes:
|
||||
logger.debug("deploy_cluster: task: node: %s com: %s" % \
|
||||
(node.id, component.name))
|
||||
bootstrap_args = [node.id, component.name]
|
||||
taskset.append({'func': bootstrap_node, 'args': bootstrap_args,
|
||||
'kwargs': {}})
|
||||
|
||||
# FIXME(mihgen): it there are no taskset items,
|
||||
# we included recipes which are not applied on nodes.
|
||||
# We have to include only recipes which are assigned to nodes
|
||||
if taskset:
|
||||
tree.push_task(taskset)
|
||||
|
||||
tree.push_task(update_cluster_status, (cluster_id,))
|
||||
res = tree.apply_async()
|
||||
return res
|
||||
|
||||
|
||||
def tcp_ping(host, port, timeout=5):
|
||||
try:
|
||||
s = socket.create_connection((str(host), int(port)), timeout)
|
||||
except socket.error:
|
||||
return False
|
||||
s.close()
|
||||
return True
|
||||
|
||||
|
||||
@task_with_callbacks
|
||||
def bootstrap_node(node_id, component_name):
|
||||
|
||||
node = Node.objects.get(id=node_id)
|
||||
|
||||
if node.status not in ["ready", "discover", "offline"]:
|
||||
raise DeployError(
|
||||
"Invalid node status '%s' - deployment aborted." \
|
||||
% node.status
|
||||
)
|
||||
|
||||
if node.status == "ready":
|
||||
logger.debug("Provisioning skipped - node %s \
|
||||
is already installed" % node_id)
|
||||
elif node.status in ["discover", "offline"]:
|
||||
logger.debug("Trying to provision node %s..." % node_id)
|
||||
_provision_node(node_id)
|
||||
logger.debug("Turning node %s status into 'deploying'" % node_id)
|
||||
node.status = "deploying"
|
||||
node.save()
|
||||
|
||||
# FIXME
|
||||
# node.ip had been got from bootstrap agent
|
||||
# there is no guarantee that installed slave node has
|
||||
# the same ip as bootstrap node had
|
||||
# it is necessary to install and launch agent on slave node
|
||||
|
||||
logger.debug("Waiting for node %s listen to %s:%s ..." \
|
||||
% (node_id, str(node.ip), "22"))
|
||||
while True:
|
||||
if tcp_ping(node.ip, 22):
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
logger.debug("Trying to connect to node %s over ssh" % node_id)
|
||||
try:
|
||||
ssh = SshConnect(node.ip, 'root', settings.PATH_TO_SSH_KEY)
|
||||
except (paramiko.AuthenticationException,
|
||||
paramiko.PasswordRequiredException,
|
||||
paramiko.SSHException):
|
||||
logger.error("Error occured while ssh connecting to node %s" % node_id)
|
||||
message = "Task %s failed:" \
|
||||
"Can't connect to IP=%s" \
|
||||
% (bootstrap_node.request.id, node.ip)
|
||||
node_set_error_status(node.id)
|
||||
raise SSHError(message)
|
||||
except Exception, error:
|
||||
message = "Task %s failed:" \
|
||||
"Error during ssh/deploy IP=%s: %s" \
|
||||
% (bootstrap_node.request.id, node.ip, str(error))
|
||||
node_set_error_status(node.id)
|
||||
raise SSHError(message)
|
||||
else:
|
||||
logger.debug("Trying to launch deploy script on node %s" % node_id)
|
||||
# Returns True if succeeded
|
||||
exit_status = ssh.run("/opt/nailgun/bin/deploy %s" % component_name)
|
||||
ssh.close()
|
||||
|
||||
# ssh.run returns True, if command executed successfully
|
||||
# FIXME(mihgen): rename it/refactor, it's unclear
|
||||
if not exit_status:
|
||||
logger.error("Error occured while deploying node %s" % node_id)
|
||||
message = "Task %s failed: " \
|
||||
"Deployment exited with non-zero exit code. IP=%s" \
|
||||
% (bootstrap_node.request.id, node.ip)
|
||||
node_set_error_status(node.id)
|
||||
raise DeployError(message)
|
||||
|
||||
logger.debug("Turning node %s status into 'ready'" % node_id)
|
||||
node.status = "ready"
|
||||
node.save()
|
||||
return exit_status
|
||||
|
||||
|
||||
def _is_node_bootstrap(node):
|
||||
ssh_user = 'root'
|
||||
ssh_key = settings.PATH_TO_BOOTSTRAP_SSH_KEY
|
||||
|
||||
logger.debug(
|
||||
"Checking if node %s is booted with bootstrap image" \
|
||||
% node.id
|
||||
)
|
||||
try:
|
||||
logger.debug(
|
||||
"Trying to establish ssh connection using bootstrap key" \
|
||||
"ip: %s key: %s user: %s" % \
|
||||
(node.ip,
|
||||
ssh_key,
|
||||
ssh_user)
|
||||
)
|
||||
ssh = SshConnect(
|
||||
node.ip,
|
||||
ssh_user,
|
||||
ssh_key
|
||||
)
|
||||
except (paramiko.AuthenticationException,
|
||||
paramiko.PasswordRequiredException):
|
||||
logger.debug("Auth error while ssh using bootstrap rsa key")
|
||||
return False
|
||||
except Exception:
|
||||
logger.debug("Unknown error while ssh using bootstrap rsa key")
|
||||
return False
|
||||
else:
|
||||
logger.debug("Ssh connection succeeded: key: %s" % \
|
||||
ssh_key)
|
||||
ssh.close()
|
||||
return True
|
||||
|
||||
|
||||
# Call to Cobbler to make node ready.
|
||||
def _provision_node(node_id):
|
||||
node = Node.objects.get(id=node_id)
|
||||
|
||||
pc = ProvisionConfig()
|
||||
pc.cn = "nailgun.provision.driver.cobbler.Cobbler"
|
||||
pc.url = settings.COBBLER_URL
|
||||
pc.user = settings.COBBLER_USER
|
||||
pc.password = settings.COBBLER_PASSWORD
|
||||
|
||||
pd = ProvisionFactory.getInstance(pc)
|
||||
|
||||
pf = ProvisionProfile(settings.COBBLER_PROFILE)
|
||||
|
||||
ndp = ProvisionPower("ssh")
|
||||
ndp.power_user = "root"
|
||||
|
||||
if _is_node_bootstrap(node):
|
||||
logger.info("Node %s seems booted with bootstrap image" % node_id)
|
||||
ndp.power_pass = "rsa:%s" % settings.PATH_TO_BOOTSTRAP_SSH_KEY
|
||||
else:
|
||||
logger.info("Node %s seems booted with real system" % node_id)
|
||||
ndp.power_pass = "rsa:%s" % settings.PATH_TO_SSH_KEY
|
||||
ndp.power_address = node.ip
|
||||
|
||||
nd = ProvisionNode(node_id)
|
||||
nd.driver = pd
|
||||
nd.mac = node.mac
|
||||
nd.profile = pf
|
||||
nd.pxe = True
|
||||
nd.kopts = ""
|
||||
nd.power = ndp
|
||||
|
||||
logger.debug(
|
||||
"Trying to save node %s into provision system: profile: %s " % \
|
||||
(node_id, pf.name)
|
||||
)
|
||||
nd.save()
|
||||
|
||||
logger.debug(
|
||||
"Trying to reboot node %s using %s in order to launch provisioning" % \
|
||||
(node_id, ndp.power_type)
|
||||
)
|
||||
nd.power_reboot()
|
@ -1,11 +0,0 @@
|
||||
from django_nose import NoseTestSuiteRunner
|
||||
from djcelery.contrib.test_runner import CeleryTestSuiteRunner
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class MyRunner(NoseTestSuiteRunner, CeleryTestSuiteRunner):
|
||||
|
||||
def setup_test_environment(self, **kwargs):
|
||||
super(MyRunner, self).setup_test_environment(**kwargs)
|
||||
# As we don't have it in production, it should not be used in tests
|
||||
settings.CELERY_EAGER_PROPAGATES_EXCEPTIONS = False
|
@ -1,472 +0,0 @@
|
||||
import simplejson as json
|
||||
import mock
|
||||
import celery
|
||||
from django import http
|
||||
from django.test import TestCase
|
||||
from django.db.models import Model
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
|
||||
from piston.emitters import Emitter
|
||||
|
||||
from nailgun import models
|
||||
from nailgun.models import Cluster
|
||||
from nailgun.models import Node
|
||||
from nailgun.models import Role
|
||||
from nailgun.models import Release
|
||||
from nailgun.models import Com
|
||||
from nailgun.models import Point
|
||||
from nailgun.models import EndPoint
|
||||
from nailgun.api import urls as api_urls
|
||||
from nailgun import tasks
|
||||
|
||||
|
||||
# monkey patch!
|
||||
def _construct_monkey(func):
|
||||
def wrapped(self=None, *args, **kwargs):
|
||||
if isinstance(self.data, Model):
|
||||
raise NotImplementedError("Don't return model from handler!")
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
Emitter.construct = _construct_monkey(Emitter.construct)
|
||||
|
||||
|
||||
class TestHandlers(TestCase):
|
||||
|
||||
fixtures = ['default_cluster']
|
||||
|
||||
def setUp(self):
|
||||
self.request = http.HttpRequest()
|
||||
|
||||
self.new_meta = {'block_device': 'new-val',
|
||||
'interfaces': 'd',
|
||||
'cpu': 'u',
|
||||
'memory': 'a'
|
||||
}
|
||||
|
||||
self.clusters = models.Cluster.objects.all()
|
||||
self.releases = models.Release.objects.all()
|
||||
self.roles = models.Role.objects.all()
|
||||
self.nodes = models.Node.objects.all()
|
||||
self.points = models.Point.objects.all()
|
||||
self.com = models.Com.objects.all()
|
||||
self.node_url = reverse('node_handler',
|
||||
kwargs={'node_id': self.nodes[0].id})
|
||||
|
||||
self.meta_json = json.dumps(self.new_meta)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_all_api_urls_500(self):
|
||||
test_urls = {}
|
||||
for pattern in api_urls.urlpatterns:
|
||||
test_urls[pattern.name] = pattern.callback.handler.allowed_methods
|
||||
|
||||
url_ids = {
|
||||
'cluster_handler': {'cluster_id': self.clusters[0].id},
|
||||
'node_handler': {'node_id': 'A' * 12},
|
||||
'task_handler': {'task_id': 'a' * 36},
|
||||
'network_handler': {'network_id': 1},
|
||||
'release_handler': {'release_id': self.releases[0].id},
|
||||
'role_handler': {'role_id': self.roles[0].id},
|
||||
'endpoint_handler': {'node_id': self.nodes[0].id,
|
||||
'component_name': 'abc'},
|
||||
'point_handler': {'point_id': self.points[0].id},
|
||||
'com_handler': {'component_id': self.com[0].id},
|
||||
'node_role_available': {
|
||||
'node_id': 'A' * 12,
|
||||
'role_id': self.roles[0].id
|
||||
},
|
||||
'deployment_type_collection_handler': {
|
||||
'cluster_id': self.clusters[0].id
|
||||
},
|
||||
}
|
||||
|
||||
skip_urls = [
|
||||
'task_handler'
|
||||
]
|
||||
|
||||
for url, methods in test_urls.iteritems():
|
||||
if url in skip_urls:
|
||||
continue
|
||||
kw = {}
|
||||
if url in url_ids:
|
||||
kw = url_ids[url]
|
||||
|
||||
if 'GET' in methods:
|
||||
test_url = reverse(url, kwargs=kw)
|
||||
resp = self.client.get(test_url)
|
||||
self.assertNotEqual(str(resp.status_code)[0], '5')
|
||||
|
||||
def test_cluster_creation(self):
|
||||
yet_another_cluster_name = 'Yet another cluster'
|
||||
resp = self.client.post(
|
||||
reverse('cluster_collection_handler'),
|
||||
json.dumps({
|
||||
'name': yet_another_cluster_name,
|
||||
'release': 1,
|
||||
'nodes': [self.nodes[0].id],
|
||||
}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
clusters_from_db = Cluster.objects.filter(
|
||||
name=yet_another_cluster_name
|
||||
)
|
||||
self.assertEquals(len(clusters_from_db), 1)
|
||||
cluster = clusters_from_db[0]
|
||||
self.assertEquals(cluster.nodes.all()[0].id, self.nodes[0].id)
|
||||
self.assertEquals(len(cluster.release.networks.all()), 3)
|
||||
# test delete
|
||||
resp = self.client.delete(
|
||||
reverse('cluster_handler', kwargs={'cluster_id': cluster.id}),
|
||||
"",
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 204)
|
||||
|
||||
def test_cluster_update(self):
|
||||
updated_name = 'Updated cluster'
|
||||
clusters_before = len(Cluster.objects.all())
|
||||
|
||||
resp = self.client.put(
|
||||
reverse('cluster_handler',
|
||||
kwargs={'cluster_id': self.clusters[0].id}),
|
||||
json.dumps({'name': updated_name}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
clusters_from_db = Cluster.objects.filter(name=updated_name)
|
||||
self.assertEquals(len(clusters_from_db), 1)
|
||||
self.assertEquals(clusters_from_db[0].name, updated_name)
|
||||
|
||||
clusters_after = len(Cluster.objects.all())
|
||||
self.assertEquals(clusters_before, clusters_after)
|
||||
|
||||
def test_cluster_node_list_update(self):
|
||||
resp = self.client.put(
|
||||
reverse('cluster_handler', kwargs={'cluster_id': 1}),
|
||||
json.dumps({'nodes': [self.nodes[0].id]}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
nodes_from_db = Node.objects.filter(cluster_id=1)
|
||||
self.assertEquals(len(nodes_from_db), 1)
|
||||
self.assertEquals(nodes_from_db[0].id, self.nodes[0].id)
|
||||
|
||||
resp = self.client.put(
|
||||
reverse('cluster_handler', kwargs={'cluster_id': 1}),
|
||||
json.dumps({'nodes': [self.nodes[1].id]}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
nodes_from_db = Node.objects.filter(cluster_id=1)
|
||||
self.assertEquals(len(nodes_from_db), 1)
|
||||
self.assertEquals(nodes_from_db[0].id, self.nodes[1].id)
|
||||
|
||||
def test_node_creation(self):
|
||||
node_id = '080000000003'
|
||||
|
||||
resp = self.client.post(
|
||||
reverse('node_collection_handler'),
|
||||
json.dumps({'id': node_id}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
nodes_from_db = Node.objects.filter(id=node_id)
|
||||
self.assertEquals(len(nodes_from_db), 1)
|
||||
|
||||
# test delete
|
||||
resp = self.client.delete(
|
||||
reverse('node_handler', kwargs={'node_id': node_id}),
|
||||
"",
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 204)
|
||||
|
||||
def test_node_creation_using_put(self):
|
||||
node_id = '080000000002'
|
||||
|
||||
resp = self.client.put(
|
||||
reverse('node_handler', kwargs={'node_id': node_id}),
|
||||
json.dumps({}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
nodes_from_db = Node.objects.filter(id=node_id)
|
||||
self.assertEquals(len(nodes_from_db), 1)
|
||||
|
||||
def test_node_valid_metadata_gets_updated(self):
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({'metadata': self.new_meta}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
nodes_from_db = Node.objects.filter(id=self.nodes[0].id)
|
||||
self.assertEquals(len(nodes_from_db), 1)
|
||||
self.assertEquals(nodes_from_db[0].metadata, self.new_meta)
|
||||
|
||||
def test_node_valid_status_gets_updated(self):
|
||||
params = {'status': 'error'}
|
||||
resp = self.client.put(self.node_url, json.dumps(params),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
def test_node_valid_list_of_new_roles_gets_updated(self):
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({
|
||||
'new_roles': [self.roles[1].id],
|
||||
'redeployment_needed': True
|
||||
}), "application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
node_from_db = Node.objects.get(id=self.nodes[0].id)
|
||||
self.assertEquals(node_from_db.redeployment_needed, True)
|
||||
self.assertEquals(len(node_from_db.roles.all()), 1)
|
||||
self.assertEquals(len(node_from_db.new_roles.all()), 1)
|
||||
self.assertEquals(node_from_db.new_roles.all()[0].id,
|
||||
self.roles[1].id)
|
||||
|
||||
def test_put_returns_400_if_no_body(self):
|
||||
resp = self.client.put(self.node_url, None, "application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
def test_put_returns_400_if_wrong_content_type(self):
|
||||
params = {'metadata': self.meta_json}
|
||||
resp = self.client.put(self.node_url, json.dumps(params), "plain/text")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
def test_put_returns_400_if_wrong_status(self):
|
||||
params = {'status': 'invalid_status'}
|
||||
resp = self.client.put(self.node_url, json.dumps(params),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
def test_put_returns_400_if_no_block_device_attr(self):
|
||||
old_meta = self.nodes[0].metadata
|
||||
new_meta = self.new_meta.copy()
|
||||
del new_meta['block_device']
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({'metadata': new_meta}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
node_from_db = Node.objects.get(id=self.nodes[0].id)
|
||||
self.assertEquals(node_from_db.metadata, old_meta)
|
||||
|
||||
def test_put_returns_400_if_no_interfaces_attr(self):
|
||||
old_meta = self.nodes[0].metadata
|
||||
new_meta = self.new_meta.copy()
|
||||
del new_meta['interfaces']
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({'metadata': new_meta}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
node_from_db = Node.objects.get(id=self.nodes[0].id)
|
||||
self.assertEquals(node_from_db.metadata, old_meta)
|
||||
|
||||
def test_put_returns_400_if_interfaces_empty(self):
|
||||
old_meta = self.nodes[0].metadata
|
||||
new_meta = self.new_meta.copy()
|
||||
new_meta['interfaces'] = ""
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({'metadata': new_meta}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
node_from_db = Node.objects.get(id=self.nodes[0].id)
|
||||
self.assertEquals(node_from_db.metadata, old_meta)
|
||||
|
||||
def test_put_returns_400_if_no_cpu_attr(self):
|
||||
old_meta = self.nodes[0].metadata
|
||||
new_meta = self.new_meta.copy()
|
||||
del new_meta['cpu']
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({'metadata': new_meta}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
node_from_db = Node.objects.get(id=self.nodes[0].id)
|
||||
self.assertEquals(node_from_db.metadata, old_meta)
|
||||
|
||||
def test_put_returns_400_if_no_memory_attr(self):
|
||||
old_meta = self.nodes[0].metadata
|
||||
new_meta = self.new_meta.copy()
|
||||
del new_meta['memory']
|
||||
resp = self.client.put(self.node_url,
|
||||
json.dumps({'metadata': new_meta}),
|
||||
"application/json")
|
||||
self.assertEquals(resp.status_code, 400)
|
||||
|
||||
node_from_db = Node.objects.get(id=self.nodes[0].id)
|
||||
self.assertEquals(node_from_db.metadata, old_meta)
|
||||
|
||||
# (mihgen): Disabled - we don't have attributes anymore
|
||||
#def test_attribute_create(self):
|
||||
#resp = self.client.put(
|
||||
#reverse('attribute_collection_handler'),
|
||||
#json.dumps({
|
||||
#'attribute': {'a': 'av'},
|
||||
#'cookbook': 'cook_name',
|
||||
#'version': '0.1',
|
||||
#}), "application/json"
|
||||
#)
|
||||
#self.assertEquals(resp.status_code, 200)
|
||||
#self.assertEquals(resp.content, '1')
|
||||
|
||||
#def test_attribute_update(self):
|
||||
#resp = self.client.put(
|
||||
#reverse('attribute_collection_handler'),
|
||||
#json.dumps({
|
||||
#'attribute': {'a': 'b'},
|
||||
#'cookbook': 'cook',
|
||||
#'version': '0.1',
|
||||
#}), "application/json"
|
||||
#)
|
||||
#self.assertEquals(resp.status_code, 200)
|
||||
#self.assertEquals(resp.content, '1')
|
||||
#resp = self.client.put(
|
||||
#reverse('attribute_collection_handler'),
|
||||
#json.dumps({
|
||||
#'attribute': {'a': 'new'},
|
||||
#'cookbook': 'cook',
|
||||
#'version': '0.1',
|
||||
#}), "application/json"
|
||||
#)
|
||||
#self.assertEquals(resp.status_code, 200)
|
||||
#self.assertEquals(resp.content, '1')
|
||||
#attrs = Attribute.objects.all()
|
||||
#self.assertEquals(len(attrs), 1)
|
||||
#self.assertEquals(attrs[0].attribute, {'a': 'new'})
|
||||
|
||||
def test_role_create(self):
|
||||
role_name = 'My role 3'
|
||||
role_release = self.releases[0].id
|
||||
role_components = [c.name for c in self.com]
|
||||
resp = self.client.post(
|
||||
reverse('role_collection_handler'),
|
||||
json.dumps({
|
||||
'name': role_name,
|
||||
'release': role_release,
|
||||
'components': role_components
|
||||
}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
roles_from_db = Role.objects.filter(name=role_name)
|
||||
self.assertEquals(len(roles_from_db), 1)
|
||||
components = [c.name for c in roles_from_db[0].components.all()]
|
||||
self.assertEquals(set(role_components), set(components))
|
||||
|
||||
@mock.patch('nailgun.tasks.deploy_cluster', celery.task.task(lambda: True))
|
||||
def test_jsons_created_for_chef_solo(self):
|
||||
url = reverse('cluster_changes_handler', kwargs={'cluster_id': 1})
|
||||
resp = self.client.put(url)
|
||||
|
||||
self.assertEquals(resp.status_code, 202)
|
||||
resp_json = json.loads(resp.content)
|
||||
self.assertEquals(len(resp_json['task_id']), 36)
|
||||
self.assertFalse(resp_json.get('error'))
|
||||
|
||||
def test_release_create(self):
|
||||
release_name = "OpenStack"
|
||||
release_version = "1.0.0"
|
||||
release_description = "This is test release"
|
||||
release_roles = [{
|
||||
"name": "compute",
|
||||
"recipes": [
|
||||
"nova::compute@0.1.0",
|
||||
"nova::monitor@0.1.0"
|
||||
]
|
||||
}, {
|
||||
"name": "controller",
|
||||
"recipes": [
|
||||
"cookbook::recipe@2.1"
|
||||
]
|
||||
}
|
||||
]
|
||||
resp = self.client.post(
|
||||
reverse('release_collection_handler'),
|
||||
json.dumps({
|
||||
'name': release_name,
|
||||
'version': release_version,
|
||||
'description': release_description,
|
||||
'roles': release_roles,
|
||||
'networks_metadata': [
|
||||
{"name": "floating", "access": "public"},
|
||||
{"name": "fixed", "access": "private"},
|
||||
{"name": "storage", "access": "private"}
|
||||
]
|
||||
}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
|
||||
# test duplicate release
|
||||
resp = self.client.post(
|
||||
reverse('release_collection_handler'),
|
||||
json.dumps({
|
||||
'name': release_name,
|
||||
'version': release_version,
|
||||
'description': release_description,
|
||||
'roles': release_roles,
|
||||
'networks_metadata': [
|
||||
{"name": "fixed", "access": "private"}
|
||||
]
|
||||
}),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 409)
|
||||
|
||||
release_from_db = Release.objects.filter(
|
||||
name=release_name,
|
||||
version=release_version,
|
||||
description=release_description
|
||||
)
|
||||
self.assertEquals(len(release_from_db), 1)
|
||||
|
||||
roles = []
|
||||
for rl in release_from_db[0].roles.all():
|
||||
roles.append({
|
||||
'name': rl.name,
|
||||
'recipes': [i.recipe for i in rl.recipes.all()]
|
||||
})
|
||||
for a, b in zip(sorted(roles), sorted(release_roles)):
|
||||
self.assertEquals(a, b)
|
||||
|
||||
def test_network_create(self):
|
||||
network_data = {
|
||||
"name": "test_network",
|
||||
"network": "10.0.0.0/24",
|
||||
"range_l": "10.0.0.5",
|
||||
"range_h": "10.0.0.10",
|
||||
"gateway": "10.0.0.1",
|
||||
"vlan_id": 100,
|
||||
"release": 1,
|
||||
"access": "public"
|
||||
}
|
||||
resp = self.client.post(
|
||||
reverse('network_collection_handler'),
|
||||
json.dumps(network_data),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 200)
|
||||
resp = self.client.post(
|
||||
reverse('network_collection_handler'),
|
||||
json.dumps(network_data),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEquals(resp.status_code, 409)
|
||||
network_data["network"] = "test_fail"
|
||||
resp = self.client.post(
|
||||
reverse('network_collection_handler'),
|
||||
json.dumps(network_data),
|
||||
"application/json"
|
||||
)
|
||||
self.assertEqual(resp.status_code, 400)
|
@ -1,24 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from nailgun.models import Node, Role
|
||||
|
||||
|
||||
class TestNodeModel(TestCase):
|
||||
|
||||
def test_creating_new_node_and_save_to_db(self):
|
||||
node = Node()
|
||||
node.id = "080000000001"
|
||||
node.cluster_id = 1
|
||||
node.name = "0-test_server.name.com"
|
||||
node.metadata = {'metakey': 'metavalue'}
|
||||
|
||||
node.save()
|
||||
|
||||
all_nodes = Node.objects.all()
|
||||
self.assertEquals(len(all_nodes), 1)
|
||||
self.assertEquals(all_nodes[0], node)
|
||||
|
||||
self.assertEquals(all_nodes[0].name, "0-test_server.name.com")
|
||||
self.assertEquals(all_nodes[0].cluster_id, 1)
|
||||
self.assertEquals(all_nodes[0].metadata,
|
||||
{'metakey': 'metavalue'})
|
@ -1,9 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestSampleEnvironmentFixtureLoad(TestCase):
|
||||
|
||||
fixtures = ['sample_environment']
|
||||
|
||||
def test(self):
|
||||
pass
|
@ -1,261 +0,0 @@
|
||||
import os
|
||||
|
||||
import json
|
||||
import mock
|
||||
from mock import call
|
||||
from django.test import TestCase
|
||||
from django.db.models import Model
|
||||
from django.conf import settings
|
||||
from celery.task import task
|
||||
|
||||
from nailgun import tasks
|
||||
from nailgun import models
|
||||
from nailgun import exceptions
|
||||
from nailgun import task_helpers
|
||||
|
||||
|
||||
class TestTasks(TestCase):
|
||||
|
||||
fixtures = ['default_cluster']
|
||||
|
||||
def setUp(self):
|
||||
self.cluster = models.Cluster.objects.get(pk=1)
|
||||
self.nodes = models.Node.objects.all()
|
||||
self.node = self.nodes[0]
|
||||
self.components = models.Com.objects.all()
|
||||
self.component = self.components[0]
|
||||
self.roles = models.Role.objects.all()
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@mock.patch('nailgun.tasks.tcp_ping')
|
||||
@mock.patch('nailgun.tasks.SshConnect')
|
||||
@mock.patch('nailgun.tasks._provision_node')
|
||||
def test_bootstrap_node(self, pn_mock, ssh_mock, tp_mock):
|
||||
ssh = ssh_mock.return_value
|
||||
ssh.run.return_value = True
|
||||
pn_mock.return_value = True
|
||||
tp_mock.return_value = True
|
||||
|
||||
self.assertEquals(self.node.status, "ready")
|
||||
res = tasks.bootstrap_node.delay(self.node.id, self.component.name)
|
||||
self.assertEquals(res.state, "SUCCESS")
|
||||
node = models.Node.objects.get(id=self.node.id)
|
||||
self.assertEquals(node.status, "ready")
|
||||
|
||||
@mock.patch('nailgun.tasks.tcp_ping')
|
||||
@mock.patch('nailgun.tasks.SshConnect')
|
||||
def test_bootstrap_calls_provision_and_ssh(self, ssh_mock, tp_mock):
|
||||
ssh = ssh_mock.return_value
|
||||
ssh.run = mock.MagicMock(return_value=True)
|
||||
tp_mock.return_value = True
|
||||
self.node.status = "discover"
|
||||
self.node.save()
|
||||
|
||||
tasks._provision_node = mock.MagicMock(return_value=None)
|
||||
tasks.bootstrap_node(self.node.id, self.component.name)
|
||||
self.assertEquals(tasks._provision_node.call_args_list,
|
||||
[call(self.node.id)])
|
||||
self.assertEquals(ssh.run.call_args_list,
|
||||
[call('/opt/nailgun/bin/deploy %s' % self.component.name)])
|
||||
|
||||
@mock.patch('nailgun.tasks.tcp_ping')
|
||||
@mock.patch('nailgun.tasks.SshConnect')
|
||||
def test_bootstrap_does_not_call_provision(self, ssh_mock, tp_mock):
|
||||
ssh = ssh_mock.return_value
|
||||
ssh.run.return_value = True
|
||||
tp_mock.return_value = True
|
||||
tasks._provision_node = mock.MagicMock(return_value=None)
|
||||
|
||||
tasks.bootstrap_node(self.node.id, self.component.name)
|
||||
self.assertEquals(tasks._provision_node.call_args_list, [])
|
||||
|
||||
@mock.patch('nailgun.tasks.tcp_ping')
|
||||
@mock.patch('nailgun.tasks.SshConnect')
|
||||
@mock.patch('nailgun.tasks._provision_node')
|
||||
def test_bootstrap_raises_deploy_error(self, pn_mock, ssh_mock, tp_mock):
|
||||
ssh = ssh_mock.return_value
|
||||
ssh.run.return_value = False
|
||||
pn_mock.return_value = True
|
||||
tp_mock.return_value = True
|
||||
|
||||
with self.assertRaises(exceptions.DeployError):
|
||||
tasks.bootstrap_node(self.node.id, self.component.name)
|
||||
|
||||
@mock.patch('nailgun.tasks.tcp_ping')
|
||||
@mock.patch('nailgun.tasks.SshConnect')
|
||||
@mock.patch('nailgun.tasks._provision_node')
|
||||
def test_bootstrap_puts_error_in_task(self, pn_mock, ssh_mock, tp_mock):
|
||||
ssh = ssh_mock.return_value
|
||||
ssh.run.return_value = False
|
||||
pn_mock.return_value = True
|
||||
tp_mock.return_value = True
|
||||
|
||||
self.assertEquals(self.node.status, "ready")
|
||||
res = tasks.bootstrap_node.delay(self.node.id, self.component.name)
|
||||
self.assertEquals(res.state, "FAILURE")
|
||||
self.assertIsInstance(res.result, exceptions.DeployError)
|
||||
self.assertTrue(res.ready)
|
||||
node = models.Node.objects.get(id=self.node.id)
|
||||
self.assertEquals(node.status, "error")
|
||||
|
||||
@mock.patch('nailgun.tasks.TaskPool')
|
||||
def test_one_recipe_deploy_cluster(self, tp):
|
||||
tasks.deploy_cluster(self.cluster.id)
|
||||
expected = [call()]
|
||||
for node in self.cluster.nodes.all():
|
||||
for role in node.roles.all():
|
||||
for component in role.components.all():
|
||||
expected.append(call().push_task([{
|
||||
'args': [node.id, component.name],
|
||||
'func': tasks.bootstrap_node,
|
||||
'kwargs': {}
|
||||
}]))
|
||||
expected.append(call().push_task(tasks.update_cluster_status,
|
||||
(self.cluster.id,)))
|
||||
expected.append(call().apply_async())
|
||||
self.assertEquals(tasks.TaskPool.mock_calls, expected)
|
||||
|
||||
# FIXME(vkramskikh): recipe test, rework using components and points
|
||||
# @mock.patch('nailgun.tasks.TaskPool')
|
||||
# def test_deploy_cluster_with_recipe_deps(self, tp):
|
||||
# # 0: 1,2; 1: 2; 2: ; 3: 2
|
||||
# # Rigth order: 2,1,0,3
|
||||
# rcps = [models.Recipe() for x in range(4)]
|
||||
# for i, rec in enumerate(rcps):
|
||||
# rec.recipe = 'cookbook::recipe%s@0.1' % i
|
||||
# rec.save()
|
||||
#
|
||||
# rcps[0].depends = [rcps[1], rcps[2]]
|
||||
# rcps[1].depends = [rcps[2]]
|
||||
# rcps[2].depends = []
|
||||
# rcps[3].depends = [rcps[2]]
|
||||
# map(lambda r: r.save(), rcps)
|
||||
#
|
||||
# roles = [models.Role() for x in range(3)]
|
||||
# for i, role in enumerate(roles):
|
||||
# role.name = "Role%s" % i
|
||||
# role.save()
|
||||
#
|
||||
# roles[0].recipes = [rcps[0], rcps[2]]
|
||||
# roles[1].recipes = [rcps[3]]
|
||||
# roles[2].recipes = [rcps[1]]
|
||||
# map(lambda r: r.save(), roles)
|
||||
#
|
||||
# nodes = [models.Node() for x in range(2)]
|
||||
# for i, node in enumerate(nodes):
|
||||
# node.name = "Node-%s" % i
|
||||
# node.id = "FF000000000%s" % i
|
||||
# node.ip = "127.0.0.%s" % i
|
||||
# node.cluster_id = 1
|
||||
# node.save()
|
||||
# nodes[0].roles = [roles[0]]
|
||||
# nodes[1].roles = [roles[1], roles[2]]
|
||||
#
|
||||
# tasks.deploy_cluster('1')
|
||||
# expected = [
|
||||
# # init
|
||||
# call(),
|
||||
# # first recipe, no deps, defined in setUp
|
||||
# call().push_task(tasks.create_solo, ('1', self.recipe.id)),
|
||||
# call().push_task([{'args': [self.node.id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}}]),
|
||||
# # Applying in order 2-> 1-> 0-> 3
|
||||
# call().push_task(tasks.create_solo, ('1', rcps[2].id)),
|
||||
# call().push_task([{'args': [nodes[0].id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}}]),
|
||||
# call().push_task(tasks.create_solo, ('1', rcps[1].id)),
|
||||
# call().push_task([{'args': [nodes[1].id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}}]),
|
||||
# call().push_task(tasks.create_solo, ('1', rcps[0].id)),
|
||||
# call().push_task([{'args': [nodes[0].id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}}]),
|
||||
# call().push_task(tasks.create_solo, ('1', rcps[3].id)),
|
||||
# call().push_task([{'args': [nodes[1].id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}}]),
|
||||
# # Last task for chord to succeed
|
||||
# call().push_task(tasks.update_cluster_status, ('1',)),
|
||||
# call().apply_async()
|
||||
# ]
|
||||
# self.assertEquals(tasks.TaskPool.mock_calls, expected)
|
||||
|
||||
# FIXME(vkramskikh): recipe test, rework using components and points
|
||||
# def test_deploy_cluster_error_when_recipe_not_in_cluster(self):
|
||||
# rcps = [models.Recipe() for x in range(4)]
|
||||
# for i, rec in enumerate(rcps):
|
||||
# rec.recipe = 'cookbook::recipe%s@0.1' % i
|
||||
# rec.save()
|
||||
# rcps[0].depends = [rcps[1], rcps[2]]
|
||||
# rcps[1].depends = [rcps[2]]
|
||||
# rcps[2].depends = [rcps[3]]
|
||||
# rcps[3].depends = []
|
||||
# map(lambda r: r.save(), rcps)
|
||||
#
|
||||
# roles = [models.Role() for x in range(3)]
|
||||
# for i, role in enumerate(roles):
|
||||
# role.name = "Role%s" % i
|
||||
# role.save()
|
||||
#
|
||||
# roles[0].recipes = [rcps[0], rcps[3]]
|
||||
# roles[1].recipes = [rcps[2]]
|
||||
# map(lambda r: r.save(), roles)
|
||||
# self.node.roles = roles
|
||||
# self.node.save()
|
||||
#
|
||||
# graph = {}
|
||||
# for recipe in models.Recipe.objects.filter(
|
||||
# recipe__in=DeployGenerator.recipes(1)):
|
||||
# graph[recipe.recipe] = [r.recipe for r in recipe.depends.all()]
|
||||
#
|
||||
# self.assertRaises(exceptions.DeployError, tasks.deploy_cluster, '1')
|
||||
|
||||
@mock.patch('nailgun.tasks.TaskPool')
|
||||
def test_deploy_cluster_takes_right_cluster(self, tp):
|
||||
node = models.Node()
|
||||
node.id = "010000000007"
|
||||
node.ip = "127.0.0.1"
|
||||
# It will be node from other cluster
|
||||
node.cluster_id = 2
|
||||
node.save()
|
||||
node.roles = [self.roles[0]]
|
||||
node.save()
|
||||
|
||||
tasks.deploy_cluster(self.cluster.id)
|
||||
expected = [call()]
|
||||
for node in self.cluster.nodes.all():
|
||||
for role in node.roles.all():
|
||||
for component in role.components.all():
|
||||
expected.append(call().push_task([{
|
||||
'args': [node.id, component.name],
|
||||
'func': tasks.bootstrap_node,
|
||||
'kwargs': {}
|
||||
}]))
|
||||
expected.append(call().push_task(tasks.update_cluster_status,
|
||||
(self.cluster.id,)))
|
||||
expected.append(call().apply_async())
|
||||
self.assertEquals(tasks.TaskPool.mock_calls, expected)
|
||||
|
||||
# FIXME(vkramskikh): recipe test, rework using components
|
||||
# def test_deploy_cluster_nodes_with_same_recipes_generates_group(self, tp):
|
||||
# # Adding second node with same recipes/roles
|
||||
# node = models.Node()
|
||||
# node.id = "FFF000000007"
|
||||
# node.ip = "127.0.0.1"
|
||||
# node.cluster_id = 1
|
||||
# node.save()
|
||||
# node.roles = [self.role]
|
||||
# node.save()
|
||||
#
|
||||
# tasks.deploy_cluster('1')
|
||||
# expected = [
|
||||
# call(),
|
||||
# call().push_task(tasks.create_solo, ('1', self.recipe.id)),
|
||||
# call().push_task([{'args': [self.node.id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}},
|
||||
# {'args': [node.id, self.component.name],
|
||||
# 'func': tasks.bootstrap_node, 'kwargs': {}}]),
|
||||
# call().push_task(tasks.update_cluster_status, ('1',)),
|
||||
# call().apply_async()
|
||||
# ]
|
||||
#
|
@ -1,10 +0,0 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
(r'^api/', include('nailgun.api.urls')),
|
||||
(r'^', include('nailgun.webui.urls')),
|
||||
)
|
@ -1 +0,0 @@
|
||||
VENV = None
|
@ -1,14 +0,0 @@
|
||||
from django.conf.urls import patterns, include, url
|
||||
from django.conf import settings
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
# from django.contrib import admin
|
||||
# admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^$', 'django.views.static.serve',
|
||||
{
|
||||
'document_root': settings.STATIC_DOC_ROOT,
|
||||
'path': 'index.html'
|
||||
}, name='index'),
|
||||
)
|
@ -1,46 +0,0 @@
|
||||
"""
|
||||
WSGI config for ngui project.
|
||||
|
||||
This module contains the WSGI application used by Django's development server
|
||||
and any production WSGI deployments. It should expose a module-level variable
|
||||
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||
this application via the ``WSGI_APPLICATION`` setting.
|
||||
|
||||
Usually you will have the standard Django WSGI application here, but it also
|
||||
might make sense to replace the whole Django WSGI application with a custom one
|
||||
that later delegates to the Django one. For example, you could introduce WSGI
|
||||
middleware here, or combine a Django application with an application of another
|
||||
framework.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import site
|
||||
import nailgun.venv
|
||||
|
||||
if nailgun.venv.VENV:
|
||||
prev_sys_path = list(sys.path)
|
||||
site.addsitedir(nailgun.venv.VENV)
|
||||
|
||||
new_sys_path = []
|
||||
for item in list(sys.path):
|
||||
if item not in prev_sys_path:
|
||||
new_sys_path.append(item)
|
||||
sys.path.remove(item)
|
||||
sys.path[:0] = new_sys_path
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "nailgun.settings")
|
||||
|
||||
import monitor
|
||||
monitor.start(interval=1.0)
|
||||
|
||||
# This application object is used by any WSGI server configured to use this
|
||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||
# setting points here.
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
application = get_wsgi_application()
|
||||
|
||||
# Apply WSGI middleware here.
|
||||
# from helloworld.wsgi import HelloWorldApplication
|
||||
# application = HelloWorldApplication(application)
|
@ -1,80 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
function usage {
|
||||
echo "Usage: $0 [OPTION]..."
|
||||
echo "Run tests"
|
||||
echo ""
|
||||
echo " -p, --pep8 Just run PEP8 and HACKING compliance check"
|
||||
echo " -x, --xunit Generate reports (useful in Jenkins environment)"
|
||||
echo " -P, --no-pep8 Don't run static code checks"
|
||||
echo " -c, --clean Only clean *.log, *.json, *.pyc, *.pid files, doesn't run tests"
|
||||
echo " -h, --help Print this usage message"
|
||||
echo ""
|
||||
echo "By default it runs tests and pep8 check."
|
||||
exit
|
||||
}
|
||||
|
||||
function process_option {
|
||||
case "$1" in
|
||||
-h|--help) usage;;
|
||||
-p|--pep8) just_pep8=1;;
|
||||
-P|--no-pep8) no_pep8=1;;
|
||||
-x|--xunit) xunit=1;;
|
||||
-c|--clean) clean=1;;
|
||||
-*) noseopts="$noseopts $1";;
|
||||
*) noseargs="$noseargs $1"
|
||||
esac
|
||||
}
|
||||
|
||||
just_pep8=0
|
||||
no_pep8=0
|
||||
xunit=0
|
||||
clean=0
|
||||
noseargs=
|
||||
noseopts=
|
||||
|
||||
for arg in "$@"; do
|
||||
process_option $arg
|
||||
done
|
||||
|
||||
function clean {
|
||||
echo "cleaning *.pyc, *.json, *.log, *.pid files"
|
||||
find . -type f -name "*.pyc" -delete
|
||||
rm -f *.json
|
||||
rm -f *.log
|
||||
rm -f *.pid
|
||||
}
|
||||
|
||||
if [ $clean -eq 1 ]; then
|
||||
clean
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If enabled, tell nose to create xunit report
|
||||
if [ $xunit -eq 1 ]; then
|
||||
noseopts="--with-xunit"
|
||||
fi
|
||||
|
||||
function run_pep8 {
|
||||
pep8 --show-source --show-pep8 --count . || return 1
|
||||
echo "PEP8 check passed successfully."
|
||||
}
|
||||
|
||||
if [ $just_pep8 -eq 1 ]; then
|
||||
run_pep8 || exit 1
|
||||
exit
|
||||
fi
|
||||
|
||||
function run_tests {
|
||||
clean
|
||||
[ -z "$noseargs" ] && test_args=nailgun || test_args="$noseargs"
|
||||
python manage.py test $noseopts $test_args
|
||||
}
|
||||
|
||||
run_tests || exit 1
|
||||
|
||||
if [ -z "$noseargs" ]; then
|
||||
if [ $no_pep8 -eq 0 ]; then
|
||||
run_pep8
|
||||
fi
|
||||
fi
|
9
nailgun/settings.py
Normal file
@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
DATABASE_PATH = 'nailgun.sqlite'
|
||||
DATABASE_ENGINE = 'sqlite:///%s' % DATABASE_PATH
|
||||
|
||||
NETWORK_POOLS = {
|
||||
'public': ['172.18.0.0/16'],
|
||||
'private': ['10.1.0.0/16']
|
||||
}
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |