Add documentation and remove some temporary changes
diff --git a/containers/xos/Dockerfile b/containers/xos/Dockerfile
index b5064ae..d17b2a2 100644
--- a/containers/xos/Dockerfile
+++ b/containers/xos/Dockerfile
@@ -93,3 +93,11 @@
# Define default command.
CMD update-ca-certificates && python /opt/xos/manage.py runserver 0.0.0.0:8000 --insecure --makemigrations
+
+# for OpenVPN
+RUN mkdir -p /opt/openvpn
+RUN chmod 777 /opt/openvpn
+RUN git clone https://github.com/OpenVPN/easy-rsa.git /opt/openvpn
+RUN git -C /opt/openvpn pull origin master
+RUN echo 'set_var EASYRSA "/opt/openvpn/easyrsa3"' | tee /opt/openvpn/vars
+RUN echo 'set_var EASYRSA_BATCH "true"' | tee -a /opt/openvpn/vars
diff --git a/xos/configurations/devel/Makefile b/xos/configurations/devel/Makefile
index 2aaec54..1e650f3 100644
--- a/xos/configurations/devel/Makefile
+++ b/xos/configurations/devel/Makefile
@@ -46,10 +46,3 @@
rebuild_synchronizer:
make -C ../../../containers/synchronizer
-
-cleanup_docker: rm
- sudo docker rm -v $(docker ps -a -q -f status=exited) || true
- docker rm -v $(docker ps -a -q -f status=exited) || true
- sudo docker rmi $(docker images -qf "dangling=true") || true
- docker rmi $(docker images -qf "dangling=true") || true
- sudo docker run -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker:/var/lib/docker --rm martin/docker-cleanup-volumes || true
diff --git a/xos/configurations/devel/docker-compose.yml b/xos/configurations/devel/docker-compose.yml
index d7d3b2a..018193d 100644
--- a/xos/configurations/devel/docker-compose.yml
+++ b/xos/configurations/devel/docker-compose.yml
@@ -17,32 +17,6 @@
- ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
- ./images:/opt/xos/images:ro
-xos_synchronizer_vpn:
- image: xosproject/xos-synchronizer-openstack
- command: bash -c "sleep 120 ; python /opt/xos/synchronizers/vpn/vpn-synchronizer.py -C /opt/xos/synchronizers/vpn/vpn_config"
- labels:
- org.xosproject.kind: synchronizer
- org.xosproject.target: vpn
- links:
- - xos_db
- extra_hosts:
- - ctl:${MYIP}
- volumes:
- - ../setup/id_rsa:/opt/xos/synchronizers/vpn/vpn_private_key:ro # private key
- volumes_from:
- - xos_synchronizer_vpn_data:rw
-
-xos_synchronizer_vpn_data:
- image: xosproject/xos-synchronizer-openstack
- labels:
- org.xosproject.kind: synchronizer
- org.xosproject.target: vpn
- links:
- - xos_db
- extra_hosts:
- - ctl:${MYIP}
- volumes:
- - /opt/openvpn
# FUTURE
#xos_swarm_synchronizer:
# image: xosproject/xos-swarm-synchronizer
diff --git a/xos/configurations/frontend/Makefile b/xos/configurations/frontend/Makefile
index 3de63b8..2b46441 100644
--- a/xos/configurations/frontend/Makefile
+++ b/xos/configurations/frontend/Makefile
@@ -32,8 +32,3 @@
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/mocks/cord.yaml
sudo docker exec frontend_xos_1 cp /opt/xos/configurations/cord/xos_cord_config /opt/xos/xos_configuration/
sudo docker exec frontend_xos_1 touch /opt/xos/xos/settings.py
-
-cleanup_docker: rm
- sudo docker rm -v $(docker ps -a -q -f status=exited) || true
- sudo docker rmi $(docker images -f "dangling=true" -q) || true
- sudo docker run -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker:/var/lib/docker --rm martin/docker-cleanup-volumes || true
diff --git a/xos/core/admin.py b/xos/core/admin.py
index e7c9002..5beb30b 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -893,10 +893,12 @@
)
class TenantRoleAdmin(XOSBaseAdmin):
+ """Admin for TenantRoles."""
model = TenantRole
fields = ('role',)
class TenantPrivilegeInline(XOSTabularInline):
+ """Inline for adding a TenantPrivilege to a Tenant.""";
model = TenantPrivilege
extra = 0
suit_classes = 'suit-tab suit-tab-tenantprivileges'
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 428dca4..f366991 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -824,11 +824,20 @@
return cls.objects.filter(id__in=trp_ids)
class TenantRole(PlCoreBase):
+ """A TenantRole option."""
ROLE_CHOICES = (('admin','Admin'), ('access','Access'))
role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
def __unicode__(self): return u'%s' % (self.role)
class TenantPrivilege(PlCoreBase):
+ """"A TenantPrivilege which defines how users can access a particular Tenant.
+
+ Attributes:
+ id (models.AutoField): The ID of the privilege.
+ user (models.ForeignKey): A Foreign Key to the a User.
+ tenant (models.ForeignKey): A ForeignKey to the Tenant.
+ role (models.ForeignKey): A ForeignKey to the TenantRole.
+ """
id = models.AutoField(primary_key=True)
user = models.ForeignKey('User', related_name="tenantprivileges")
tenant = models.ForeignKey('Tenant', related_name="tenantprivileges")
diff --git a/xos/core/xoslib/methods/vpnview.py b/xos/core/xoslib/methods/vpnview.py
index e0fbea0..8cb745c 100644
--- a/xos/core/xoslib/methods/vpnview.py
+++ b/xos/core/xoslib/methods/vpnview.py
@@ -1,7 +1,7 @@
from core.models import TenantPrivilege
from plus import PlusSerializerMixin
from rest_framework import serializers
-from services.vpn.models import VPNService, VPNTenant, VPN_KIND
+from services.vpn.models import VPNService, VPNTenant
from xos.apibase import XOSListCreateAPIView
if hasattr(serializers, "ReadOnlyField"):
@@ -20,22 +20,40 @@
class VPNTenantSerializer(serializers.ModelSerializer, PlusSerializerMixin):
- id = ReadOnlyField()
- server_network = ReadOnlyField()
- vpn_subnet = ReadOnlyField()
- script_text = serializers.SerializerMethodField()
+ """A Serializer for the VPNTenant that has the minimum information required for clients.
- class Meta:
- model = VPNTenant
- fields = ('id', 'service_specific_attribute', 'vpn_subnet',
- 'server_network', 'script_text')
+ Attributes:
+ id (ReadOnlyField): The ID of VPNTenant.
+ server_network (ReadOnlyField): The network of the VPN.
+ vpn_subnet (ReadOnlyField): The subnet of the VPN.
+ script_text (SerializerMethodField): The text of the script for the client to use to
+ connect.
+ """
+ id = ReadOnlyField()
+ server_network = ReadOnlyField()
+ vpn_subnet = ReadOnlyField()
+ script_text = serializers.SerializerMethodField()
- def get_script_text(self, obj):
- return obj.create_client_script(
- self.context['request'].user.email + "-" + str(obj.id))
+ class Meta:
+ model = VPNTenant
+ fields = ('id', 'service_specific_attribute', 'vpn_subnet',
+ 'server_network', 'script_text')
+
+ def get_script_text(self, obj):
+ """Gets the text of the client script for the requesting user.
+
+ Parameters:
+ obj (services.vpn.models.VPNTenant): The VPNTenant to connect to.
+
+ Returns:
+ str: The client script as a str.
+ """
+ return obj.create_client_script(
+ self.context['request'].user.email + "-" + str(obj.id))
class VPNTenantList(XOSListCreateAPIView):
+ """Class that provides a list of VPNTenants that the user has permission to access."""
serializer_class = VPNTenantSerializer
method_kind = "list"
method_name = "vpntenant"
diff --git a/xos/services/vpn/admin.py b/xos/services/vpn/admin.py
index deb44f5..90420ba 100644
--- a/xos/services/vpn/admin.py
+++ b/xos/services/vpn/admin.py
@@ -106,14 +106,17 @@
Attributes:
creator (forms.ModelChoiceField): The XOS user that created this
tenant.
- client_conf (forms.CharField): The readonly configuration used on the
- client to connect to this Tenant.
- server_address (forms.GenericIPAddressField): The ip address on the VPN
- of this Tenant.
- client_address (forms.GenericIPAddressField): The ip address on the VPN
- of the client.
+ server_network (forms.GenericIPAddressField): The IP address of the VPN network.
+ vpn_subnet (forms.GenericIPAddressField): The subnet used by the VPN network.
is_persistent (forms.BooleanField): Determines if this Tenant keeps
this connection alive through failures.
+ clients_can_see_each_other (forms.BooleanField): Determines if the clients on the VPN can
+ communicate with each other.
+ failover_servers (forms.ModelMultipleChoiceField): The other VPNTenants to use as failover
+ servers.
+ protocol (forms.ChoiceField): The protocol to use.
+ use_ca_from (forms.ModelChoiceField): Another VPNTenant to use the CA of, this is a very
+ hacky way to let VPNs have the same clients.
"""
creator = forms.ModelChoiceField(queryset=User.objects.all())
server_network = forms.GenericIPAddressField(
diff --git a/xos/services/vpn/models.py b/xos/services/vpn/models.py
index 8586eb8..8479e44 100644
--- a/xos/services/vpn/models.py
+++ b/xos/services/vpn/models.py
@@ -11,15 +11,26 @@
"""Defines the Service for creating VPN servers."""
KIND = VPN_KIND
OPENVPN_PREFIX = "/opt/openvpn/"
+ """The location of the openvpn EASY RSA files and PKIs."""
SERVER_PREFIX = OPENVPN_PREFIX + "server-"
+ """The prefix for server PKIs."""
VARS = OPENVPN_PREFIX + "vars"
+ """The location of the vars file with information for using EASY RSA."""
EASYRSA_LOC = OPENVPN_PREFIX + "easyrsa3/easyrsa"
- EASYRSA_COMMAND = EASYRSA_LOC + " --vars=" + VARS
+ """The location of the EASY RSA binary."""
+ EASYRSA_COMMAND_PREFIX = EASYRSA_LOC + " --vars=" + VARS
+ """Prefix for EASY RSA commands."""
@classmethod
def execute_easyrsa_command(cls, pki_dir, command):
+ """Executes the given EASY RSA command using the given PKI.
+
+ Parameters:
+ pki_dir (str): The directory for the pki to execute the command on.
+ command (str): The command to execute using ESAY RSA.
+ """
full_command = (
- VPNService.EASYRSA_COMMAND + " --pki-dir=" +
+ VPNService.EASYRSA_COMMAND_PREIX + " --pki-dir=" +
pki_dir + " " + command)
proc = Popen(
full_command, shell=True, stdout=PIPE, stderr=PIPE
@@ -32,6 +43,14 @@
@classmethod
def get_pki_dir(cls, tenant):
+ """Gets the directory of the PKI for the given tenant.
+
+ Parameters:
+ tenant (services.vpn.models.VPNTenant): The tenant to get the PKI directory for.
+
+ Returns:
+ str: The pki directory for the tenant.
+ """
return VPNService.SERVER_PREFIX + str(tenant.id)
class Meta:
@@ -45,6 +64,7 @@
@property
def exposed_ports(self):
+ """Mapping[str, list(str)]: maps protocols to a list of ports for that protocol."""
return self.get_attribute("exposed_ports",
self.default_attributes["exposed_ports"])
@@ -54,6 +74,7 @@
@property
def exposed_ports_str(self):
+ """str: a raw str representing the exposed ports."""
return self.get_attribute("exposed_ports_str",
self.default_attributes["exposed_ports_str"])
@@ -62,6 +83,18 @@
self.set_attribute("exposed_ports_str", value)
def get_next_available_port(self, protocol):
+ """Gets the next free port for the given protocol.
+
+ Parameters:
+ protocol (str): The protocol to get a port for, must be tcp or udp.
+
+ Returns:
+ int: a port number.
+
+ Raises:
+ xos.exceptions.XOSValidationError: If there the protocol is not udp or tcp.
+ xos.exceptions.XOSValidationError: If there are no available ports for the protocol.
+ """
if protocol != "udp" and protocol != "tcp":
raise XOSValidationError("Port protocol must be udp or tcp")
if not self.exposed_ports[protocol]:
@@ -116,6 +149,7 @@
@property
def protocol(self):
+ """str: The protocol that this tenant is listening on."""
return self.get_attribute(
"protocol", self.default_attributes["protocol"])
@@ -125,6 +159,7 @@
@property
def use_ca_from_id(self):
+ """int: The ID of VPNTenant to use to obtain a CA."""
return self.get_attribute(
"use_ca_from_id", self.default_attributes["use_ca_from_id"])
@@ -195,6 +230,7 @@
@property
def failover_server_ids(self):
+ """list(int): The IDs of the VPNTenants to use as failover servers."""
return self.get_attribute(
"failover_server_ids", self.default_attributes["failover_server_ids"])
@@ -224,6 +260,14 @@
self.set_attribute("port", value)
def create_client_script(self, client_name):
+ """Create a script that a client can use to access this VPNTenant.
+
+ Parameters:
+ client_name (str): The name of the client to use when creating the cerificate.
+
+ Returns:
+ str: A str representing the client script.
+ """
pki_dir = VPNService.get_pki_dir(self)
script = ""
# write the configuration portion
@@ -250,20 +294,51 @@
return script
def get_ca_crt(self, pki_dir):
+ """Gets the lines fo the ca.crt file for this VPNTenant.
+
+ Parameters:
+ pki_dir (str): The PKI directory to look in.
+
+ Returns:
+ list(str): The lines of the ca.crt file for this VPNTenant.
+ """
with open(pki_dir + "/ca.crt", 'r') as f:
return f.readlines()
def get_client_cert(self, client_name, pki_dir):
+ """Gets the lines fo the crt file for a client.
+
+ Parameters:
+ pki_dir (str): The PKI directory to look in.
+ client_name (str): The client name to use.
+
+ Returns:
+ list(str): The lines of the crt file for the client.
+ """
with open(pki_dir + "/issued/" + client_name + ".crt", 'r') as f:
return f.readlines()
def get_client_key(self, client_name, pki_dir):
+ """Gets the lines fo the key file for a client.
+
+ Parameters:
+ pki_dir (str): The PKI directory to look in.
+ client_name (str): The client name to use.
+
+ Returns:
+ list(str): The lines of the key file for the client.
+ """
with open(pki_dir + "/private/" + client_name + ".key", 'r') as f:
return f.readlines()
def generate_client_conf(self, client_name):
- """str: Generates the client configuration to use to connect to this
- VPN server.
+ """Returns the conf file for the given client.
+
+ Parameters:
+ client_name (str): The client name to use.
+
+ Returns:
+ str: Generates the client configuration to use to connect to this VPN server.
"""
conf = ("client\n" +
"dev tun\n" +
@@ -293,7 +368,11 @@
def model_policy_vpn_tenant(pk):
- """Manages the contain for the VPN Tenant."""
+ """Manages the container for the VPN Tenant.
+
+ Parameters
+ pk (int): The ID of this VPNTenant.
+ """
# This section of code is atomic to prevent race conditions
with transaction.atomic():
# We find all of the tenants that are waiting to update
diff --git a/xos/synchronizers/vpn/steps/sync_tenantprivilege.py b/xos/synchronizers/vpn/steps/sync_tenantprivilege.py
index 9b10192..e155dc4 100644
--- a/xos/synchronizers/vpn/steps/sync_tenantprivilege.py
+++ b/xos/synchronizers/vpn/steps/sync_tenantprivilege.py
@@ -10,7 +10,14 @@
class SyncTenantPrivilege(SyncStep):
- """Class for syncing a TenantPrivilege."""
+ """Class for syncing a TenantPrivilege for a VPNTenant.
+
+ This SyncStep isolates the updated TenantPrivileges that are for VPNTenants and performs
+ actions if the TenantPrivilege has been added or deleted. For added privileges a new client
+ certificate and key are made, signed with the ca.crt file used by this VPNTenant. For deleted
+ privileges the client certificate is revoked and the files associated are deleted. In both
+ cases the associated VPNTenant is saved causing the VPNTenant synchronizer to run.
+ """
provides = [TenantPrivilege]
observes = TenantPrivilege
requested_interval = 0
@@ -59,5 +66,14 @@
record.delete()
def get_certificate_name(self, tenant_privilege):
+ """Gets the name of a certificate for the given TenantPrivilege
+
+ Parameters:
+ tenant_privilege (core.models.TenantPrivilege): The TenantPrivilege to use to generate
+ the certificate name.
+
+ Returns:
+ str: The certificate name.
+ """"
return (str(tenant_privilege.user.email) +
"-" + str(tenant_privilege.tenant.id))
diff --git a/xos/synchronizers/vpn/steps/sync_vpntenant.py b/xos/synchronizers/vpn/steps/sync_vpntenant.py
index 640b6a7..13f2c9a 100644
--- a/xos/synchronizers/vpn/steps/sync_vpntenant.py
+++ b/xos/synchronizers/vpn/steps/sync_vpntenant.py
@@ -12,7 +12,11 @@
class SyncVPNTenant(SyncInstanceUsingAnsible):
- """Class for syncing a VPNTenant using Ansible."""
+ """Class for syncing a VPNTenant using Ansible.
+
+ This SyncStep creates any necessary files for the VPNTenant using ESAY RSA and then runs the
+ Ansible template to start the server on an instance.
+ """
provides = [VPNTenant]
observes = VPNTenant
requested_interval = 0