final merging with master branch
diff --git a/apiary.apib b/apiary.apib
index 43a81c7..25915e9 100644
--- a/apiary.apib
+++ b/apiary.apib
@@ -3,8 +3,576 @@
# XOS
+# Group Deployments
+
+List of the XOS deployments
+
+## Deployments [/api/core/deployments/{id}/]
+
+### List all deployments [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "MyDeployment",
+ "id": 1,
+ "created": "2016-04-29T16:19:03.549901Z",
+ "updated": "2016-04-29T16:19:05.624151Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "MyDeployment",
+ "accessControl": "allow all",
+ "images": [
+ "1"
+ ],
+ "sites": [
+ "1"
+ ],
+ "flavors": [
+ "1",
+ "2",
+ "3"
+ ],
+ "dashboardviews": [
+ "1"
+ ]
+ }
+ ]
+
+### Create a deployment [POST]
+
++ Request (application/json)
+
+ {
+ "humanReadableName": "MyDeployment",
+ }
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "MyDeployment",
+ "id": 1,
+ "created": "2016-04-29T16:19:03.549901Z",
+ "updated": "2016-04-29T16:19:05.624151Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "MyDeployment",
+ "accessControl": "allow all",
+ "images": [
+ "1"
+ ],
+ "sites": [
+ "1"
+ ],
+ "flavors": [
+ "1",
+ "2",
+ "3"
+ ],
+ "dashboardviews": [
+ "1"
+ ]
+ }
+
+### View a Deployment Detail [GET]
+
++ Parameters
+ + id: 1 (number) - ID of the Deployment in the form of an integer
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "MyDeployment",
+ "id": 1,
+ "created": "2016-04-27T21:46:57.354544Z",
+ "updated": "2016-04-27T21:47:05.221720Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "MyDeployment",
+ "accessControl": "allow all",
+ "images": [],
+ "sites": [
+ "1"
+ ],
+ "flavors": [
+ "3",
+ "2",
+ "1"
+ ],
+ "dashboardviews": [
+ "3"
+ ]
+ }
+
+### Delete a Deployment [DELETE]
+
++ Parameters
+ + id: 1 (number) - ID of the Deployment in the form of an integer
+
++ Response 204
+
+
+# Group Flavors
+
+List of the XOS flavors
+
+## Flavors [/api/core/flavors/{id}/]
+
+### List all flavors [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "m1.large",
+ "id": 1,
+ "created": "2016-04-29T16:19:01.979548Z",
+ "updated": "2016-04-29T16:19:03.568238Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "m1.large",
+ "description": null,
+ "flavor": "m1.large",
+ "order": 0,
+ "default": false,
+ "deployments": [
+ "1"
+ ]
+ }
+ ]
+
+### Create a Flavor [POST]
+
++ Request (application/json)
+
+ {
+ "humanReadableName": "mq.test",
+ }
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "m1.large",
+ "id": 1,
+ "created": "2016-04-29T16:19:01.979548Z",
+ "updated": "2016-04-29T16:19:03.568238Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "m1.large",
+ "description": null,
+ "flavor": "m1.large",
+ "order": 0,
+ "default": false,
+ "deployments": [
+ "1"
+ ]
+ }
+
+### View a Flavors Detail [GET]
+
++ Parameters
+ + id: 1 (number) - ID of the Flavors in the form of an integer
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "m1.large",
+ "id": 1,
+ "created": "2016-04-29T16:19:01.979548Z",
+ "updated": "2016-04-29T16:19:03.568238Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "m1.large",
+ "description": null,
+ "flavor": "m1.large",
+ "order": 0,
+ "default": false,
+ "deployments": [
+ "1"
+ ]
+ }
+
+### Delete a Flavors Detail [DELETE]
+
++ Parameters
+ + id: 1 (number) - ID of the Flavors in the form of an integer
+
++ Response 204
+
+
+# Group Instances
+
+List of the XOS instances
+
+## Instances Collection [/api/core/instances/{?no_hyperlinks}]
+
+ + no_hyperlinks (number, optional) - Wheter to return relation with links or ids
+ + Default: `0`
+
+### List all Instances [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "id": 1,
+ "humanReadableName": "uninstantiated-1",
+ "created": "2016-04-26T00:36:22.465259Z",
+ "updated": "2016-04-26T00:36:22.465288Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "instance_id": null,
+ "instance_uuid": null,
+ "name": "mysite_vcpe",
+ "instance_name": null,
+ "ip": null,
+ "image": "1",
+ "creator": "1",
+ "slice": "1",
+ "deployment": "1",
+ "node": "1",
+ "numberCores": 0,
+ "flavor": "1",
+ "userData": null,
+ "isolation": "vm",
+ "volumes": "/etc/dnsmasq.d,/etc/ufw",
+ "parent": null,
+ "networks": [
+ "1"
+ ]
+ }
+ ]
+
+### Create an Instance [POST]
+
++ Parameters
+ + no_hyperlinks: 1
+
++ Request (application/json)
+
+ {
+ "name": "test-instance",
+ "image": 1,
+ "slice": 1,
+ "deployment": 1,
+ "node": 1
+ }
+
++ Response 200 (application/json)
+
+ {
+ "id": 1,
+ "humanReadableName": "uninstantiated-1",
+ "created": "2016-04-26T00:36:22.465259Z",
+ "updated": "2016-04-26T00:36:22.465288Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "instance_id": null,
+ "instance_uuid": null,
+ "name": "test-instance",
+ "instance_name": null,
+ "ip": null,
+ "image": "1",
+ "creator": "1",
+ "slice": "1",
+ "deployment": "1",
+ "node": "1",
+ "numberCores": 0,
+ "flavor": "1",
+ "userData": null,
+ "isolation": "vm",
+ "volumes": "/etc/dnsmasq.d,/etc/ufw",
+ "parent": null,
+ "networks": [
+ "1"
+ ]
+ }
+
+## Instances Detail [/api/core/instances/{id}/]
+
+### Get instance details [GET]
+
++ Parameters
+ + id: 1 (number) - ID of the Instance in the form of an integer
+
++ Response 200 (application/json)
+
+ {
+ "id": 1,
+ "humanReadableName": "uninstantiated-1",
+ "created": "2016-04-26T00:36:22.465259Z",
+ "updated": "2016-04-26T00:36:22.465288Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "instance_id": null,
+ "instance_uuid": null,
+ "name": "mysite_vcpe",
+ "instance_name": null,
+ "ip": null,
+ "image": "1",
+ "creator": "1",
+ "slice": "1",
+ "deployment": "1",
+ "node": "1",
+ "numberCores": 0,
+ "flavor": "1",
+ "userData": null,
+ "isolation": "vm",
+ "volumes": "/etc/dnsmasq.d,/etc/ufw",
+ "parent": null,
+ "networks": [
+ "1"
+ ]
+ }
+
+### Delete instance [DELETE]
+
++ Parameters
+ + id: 1 (number) - ID of the Instance in the form of an integer
+
++ Response 204
+
+
+# Group Nodes
+
+List of the XOS nodes
+
+## Nodes [/api/core/nodes/{id}/]
+
+### List all nodes [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "node2.opencloud.us",
+ "id": 1,
+ "created": "2016-04-29T16:19:05.661567Z",
+ "updated": "2016-04-29T16:19:05.661454Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "node2.opencloud.us",
+ "site_deployment": "1",
+ "site": "1",
+ "nodelabels": []
+ }
+ ]
+
+
+
+# Group Sites
+
+List of the XOS sites
+
+## Sites [/api/core/sites/{id}/]
+
+### List all sites [GET]
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "MySite",
+ "id": 1,
+ "created": "2016-04-29T16:19:03.587770Z",
+ "updated": "2016-04-29T16:19:05.651933Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "name": "MySite",
+ "site_url": "http://opencord.us/",
+ "enabled": true,
+ "hosts_nodes": true,
+ "hosts_users": true,
+ "location": null,
+ "longitude": null,
+ "latitude": null,
+ "login_base": "mysite",
+ "is_public": true,
+ "abbreviated_name": "",
+ "deployments": [
+ "1"
+ ]
+ }
+
+
+# Group Slices
+
+List of the XOS slices
+
+## Slices [/api/core/slices/{id}/]
+
+### List all slices [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "mysite_slice",
+ "id": 1,
+ "created": "2016-04-29T16:23:22.505072Z",
+ "updated": "2016-04-29T16:23:22.504691Z",
+ "enacted": null,
+ "policed": "2016-04-29T16:23:22.781298Z",
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "name": "mysite_slice",
+ "enabled": true,
+ "omf_friendly": false,
+ "description": "",
+ "slice_url": "",
+ "site": "http://apt118.apt.emulab.net/api/core/sites/1/",
+ "max_instances": 10,
+ "service": null,
+ "network": null,
+ "exposed_ports": null,
+ "serviceClass": "http://apt118.apt.emulab.net/api/core/serviceclasses/1/",
+ "creator": "http://apt118.apt.emulab.net/api/core/users/1/",
+ "default_flavor": null,
+ "default_image": null,
+ "mount_data_sets": "GenBank",
+ "default_isolation": "vm",
+ "networks": [
+ "http://apt118.apt.emulab.net/api/core/networks/1/"
+ ]
+ }
+ ]
+
+
+
+# Group Users
+
+List of the XOS users
+
+## Users [/api/core/users/{id}/]
+
+### List all Users [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "id": 2,
+ "password": "pbkdf2_sha256$12000$9gn8DmZuIoz2$YPQkx3AOOV7jZNYr2ddrgUCkiuaPpvb8+aJR7RwLZNA=",
+ "last_login": "2016-04-12T18:50:45.880823Z",
+ "email": "johndoe@myhouse.com",
+ "username": "johndoe@myhouse.com",
+ "firstname": "john",
+ "lastname": "doe",
+ "phone": null,
+ "user_url": null,
+ "site": "http://xos.dev:9999/api/core/sites/1/",
+ "public_key": null,
+ "is_active": true,
+ "is_admin": false,
+ "is_staff": true,
+ "is_readonly": false,
+ "is_registering": false,
+ "is_appuser": false,
+ "login_page": null,
+ "created": "2016-04-12T18:50:45.912602Z",
+ "updated": "2016-04-12T18:50:45.912671Z",
+ "enacted": null,
+ "policed": null,
+ "backend_status": "Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "timezone": "America/New_York"
+ }
+ ]
+
+
+
+# Group Example
+
+## Example Services Collection [/api/service/exampleservice/]
+
+### List all Example Services [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "MyExample",
+ "id": 1,
+ "service_message": "This is the test message"
+ }
+ ]
+
+
# Group ONOS Services
+List of the active onos services
+
## ONOS Services Collection [/api/service/onos/]
### List all ONOS Services [GET]
@@ -35,8 +603,6 @@
{
"humanReadableName": "service_vsg",
"id": 2,
- "wan_container_gateway_ip": "",
- "wan_container_gateway_mac": "",
"dns_servers": "8.8.8.8",
"url_filter_kind": null,
"node_label": null
@@ -44,11 +610,45 @@
]
+# Group Utility
+
+List of the XOS Utility API
+
+## Login [/api/utility/login/]
+
+### Log a user in the system [POST]
+
++ Request (application/json)
+
+ {
+ "username": "padmin@vicci.org",
+ "password": "letmein"
+ }
+
++ Response 200 (application/json)
+
+ {
+ "xoscsrftoken":"xuvsRC1jkXAsnrdRlgJvcXpmtthTAqqf",
+ "xossessionid":"7ds5a3wzjlgbjqo4odkd25qsm0j2s6zg",
+ "user": "{\"policed\": null, \"site\": 3, \"is_appuser\": false, \"is_staff\": true, \"backend_status\": \"Provisioning in progress\", \"id\": 3, \"is_registering\": false, \"last_login\": \"2016-04-30T22:51:04.788675+00:00\", \"email\": \"padmin@vicci.org\", \"no_sync\": false, \"username\": \"padmin@vicci.org\", \"dashboards\": [11], \"login_page\": null, \"firstname\": \"XOS\", \"user_url\": null, \"deleted\": false, \"lastname\": \"admin\", \"is_active\": true, \"lazy_blocked\": false, \"phone\": null, \"is_admin\": true, \"enacted\": null, \"public_key\": null, \"is_readonly\": false, \"no_policy\": false, \"write_protect\": false}"
+ }
+
+## Logout [/api/utility/logout/]
+
+### Log a user out of the system [POST]
+
++ Request (application/json)
+ {xossessionid: "sessionId"}
+
++ Response 200 (application/json)
+
+
+
# Group Subscribers
Resource related to the CORD Subscribers.
-## Subscribers Collection [/api/tenant/cord/subscriber/]
+## Subscribers [/api/tenant/cord/subscriber/{subscriber_id}/]
### List All Subscribers [GET]
@@ -82,13 +682,12 @@
}
]
-## Subscriber Detail [/api/tenant/cord/subscriber/{subscriber_id}/]
+
+### View a Subscriber Detail [GET]
+ Parameters
+ subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-### View a Subscriber Detail [GET]
-
+ Response 200 (application/json)
{
@@ -119,6 +718,9 @@
### Delete a Subscriber [DELETE]
++ Parameters
+ + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
+
+ Response 204
### Subscriber features [/api/tenant/cord/subscriber/{subscriber_id}/features/]
@@ -278,7 +880,7 @@
Virtual Truckroll, enable to perform basic test on user connectivity such as ping, traceroute and tcpdump.
-## Truckroll Collection [/api/tenant/truckroll/]
+## Truckroll Collection [/api/tenant/truckroll/{truckroll_id}/]
### List all Truckroll [GET]
@@ -302,6 +904,8 @@
### Create a Truckroll [POST]
+A virtual truckroll is complete once is_synced equal true
+
+ Request (application/json)
{
@@ -327,15 +931,12 @@
"backend_status": "0 - Provisioning in progress"
}
-## Truckroll Detail [/api/tenant/truckroll/{truckroll_id}/]
-A virtual truckroll is complete once is_synced equal true
+### View a Truckroll Detail [GET]
+ Parameters
+ truckroll_id: 1 (number) - ID of the Truckroll in the form of an integer
-### View a Truckroll Detail [GET]
-
+ Response 200 (application/json)
{
@@ -354,6 +955,9 @@
### Delete a Truckroll Detail [DELETE]
++ Parameters
+ + truckroll_id: 1 (number) - ID of the Truckroll in the form of an integer
+
+ Response 204
@@ -362,7 +966,7 @@
OLT devices aggregate a set of subscriber connections
-## vOLT Collection [/api/tenant/cord/volt/]
+## vOLT Collection [/api/tenant/cord/volt/{volt_id}/]
### List all vOLT [GET]
@@ -414,15 +1018,11 @@
}
}
-## vOLT Detail [/api/tenant/cord/volt/{volt_id}/]
-
-A virtual volt is complete once is_synced equal true
+### View a vOLT Detail [GET]
+ Parameters
+ volt_id: 1 (number) - ID of the vOLT in the form of an integer
-### View a vOLT Detail [GET]
-
+ Response 200 (application/json)
{
@@ -445,7 +1045,7 @@
# Group ONOS Apps
-## app Collection [/api/tenant/onos/app/]
+## ONOS App Collection [/api/tenant/onos/app/]
### List all apps [GET]
diff --git a/containers/openvpn/Dockerfile b/containers/openvpn/Dockerfile
new file mode 100644
index 0000000..8ae8484
--- /dev/null
+++ b/containers/openvpn/Dockerfile
@@ -0,0 +1,12 @@
+FROM xosproject/xos-synchronizer-openstack
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \
+ openvpn
+
+# 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/containers/openvpn/Makefile b/containers/openvpn/Makefile
new file mode 100644
index 0000000..bdfb126
--- /dev/null
+++ b/containers/openvpn/Makefile
@@ -0,0 +1,18 @@
+IMAGE_NAME:=xosproject/xos-openvpn
+CONTAINER_NAME:=xos-synchronizer
+NO_DOCKER_CACHE?=true
+
+.PHONY: build
+build: ; sudo docker build --no-cache=${NO_DOCKER_CACHE} --rm -t ${IMAGE_NAME} .
+
+.PHONY: run
+run: ; sudo docker run -d --name ${CONTAINER_NAME} -v /usr/local/share/ca-certificates:/usr/local/share/ca-certificates:ro ${IMAGE_NAME}
+
+.PHONY: stop
+stop: ; sudo docker stop ${CONTAINER_NAME}
+
+.PHONY: rm
+rm: ; sudo docker rm ${CONTAINER_NAME}
+
+.PHONY: rmi
+rmi: ; docker rmi ${IMAGE_NAME}
diff --git a/containers/openvpn/conf/ansible-hosts b/containers/openvpn/conf/ansible-hosts
new file mode 100644
index 0000000..0dd74f1
--- /dev/null
+++ b/containers/openvpn/conf/ansible-hosts
@@ -0,0 +1,2 @@
+[localhost]
+127.0.0.1
diff --git a/containers/openvpn/conf/synchronizer.conf b/containers/openvpn/conf/synchronizer.conf
new file mode 100644
index 0000000..2131a25
--- /dev/null
+++ b/containers/openvpn/conf/synchronizer.conf
@@ -0,0 +1,9 @@
+[supervisord]
+logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log)
+pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
+nodaemon=true
+
+[program:synchronizer]
+command=python /opt/xos/synchronizers/openstack/xos-synchronizer.py
+stderr_logfile=/var/log/supervisor/synchronizer.err.log
+stdout_logfile=/var/log/supervisor/synchronizer.out.log
diff --git a/containers/xos/Dockerfile b/containers/xos/Dockerfile
index b5064ae..afc7c9d 100644
--- a/containers/xos/Dockerfile
+++ b/containers/xos/Dockerfile
@@ -50,6 +50,7 @@
django-timezones \
djangorestframework==3.3.3 \
dnslib \
+ jinja2 \
lxml \
markdown \
netaddr \
diff --git a/containers/xos/Dockerfile.devel b/containers/xos/Dockerfile.devel
index dcb2190..1e2532f 100644
--- a/containers/xos/Dockerfile.devel
+++ b/containers/xos/Dockerfile.devel
@@ -50,6 +50,7 @@
django-timezones \
djangorestframework==3.3.3 \
dnslib \
+ jinja2 \
lxml \
markdown \
netaddr \
diff --git a/views/ngXosLib/.gitignore b/views/ngXosLib/.gitignore
index 5df6f2d..24b9855 100644
--- a/views/ngXosLib/.gitignore
+++ b/views/ngXosLib/.gitignore
@@ -2,4 +2,6 @@
bower_components
docs
xosHelpers/.tmp
-xos
\ No newline at end of file
+xos
+coverage
+test-result
\ No newline at end of file
diff --git a/views/ngXosLib/apigen/blueprintToNgResource.js b/views/ngXosLib/apigen/blueprintToNgResource.js
index 2a3cde3..fdb1050 100644
--- a/views/ngXosLib/apigen/blueprintToNgResource.js
+++ b/views/ngXosLib/apigen/blueprintToNgResource.js
@@ -26,10 +26,8 @@
// Loop APIs endpoint
const loopApiEndpoint = (group) => {
- // {name: 'ResourceName', attributes: {href: '/ahhsiiis'}}
+ // removing description
_.remove(group, {element: 'copy'})
- // console.log(group);
- // _.forEach(group, d => console.log(d));
return _.map(group, g => {
return {
@@ -46,6 +44,7 @@
}
_.forEach(defs, d => {
console.info(chalk.blue.bold(`Parsing Group: ${d.meta.title}`));
+
var data = {
description: getGroupDescription(d.content),
ngModule: angualarModuleName,
@@ -70,7 +69,7 @@
fs.readFileAsync(path.join(__dirname, './ngResourceTemplate.handlebars'), 'utf8')
.then((template) => {
handlebarsTemplate = Handlebars.compile(template);
- return fs.readFileAsync(path.join(__dirname, '../../../xos/tests/api/apiary.apib'), 'utf8')
+ return fs.readFileAsync(path.join(__dirname, '../../../apiary.apib'), 'utf8')
})
.then(data => protagonist.parseAsync(data))
.then(result => loopApiDefinitions(result.content))
diff --git a/views/ngXosLib/apigen/ngResourceTemplate.handlebars b/views/ngXosLib/apigen/ngResourceTemplate.handlebars
index 6176633..c1bc8f5 100644
--- a/views/ngXosLib/apigen/ngResourceTemplate.handlebars
+++ b/views/ngXosLib/apigen/ngResourceTemplate.handlebars
@@ -9,7 +9,9 @@
* @description Angular resource to fetch {{param.href}}
**/
.service('{{name}}', function($resource){
- return $resource('{{param.href}}'{{#if param.name}}, { {{param.name}}: '@id' }{{/if}});
+ return $resource('{{param.href}}'{{#if param.name}}, { {{param.name}}: '@id' }, {
+ update: { method: 'PUT' }
+ }{{/if}});
})
{{/each}}
})();
\ No newline at end of file
diff --git a/views/ngXosLib/bower.json b/views/ngXosLib/bower.json
index 3726b85..56fed7f 100644
--- a/views/ngXosLib/bower.json
+++ b/views/ngXosLib/bower.json
@@ -19,9 +19,14 @@
"angular-resource": "1.4.7",
"ng-lodash": "0.3.0",
"angular-cookies": "1.4.7",
- "angular-animate": "1.4.7"
+ "angular-animate": "1.4.7",
+ "lodash": "~4.11.1"
},
"devDependencies": {
- "angular-mocks": "1.4.7"
+ "angular-mocks": "1.4.7",
+ "jasmine-jquery": "~2.1.1"
+ },
+ "resolutions": {
+ "angular": "1.4.7"
}
}
diff --git a/views/ngXosLib/generator-xos/app/index.js b/views/ngXosLib/generator-xos/app/index.js
index 9fab96f..1157973 100755
--- a/views/ngXosLib/generator-xos/app/index.js
+++ b/views/ngXosLib/generator-xos/app/index.js
@@ -4,6 +4,7 @@
var user = require('../node_modules/yeoman-generator/lib/actions/user');
var config = {};
+var userName;
module.exports = generators.Base.extend({
_fistCharToUpper: function(string){
@@ -59,6 +60,7 @@
},
writing: {
rcFiles: function(){
+ userName = user.git.name().split(' ');
this.fs.copy(this.templatePath('.bowerrc'), this.destinationPath(`${this.config.get('folder')}/${config.name}/.bowerrc`));
this.fs.copy(this.templatePath('.gitignore'), this.destinationPath(`${this.config.get('folder')}/${config.name}/.gitignore`));
},
@@ -87,7 +89,7 @@
this.fs.copyTpl(
this.templatePath('src/index.html'),
this.destinationPath(`${this.config.get('folder')}/${config.name}/src/index.html`),
- { name: config.name, fileName: this._fistCharToUpper(config.name) }
+ { name: config.name, fileName: this._fistCharToUpper(config.name), user: {firstname: userName[0]}}
);
},
css: function(){
@@ -129,7 +131,6 @@
);
},
spec: function(){
- const userName = user.git.name().split(' ');
this.fs.copyTpl(
this.templatePath('spec/sample.test.js'),
this.destinationPath(`${this.config.get('folder')}/${config.name}/spec/sample.test.js`),
diff --git a/views/ngXosLib/generator-xos/app/templates/bower.json b/views/ngXosLib/generator-xos/app/templates/bower.json
index 721e2de..9336a98 100644
--- a/views/ngXosLib/generator-xos/app/templates/bower.json
+++ b/views/ngXosLib/generator-xos/app/templates/bower.json
@@ -24,7 +24,7 @@
"angular-cookies": "1.4.7",
"angular-animate": "1.4.7",
"angular-resource": "1.4.7",
- "ng-lodash": "0.3.0",
+ "lodash": "~4.11.1",
"bootstrap-css": "3.3.6"
}
}
diff --git a/views/ngXosLib/generator-xos/app/templates/gulp/server.js b/views/ngXosLib/generator-xos/app/templates/gulp/server.js
index 13659c3..c0678d9 100644
--- a/views/ngXosLib/generator-xos/app/templates/gulp/server.js
+++ b/views/ngXosLib/generator-xos/app/templates/gulp/server.js
@@ -35,12 +35,8 @@
module.exports = function(options){
- // open in browser with sync and proxy to 0.0.0.0
gulp.task('browser', function() {
browserSync.init({
- // reloadDelay: 500,
- // logLevel: 'debug',
- // logConnections: true,
startPath: '#/',
snippetOptions: {
rule: {
@@ -50,13 +46,15 @@
server: {
baseDir: options.src,
routes: {
- '/xosHelpers/src': options.helpers
+ '/xos/core/xoslib/static/js/vendor': options.helpers,
+ '/xos/core/static': options.static + '../../static/'
},
middleware: function(req, res, next){
if(
- req.url.indexOf('/xos/') !== -1 ||
- req.url.indexOf('/xoslib/') !== -1 ||
- req.url.indexOf('/hpcapi/') !== -1 ||
+ // to be removed, deprecated API
+ // req.url.indexOf('/xos/') !== -1 ||
+ // req.url.indexOf('/xoslib/') !== -1 ||
+ // req.url.indexOf('/hpcapi/') !== -1 ||
req.url.indexOf('/api/') !== -1
){
if(conf.xoscsrftoken && conf.xossessionid){
@@ -85,6 +83,13 @@
gulp.watch(`${options.sass}/**/*.scss`, ['sass'], function(){
browserSync.reload();
});
+
+ gulp.watch([
+ options.helpers + 'ngXosHelpers.js',
+ options.static + '../../static/xosNgLib.css'
+ ], function(){
+ browserSync.reload();
+ });
});
// compile sass
@@ -108,7 +113,7 @@
inject(
gulp.src([
options.tmp + '**/*.js',
- options.helpers + '**/*.js' // todo add Babel here
+ options.helpers + 'ngXosHelpers.js'
])
.pipe(angularFilesort()),
{
@@ -124,7 +129,10 @@
return gulp.src(options.src + 'index.html')
.pipe(
inject(
- gulp.src(options.src + 'css/*.css'),
+ gulp.src([
+ options.src + 'css/*.css',
+ options.static + '../../static/xosNgLib.css'
+ ]),
{
ignorePath: [options.src]
}
diff --git a/views/ngXosLib/generator-xos/app/templates/gulpfile.js b/views/ngXosLib/generator-xos/app/templates/gulpfile.js
index 8d30660..08df554 100644
--- a/views/ngXosLib/generator-xos/app/templates/gulpfile.js
+++ b/views/ngXosLib/generator-xos/app/templates/gulpfile.js
@@ -11,7 +11,7 @@
tmp: 'src/.tmp',
dist: 'dist/',
api: '../../ngXosLib/api/',
- helpers: '../../ngXosLib/xosHelpers/src/',
+ helpers: '../../../xos/core/xoslib/static/js/vendor/',
static: '../../../xos/core/xoslib/static/', // this is the django static folder
dashboards: '../../../xos/core/xoslib/dashboards/' // this is the django html folder
};
diff --git a/views/ngXosLib/generator-xos/app/templates/src/index.html b/views/ngXosLib/generator-xos/app/templates/src/index.html
index eeb0dec..504bc7c 100644
--- a/views/ngXosLib/generator-xos/app/templates/src/index.html
+++ b/views/ngXosLib/generator-xos/app/templates/src/index.html
@@ -1,14 +1,91 @@
<!-- browserSync -->
<!-- bower:css -->
-<!-- endbower --><!-- endcss -->
+<!-- endbower -->
+<!-- endcss -->
<!-- inject:css -->
<!-- endinject -->
<div ng-app="xos.<%= name %>" id="xos<%= fileName %>" class="container-fluid">
- <div ui-view></div>
+ <div class="row">
+ <div class="col-xs-12">
+ <h1>Hi <%= user.firstname %>!</h1>
+ <h3>Welcome to you development environment.</h3>
+ <p>
+ We provided this environment to help you creating a custom view.
+ </p>
+ <p>
+ When the environment is running you will have an
+ <code>auto-reload</code>
+ feature enabled, so any time you update one of your files, the browser will be reloaded.
+ </p>
+ <p> <i>Note that is environment is already functional and that it is loading information from the XOS APIs and presenting them using the
+ <code>xos-table</code>
+ component.</i>
+ </p>
+ <h3>Development notes:</h3>
+ <p>
+ This views are designed using
+ <a href="https://angularjs.org/" target="_blank">Angular Js</a>
+ version 1.4.7 and
+ <a href="http://getbootstrap.com/" target="_blank">Bootstrap</a>
+ 3.3.6 is included.
+ </p>
+ <p>
+ We just want to remind you that this development environment provide you three helper command:
+ <ul>
+ <li>
+ <code>npm start</code>
+ - will start your setup (you should already be familiar with it)
+ </li>
+ <li>
+ <code>npm test</code>
+ - will execute your unit tests defined with
+ <a href="https://karma-runner.github.io/0.13/index.html" target="_blank">Karma</a>
+ and
+ <a href="jasmine.github.io" target="_blank">Jasmine</a>
+ . You can check the
+ <code>spec/</code>
+ folder to see an example of your first test.
+ </li>
+ <li>
+ <code>npm run build</code>
+ - will build your dashboard and make it available to XOS
+ </li>
+ </ul>
+ </p>
+ <h3>Helpers:</h3>
+ <p>
+ We provide a set of helpers that you can leverage in your dashboard:
+ <ul>
+ <li>
+ <code>xos.helpers</code>
+ - A set of
+ <a href="https://docs.angularjs.org/guide/services" target="_blank">Angular Services</a>
+ </li>
+ <li>
+ <code>xos.uiComponents</code>
+ - A set of
+ <a href="https://docs.angularjs.org/guide/directive" target="_blank">Angular Directives</a>
+ </li>
+ <li>
+ <code>xos.rest</code>
+ - A set of
+ <a href="https://docs.angularjs.org/api/ngResource/service/$resource" target="_blank">Angular $resources</a>
+ </li>
+ </ul>
+ To know more about this helpers you can naviate to
+ <code>/views/ngXosLib/</code>
+ and generate the documentation with
+ <code>npm run doc</code>
+ </p>
+ <h3>Example:</h3>
+ </div>
+ </div>
+ <div ui-view></div>
</div>
<!-- bower:js -->
-<!-- endbower --><!-- endjs -->
+<!-- endbower -->
+<!-- endjs -->
<!-- inject:js -->
-<!-- endinject -->
+<!-- endinject -->
\ No newline at end of file
diff --git a/views/ngXosLib/generator-xos/app/templates/src/js/main.js b/views/ngXosLib/generator-xos/app/templates/src/js/main.js
index 9c0e259..803f344 100644
--- a/views/ngXosLib/generator-xos/app/templates/src/js/main.js
+++ b/views/ngXosLib/generator-xos/app/templates/src/js/main.js
@@ -3,7 +3,6 @@
angular.module('xos.<%= name %>', [
'ngResource',
'ngCookies',
- 'ngLodash',
'ui.router',
'xos.helpers'
])
@@ -25,6 +24,32 @@
controllerAs: 'vm',
templateUrl: 'templates/users-list.tpl.html',
controller: function(Users){
+
+ this.tableConfig = {
+ columns: [
+ {
+ label: 'E-Mail',
+ prop: 'email'
+ },
+ {
+ label: 'First Name',
+ prop: 'firstname'
+ },
+ {
+ label: 'Last Name',
+ prop: 'lastname'
+ },
+ {
+ label: 'Created',
+ prop: 'created'
+ },
+ {
+ label: 'Is Admin',
+ prop: 'is_admin'
+ }
+ ]
+ };
+
// retrieving user list
Users.query().$promise
.then((users) => {
diff --git a/views/ngXosLib/generator-xos/app/templates/src/templates/users-list.tpl.html b/views/ngXosLib/generator-xos/app/templates/src/templates/users-list.tpl.html
index fd8d208..1fee0e2 100644
--- a/views/ngXosLib/generator-xos/app/templates/src/templates/users-list.tpl.html
+++ b/views/ngXosLib/generator-xos/app/templates/src/templates/users-list.tpl.html
@@ -1,16 +1 @@
-<div class="row">
- <div class="col-xs-12">
- <h1>Users List</h1>
- <p>This is only an example view.</p>
- </div>
-</div>
-<div class="row">
- <div class="col-xs-4">Email</div>
- <div class="col-xs-4">First Name</div>
- <div class="col-xs-4">Last Name</div>
-</div>
-<div class="row" ng-repeat="user in vm.users">
- <div class="col-xs-4">{{user.email}}</div>
- <div class="col-xs-4">{{user.firstname}}</div>
- <div class="col-xs-4">{{user.lastname}}</div>
-</div>
\ No newline at end of file
+<xos-table config="vm.tableConfig" data="vm.users"></xos-table>
\ No newline at end of file
diff --git a/views/ngXosLib/gulp/ngXosHelpers.js b/views/ngXosLib/gulp/ngXosHelpers.js
index f046681..435e239 100644
--- a/views/ngXosLib/gulp/ngXosHelpers.js
+++ b/views/ngXosLib/gulp/ngXosHelpers.js
@@ -8,9 +8,20 @@
var babel = require('gulp-babel');
const sourcemaps = require('gulp-sourcemaps');
var browserSync = require('browser-sync').create();
+var rename = require('gulp-rename');
+var sass = require('gulp-sass');
module.exports = function(options){
+ gulp.task('style', function(){
+ return gulp.src(`${options.xosHelperSource}styles/main.scss`)
+ .pipe(sourcemaps.init())
+ .pipe(sass().on('error', sass.logError))
+ .pipe(rename('xosNgLib.css'))
+ .pipe(sourcemaps.write())
+ .pipe(gulp.dest(options.ngXosStyles));
+ });
+
// transpile js with sourceMaps
gulp.task('babel', function(){
return gulp.src(options.xosHelperSource + '**/*.js')
@@ -31,7 +42,7 @@
});
// build
- gulp.task('helpers', ['babel'], function(){
+ gulp.task('helpers', ['babel', 'style'], function(){
return gulp.src([options.xosHelperTmp + '**/*.js'])
.pipe(angularFilesort())
.pipe(concat('ngXosHelpers.js'))
@@ -56,12 +67,17 @@
gulp.task('makeDocs', ['cleanDocs'], function(){
var ngOptions = {
scripts: [
- 'http://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.min.js',
- 'http://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular-animate.min.js',
- `${options.ngXosVendor}ngXosHelpers.js`
+ 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.min.js',
+ 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular-animate.min.js',
+ 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular-cookies.min.js',
+ 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular-resource.js',
+ 'https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular-mocks.js',
+ 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.11.2/lodash.js',
+ `./${options.ngXosVendor}ngXosHelpers.js`,
],
styles: [
- 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css',
+ `./${options.ngXosStyles}xosNgLib.css`,
+ 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.css',
],
html5Mode: false,
title: 'XOS Helpers documentation',
@@ -92,7 +108,8 @@
server: {
baseDir: './docs',
routes: {
- '/xos/core/xoslib/static/js/vendor': options.ngXosVendor
+ '/xos/core/xoslib/static/js/vendor': options.ngXosVendor,
+ '/xos/core/static': options.ngXosStyles
}
}
});
@@ -108,12 +125,16 @@
gulp.watch(files, ['makeDocs']);
- gulp.watch(files, function(){
- browserSync.reload();
- });
+ // uncomment to enable autoreload, now it is broken (reload a wrong page)
+ // https://github.com/nikhilmodak/gulp-ngdocs/issues/81
+
+ // gulp.watch(files, function(){
+ // browserSync.reload();
+ // });
})
gulp.task('dev', function(){
+ gulp.watch(`${options.xosHelperSource}**/*.scss`, ['style']);
gulp.watch(options.xosHelperSource + '**/*.js', ['helpersDev']);
});
};
\ No newline at end of file
diff --git a/views/ngXosLib/gulpfile.js b/views/ngXosLib/gulpfile.js
index 77a5c07..e514d7a 100644
--- a/views/ngXosLib/gulpfile.js
+++ b/views/ngXosLib/gulpfile.js
@@ -5,6 +5,7 @@
var options = {
ngXosVendor: '../../xos/core/xoslib/static/js/vendor/', //save here the minfied vendor file, this is automatically loaded in the django page
+ ngXosStyles: '../../xos/core/static/', // TODO move in xoslib
xosHelperSource: './xosHelpers/src/',
xosHelperTmp: './xosHelpers/.tmp/',
docs: './docs'
diff --git a/views/ngXosLib/karma.conf.ci.js b/views/ngXosLib/karma.conf.ci.js
new file mode 100644
index 0000000..c76d029
--- /dev/null
+++ b/views/ngXosLib/karma.conf.ci.js
@@ -0,0 +1,108 @@
+// Karma configuration
+// Generated on Tue Oct 06 2015 09:27:10 GMT+0000 (UTC)
+
+/* eslint indent: [2,2], quotes: [2, "single"]*/
+
+// CONFIGURATION FOR JENKINS TESTS
+
+/*eslint-disable*/
+var wiredep = require('wiredep');
+var path = require('path');
+
+var bowerComponents = wiredep({devDependencies: true})[ 'js' ].map(function( file ){
+ return path.relative(process.cwd(), file);
+});
+
+var files = bowerComponents.concat([
+ 'node_modules/babel-polyfill/dist/polyfill.js',
+ '../../xos/core/xoslib/static/js/vendor/ngXosHelpers.js',
+ 'xosHelpers/spec/**/*.test.js'
+]);
+
+module.exports = function(config) {
+/*eslint-enable*/
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: files,
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'xosHelpers/**/*.js': ['babel', 'coverage'],
+ },
+
+ babelPreprocessor: {
+ options: {
+ presets: ['es2015'],
+ sourceMap: 'inline'
+ },
+ filename: function (file) {
+ return file.originalPath;
+ },
+ },
+
+ //ngHtml2JsPreprocessor: {
+ // stripPrefix: 'src/', //strip the src path from template url (http://stackoverflow.com/questions/22869668/karma-unexpected-request-when-testing-angular-directive-even-with-ng-html2js)
+ // moduleName: 'templates' // define the template module name
+ //},
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['dots', 'junit', 'coverage'],
+
+ junitReporter: {
+ outputDir: 'test-result',
+ useBrowserName: false,
+ outputFile: 'test-results.xml'
+ },
+
+ coverageReporter: {
+ type: 'cobertura',
+ subdir: '.',
+ dir: 'test-result/'
+ },
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_ERROR,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: false,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true
+ });
+};
diff --git a/views/ngXosLib/karma.conf.js b/views/ngXosLib/karma.conf.js
index b602d66..49f72a1 100644
--- a/views/ngXosLib/karma.conf.js
+++ b/views/ngXosLib/karma.conf.js
@@ -12,10 +12,11 @@
});
var files = bowerComponents.concat([
- 'api/**/*.js',
+ 'node_modules/babel-polyfill/dist/polyfill.js',
'xosHelpers/src/**/*.module.js',
'xosHelpers/src/**/*.js',
'xosHelpers/spec/**/*.test.js'
+ // 'xosHelpers/spec/ui/form.test.js'
]);
module.exports = function(config) {
@@ -49,7 +50,7 @@
babelPreprocessor: {
options: {
presets: ['es2015'],
- sourceMap: 'inline'
+ sourceMap: 'both'
},
filename: function (file) {
return file.originalPath;
@@ -77,7 +78,7 @@
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
- logLevel: config.LOG_ERROR,
+ logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
@@ -86,7 +87,10 @@
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
- browsers: ['PhantomJS'],
+ browsers: [
+ 'PhantomJS',
+ 'Chrome'
+ ],
// Continuous Integration mode
diff --git a/views/ngXosLib/package.json b/views/ngXosLib/package.json
index 9e8906c..c4f902d 100644
--- a/views/ngXosLib/package.json
+++ b/views/ngXosLib/package.json
@@ -5,9 +5,11 @@
"main": "index.js",
"scripts": {
"test": "karma start",
+ "test:ci": "karma start karma.conf.ci.js",
"apigen": "node apigen/blueprintToNgResource.js",
"swagger": "node xos-swagger-def.js",
"doc": "gulp docs; cd ./docs",
+ "doc:ci": "gulp makeDocs;",
"build": "gulp vendor && gulp helpers",
"dev": "gulp dev"
},
@@ -24,6 +26,7 @@
"swagger-js-codegen": "^1.1.5"
},
"devDependencies": {
+ "babel-polyfill": "^6.7.4",
"babel-preset-es2015": "^6.6.0",
"browser-sync": "^2.12.3",
"concat": "^2.0.0",
@@ -34,13 +37,17 @@
"gulp-concat": "^2.6.0",
"gulp-ng-annotate": "^1.1.0",
"gulp-ngdocs": "^0.2.13",
+ "gulp-rename": "^1.2.2",
+ "gulp-sass": "^2.3.1",
"gulp-sourcemaps": "^1.6.0",
"gulp-uglify": "^1.4.2",
"jasmine-core": "^2.4.1",
- "karma": "^0.13.19",
+ "karma": "^0.13.22",
"karma-babel-preprocessor": "^6.0.1",
"karma-chrome-launcher": "^0.2.3",
+ "karma-coverage": "^0.5.5",
"karma-jasmine": "^0.3.6",
+ "karma-junit-reporter": "^0.4.2",
"karma-mocha-reporter": "^1.1.3",
"karma-ng-html2js-preprocessor": "^0.2.0",
"karma-phantomjs-launcher": "^0.2.1",
diff --git a/views/ngXosLib/xosHelpers/spec/label_formatter.test.js b/views/ngXosLib/xosHelpers/spec/label_formatter.test.js
new file mode 100644
index 0000000..119f4ce
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/label_formatter.test.js
@@ -0,0 +1,42 @@
+(function () {
+ 'use strict';
+
+ describe('The xos.helper module', function(){
+ describe('The label formatter service', () => {
+
+ let service;
+
+ // load the application module
+ beforeEach(module('xos.helpers'));
+
+ // inject the cartService
+ beforeEach(inject(function (_LabelFormatter_) {
+ // The injector unwraps the underscores (_) from around the parameter names when matching
+ service = _LabelFormatter_;
+ }));
+
+ it('should replace underscores in a string', () => {
+ expect(service._formatByUnderscore('my_test')).toEqual('my test');
+ expect(service._formatByUnderscore('_test')).toEqual('test');
+ });
+
+ it('should split a camel case string', () => {
+ expect(service._formatByUppercase('myTest')).toEqual('my test');
+ });
+
+ it('should capitalize a string', () => {
+ expect(service._capitalize('my test')).toEqual('My test');
+ });
+
+ it('should format an object property to a label', () => {
+ expect(service.format('myWeird_String')).toEqual('My weird string:');
+ });
+
+ it('should not add column if already present', () => {
+ expect(service.format('myWeird_String:')).toEqual('My weird string:');
+ });
+
+ });
+ });
+
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/spec/log.test.js b/views/ngXosLib/xosHelpers/spec/log.test.js
new file mode 100644
index 0000000..aa5c737
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/log.test.js
@@ -0,0 +1,55 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/18/16.
+ */
+
+// TODO write tests for log
+
+(function () {
+ 'use strict';
+
+ xdescribe('The xos.helper module', function(){
+
+ let log;
+
+ // beforeEach(module('xos.helpers'));
+
+ var mockLog;
+
+ beforeEach(function() {
+ mockLog = jasmine.createSpyObj('logMock', ['info']);
+ });
+
+ beforeEach(function() {
+ angular.mock.module('xos.helpers', function($injector, $provide) {
+ $provide.value('$log', mockLog);
+ $provide.decorator('$log', $injector.get('logDecorator'));
+ });
+ });
+
+ // beforeEach(inject(($log) => {
+ // log = $log;
+ // log.reset();
+ // }));
+
+ describe('The log decorator', () => {
+ it('should not print anything', inject(($log) => {
+ // spyOn(log, 'info');
+ $log.info('test');
+ // expect(mockLog.info).not.toHaveBeenCalled();
+ }));
+
+ xdescribe('if logging is enabled', () => {
+ beforeEach(() => {
+ window.location.href += '?debug=true'
+ });
+
+ it('should should log', () => {
+ log.info('test');
+ console.log(log.info.logs);
+ });
+ });
+ });
+ });
+})();
diff --git a/views/ngXosLib/xosHelpers/spec/ui/form.test.js b/views/ngXosLib/xosHelpers/spec/ui/form.test.js
new file mode 100644
index 0000000..226a62a
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/ui/form.test.js
@@ -0,0 +1,403 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/18/16.
+ */
+
+(function () {
+ 'use strict';
+
+ describe('The xos.helper module', function(){
+
+ describe('The XosFormHelper service', () => {
+ let service;
+
+ let fields = [
+ 'id',
+ 'name',
+ 'mail',
+ 'active',
+ 'created',
+ 'custom'
+ ];
+
+ let modelField = {
+ id: {},
+ name: {},
+ mail: {},
+ active: {},
+ created: {},
+ custom: {}
+ };
+
+ let model = {
+ id: 1,
+ name: 'test',
+ mail: 'test@onlab.us',
+ active: true,
+ created: '2016-04-18T23:44:16.883181Z',
+ custom: 'MyCustomValue'
+ };
+
+ let customField = {
+ custom: {
+ label: 'Custom Label',
+ type: 'number',
+ validators: {}
+ }
+ };
+
+ let formObject = {
+ id: {
+ label: 'Id:',
+ type: 'number',
+ validators: {}
+ },
+ name: {
+ label: 'Name:',
+ type: 'string',
+ validators: {}
+ },
+ mail: {
+ label: 'Mail:',
+ type: 'email',
+ validators: {}
+ },
+ active: {
+ label: 'Active:',
+ type: 'boolean',
+ validators: {}
+ },
+ created: {
+ label: 'Created:',
+ type: 'date',
+ validators: {}
+ },
+ custom: {
+ label: 'Custom Label:',
+ type: 'number',
+ validators: {}
+ }
+ };
+
+ // load the application module
+ beforeEach(module('xos.helpers'));
+
+ // inject the cartService
+ beforeEach(inject(function (_XosFormHelpers_) {
+ // The injector unwraps the underscores (_) from around the parameter names when matching
+ service = _XosFormHelpers_;
+ }));
+
+ describe('the _isEmail method', () => {
+ it('should return true', () => {
+ expect(service._isEmail('test@onlab.us')).toEqual(true);
+ });
+ it('should return false', () => {
+ expect(service._isEmail('testonlab.us')).toEqual(false);
+ expect(service._isEmail('test@onlab')).toEqual(false);
+ });
+ });
+
+ describe('the _getFieldFormat method', () => {
+ it('should return string', () => {
+ expect(service._getFieldFormat('string')).toEqual('string');
+ expect(service._getFieldFormat(null)).toEqual('string');
+ });
+ it('should return mail', () => {
+ expect(service._getFieldFormat('test@onlab.us')).toEqual('email');
+ });
+ it('should return number', () => {
+ expect(service._getFieldFormat(1)).toEqual('number');
+ // this is skipped because not realistic and js Date sucks
+ // expect(service._getFieldFormat('1')).toEqual('number');
+ });
+ it('should return boolean', () => {
+ expect(service._getFieldFormat(false)).toEqual('boolean');
+ expect(service._getFieldFormat(true)).toEqual('boolean');
+ });
+
+ it('should return date', () => {
+ expect(service._getFieldFormat('2016-04-19T23:09:1092Z')).toEqual('string');
+ expect(service._getFieldFormat(new Date())).toEqual('date');
+ expect(service._getFieldFormat('2016-04-19T23:09:10.208092Z')).toEqual('date');
+ });
+ });
+
+ it('should convert the fields array in an empty form object', () => {
+ expect(service.parseModelField(fields)).toEqual(modelField);
+ });
+
+ describe('when modelField are provided', () => {
+ it('should combine modelField and customField in a form object', () => {
+ expect(service.buildFormStructure(modelField, customField, model)).toEqual(formObject);
+ });
+ });
+
+ describe('when model field is an empty array', () => {
+ let empty_modelField = {
+ // 5: {}
+ };
+ let empty_customFields = {
+ id: {
+ label: 'Id',
+ type: 'number'
+ },
+ name: {
+ label: 'Name',
+ type: 'string'
+ },
+ mail: {
+ label: 'Mail',
+ type: 'email'
+ },
+ active: {
+ label: 'Active',
+ type: 'boolean'
+ },
+ created: {
+ label: 'Created',
+ type: 'date'
+ },
+ custom: {
+ label: 'Custom Label',
+ type: 'number'
+ }
+ };
+
+ let empty_formObject = {
+ id: {
+ label: 'Id:',
+ type: 'number',
+ validators: {}
+ },
+ name: {
+ label: 'Name:',
+ type: 'string',
+ validators: {}
+ },
+ mail: {
+ label: 'Mail:',
+ type: 'email',
+ validators: {}
+ },
+ active: {
+ label: 'Active:',
+ type: 'boolean',
+ validators: {}
+ },
+ created: {
+ label: 'Created:',
+ type: 'date',
+ validators: {}
+ },
+ custom: {
+ label: 'Custom Label:',
+ type: 'number',
+ validators: {}
+ }
+ };
+
+ let empty_model = {5: 'Nan'}
+
+ it('should create a form object', () => {
+ let res = service.buildFormStructure(empty_modelField, empty_customFields, empty_model)
+ expect(res.id).toEqual(empty_formObject.id);
+ expect(res.name).toEqual(empty_formObject.name);
+ expect(res.mail).toEqual(empty_formObject.mail);
+ expect(res.active).toEqual(empty_formObject.active);
+ expect(res.created).toEqual(empty_formObject.created);
+ expect(res.custom).toEqual(empty_formObject.custom);
+ expect(res).toEqual(empty_formObject);
+ });
+ });
+ });
+
+ describe('The xos-form component', () => {
+
+ let element, scope, isolatedScope;
+
+ beforeEach(module('xos.helpers'));
+
+ it('should throw an error if no config is specified', inject(($compile, $rootScope) => {
+ function errorFunctionWrapper(){
+ $compile(angular.element('<xos-form></xos-form>'))($rootScope);
+ $rootScope.$digest();
+ }
+ expect(errorFunctionWrapper).toThrow(new Error('[xosForm] Please provide a configuration via the "config" attribute'));
+ }));
+
+ it('should throw an error if no actions is specified', inject(($compile, $rootScope) => {
+ function errorFunctionWrapper(){
+ let scope = $rootScope.$new();
+ scope.config = 'green';
+ $compile(angular.element('<xos-form config="config"></xos-form>'))(scope);
+ $rootScope.$digest();
+ }
+ expect(errorFunctionWrapper).toThrow(new Error('[xosForm] Please provide an action list in the configuration'));
+ }));
+
+ describe('when correctly configured', () => {
+
+ let cb = jasmine.createSpy('callback');
+
+ beforeEach(inject(($compile, $rootScope) => {
+
+
+ scope = $rootScope.$new();
+
+ scope.config = {
+ exclude: ['excludedField'],
+ formName: 'testForm',
+ actions: [
+ {
+ label: 'Save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: cb,
+ class: 'success'
+ }
+ ],
+ fields: {
+ first_name: {
+ label: 'Custom Label'
+ }
+ }
+ };
+
+ scope.model = {
+ id: 1,
+ first_name: 'Jhon',
+ last_name: 'Snow',
+ age: 25,
+ email: 'test@onlab.us',
+ birthDate: '2016-04-18T23:44:16.883181Z',
+ enabled: true,
+ role: 'user', //select
+ a_permissions: [
+
+ ],
+ o_permissions: {
+
+ }
+ };
+
+ element = angular.element(`<xos-form config="config" ng-model="model"></xos-form>`);
+ $compile(element)(scope);
+ scope.$digest();
+ isolatedScope = element.isolateScope().vm;
+ }));
+
+ it('should add excluded properties to the list', () => {
+ let expected = ['id', 'validators', 'created', 'updated', 'deleted', 'backend_status', 'excludedField'];
+ expect(isolatedScope.excludedField).toEqual(expected);
+ });
+
+ it('should render 8 input field', () => {
+ // boolean are in the form model, but are not input
+ expect(Object.keys(isolatedScope.formField).length).toEqual(9);
+ var field = element[0].getElementsByTagName('input');
+ expect(field.length).toEqual(8);
+ });
+
+ it('should render 1 boolean field', () => {
+ expect($(element).find('.boolean-field > button').length).toEqual(2)
+ });
+
+ it('when clicking on action should invoke callback', () => {
+ var link = $(element).find('[role="button"]');
+ link.click();
+ expect(cb).toHaveBeenCalledWith(scope.model);
+ });
+
+ it('should set a custom label', () => {
+ let nameField = element[0].getElementsByClassName('form-group')[0];
+ let label = angular.element(nameField.getElementsByTagName('label')[0]).text()
+ expect(label).toEqual('Custom Label:');
+ });
+
+ it('should use the correct input type', () => {
+ expect($(element).find('[name="age"]')).toHaveAttr('type', 'number');
+ expect($(element).find('[name="birthDate"]')).toHaveAttr('type', 'date');
+ expect($(element).find('[name="email"]')).toHaveAttr('type', 'email');
+ });
+
+ describe('the boolean field', () => {
+
+ let setFalse, setTrue;
+
+ beforeEach(() => {
+ setFalse= $(element).find('.boolean-field > button:first-child');
+ setTrue = $(element).find('.boolean-field > button:last-child');
+ });
+
+ it('should change value to false', () => {
+ expect(isolatedScope.ngModel.enabled).toEqual(true);
+ setFalse.click()
+ expect(isolatedScope.ngModel.enabled).toEqual(false);
+ });
+
+ it('should change value to false', () => {
+ isolatedScope.ngModel.enabled = false;
+ scope.$apply();
+ expect(isolatedScope.ngModel.enabled).toEqual(false);
+ setTrue.click()
+ expect(isolatedScope.ngModel.enabled).toEqual(true);
+ });
+ });
+
+ // NOTE not sure why this tests are failing
+ xdescribe('the custom validation options', () => {
+ beforeEach(() => {
+ scope.config.fields.first_name.validators = {
+ minlength: 10,
+ maxlength: 15,
+ required: true
+ };
+
+ scope.config.fields.age = {
+ validators: {
+ min: 10,
+ max: 20
+ }
+ };
+
+ scope.$digest();
+ });
+
+ it('should validate required', () => {
+ scope.model.first_name = null;
+ scope.$digest();
+
+ expect(isolatedScope.testForm.first_name.$valid).toBeFalsy();
+ expect(isolatedScope.testForm.first_name.$error.required).toBeTruthy();
+ });
+
+ it('should validate minlength', () => {
+ scope.model.first_name = 'short';
+ scope.$digest();
+
+ expect(isolatedScope.testForm.first_name.$valid).toBeFalsy();
+ expect(isolatedScope.testForm.first_name.$error.minlength).toBeTruthy();
+ });
+
+ it('should validate maxlength', () => {
+ scope.model.first_name = 'this is way too long!';
+ scope.$digest();
+
+ expect(isolatedScope.testForm.first_name.$valid).toBeFalsy();
+ expect(isolatedScope.testForm.first_name.$error.maxlength).toBeTruthy();
+ });
+
+ it('should validate min', () => {
+ // not validating min and max for now
+ scope.model.age = 8;
+ scope.$digest();
+
+ expect(isolatedScope.testForm.age.$valid).toBeFalsy();
+ expect(isolatedScope.testForm.age.$error.min).toBeTruthy();
+ });
+ });
+ });
+ });
+ });
+})();
diff --git a/views/ngXosLib/xosHelpers/spec/ui/smart-table.test.js b/views/ngXosLib/xosHelpers/spec/ui/smart-table.test.js
new file mode 100644
index 0000000..09445ca
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/ui/smart-table.test.js
@@ -0,0 +1,224 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+ 'use strict';
+
+ let mockData;
+
+ describe('The xos.helper module', function(){
+ describe('The xos-smart-table component', () => {
+
+ var spy, emptySpy, scope, isolatedScope, element;
+
+ beforeEach(module('xos.helpers'));
+
+ beforeEach(function() {
+
+ // set mockData
+ mockData = [
+ {
+ id: 1,
+ first_name: 'Jon',
+ last_name: 'Snow',
+ hidden_field: 'hidden'
+ }
+ ]
+
+ jasmine.addMatchers({
+ toBeInstanceOf: function() {
+
+ return {
+ compare: (actual, expected) => {
+ var actual = actual;
+ var result = {};
+ result.pass = actual instanceof expected.constructor;
+
+ result.message = 'Expected ' + actual + ' to be instance of ' + expected;
+
+ return result;
+ },
+ negativeCompare: (actual, expected) => {
+ var actual = actual;
+ var result = {};
+ result.pass = actual instanceof expected.constructor === false;
+
+ result.message = 'Expected ' + actual + ' to be instance of ' + expected;
+
+ return result;
+ }
+ }
+ }
+ });
+ });
+
+ // mock the service
+ beforeEach(function(){
+ module(function($provide){
+ $provide.service('MockResource', function(){
+ return {
+ query: '',
+ delete: ''
+ }
+ });
+
+ $provide.service('EmptyResource', function(){
+ return {
+ query: ''
+ }
+ });
+ });
+ })
+
+ beforeEach(inject(function ($compile, $rootScope, $q, MockResource) {
+ scope = $rootScope.$new();
+
+ scope.config = {
+ resource: 'MockResource',
+ hiddenFields: ['hidden_field']
+ };
+
+ spy = MockResource;
+
+ spyOn(MockResource, 'query').and.callFake(function() {
+ var deferred = $q.defer();
+ deferred.resolve(mockData);
+ return {$promise: deferred.promise};
+ });
+
+ spyOn(MockResource, 'delete').and.callFake(function() {
+ var deferred = $q.defer();
+ deferred.resolve();
+ return {$promise: deferred.promise};
+ });
+
+ element = angular.element('<xos-smart-table config="config"></xos-smart-table>');
+ $compile(element)(scope);
+ scope.$digest();
+ isolatedScope = element.isolateScope().vm;
+ }));
+
+ it('should query elements', () => {
+ expect(spy.query).toHaveBeenCalled();
+ expect($(element).find('.alert').parent().parent()).toHaveClass('ng-hide');
+ });
+
+ it('should hide hidden fields', () => {
+ // the 4th field is the mocked save method
+ expect($(element).find('thead th').length).toEqual(3);
+ expect($(element).find('thead th')[0]).toContainText('First name:');
+ expect($(element).find('thead th')[1]).toContainText('Last name:');
+ expect($(element).find('thead th')[2]).toContainText('Actions:');
+ });
+
+ it('should delete a model', () => {
+ // saving mockData (they are going to be deleted)
+ let mock = angular.copy(mockData);
+ $(element).find('a[title="delete"]')[0].click();
+ expect(spy.delete).toHaveBeenCalledWith({id: mock[0].id});
+ expect($(element).find('.alert')).toContainText(`MockResource with id ${mock[0].id} successfully deleted`);
+ });
+
+ it('should show the form', () => {
+ expect($(element).find('.panel')[0]).toHaveClass('ng-hide');
+ $(element).find('a[title="details"]')[0].click();
+ expect($(element).find('.panel')).not.toHaveClass('ng-hide');
+ });
+
+ it('should hide the form', () => {
+ isolatedScope.detailedItem = {
+ some: 'model'
+ };
+ scope.$apply();
+ expect($(element).find('.panel')).not.toHaveClass('ng-hide');
+ $(element).find('.panel .col-xs-1 a')[0].click();
+ expect($(element).find('.panel')[0]).toHaveClass('ng-hide');
+ });
+
+ it('should save an item', inject(($q) => {
+
+ let model = {
+ id: 1,
+ first_name: 'Jon',
+ last_name: 'Snow',
+ hidden_field: 'hidden',
+ $save: '',
+ $update: ''
+ };
+
+ spyOn(model, '$save').and.callFake(function() {
+ var deferred = $q.defer();
+ deferred.resolve();
+ return deferred.promise;
+ });
+
+ spyOn(model, '$update').and.callFake(function() {
+ var deferred = $q.defer();
+ deferred.resolve();
+ return deferred.promise;
+ });
+
+ isolatedScope.detailedItem = model;
+ scope.$apply();
+ $(element).find('xos-form .btn.btn-success').click();
+ expect(model.$update).toHaveBeenCalled();
+ }));
+
+ it('should have an add button', () => {
+ let addBtn = $(element).find('.row .btn.btn-success');
+ expect(addBtn.parent().parent()).not.toHaveClass('ng-hide');
+ });
+
+ describe('when the add button is clicked', () => {
+ beforeEach(() => {
+ let btn = $(element).find('.row .btn.btn-success')
+ btn[0].click();
+ });
+
+ xit('should create a new model', () => {
+ expect(isolatedScope.detailedItem).toBeDefined();
+ expect(isolatedScope.detailedItem).toBeInstanceOf('Resource');
+ });
+ });
+
+ describe('when fetching an empty collection', () => {
+ beforeEach(inject(function ($compile, $rootScope, $q, EmptyResource) {
+ scope = $rootScope.$new();
+
+ scope.config = {
+ resource: 'EmptyResource'
+ };
+
+ emptySpy = EmptyResource;
+
+ spyOn(EmptyResource, 'query').and.callFake(function() {
+ var deferred = $q.defer();
+ deferred.resolve([]);
+ return {$promise: deferred.promise};
+ });
+
+ element = angular.element('<xos-smart-table config="config"></xos-smart-table>');
+ $compile(element)(scope);
+ scope.$digest();
+ isolatedScope = element.isolateScope().vm;
+ }));
+
+ it('should display an alert', () => {
+ expect(emptySpy.query).toHaveBeenCalled();
+ expect($(element).find('.alert').parent().parent()).not.toHaveClass('ng-hide');
+ expect($(element).find('.alert')).toContainText('No data to show');
+ });
+
+ it('should not have an add button', () => {
+ let addBtn = $(element).find('.row .btn.btn-success');
+ expect(addBtn.parent().parent()).toHaveClass('ng-hide');
+ });
+ });
+
+
+ });
+ });
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/spec/ui/table.test.js b/views/ngXosLib/xosHelpers/spec/ui/table.test.js
index 50a5d72..7279995 100644
--- a/views/ngXosLib/xosHelpers/spec/ui/table.test.js
+++ b/views/ngXosLib/xosHelpers/spec/ui/table.test.js
@@ -91,6 +91,66 @@
});
});
+ xdescribe('when a field type is provided', () => {
+ describe('and is boolean', () => {
+ beforeEach(() => {
+ console.log('iS: ' + isolatedScope);
+ isolatedScope.config = {
+ columns: [
+ {
+ label: 'Label 1',
+ prop: 'label-1',
+ type: 'boolean'
+ }
+ ]
+ };
+ isolatedScope.data = [
+ {
+ 'label-1': true
+ },
+ {
+ 'label-1': false
+ }
+ ];
+ scope.$digest();
+ });
+
+ it('should render an incon', () => {
+ let td1 = $(element).find('tbody tr:first-child td')[0];
+ let td2 = $(element).find('tbody tr:last-child td')[0];
+ console.log(td2);
+ expect($(td1).find('i')).toHaveClass('glyphicon-ok');
+ expect($(td2).find('i')).toHaveClass('glyphicon-remove');
+ });
+ });
+
+ describe('and is date', () => {
+ beforeEach(() => {
+ console.log('iS: ' + isolatedScope);
+ isolatedScope.config = {
+ columns: [
+ {
+ label: 'Label 1',
+ prop: 'label-1',
+ type: 'date'
+ }
+ ]
+ };
+ isolatedScope.data = [
+ {
+ 'label-1': '2015-02-17T22:06:38.059000Z'
+ }
+ ];
+ scope.$digest();
+ });
+
+ it('should render an formatted date', () => {
+ let td1 = $(element).find('tbody tr:first-child td')[0];
+ expect($(td1).text()).toEqual('02-17-2015 14:06:38');
+ });
+ });
+ });
+
describe('when actions are passed', () => {
let cb = jasmine.createSpy('callback')
diff --git a/views/ngXosLib/xosHelpers/spec/ui/validation.test.js b/views/ngXosLib/xosHelpers/spec/ui/validation.test.js
new file mode 100644
index 0000000..782f03f
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/spec/ui/validation.test.js
@@ -0,0 +1,68 @@
+/**
+ * © OpenCORD
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+ 'use strict';
+
+ describe('The xos.helper module', function(){
+ describe('The xos-validation component', () => {
+
+ let element, scope, isolatedScope;
+
+ beforeEach(module('xos.helpers'));
+
+ beforeEach(inject(($compile, $rootScope) => {
+
+ scope = $rootScope.$new();
+
+ scope.errors = {};
+
+ element = angular.element(`<xos-validation errors="errors"></xos-validation>`);
+ $compile(element)(scope);
+ scope.$digest();
+ isolatedScope = element.isolateScope().vm;
+ }));
+
+ it('should not show an alert', () => {
+ expect($(element).find('xos-alert > .alert')[0]).toHaveClass('ng-hide');
+ });
+
+ let availableErrors = [
+ {
+ type: 'required',
+ message: 'Field required'
+ },
+ {
+ type: 'email',
+ message: 'This is not a valid email'
+ },
+ {
+ type: 'minlength',
+ message: 'Too short'
+ },
+ {
+ type: 'maxlength',
+ message: 'Too long'
+ },
+ {
+ type: 'custom',
+ message: 'Field invalid'
+ },
+ ];
+
+ // use a loop to generate similar test
+ availableErrors.forEach((e, i) => {
+ it(`should show an alert for ${e.type} errors`, () => {
+ scope.errors[e.type] = true;
+ scope.$digest();
+ let alert = $(element).find('xos-alert > .alert')[i];
+ expect(alert).not.toHaveClass('ng-hide');
+ expect(alert).toHaveText(e.message);
+ });
+ });
+ });
+ });
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/label_formatter.service.js b/views/ngXosLib/xosHelpers/src/services/label_formatter.service.js
new file mode 100644
index 0000000..3b63ecd
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/label_formatter.service.js
@@ -0,0 +1,39 @@
+(function() {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name xos.helpers.LabelFormatter
+ * @description This factory define a set of helper function to format label started from an object property
+ **/
+
+ angular
+ .module('xos.uiComponents')
+ .factory('LabelFormatter', labelFormatter);
+
+ function labelFormatter() {
+
+ const _formatByUnderscore = string => string.split('_').join(' ').trim();
+
+ const _formatByUppercase = string => string.split(/(?=[A-Z])/).map(w => w.toLowerCase()).join(' ');
+
+ const _capitalize = string => string.slice(0, 1).toUpperCase() + string.slice(1);
+
+ const format = (string) => {
+ string = _formatByUnderscore(string);
+ string = _formatByUppercase(string);
+
+ string = _capitalize(string).replace(/\s\s+/g, ' ') + ':';
+ return string.replace('::', ':');
+ };
+
+ return {
+ // test export
+ _formatByUnderscore: _formatByUnderscore,
+ _formatByUppercase: _formatByUppercase,
+ _capitalize: _capitalize,
+ // export to use
+ format: format
+ };
+ }
+})();
diff --git a/views/ngXosLib/xosHelpers/src/services/log.decorator.js b/views/ngXosLib/xosHelpers/src/services/log.decorator.js
new file mode 100644
index 0000000..382f78e
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/log.decorator.js
@@ -0,0 +1,40 @@
+// TODO write tests for log
+
+angular.module('xos.helpers')
+.config([ '$provide', function( $provide )
+{
+ // Use the `decorator` solution to substitute or attach behaviors to
+ // original service instance; @see angular-mocks for more examples....
+
+ $provide.decorator( '$log', [ '$delegate', function( $delegate )
+ {
+
+ const isLogEnabled = () => {
+ return window.location.href.indexOf('debug=true') >= 0;
+ }
+ // Save the original $log.debug()
+ let debugFn = $delegate.info;
+
+ // create the replacement function
+ const replacement = (fn) => {
+ return function(){
+ if(!isLogEnabled()){
+ console.log('logging is disabled');
+ return;
+ }
+ let args = [].slice.call(arguments);
+ let now = new Date();
+
+ // Prepend timestamp
+ args[0] = `[${now.getHours()}:${now.getMinutes()}:${now.getSeconds()}] ${args[0]}`;
+
+ // Call the original with the output prepended with formatted timestamp
+ fn.apply(null, args)
+ };
+ };
+
+ $delegate.info = replacement(debugFn);
+
+ return $delegate;
+ }]);
+}]);
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Deployments.js b/views/ngXosLib/xosHelpers/src/services/rest/Deployments.js
new file mode 100644
index 0000000..4fecbc2
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Deployments.js
@@ -0,0 +1,15 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Deployments
+ * @description Angular resource to fetch /api/core/deployments/:id/
+ **/
+ .service('Deployments', function($resource){
+ return $resource('/api/core/deployments/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Example.js b/views/ngXosLib/xosHelpers/src/services/rest/Example.js
new file mode 100644
index 0000000..b13ccda
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Example.js
@@ -0,0 +1,13 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Example-Services-Collection
+ * @description Angular resource to fetch /api/service/exampleservice/
+ **/
+ .service('Example-Services-Collection', function($resource){
+ return $resource('/api/service/exampleservice/');
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Flavors.js b/views/ngXosLib/xosHelpers/src/services/rest/Flavors.js
new file mode 100644
index 0000000..348b770
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Flavors.js
@@ -0,0 +1,15 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Flavors
+ * @description Angular resource to fetch /api/core/flavors/:id/
+ **/
+ .service('Flavors', function($resource){
+ return $resource('/api/core/flavors/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Instances.js b/views/ngXosLib/xosHelpers/src/services/rest/Instances.js
new file mode 100644
index 0000000..f1e8521
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Instances.js
@@ -0,0 +1,15 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Instances
+ * @description Angular resource to fetch /api/core/instances/:id/
+ **/
+ .service('Instances', function($resource){
+ return $resource('/api/core/instances/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Nodes.js b/views/ngXosLib/xosHelpers/src/services/rest/Nodes.js
new file mode 100644
index 0000000..bf2e387
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Nodes.js
@@ -0,0 +1,15 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Nodes
+ * @description Angular resource to fetch /api/core/nodes/:id/
+ **/
+ .service('Nodes', function($resource){
+ return $resource('/api/core/nodes/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Sites.js b/views/ngXosLib/xosHelpers/src/services/rest/Sites.js
new file mode 100644
index 0000000..818d741
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Sites.js
@@ -0,0 +1,15 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Sites
+ * @description Angular resource to fetch /api/core/sites/:id/
+ **/
+ .service('Sites', function($resource){
+ return $resource('/api/core/sites/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Slices.js b/views/ngXosLib/xosHelpers/src/services/rest/Slices.js
new file mode 100644
index 0000000..5a0da11
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Slices.js
@@ -0,0 +1,15 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Slices
+ * @description Angular resource to fetch /api/core/slices/:id/
+ **/
+ .service('Slices', function($resource){
+ return $resource('/api/core/slices/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Subscribers.js b/views/ngXosLib/xosHelpers/src/services/rest/Subscribers.js
index ebab252..342d856 100644
--- a/views/ngXosLib/xosHelpers/src/services/rest/Subscribers.js
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Subscribers.js
@@ -8,7 +8,9 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/
**/
.service('Subscribers', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
/**
* @ngdoc service
@@ -16,7 +18,9 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/
**/
.service('Subscriber-features', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
/**
* @ngdoc service
@@ -24,7 +28,9 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/uplink_speed/
**/
.service('Subscriber-features-uplink_speed', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/uplink_speed/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/uplink_speed/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
/**
* @ngdoc service
@@ -32,7 +38,9 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/downlink_speed/
**/
.service('Subscriber-features-downlink_speed', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/downlink_speed/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/downlink_speed/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
/**
* @ngdoc service
@@ -40,7 +48,9 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/cdn/
**/
.service('Subscriber-features-cdn', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/cdn/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/cdn/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
/**
* @ngdoc service
@@ -48,7 +58,9 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/uverse/
**/
.service('Subscriber-features-uverse', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/uverse/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/uverse/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
/**
* @ngdoc service
@@ -56,6 +68,8 @@
* @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/status/
**/
.service('Subscriber-features-status', function($resource){
- return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/status/', { subscriber_id: '@id' });
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/status/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Truckroll.js b/views/ngXosLib/xosHelpers/src/services/rest/Truckroll.js
index 9927967..0895a99 100644
--- a/views/ngXosLib/xosHelpers/src/services/rest/Truckroll.js
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Truckroll.js
@@ -8,6 +8,8 @@
* @description Angular resource to fetch /api/tenant/truckroll/:truckroll_id/
**/
.service('Truckroll-Collection', function($resource){
- return $resource('/api/tenant/truckroll/:truckroll_id/', { truckroll_id: '@id' });
+ return $resource('/api/tenant/truckroll/:truckroll_id/', { truckroll_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Users.js b/views/ngXosLib/xosHelpers/src/services/rest/Users.js
index deb18a4..8be0fdd 100644
--- a/views/ngXosLib/xosHelpers/src/services/rest/Users.js
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Users.js
@@ -5,9 +5,11 @@
/**
* @ngdoc service
* @name xos.helpers.Users
- * @description Angular resource to fetch /api/core/users/
+ * @description Angular resource to fetch /api/core/users/:id/
**/
.service('Users', function($resource){
- return $resource('/api/core/users/');
+ return $resource('/api/core/users/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/Utility.js b/views/ngXosLib/xosHelpers/src/services/rest/Utility.js
new file mode 100644
index 0000000..a735c46
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/services/rest/Utility.js
@@ -0,0 +1,21 @@
+(function() {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Login
+ * @description Angular resource to fetch /api/utility/login/
+ **/
+ .service('Login', function($resource){
+ return $resource('/api/utility/login/');
+ })
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Logout
+ * @description Angular resource to fetch /api/utility/logout/
+ **/
+ .service('Logout', function($resource){
+ return $resource('/api/utility/logout/');
+ })
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/services/rest/vOLT.js b/views/ngXosLib/xosHelpers/src/services/rest/vOLT.js
index f182c06..424c48d 100644
--- a/views/ngXosLib/xosHelpers/src/services/rest/vOLT.js
+++ b/views/ngXosLib/xosHelpers/src/services/rest/vOLT.js
@@ -8,6 +8,8 @@
* @description Angular resource to fetch /api/tenant/cord/volt/:volt_id/
**/
.service('vOLT-Collection', function($resource){
- return $resource('/api/tenant/cord/volt/:volt_id/', { volt_id: '@id' });
+ return $resource('/api/tenant/cord/volt/:volt_id/', { volt_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
})
})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/styles/animations.scss b/views/ngXosLib/xosHelpers/src/styles/animations.scss
new file mode 100644
index 0000000..f565ff7
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/styles/animations.scss
@@ -0,0 +1,44 @@
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible;
+ }
+
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0);
+ }
+
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0);
+ }
+}
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0);
+ }
+
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1;
+ }
+
+ to {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0);
+ }
+}
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/styles/main.scss b/views/ngXosLib/xosHelpers/src/styles/main.scss
new file mode 100644
index 0000000..800466e
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/styles/main.scss
@@ -0,0 +1,17 @@
+@import './animations.scss';
+@import '../../../../../../style/sass/bootstrap/bootstrap/_variables.scss';
+
+@import '../ui_components/dumbComponents/table/table.scss';
+@import '../ui_components/dumbComponents/alert/alert.scss';
+@import '../ui_components/dumbComponents/validation/validation.scss';
+
+@import '../ui_components/smartComponents/smartTable/smartTable.scss';
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+ display: none !important;
+}
+
+.row + .row {
+ /* TODO move in xos.scss*/
+ margin-top: $form-group-margin-bottom;
+}
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/table/alert.component.js b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/alert/alert.component.js
similarity index 94%
rename from views/ngXosLib/xosHelpers/src/ui_components/table/alert.component.js
rename to views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/alert/alert.component.js
index 7d7a026..9b60def 100644
--- a/views/ngXosLib/xosHelpers/src/ui_components/table/alert.component.js
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/alert/alert.component.js
@@ -75,7 +75,7 @@
</file>
</example>
- <example module="sampleAlert2">
+ <example module="sampleAlert2" animations="true">
<file name="index.html">
<div ng-controller="SampleCtrl as vm" class="row">
<div class="col-sm-4">
@@ -90,7 +90,7 @@
</div>
</file>
<file name="script.js">
- angular.module('sampleAlert2', ['xos.uiComponents'])
+ angular.module('sampleAlert2', ['xos.uiComponents', 'ngAnimate'])
.controller('SampleCtrl', function(){
this.config1 = {
type: 'success'
@@ -110,7 +110,7 @@
show: '=?'
},
template: `
- <div class="alert alert-{{vm.config.type}}" ng-show="vm.show">
+ <div ng-cloak class="alert alert-{{vm.config.type}}" ng-hide="!vm.show">
<button type="button" class="close" ng-if="vm.config.closeBtn" ng-click="vm.dismiss()">
<span aria-hidden="true">×</span>
</button>
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/alert/alert.scss b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/alert/alert.scss
new file mode 100644
index 0000000..f1330fe
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/alert/alert.scss
@@ -0,0 +1,10 @@
+@import '../../../styles/animations.scss';
+
+xos-alert {
+
+ /* when hiding */
+ .ng-hide-add { animation:0.5s fadeOutDown ease-in-out; }
+
+ /* when showing */
+ .ng-hide-remove { animation:0.5s fadeInUp ease-in-out; }
+}
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/form/form.component.js b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/form/form.component.js
new file mode 100644
index 0000000..5294229
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/form/form.component.js
@@ -0,0 +1,308 @@
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 4/18/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosForm
+ * @restrict E
+ * @description The xos-form directive.
+ * This components have two usage, given a model it is able to autogenerate a form or it can be configured to create a custom form.
+ * @param {Object} config The configuration object
+ * ```
+ * {
+ * exclude: ['id', 'validators', 'created', 'updated', 'deleted'], //field to be skipped in the form, the provide values are concatenated
+ * actions: [ // define the form buttons with related callback
+ * {
+ label: 'save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ class: 'success'
+ }
+ * ],
+ * fields: {
+ * field_name: {
+ * label: 'Field Label',
+ * type: 'string' // options are: [date, boolean, number, email, string],
+ * validators: {
+ * minlength: number,
+ maxlength: number,
+ required: boolean,
+ min: number,
+ max: number
+ * }
+ * }
+ * }
+ * }
+ * ```
+ * @element ANY
+ * @scope
+ * @example
+
+ Autogenerated form
+
+ <example module="sampleForm">
+ <file name="script.js">
+ angular.module('sampleForm', ['xos.uiComponents'])
+ .factory('_', function($window){
+ return $window._;
+ })
+ .controller('SampleCtrl', function(){
+ this.model = {
+ first_name: 'Jhon',
+ last_name: 'Doe',
+ email: 'jhon.doe@sample.com',
+ active: true,
+ birthDate: '2015-02-17T22:06:38.059000Z'
+ }
+ this.config = {
+ exclude: ['password', 'last_login'],
+ formName: 'sampleForm',
+ actions: [
+ {
+ label: 'Save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ class: 'success'
+ }
+ ]
+ };
+ });
+ </file>
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm">
+ <xos-form ng-model="vm.model" config="vm.config"></xos-form>
+ </div>
+ </file>
+ </example>
+
+ Configuration defined form
+
+ <example module="sampleForm1">
+ <file name="script.js">
+ angular.module('sampleForm1', ['xos.uiComponents'])
+ .factory('_', function($window){
+ return $window._;
+ })
+ .controller('SampleCtrl1', function(){
+ this.model = {
+ };
+
+ this.config = {
+ exclude: ['password', 'last_login'],
+ formName: 'sampleForm1',
+ actions: [
+ {
+ label: 'Save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ class: 'success'
+ }
+ ],
+ fields: {
+ first_name: {
+ type: 'string',
+ validators: {
+ required: true
+ }
+ },
+ last_name: {
+ label: 'Surname',
+ type: 'string',
+ validators: {
+ required: true,
+ minlength: 10
+ }
+ },
+ age: {
+ type: 'number',
+ validators: {
+ required: true,
+ min: 21
+ }
+ },
+ }
+ };
+ });
+ </file>
+ <file name="index.html">
+ <div ng-controller="SampleCtrl1 as vm">
+ <xos-form ng-model="vm.model" config="vm.config"></xos-form>
+ </div>
+ </file>
+ </example>
+
+ **/
+
+ .directive('xosForm', function(){
+ return {
+ restrict: 'E',
+ scope: {
+ config: '=',
+ ngModel: '='
+ },
+ template: `
+ <ng-form name="vm.{{vm.config.formName || 'form'}}">
+ <div class="form-group" ng-repeat="(name, field) in vm.formField">
+ <label>{{field.label}}</label>
+ <input
+ ng-if="field.type !== 'boolean'"
+ type="{{field.type}}"
+ name="{{name}}"
+ class="form-control"
+ ng-model="vm.ngModel[name]"
+ ng-minlength="field.validators.minlength || 0"
+ ng-maxlength="field.validators.maxlength || 2000"
+ ng-required="field.validators.required || false" />
+ <span class="boolean-field" ng-if="field.type === 'boolean'">
+ <button
+ class="btn btn-success"
+ ng-show="vm.ngModel[name]"
+ ng-click="vm.ngModel[name] = false">
+ <i class="glyphicon glyphicon-ok"></i>
+ </button>
+ <button
+ class="btn btn-danger"
+ ng-show="!vm.ngModel[name]"
+ ng-click="vm.ngModel[name] = true">
+ <i class="glyphicon glyphicon-remove"></i>
+ </button>
+ </span>
+ <!-- <pre>{{vm[vm.config.formName][name].$error | json}}</pre> -->
+ <xos-validation errors="vm[vm.config.formName || 'form'][name].$error"></xos-validation>
+ </div>
+ <div class="form-group" ng-if="vm.config.actions">
+ <button role="button" href=""
+ ng-repeat="action in vm.config.actions"
+ ng-click="action.cb(vm.ngModel)"
+ class="btn btn-{{action.class}}"
+ title="{{action.label}}">
+ <i class="glyphicon glyphicon-{{action.icon}}"></i>
+ {{action.label}}
+ </button>
+ </div>
+ </ng-form>
+ `,
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: function($scope, $log, _, XosFormHelpers){
+
+ if(!this.config){
+ throw new Error('[xosForm] Please provide a configuration via the "config" attribute');
+ }
+
+ if(!this.config.actions){
+ throw new Error('[xosForm] Please provide an action list in the configuration');
+ }
+
+ this.excludedField = ['id', 'validators', 'created', 'updated', 'deleted', 'backend_status'];
+ if(this.config && this.config.exclude){
+ this.excludedField = this.excludedField.concat(this.config.exclude);
+ }
+
+
+ this.formField = [];
+ $scope.$watch(() => this.ngModel, (model) => {
+
+ // empty from old stuff
+ this.formField = {};
+
+ if(!model){
+ return;
+ }
+
+ let diff = _.difference(Object.keys(model), this.excludedField);
+ let modelField = XosFormHelpers.parseModelField(diff);
+ this.formField = XosFormHelpers.buildFormStructure(modelField, this.config.fields, model);
+ });
+
+ }
+ }
+ })
+ .service('XosFormHelpers', function(_, LabelFormatter){
+
+ this._isEmail = (text) => {
+ var re = /(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
+ return re.test(text);
+ };
+
+ this._getFieldFormat = (value) => {
+
+ // check if is date
+ if (_.isDate(value) || (!Number.isNaN(Date.parse(value)) && new Date(value).getTime() > 631180800000)){
+ return 'date';
+ }
+
+ // check if is boolean
+ // isNaN(false) = false, false is a number (0), true is a number (1)
+ if(typeof value === 'boolean'){
+ return 'boolean';
+ }
+
+ // check if a string is a number
+ if(!isNaN(value) && value !== null){
+ return 'number';
+ }
+
+ // check if a string is an email
+ if(this._isEmail(value)){
+ return 'email';
+ }
+
+ // if null return string
+ if(value === null){
+ return 'string';
+ }
+
+ return typeof value;
+ };
+
+ this.buildFormStructure = (modelField, customField, model) => {
+
+ modelField = Object.keys(modelField).length > 0 ? modelField : customField; //if no model field are provided, check custom
+ customField = customField || {};
+
+ return _.reduce(Object.keys(modelField), (form, f) => {
+
+ form[f] = {
+ label: (customField[f] && customField[f].label) ? `${customField[f].label}:` : LabelFormatter.format(f),
+ type: (customField[f] && customField[f].type) ? customField[f].type : this._getFieldFormat(model[f]),
+ validators: (customField[f] && customField[f].validators) ? customField[f].validators : {}
+ };
+
+ if(form[f].type === 'date'){
+ model[f] = new Date(model[f]);
+ }
+
+ if(form[f].type === 'number'){
+ model[f] = parseInt(model[f], 10);
+ }
+
+ return form;
+ }, {});
+ };
+
+ this.parseModelField = (fields) => {
+ return _.reduce(fields, (form, f) => {
+ form[f] = {};
+ return form;
+ }, {});
+ }
+
+ })
+})();
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/table/pagination.component.js b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/pagination/pagination.component.js
similarity index 100%
rename from views/ngXosLib/xosHelpers/src/ui_components/table/pagination.component.js
rename to views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/pagination/pagination.component.js
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/table/table.component.js b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/table/table.component.js
similarity index 95%
rename from views/ngXosLib/xosHelpers/src/ui_components/table/table.component.js
rename to views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/table/table.component.js
index daca484..745c3ca 100644
--- a/views/ngXosLib/xosHelpers/src/ui_components/table/table.component.js
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/table/table.component.js
@@ -81,14 +81,14 @@
</file>
</example>
- <example module="sampleTable2">
+ <example module="sampleTable2" animations="true">
<file name="index.html">
<div ng-controller="SampleCtrl2 as vm">
<xos-table data="vm.data" config="vm.config"></xos-table>
</div>
</file>
<file name="script.js">
- angular.module('sampleTable2', ['xos.uiComponents'])
+ angular.module('sampleTable2', ['xos.uiComponents', 'ngAnimate'])
.controller('SampleCtrl2', function(){
this.config = {
columns: [
@@ -196,7 +196,7 @@
ng-model="vm.query"/>
</div>
</div>
- <table ng-class="vm.classes" ng-show="vm.data.length > 0">
+ <table ng-class="vm.classes" ng-hide="vm.data.length == 0">
<thead>
<tr>
<th ng-repeat="col in vm.columns">
@@ -210,7 +210,7 @@
</a>
</span>
</th>
- <th ng-if="vm.config.actions">Actions</th>
+ <th ng-if="vm.config.actions">Actions:</th>
</tr>
</thead>
<tbody ng-if="vm.config.filter == 'field'">
@@ -249,9 +249,9 @@
</xos-pagination>
</div>
<div ng-show="vm.data.length == 0 || !vm.data">
- <div class="alert alert-info">
+ <xos-alert config="{type: 'info'}">
No data to show.
- </div>
+ </xos-alert>
</div>
`,
bindToController: true,
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/table/table.scss b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/table/table.scss
new file mode 100644
index 0000000..46c4460
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/table/table.scss
@@ -0,0 +1,26 @@
+@import '../../../styles/animations.scss';
+
+xos-table {
+
+ display: block;
+
+ tr.ng-move,
+ tr.ng-enter,
+ tr.ng-leave {
+ transition:all linear 0.5s;
+ }
+
+ tr.ng-leave.ng-leave-active,
+ tr.ng-move,
+ tr.ng-enter {
+ opacity:0;
+ animation: 0.5s slideOutRight ease-in-out;
+ }
+
+ tr.ng-leave,
+ tr.ng-move.ng-move-active,
+ tr.ng-enter.ng-enter-active {
+ opacity:1;
+ animation: 0.5s slideInRight ease-in-out;
+ }
+}
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/validation/validation.component.js b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/validation/validation.component.js
new file mode 100644
index 0000000..84eb91a
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/validation/validation.component.js
@@ -0,0 +1,109 @@
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosValidation
+ * @restrict E
+ * @description The xos-validation directive
+ * @param {Object} errors The error object
+ * @element ANY
+ * @scope
+ * @example
+ <example module="sampleValidation">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm">
+ <div class="row">
+ <div class="col-xs-12">
+ <label>Set an error type:</label>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.required = !vm.errors.required"
+ ng-class="{'btn-default': !vm.errors.required, 'btn-success': vm.errors.required}">
+ Required
+ </a>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.email = !vm.errors.email"
+ ng-class="{'btn-default': !vm.errors.email, 'btn-success': vm.errors.email}">
+ Email
+ </a>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.minlength = !vm.errors.minlength"
+ ng-class="{'btn-default': !vm.errors.minlength, 'btn-success': vm.errors.minlength}">
+ Min Length
+ </a>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.maxlength = !vm.errors.maxlength"
+ ng-class="{'btn-default': !vm.errors.maxlength, 'btn-success': vm.errors.maxlength}">
+ Max Length
+ </a>
+ </div>
+ </div>
+ <xos-validation errors="vm.errors"></xos-validation>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleValidation', ['xos.uiComponents'])
+ .controller('SampleCtrl', function(){
+ this.errors = {
+ email: false
+ }
+ });
+ </file>
+ </example>
+ */
+
+ .directive('xosValidation', function(){
+ return {
+ restrict: 'E',
+ scope: {
+ errors: '='
+ },
+ template: `
+ <div ng-cloak>
+ <!-- <pre>{{vm.errors.email | json}}</pre> -->
+ <xos-alert config="vm.config" show="vm.errors.required !== undefined && vm.errors.required !== false">
+ Field required
+ </xos-alert>
+ <xos-alert config="vm.config" show="vm.errors.email !== undefined && vm.errors.email !== false">
+ This is not a valid email
+ </xos-alert>
+ <xos-alert config="vm.config" show="vm.errors.minlength !== undefined && vm.errors.minlength !== false">
+ Too short
+ </xos-alert>
+ <xos-alert config="vm.config" show="vm.errors.maxlength !== undefined && vm.errors.maxlength !== false">
+ Too long
+ </xos-alert>
+ <xos-alert config="vm.config" show="vm.errors.custom !== undefined && vm.errors.custom !== false">
+ Field invalid
+ </xos-alert>
+ </div>
+ `,
+ transclude: true,
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: function(){
+ this.config = {
+ type: 'danger'
+ }
+ }
+ }
+ })
+})();
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/validation/validation.scss b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/validation/validation.scss
new file mode 100644
index 0000000..aaf6e2f
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/dumbComponents/validation/validation.scss
@@ -0,0 +1,7 @@
+@import '../../../styles/animations.scss';
+@import '../../../../../../style/sass/bootstrap/bootstrap/_variables.scss';
+
+input + xos-validation {
+ margin-top: $form-group-margin-bottom;
+ display: block;
+}
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/smartComponents/smartTable/smartTable.component.js b/views/ngXosLib/xosHelpers/src/ui_components/smartComponents/smartTable/smartTable.component.js
new file mode 100644
index 0000000..8cbe0af
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/smartComponents/smartTable/smartTable.component.js
@@ -0,0 +1,262 @@
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosSmartTable
+ * @link xos.uiComponents.directive:xosTable xosTable
+ * @link xos.uiComponents.directive:xosForm xosForm
+ * @restrict E
+ * @description The xos-table directive
+ * @param {Object} config The configuration for the component,
+ * it is composed by the name of an angular [$resource](https://docs.angularjs.org/api/ngResource/service/$resource)
+ * and an array of fields that shouldn't be printed.
+ * ```
+ * {
+ resource: 'Users',
+ hiddenFields: []
+ }
+ * ```
+ * @scope
+ * @example
+
+ <example module="sampleSmartTable">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm">
+ <xos-smart-table config="vm.config"></xos-smart-table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleSmartTable', ['xos.uiComponents', 'ngResource', 'ngMockE2E'])
+ // This is only for documentation purpose
+ .run(function($httpBackend, _){
+ let datas = [{id: 1, name: 'Jhon', surname: 'Doe'}];
+ let count = 1;
+
+ let paramsUrl = new RegExp(/\/test\/(.+)/);
+
+ $httpBackend.whenDELETE(paramsUrl, undefined, ['id']).respond((method, url, data, headers, params) => {
+ data = angular.fromJson(data);
+ let id = url.match(paramsUrl)[1];
+ _.remove(datas, (d) => {
+ return d.id === parseInt(id);
+ })
+ return [204];
+ });
+
+ $httpBackend.whenGET('/test').respond(200, datas)
+ $httpBackend.whenPOST('/test').respond((method, url, data) => {
+ data = angular.fromJson(data);
+ data.id = ++count;
+ datas.push(data);
+ return [201, data, {}];
+ });
+ })
+ .factory('_', function($window){
+ return $window._;
+ })
+ .service('SampleResource', function($resource){
+ return $resource('/test/:id', {id: '@id'});
+ })
+ // End of documentation purpose, example start
+ .controller('SampleCtrl', function(){
+ this.config = {
+ resource: 'SampleResource'
+ };
+ });
+ </file>
+ </example>
+ */
+
+ .directive('xosSmartTable', function(){
+ return {
+ restrict: 'E',
+ scope: {
+ config: '='
+ },
+ template: `
+ <div class="row" ng-show="vm.data.length > 0">
+ <div class="col-xs-12 text-right">
+ <a href="" class="btn btn-success" ng-click="vm.createItem()">
+ Add
+ </a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-xs-12 table-responsive">
+ <xos-table config="vm.tableConfig" data="vm.data"></xos-table>
+ </div>
+ </div>
+ <div class="panel panel-default" ng-show="vm.detailedItem">
+ <div class="panel-heading">
+ <div class="row">
+ <div class="col-xs-11">
+ <h3 class="panel-title" ng-show="vm.detailedItem.id">Update {{vm.config.resource}} {{vm.detailedItem.id}}</h3>
+ <h3 class="panel-title" ng-show="!vm.detailedItem.id">Create {{vm.config.resource}} item</h3>
+ </div>
+ <div class="col-xs-1">
+ <a href="" ng-click="vm.cleanForm()">
+ <i class="glyphicon glyphicon-remove pull-right"></i>
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="panel-body">
+ <xos-form config="vm.formConfig" ng-model="vm.detailedItem"></xos-form>
+ </div>
+ </div>
+ <xos-alert config="{type: 'success', closeBtn: true}" show="vm.responseMsg">{{vm.responseMsg}}</xos-alert>
+ <xos-alert config="{type: 'danger', closeBtn: true}" show="vm.responseErr">{{vm.responseErr}}</xos-alert>
+ `,
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: function($injector, LabelFormatter, _, XosFormHelpers){
+
+ // TODO
+ // - Validate the config (what if resource does not exist?)
+
+ // NOTE
+ // Corner case
+ // - if response is empty, how can we generate a form ?
+
+ this.responseMsg = false;
+ this.responseErr = false;
+
+ this.tableConfig = {
+ columns: [
+ ],
+ actions: [
+ {
+ label: 'delete',
+ icon: 'remove',
+ cb: (item) => {
+ console.log(item);
+ this.Resource.delete({id: item.id}).$promise
+ .then(() => {
+ _.remove(this.data, (d) => d.id === item.id);
+ this.responseMsg = `${this.config.resource} with id ${item.id} successfully deleted`;
+ })
+ .catch(err => {
+ this.responseErr = err.data.detail || `Error while deleting ${this.config.resource} with id ${item.id}`;
+ });
+ },
+ color: 'red'
+ },
+ {
+ label: 'details',
+ icon: 'search',
+ cb: (item) => {
+ this.detailedItem = item;
+ }
+ }
+ ],
+ classes: 'table table-striped table-bordered table-responsive',
+ filter: 'field',
+ order: true,
+ pagination: {
+ pageSize: 10
+ }
+ };
+
+ this.formConfig = {
+ exclude: this.config.hiddenFields,
+ fields: {},
+ formName: `${this.config.resource}Form`,
+ actions: [
+ {
+ label: 'Save',
+ icon: 'ok',
+ cb: (item) => {
+ let p;
+ let isNew = true;
+
+ if(item.id){
+ p = item.$update();
+ isNew = false;
+ }
+ else {
+ p = item.$save();
+ }
+
+ p.then((res) => {
+ if(isNew){
+ this.data.push(angular.copy(res));
+ }
+ delete this.detailedItem;
+ this.responseMsg = `${this.config.resource} with id ${item.id} successfully saved`;
+ })
+ .catch((err) => {
+ this.responseErr = err.data.detail || `Error while saving ${this.config.resource} with id ${item.id}`;
+ })
+ },
+ class: 'success'
+ }
+ ]
+ };
+
+ this.cleanForm = () => {
+ delete this.detailedItem;
+ };
+
+ this.createItem = () => {
+ this.detailedItem = new this.Resource();
+ };
+
+ this.Resource = $injector.get(this.config.resource);
+
+ const getData = () => {
+ this.Resource.query().$promise
+ .then((res) => {
+
+ if(!res[0]){
+ return;
+ }
+
+ let item = res[0];
+ let props = Object.keys(item);
+
+ _.remove(props, p => {
+ return p == 'id' || p == 'validators'
+ });
+
+ // TODO move out cb
+ if(angular.isArray(this.config.hiddenFields)){
+ props = _.difference(props, this.config.hiddenFields)
+ }
+
+ let labels = props.map(p => LabelFormatter.format(p));
+
+ props.forEach((p, i) => {
+ this.tableConfig.columns.push({
+ label: labels[i],
+ prop: p
+ });
+ });
+
+ // build form structure
+ props.forEach((p, i) => {
+ this.formConfig.fields[p] = {
+ label: LabelFormatter.format(labels[i]).replace(':', ''),
+ type: XosFormHelpers._getFieldFormat(item[p])
+ };
+ });
+
+ this.data = res;
+ });
+ }
+
+ getData();
+ }
+ };
+ });
+})();
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/smartComponents/smartTable/smartTable.scss b/views/ngXosLib/xosHelpers/src/ui_components/smartComponents/smartTable/smartTable.scss
new file mode 100644
index 0000000..fc63fdf
--- /dev/null
+++ b/views/ngXosLib/xosHelpers/src/ui_components/smartComponents/smartTable/smartTable.scss
@@ -0,0 +1,3 @@
+xos-smart-table{
+
+}
\ No newline at end of file
diff --git a/views/ngXosLib/xosHelpers/src/ui_components/ui-components.module.js b/views/ngXosLib/xosHelpers/src/ui_components/ui-components.module.js
index bd07908..03d6308 100644
--- a/views/ngXosLib/xosHelpers/src/ui_components/ui-components.module.js
+++ b/views/ngXosLib/xosHelpers/src/ui_components/ui-components.module.js
@@ -12,7 +12,15 @@
/**
* @ngdoc overview
* @name xos.uiComponents
- * @description A collection of UI components useful for Dashboard development
+ * @description
+ * A collection of UI components useful for Dashboard development.
+ * Currently available components are:
+ * - [xosAlert](/#/module/xos.uiComponents.directive:xosAlert)
+ * - [xosForm](/#/module/xos.uiComponents.directive:xosForm)
+ * - [xosPagination](/#/module/xos.uiComponents.directive:xosPagination)
+ * - [xosSmartTable](/#/module/xos.uiComponents.directive:xosSmartTable)
+ * - [xosTable](/#/module/xos.uiComponents.directive:xosTable)
+ * - [xosValidation](/#/module/xos.uiComponents.directive:xosValidation)
**/
angular.module('xos.uiComponents', [
diff --git a/views/ngXosLib/xosHelpers/src/xosHelpers.module.js b/views/ngXosLib/xosHelpers/src/xosHelpers.module.js
index d7cd958..b74ddf9 100644
--- a/views/ngXosLib/xosHelpers/src/xosHelpers.module.js
+++ b/views/ngXosLib/xosHelpers/src/xosHelpers.module.js
@@ -22,10 +22,12 @@
.module('xos.helpers', [
'ngCookies',
'ngResource',
+ 'ngAnimate',
'bugSnag',
- 'xos.uiComponents'
+ 'xos.uiComponents',
])
- .config(config);
+ .config(config)
+ .factory('_', $window => $window._ );
function config($httpProvider, $interpolateProvider, $resourceProvider) {
$httpProvider.interceptors.push('SetCSRFToken');
diff --git a/views/ngXosViews/openVPNDashboard/.bowerrc b/views/ngXosViews/openVPNDashboard/.bowerrc
new file mode 100644
index 0000000..e491038
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "src/vendor/"
+}
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/.eslintrc b/views/ngXosViews/openVPNDashboard/.eslintrc
new file mode 100644
index 0000000..c852748
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/.eslintrc
@@ -0,0 +1,42 @@
+{
+ "ecmaFeatures": {
+ "blockBindings": true,
+ "forOf": true,
+ "destructuring": true,
+ "arrowFunctions": true,
+ "templateStrings": true
+ },
+ "env": {
+ "browser": true,
+ "node": true,
+ "es6": true
+ },
+ "plugins": [
+ //"angular"
+ ],
+ "rules": {
+ "quotes": [2, "single"],
+ "camelcase": [1, {"properties": "always"}],
+ "no-underscore-dangle": 1,
+ "eqeqeq": [2, "smart"],
+ "no-alert": 1,
+ "key-spacing": [1, { "beforeColon": false, "afterColon": true }],
+ "indent": [2, 2],
+ "no-irregular-whitespace": 1,
+ "eol-last": 0,
+ "max-nested-callbacks": [2, 4],
+ "comma-spacing": [1, {"before": false, "after": true}],
+ "no-trailing-spaces": [1, { skipBlankLines: true }],
+ "no-unused-vars": [1, {"vars": "all", "args": "after-used"}],
+ "new-cap": 0,
+
+ //"angular/ng_module_name": [2, '/^xos\.*[a-z]*$/'],
+ //"angular/ng_controller_name": [2, '/^[a-z].*Ctrl$/'],
+ //"angular/ng_service_name": [2, '/^[A-Z].*Service$/'],
+ //"angular/ng_directive_name": [2, '/^[a-z]+[[A-Z].*]*$/'],
+ //"angular/ng_di": [0, "function or array"]
+ },
+ "globals" :{
+ "angular": true
+ }
+}
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/.gitignore b/views/ngXosViews/openVPNDashboard/.gitignore
new file mode 100644
index 0000000..567aee4
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/.gitignore
@@ -0,0 +1,6 @@
+dist/
+src/vendor
+.tmp
+node_modules
+npm-debug.log
+dist/
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/bower.json b/views/ngXosViews/openVPNDashboard/bower.json
new file mode 100644
index 0000000..01b2715
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/bower.json
@@ -0,0 +1,29 @@
+{
+ "name": "xos-openVPNDashboard",
+ "version": "0.0.0",
+ "authors": [
+ "Jeremy Mowery <jermowery@email.arizona.edu>"
+ ],
+ "description": "The OpenVPN Dashboard",
+ "license": "MIT",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "static/js/vendor/",
+ "test",
+ "tests"
+ ],
+ "dependencies": {
+ },
+ "devDependencies": {
+ "jquery": "2.1.4",
+ "angular-mocks": "1.4.7",
+ "angular": "1.4.7",
+ "angular-ui-router": "0.2.15",
+ "angular-cookies": "1.4.7",
+ "angular-resource": "1.4.7",
+ "ng-lodash": "0.3.0",
+ "bootstrap-css": "2.3.2"
+ }
+}
diff --git a/views/ngXosViews/openVPNDashboard/env/default.js b/views/ngXosViews/openVPNDashboard/env/default.js
new file mode 100644
index 0000000..5b198ec
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/env/default.js
@@ -0,0 +1,13 @@
+// This is a default configuration for your development environment.
+// You can duplicate this configuration for any of your Backend Environments.
+// Different configurations are loaded setting a NODE_ENV variable that contain the config file name.
+// `NODE_ENV=local npm start`
+//
+// If xoscsrftoken or xossessionid are not specified the browser value are used
+// (works only for local environment as both application are served on the same domain)
+
+module.exports = {
+ host: '',
+ xoscsrftoken: '',
+ xossessionid: ''
+};
diff --git a/views/ngXosViews/openVPNDashboard/gulp/build.js b/views/ngXosViews/openVPNDashboard/gulp/build.js
new file mode 100644
index 0000000..625e3ee
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/gulp/build.js
@@ -0,0 +1,150 @@
+'use strict';
+
+// BUILD
+//
+// The only purpose of this gulpfile is to build a XOS view and copy the correct files into
+// .html => dashboards
+// .js (minified and concat) => static/js
+//
+// The template are parsed and added to js with angular $templateCache
+
+var gulp = require('gulp');
+var ngAnnotate = require('gulp-ng-annotate');
+var uglify = require('gulp-uglify');
+var templateCache = require('gulp-angular-templatecache');
+var runSequence = require('run-sequence');
+var concat = require('gulp-concat');
+var del = require('del');
+var wiredep = require('wiredep');
+var angularFilesort = require('gulp-angular-filesort');
+var _ = require('lodash');
+var eslint = require('gulp-eslint');
+var inject = require('gulp-inject');
+var rename = require('gulp-rename');
+var replace = require('gulp-replace');
+var postcss = require('gulp-postcss');
+var autoprefixer = require('autoprefixer');
+var mqpacker = require('css-mqpacker');
+var csswring = require('csswring');
+
+var TEMPLATE_FOOTER = `}]);
+angular.module('xos.openVPNDashboard').run(function($location){$location.path('/')});
+angular.bootstrap(angular.element('#xosOpenVPNDashboard'), ['xos.openVPNDashboard']);`;
+
+module.exports = function(options){
+
+ // delete previous builded file
+ gulp.task('clean', function(){
+ return del(
+ [options.dashboards + 'xosOpenVPNDashboard.html'],
+ {force: true}
+ );
+ });
+
+ // minify css
+ gulp.task('css', function () {
+ var processors = [
+ autoprefixer({browsers: ['last 1 version']}),
+ mqpacker,
+ csswring
+ ];
+
+ gulp.src([
+ `${options.css}**/*.css`,
+ `!${options.css}dev.css`
+ ])
+ .pipe(postcss(processors))
+ .pipe(gulp.dest(options.tmp + '/css/'));
+ });
+
+ gulp.task('copyCss', ['css'], function(){
+ return gulp.src([`${options.tmp}/css/*.css`])
+ .pipe(concat('xosOpenVPNDashboard.css'))
+ .pipe(gulp.dest(options.static + 'css/'))
+ });
+
+ // compile and minify scripts
+ gulp.task('scripts', function() {
+ return gulp.src([
+ options.tmp + '**/*.js'
+ ])
+ .pipe(ngAnnotate())
+ .pipe(angularFilesort())
+ .pipe(concat('xosOpenVPNDashboard.js'))
+ .pipe(uglify())
+ .pipe(gulp.dest(options.static + 'js/'));
+ });
+
+ // set templates in cache
+ gulp.task('templates', function(){
+ return gulp.src('./src/templates/*.html')
+ .pipe(templateCache({
+ module: 'xos.openVPNDashboard',
+ root: 'templates/',
+ templateFooter: TEMPLATE_FOOTER
+ }))
+ .pipe(gulp.dest(options.tmp));
+ });
+
+ // copy html index to Django Folder
+ gulp.task('copyHtml', ['clean'], function(){
+ return gulp.src(options.src + 'index.html')
+ // remove dev dependencies from html
+ .pipe(replace(/<!-- bower:css -->(\n.*)*\n<!-- endbower --><!-- endcss -->/, ''))
+ .pipe(replace(/<!-- bower:js -->(\n.*)*\n<!-- endbower --><!-- endjs -->/, ''))
+ .pipe(replace(/ng-app=".*"\s/, ''))
+ // rewriting css path
+ // .pipe(replace(/(<link.*">)/, ''))
+ // injecting minified files
+ .pipe(
+ inject(
+ gulp.src([
+ options.static + 'js/vendor/xosOpenVPNDashboardVendor.js',
+ options.static + 'js/xosOpenVPNDashboard.js',
+ options.static + 'css/xosOpenVPNDashboard.css'
+ ]),
+ {ignorePath: '/../../../xos/core/xoslib'}
+ )
+ )
+ .pipe(rename('xosOpenVPNDashboard.html'))
+ .pipe(gulp.dest(options.dashboards));
+ });
+
+ // minify vendor js files
+ gulp.task('wiredep', function(){
+ var bowerDeps = wiredep().js;
+ if(!bowerDeps){
+ return;
+ }
+
+ // remove angular (it's already loaded)
+ _.remove(bowerDeps, function(dep){
+ return dep.indexOf('angular/angular.js') !== -1;
+ });
+
+ return gulp.src(bowerDeps)
+ .pipe(concat('xosOpenVPNDashboardVendor.js'))
+ .pipe(uglify())
+ .pipe(gulp.dest(options.static + 'js/vendor/'));
+ });
+
+ gulp.task('lint', function () {
+ return gulp.src(['src/js/**/*.js'])
+ .pipe(eslint())
+ .pipe(eslint.format())
+ .pipe(eslint.failAfterError());
+ });
+
+ gulp.task('build', function() {
+ runSequence(
+ 'lint',
+ 'templates',
+ 'babel',
+ 'scripts',
+ 'wiredep',
+ 'copyHtml',
+ 'copyCss',
+ 'cleanTmp'
+ );
+ });
+};
diff --git a/views/ngXosViews/openVPNDashboard/gulp/server.js b/views/ngXosViews/openVPNDashboard/gulp/server.js
new file mode 100644
index 0000000..7605294
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/gulp/server.js
@@ -0,0 +1,146 @@
+'use strict';
+
+var gulp = require('gulp');
+var browserSync = require('browser-sync').create();
+var inject = require('gulp-inject');
+var runSequence = require('run-sequence');
+var angularFilesort = require('gulp-angular-filesort');
+var babel = require('gulp-babel');
+var wiredep = require('wiredep').stream;
+var httpProxy = require('http-proxy');
+var del = require('del');
+
+const environment = process.env.NODE_ENV;
+
+if (environment){
+ var conf = require(`../env/${environment}.js`);
+}
+else{
+ var conf = require('../env/default.js')
+}
+
+var proxy = httpProxy.createProxyServer({
+ target: conf.host || 'http://0.0.0.0:9999'
+});
+
+
+proxy.on('error', function(error, req, res) {
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+
+ console.error('[Proxy]', error);
+});
+
+module.exports = function(options){
+
+ // open in browser with sync and proxy to 0.0.0.0
+ gulp.task('browser', function() {
+ browserSync.init({
+ // reloadDelay: 500,
+ // logLevel: 'debug',
+ // logConnections: true,
+ startPath: '#/',
+ snippetOptions: {
+ rule: {
+ match: /<!-- browserSync -->/i
+ }
+ },
+ server: {
+ baseDir: options.src,
+ routes: {
+ '/api': options.api,
+ '/xosHelpers/src': options.helpers
+ },
+ middleware: function(req, res, next){
+ if(
+ req.url.indexOf('/xos/') !== -1 ||
+ req.url.indexOf('/xoslib/') !== -1 ||
+ req.url.indexOf('/hpcapi/') !== -1
+ ){
+ if(conf.xoscsrftoken && conf.xossessionid){
+ req.headers.cookie = `xoscsrftoken=${conf.xoscsrftoken}; xossessionid=${conf.xossessionid}`;
+ req.headers['x-csrftoken'] = conf.xoscsrftoken;
+ }
+ proxy.web(req, res);
+ }
+ else{
+ next();
+ }
+ }
+ }
+ });
+
+ gulp.watch(options.src + 'js/**/*.js', ['js-watch']);
+ gulp.watch(options.src + 'vendor/**/*.js', ['bower'], function(){
+ browserSync.reload();
+ });
+ gulp.watch(options.src + '**/*.html', function(){
+ browserSync.reload();
+ });
+ });
+
+ // transpile js with sourceMaps
+ gulp.task('babel', function(){
+ return gulp.src(options.scripts + '**/*.js')
+ .pipe(babel({sourceMaps: true}))
+ .pipe(gulp.dest(options.tmp));
+ });
+
+ // inject scripts
+ gulp.task('injectScript', ['cleanTmp', 'babel'], function(){
+ return gulp.src(options.src + 'index.html')
+ .pipe(
+ inject(
+ gulp.src([
+ options.tmp + '**/*.js',
+ options.api + '*.js',
+ options.helpers + '**/*.js'
+ ])
+ .pipe(angularFilesort()),
+ {
+ ignorePath: [options.src, '/../../ngXosLib']
+ }
+ )
+ )
+ .pipe(gulp.dest(options.src));
+ });
+
+ // inject CSS
+ gulp.task('injectCss', function(){
+ return gulp.src(options.src + 'index.html')
+ .pipe(
+ inject(
+ gulp.src(options.src + 'css/*.css'),
+ {
+ ignorePath: [options.src]
+ }
+ )
+ )
+ .pipe(gulp.dest(options.src));
+ });
+
+ // inject bower dependencies with wiredep
+ gulp.task('bower', function () {
+ return gulp.src(options.src + 'index.html')
+ .pipe(wiredep({devDependencies: true}))
+ .pipe(gulp.dest(options.src));
+ });
+
+ gulp.task('js-watch', ['injectScript'], function(){
+ browserSync.reload();
+ });
+
+ gulp.task('cleanTmp', function(){
+ return del([options.tmp + '**/*']);
+ });
+
+ gulp.task('serve', function() {
+ runSequence(
+ 'bower',
+ 'injectScript',
+ 'injectCss',
+ ['browser']
+ );
+ });
+};
diff --git a/views/ngXosViews/openVPNDashboard/gulpfile.js b/views/ngXosViews/openVPNDashboard/gulpfile.js
new file mode 100644
index 0000000..a3523ee
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/gulpfile.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var gulp = require('gulp');
+var wrench = require('wrench');
+
+var options = {
+ src: 'src/',
+ css: 'src/css/',
+ scripts: 'src/js/',
+ tmp: 'src/.tmp',
+ dist: 'dist/',
+ api: '../../ngXosLib/api/',
+ helpers: '../../ngXosLib/xosHelpers/src/',
+ static: '../../../xos/core/xoslib/static/', // this is the django static folder
+ dashboards: '../../../xos/core/xoslib/dashboards/' // this is the django html folder
+};
+
+wrench.readdirSyncRecursive('./gulp')
+.map(function(file) {
+ require('./gulp/' + file)(options);
+});
+
+gulp.task('default', function () {
+ gulp.start('build');
+});
diff --git a/views/ngXosViews/openVPNDashboard/karma.conf.js b/views/ngXosViews/openVPNDashboard/karma.conf.js
new file mode 100644
index 0000000..dbd344a
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/karma.conf.js
@@ -0,0 +1,89 @@
+// Karma configuration
+// Generated on Tue Oct 06 2015 09:27:10 GMT+0000 (UTC)
+
+/* eslint indent: [2,2], quotes: [2, "single"]*/
+
+/*eslint-disable*/
+var wiredep = require('wiredep');
+var path = require('path');
+
+var bowerComponents = wiredep( {devDependencies: true} )[ 'js' ].map(function( file ){
+ return path.relative(process.cwd(), file);
+});
+
+module.exports = function(config) {
+/*eslint-enable*/
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: bowerComponents.concat([
+ 'src/css/**/*.css',
+ '../../static/js/xosApi.js',
+ '../../static/js/vendor/ngXosHelpers.js',
+ 'src/js/**/*.js',
+ 'spec/**/*.mock.js',
+ 'spec/**/*.test.js',
+ 'src/**/*.html'
+ ]),
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ 'src/js/**/*.js': ['babel'],
+ 'spec/**/*.test.js': ['babel'],
+ 'src/**/*.html': ['ng-html2js']
+ },
+
+ ngHtml2JsPreprocessor: {
+ stripPrefix: 'src/', //strip the src path from template url (http://stackoverflow.com/questions/22869668/karma-unexpected-request-when-testing-angular-directive-even-with-ng-html2js)
+ moduleName: 'templates' // define the template module name
+ },
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['mocha'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: false
+ });
+};
diff --git a/views/ngXosViews/openVPNDashboard/package.json b/views/ngXosViews/openVPNDashboard/package.json
new file mode 100644
index 0000000..412afec
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "xos-openVPNDashboard",
+ "version": "1.0.0",
+ "description": "Angular Application for XOS, created with generator-xos",
+ "scripts": {
+ "prestart": "npm install && bower install",
+ "start": "gulp serve",
+ "prebuild": "npm install && bower install",
+ "build": "gulp",
+ "test": "karma start",
+ "lint": "eslint src/js/"
+ },
+ "keywords": [
+ "XOS",
+ "Angular",
+ "XOSlib"
+ ],
+ "author": "Jeremy Mowery",
+ "license": "MIT",
+ "dependencies": {},
+ "devDependencies": {
+ "browser-sync": "^2.9.11",
+ "del": "^2.0.2",
+ "gulp": "^3.9.0",
+ "gulp-angular-filesort": "^1.1.1",
+ "gulp-angular-templatecache": "^1.8.0",
+ "gulp-babel": "^5.3.0",
+ "gulp-concat": "^2.6.0",
+ "gulp-inject": "^3.0.0",
+ "gulp-minify-html": "^1.0.4",
+ "gulp-rename": "^1.2.2",
+ "gulp-replace": "^0.5.4",
+ "gulp-uglify": "^1.4.2",
+ "http-proxy": "^1.12.0",
+ "proxy-middleware": "^0.15.0",
+ "run-sequence": "^1.1.4",
+ "wiredep": "^3.0.0-beta",
+ "wrench": "^1.5.8",
+ "gulp-ng-annotate": "^1.1.0",
+ "lodash": "^3.10.1",
+ "eslint": "^1.8.0",
+ "eslint-plugin-angular": "linkmesrl/eslint-plugin-angular",
+ "gulp-eslint": "^1.0.0"
+ }
+}
diff --git a/views/ngXosViews/openVPNDashboard/spec/sample.test.js b/views/ngXosViews/openVPNDashboard/spec/sample.test.js
new file mode 100644
index 0000000..822c114
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/spec/sample.test.js
@@ -0,0 +1,37 @@
+'use strict';
+
+describe('The User List', () => {
+
+ var scope, element, isolatedScope, httpBackend;
+
+ beforeEach(module('xos.openVPNDashboard'));
+ beforeEach(module('templates'));
+
+ beforeEach(inject(function($httpBackend, $compile, $rootScope){
+
+ httpBackend = $httpBackend;
+ // Setting up mock request
+ $httpBackend.expectGET('/xos/users/?no_hyperlinks=1').respond([
+ {
+ email: 'jermowery@email.arizona.edu',
+ firstname: 'Jeremy',
+ lastname: 'Mowery'
+ }
+ ]);
+
+ scope = $rootScope.$new();
+ element = angular.element('<users-list></users-list>');
+ $compile(element)(scope);
+ scope.$digest();
+ isolatedScope = element.isolateScope().vm;
+ }));
+
+ it('should load 1 users', () => {
+ httpBackend.flush();
+ expect(isolatedScope.users.length).toBe(1);
+ expect(isolatedScope.users[0].email).toEqual('jermowery@email.arizona.edu');
+ expect(isolatedScope.users[0].firstname).toEqual('Jeremy');
+ expect(isolatedScope.users[0].lastname).toEqual('Mowery');
+ });
+
+});
\ No newline at end of file
diff --git a/views/ngXosViews/openVPNDashboard/src/css/openVPNDashboard.css b/views/ngXosViews/openVPNDashboard/src/css/openVPNDashboard.css
new file mode 100644
index 0000000..085d5d4
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/css/openVPNDashboard.css
@@ -0,0 +1,14 @@
+#xosOpenVPNDashboard{
+ width: 70%;
+ margin: auto;
+}
+.vpn-row {
+ display: table-row;
+}
+.vpn-cell {
+ display: table-cell;
+ padding: 5px;
+}
+.vpn-header {
+ font-weight: bold;
+}
diff --git a/views/ngXosViews/openVPNDashboard/src/index.html b/views/ngXosViews/openVPNDashboard/src/index.html
new file mode 100644
index 0000000..83048df
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/index.html
@@ -0,0 +1,34 @@
+<!-- browserSync -->
+<!-- bower:css -->
+<link rel="stylesheet" href="vendor/bootstrap-css/css/bootstrap.css" />
+<!-- endbower --><!-- endcss -->
+<!-- inject:css -->
+<link rel="stylesheet" href="/css/openVPNDashboard.css">
+<!-- endinject -->
+
+<div ng-app="xos.openVPNDashboard" id="xosOpenVPNDashboard">
+ <div ui-view></div>
+</div>
+
+<!-- bower:js -->
+<script src="vendor/jquery/dist/jquery.js"></script>
+<script src="vendor/angular/angular.js"></script>
+<script src="vendor/angular-mocks/angular-mocks.js"></script>
+<script src="vendor/angular-ui-router/release/angular-ui-router.js"></script>
+<script src="vendor/angular-cookies/angular-cookies.js"></script>
+<script src="vendor/angular-resource/angular-resource.js"></script>
+<script src="vendor/ng-lodash/build/ng-lodash.js"></script>
+<script src="vendor/bootstrap-css/js/bootstrap.js"></script>
+<!-- endbower --><!-- endjs -->
+<!-- inject:js -->
+<script src="/xosHelpers/src/xosHelpers.module.js"></script>
+<script src="/xosHelpers/src/ui_components/table/table.component.js"></script>
+<script src="/xosHelpers/src/ui_components/ui-components.module.js"></script>
+<script src="/xosHelpers/src/services/noHyperlinks.interceptor.js"></script>
+<script src="/xosHelpers/src/services/csrfToken.interceptor.js"></script>
+<script src="/xosHelpers/src/services/api.services.js"></script>
+<script src="/api/ng-xoslib.js"></script>
+<script src="/api/ng-xos.js"></script>
+<script src="/api/ng-hpcapi.js"></script>
+<script src="/.tmp/main.js"></script>
+<!-- endinject -->
diff --git a/views/ngXosViews/openVPNDashboard/src/js/main.js b/views/ngXosViews/openVPNDashboard/src/js/main.js
new file mode 100644
index 0000000..b16c2fb
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/js/main.js
@@ -0,0 +1,61 @@
+'use strict';
+
+angular.module('xos.openVPNDashboard', [
+ 'ngResource',
+ 'ngCookies',
+ 'ngLodash',
+ 'ui.router',
+ 'xos.helpers'
+])
+.config(($stateProvider) => {
+ $stateProvider
+ .state('openVPNList', {
+ url: '/',
+ template: '<vpn-list></vpn-list>'
+ });
+})
+.config(($compileProvider) => {
+ $compileProvider.aHrefSanitizationWhitelist(
+ /^\s*(https?|ftp|mailto|tel|file|blob):/);
+})
+.service('Vpn', function($http, $q){
+
+ this.getOpenVpnTenants = () => {
+ let deferred = $q.defer();
+
+ $http.get('/api/tenant/openvpn/list/')
+ .then((res) => {
+ deferred.resolve(res.data)
+ })
+ .catch((e) => {
+ deferred.reject(e);
+ });
+
+ return deferred.promise;
+ }
+})
+.config(function($httpProvider){
+ $httpProvider.interceptors.push('NoHyperlinks');
+})
+.directive('vpnList', function(){
+ return {
+ restrict: 'E',
+ scope: {},
+ bindToController: true,
+ controllerAs: 'vm',
+ templateUrl: 'templates/openvpn-list.tpl.html',
+ controller: function(Vpn){
+ Vpn.getOpenVpnTenants()
+ .then((vpns) => {
+ this.vpns = vpns;
+ for (var i = 0; i < this.vpns.length; i++) {
+ var blob = new Blob([this.vpns[i].script_text], {type: 'text/plain'});
+ this.vpns[i].script_text = (window.URL || window.webkitURL).createObjectURL( blob );
+ }
+ })
+ .catch((e) => {
+ throw new Error(e);
+ });
+ }
+ };
+});
diff --git a/views/ngXosViews/openVPNDashboard/src/templates/openvpn-list.tpl.html b/views/ngXosViews/openVPNDashboard/src/templates/openvpn-list.tpl.html
new file mode 100644
index 0000000..0c7635f
--- /dev/null
+++ b/views/ngXosViews/openVPNDashboard/src/templates/openvpn-list.tpl.html
@@ -0,0 +1,19 @@
+<div style="display: table;">
+ <div class="vpn-row">
+ <h1 class="vpn-cell">VPN List</h1>
+ </div>
+ <div class="vpn-row">
+ <div class="vpn-cell vpn-header">ID</div>
+ <div class="vpn-cell vpn-header">VPN Network</div>
+ <div class="vpn-cell vpn-header">VPN Subnet</div>
+ <div class="vpn-cell vpn-header">Script Link</div>
+ </div>
+ <div class="vpn-row" ng-repeat="vpn in vm.vpns">
+ <div class="vpn-cell">{{ vpn.id }}</div>
+ <div class="vpn-cell">{{ vpn.server_network }}</div>
+ <div class="vpn-cell">{{ vpn.vpn_subnet }}</div>
+ <div class="vpn-cell">
+ <a download="connect-{{ vpn.id }}.vpn" ng-href="{{ vpn.script_text }}">Script</a>
+ </div>
+ </div>
+</div>
diff --git a/views/ngXosViews/sampleView/bower.json b/views/ngXosViews/sampleView/bower.json
index cc5fc64..a940676 100644
--- a/views/ngXosViews/sampleView/bower.json
+++ b/views/ngXosViews/sampleView/bower.json
@@ -22,8 +22,9 @@
"angular": "1.4.7",
"angular-ui-router": "0.2.15",
"angular-cookies": "1.4.7",
+ "angular-animate": "1.4.7",
"angular-resource": "1.4.7",
- "ng-lodash": "0.3.0",
+ "lodash": "~4.11.1",
"bootstrap-css": "3.3.6"
}
}
diff --git a/views/ngXosViews/sampleView/env/default.js b/views/ngXosViews/sampleView/env/default.js
index 353973a..6343727 100644
--- a/views/ngXosViews/sampleView/env/default.js
+++ b/views/ngXosViews/sampleView/env/default.js
@@ -8,6 +8,6 @@
module.exports = {
host: 'http://xos.dev:9999/',
- xoscsrftoken: 'jkKF9NebQoyaxKFT42l1EFjGx6ESPuP4',
- xossessionid: 'kvq9qxycbu0298wxfg8v11at94q9m819'
+ xoscsrftoken: '6OdNq1P7Aydyut3KjWBZoXLPJhb4DcuQ',
+ xossessionid: 'fpyv5s71bj779wmguxtg8wori16kiyrz'
};
diff --git a/views/ngXosViews/sampleView/gulp/server.js b/views/ngXosViews/sampleView/gulp/server.js
index 3c6a8e3..c0678d9 100644
--- a/views/ngXosViews/sampleView/gulp/server.js
+++ b/views/ngXosViews/sampleView/gulp/server.js
@@ -3,7 +3,6 @@
var gulp = require('gulp');
var browserSync = require('browser-sync').create();
var inject = require('gulp-inject');
-var es = require('event-stream');
var runSequence = require('run-sequence');
var angularFilesort = require('gulp-angular-filesort');
var babel = require('gulp-babel');
@@ -11,7 +10,6 @@
var httpProxy = require('http-proxy');
var del = require('del');
var sass = require('gulp-sass');
-var debug = require('gulp-debug');
const environment = process.env.NODE_ENV;
@@ -36,13 +34,9 @@
});
module.exports = function(options){
- // open in browser with sync and proxy to 0.0.0.0
+
gulp.task('browser', function() {
- console.log(options.helpers);
browserSync.init({
- // reloadDelay: 500,
- // logLevel: 'debug',
- // logConnections: true,
startPath: '#/',
snippetOptions: {
rule: {
@@ -52,11 +46,12 @@
server: {
baseDir: options.src,
routes: {
- // '/xosHelpers/src': options.helpers,
- '/xos/core/xoslib/static/js/vendor': options.helpers
+ '/xos/core/xoslib/static/js/vendor': options.helpers,
+ '/xos/core/static': options.static + '../../static/'
},
middleware: function(req, res, next){
if(
+ // to be removed, deprecated API
// req.url.indexOf('/xos/') !== -1 ||
// req.url.indexOf('/xoslib/') !== -1 ||
// req.url.indexOf('/hpcapi/') !== -1 ||
@@ -85,12 +80,14 @@
gulp.watch(options.css + '**/*.css', function(){
browserSync.reload();
});
-
- gulp.watch(options.helpers + 'ngXosHelpers.js', function(){
+ gulp.watch(`${options.sass}/**/*.scss`, ['sass'], function(){
browserSync.reload();
});
-
- gulp.watch(`${options.sass}/**/*.scss`, ['sass'], function(){
+
+ gulp.watch([
+ options.helpers + 'ngXosHelpers.js',
+ options.static + '../../static/xosNgLib.css'
+ ], function(){
browserSync.reload();
});
});
@@ -109,40 +106,16 @@
.pipe(gulp.dest(options.tmp));
});
- // // inject sourceMap
- // gulp.task('injectMaps', function(){
- // return gulp.src(options.src + 'index.html')
- // .pipe(
- // inject(
- // gulp.src([
- // options.helpersSourceMaps + '**/*.js.map'
- // ], {read: false}).pipe(debug()),
- // {
- // starttag: '<!-- inject:maps -->',
- // // ignorePath: [options.src, '/../../ngXosLib']
- // }
- // )
- // )
- // .pipe(gulp.dest(options.src));
- // });
-
// inject scripts
gulp.task('injectScript', ['cleanTmp', 'babel'], function(){
-
- var appScripts = gulp.src([
- options.tmp + '**/*.js',
- options.helpers + 'ngXosHelpers.js'
- ])
- .pipe(angularFilesort()).pipe(debug());
-
- var helpersSourceMaps = gulp.src([
- options.helpersSourceMaps + '**/*.js.map'
- ]).pipe(debug());
-
return gulp.src(options.src + 'index.html')
.pipe(
inject(
- es.merge(appScripts, helpersSourceMaps),
+ gulp.src([
+ options.tmp + '**/*.js',
+ options.helpers + 'ngXosHelpers.js'
+ ])
+ .pipe(angularFilesort()),
{
ignorePath: [options.src, '/../../ngXosLib']
}
@@ -156,7 +129,10 @@
return gulp.src(options.src + 'index.html')
.pipe(
inject(
- gulp.src(options.src + 'css/*.css'),
+ gulp.src([
+ options.src + 'css/*.css',
+ options.static + '../../static/xosNgLib.css'
+ ]),
{
ignorePath: [options.src]
}
diff --git a/views/ngXosViews/sampleView/gulpfile.js b/views/ngXosViews/sampleView/gulpfile.js
index 8b50345..08df554 100644
--- a/views/ngXosViews/sampleView/gulpfile.js
+++ b/views/ngXosViews/sampleView/gulpfile.js
@@ -12,7 +12,6 @@
dist: 'dist/',
api: '../../ngXosLib/api/',
helpers: '../../../xos/core/xoslib/static/js/vendor/',
- helpersSourceMaps: '../../ngXosLib/xosHelpers/.tmp/maps/',
static: '../../../xos/core/xoslib/static/', // this is the django static folder
dashboards: '../../../xos/core/xoslib/dashboards/' // this is the django html folder
};
diff --git a/views/ngXosViews/sampleView/package.json b/views/ngXosViews/sampleView/package.json
index e92f937..0ffe32c 100644
--- a/views/ngXosViews/sampleView/package.json
+++ b/views/ngXosViews/sampleView/package.json
@@ -28,14 +28,12 @@
"easy-mocker": "^1.2.0",
"eslint": "^1.8.0",
"eslint-plugin-angular": "linkmesrl/eslint-plugin-angular",
- "event-stream": "^3.3.2",
"gulp": "^3.9.0",
"gulp-angular-filesort": "^1.1.1",
"gulp-angular-templatecache": "^1.8.0",
"gulp-babel": "^5.3.0",
"gulp-concat": "^2.6.0",
"gulp-concat-util": "^0.5.5",
- "gulp-debug": "^2.1.2",
"gulp-eslint": "^1.0.0",
"gulp-inject": "^3.0.0",
"gulp-minify-html": "^1.0.4",
diff --git a/views/ngXosViews/sampleView/spec/sample.test.js b/views/ngXosViews/sampleView/spec/sample.test.js
index 6005af7..f0db699 100644
--- a/views/ngXosViews/sampleView/spec/sample.test.js
+++ b/views/ngXosViews/sampleView/spec/sample.test.js
@@ -15,7 +15,7 @@
{
email: 'teo@onlab.us',
firstname: 'Matteo',
- lastname: 'Scandolo'
+ lastname: 'Scandolo'
}
]);
diff --git a/views/ngXosViews/sampleView/src/index.html b/views/ngXosViews/sampleView/src/index.html
index d04fc3b..d9905a4 100644
--- a/views/ngXosViews/sampleView/src/index.html
+++ b/views/ngXosViews/sampleView/src/index.html
@@ -1,13 +1,90 @@
<!-- browserSync -->
<!-- bower:css -->
<link rel="stylesheet" href="vendor/bootstrap-css/css/bootstrap.min.css" />
-<!-- endbower --><!-- endcss -->
+<!-- endbower -->
+<!-- endcss -->
<!-- inject:css -->
<link rel="stylesheet" href="/css/main.css">
+<link rel="stylesheet" href="/../../../xos/core/static/xosNgLib.css">
<!-- endinject -->
<div ng-app="xos.sampleView" id="xosSampleView" class="container-fluid">
- <div ui-view></div>
+ <div class="row">
+ <div class="col-xs-12">
+ <h1>Hi Matteo!</h1>
+ <h3>Welcome to you development environment.</h3>
+ <p>
+ We provided this environment to help you creating a custom view.
+ </p>
+ <p>
+ When the environment is running you will have an
+ <code>auto-reload</code>
+ feature enabled, so any time you update one of your files, the browser will be reloaded.
+ </p>
+ <p> <i>Note that is environment is already functional and that it is loading information from the XOS APIs and presenting them using the
+ <code>xos-table</code>
+ component.</i>
+ </p>
+ <h3>Development notes:</h3>
+ <p>
+ This views are designed using
+ <a href="https://angularjs.org/" target="_blank">Angular Js</a>
+ version 1.4.7 and
+ <a href="http://getbootstrap.com/" target="_blank">Bootstrap</a>
+ 3.3.6 is included.
+ </p>
+ <p>
+ We just want to remind you that this development environment provide you three helper command:
+ <ul>
+ <li>
+ <code>npm start</code>
+ - will start your setup (you should already be familiar with it)
+ </li>
+ <li>
+ <code>npm test</code>
+ - will execute your unit tests defined with
+ <a href="https://karma-runner.github.io/0.13/index.html" target="_blank">Karma</a>
+ and
+ <a href="jasmine.github.io" target="_blank">Jasmine</a>
+ . You can check the
+ <code>spec/</code>
+ folder to see an example of your first test.
+ </li>
+ <li>
+ <code>npm run build</code>
+ - will build your dashboard and make it available to XOS
+ </li>
+ </ul>
+ </p>
+ <h3>Helpers:</h3>
+ <p>
+ We provide a set of helpers that you can leverage in your dashboard:
+ <ul>
+ <li>
+ <code>xos.helpers</code>
+ - A set of
+ <a href="https://docs.angularjs.org/guide/services" target="_blank">Angular Services</a>
+ </li>
+ <li>
+ <code>xos.uiComponents</code>
+ - A set of
+ <a href="https://docs.angularjs.org/guide/directive" target="_blank">Angular Directives</a>
+ </li>
+ <li>
+ <code>xos.rest</code>
+ - A set of
+ <a href="https://docs.angularjs.org/api/ngResource/service/$resource" target="_blank">Angular $resources</a>
+ </li>
+ </ul>
+ To know more about this helpers you can naviate to
+ <code>/views/ngXosLib/</code>
+ and generate the documentation with
+ <code>npm run doc</code>
+ </p>
+ <h3>Example:</h3>
+ </div>
+ </div>
+ <div ui-view></div>
</div>
<!-- bower:js -->
@@ -16,14 +93,13 @@
<script src="vendor/angular-mocks/angular-mocks.js"></script>
<script src="vendor/angular-ui-router/release/angular-ui-router.js"></script>
<script src="vendor/angular-cookies/angular-cookies.js"></script>
+<script src="vendor/angular-animate/angular-animate.js"></script>
<script src="vendor/angular-resource/angular-resource.js"></script>
-<script src="vendor/ng-lodash/build/ng-lodash.js"></script>
+<script src="vendor/lodash/lodash.js"></script>
<script src="vendor/bootstrap-css/js/bootstrap.min.js"></script>
-<!-- endbower --><!-- endjs -->
+<!-- endbower -->
+<!-- endjs -->
<!-- inject:js -->
<script src="/../../../xos/core/xoslib/static/js/vendor/ngXosHelpers.js"></script>
<script src="/.tmp/main.js"></script>
-<!-- endinject -->
-
-<!-- inject:map -->
<!-- endinject -->
\ No newline at end of file
diff --git a/views/ngXosViews/sampleView/src/js/main.js b/views/ngXosViews/sampleView/src/js/main.js
index b00aa0b..b07768a 100644
--- a/views/ngXosViews/sampleView/src/js/main.js
+++ b/views/ngXosViews/sampleView/src/js/main.js
@@ -3,7 +3,6 @@
angular.module('xos.sampleView', [
'ngResource',
'ngCookies',
- 'ngLodash',
'ui.router',
'xos.helpers'
])
@@ -27,52 +26,9 @@
controller: function(Users){
this.tableConfig = {
- columns: [
- {
- label: 'E-Mail',
- prop: 'email'
- },
- {
- label: 'First Name',
- prop: 'firstname'
- },
- {
- label: 'Last Name',
- prop: 'lastname'
- }
- ],
- classes: 'table table-striped table-condensed',
- actions: [
- {
- label: 'delete',
- icon: 'remove',
- cb: (user) => {
- console.log(user);
- },
- color: 'red'
- }
- ],
- filter: 'field',
- order: true,
- // pagination: {
- // pageSize: 6
- // }
+ resource: 'Users'
};
-
- this.alertConfig = {
- type: 'danger',
- closeBtn: true
- }
-
-
- // retrieving user list
- Users.query().$promise
- .then((users) => {
- this.users = users;
- })
- .catch((e) => {
- throw new Error(e);
- });
+
}
};
});
\ No newline at end of file
diff --git a/views/ngXosViews/sampleView/src/sass/main.scss b/views/ngXosViews/sampleView/src/sass/main.scss
index efe6cdc..76ed61e 100644
--- a/views/ngXosViews/sampleView/src/sass/main.scss
+++ b/views/ngXosViews/sampleView/src/sass/main.scss
@@ -1,5 +1,5 @@
@import '../../../../style/sass/lib/_variables.scss';
#xosSampleView {
-
+
}
\ No newline at end of file
diff --git a/views/ngXosViews/sampleView/src/templates/users-list.tpl.html b/views/ngXosViews/sampleView/src/templates/users-list.tpl.html
index c0c58cb..0641dd9 100644
--- a/views/ngXosViews/sampleView/src/templates/users-list.tpl.html
+++ b/views/ngXosViews/sampleView/src/templates/users-list.tpl.html
@@ -1,14 +1 @@
-<div class="row">
- <div class="col-xs-12">
- <h1>Users List</h1>
- <p>This is only an example view.</p>
- </div>
-</div>
-
- <xos-alert config="vm.alertConfig">Alert message Here</xos-alert>
-
-<div class="row">
- <div class="col-xs-12">
- <xos-table data="vm.users" config="vm.tableConfig"></xos-table>
- </div>
-</div>
\ No newline at end of file
+<xos-smart-table config="vm.tableConfig"></xos-smart-table>
\ No newline at end of file
diff --git a/views/npm-debug.log b/views/npm-debug.log
new file mode 100644
index 0000000..38c9da9
--- /dev/null
+++ b/views/npm-debug.log
@@ -0,0 +1,20 @@
+0 info it worked if it ends with ok
+1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'start' ]
+2 info using npm@3.6.0
+3 info using node@v5.7.0
+4 verbose stack Error: ENOENT: no such file or directory, open '/home/jeremy/xos/views/package.json'
+4 verbose stack at Error (native)
+5 verbose cwd /home/jeremy/xos/views
+6 error Linux 4.2.0-19-generic
+7 error argv "/usr/bin/nodejs" "/usr/bin/npm" "start"
+8 error node v5.7.0
+9 error npm v3.6.0
+10 error path /home/jeremy/xos/views/package.json
+11 error code ENOENT
+12 error errno -2
+13 error syscall open
+14 error enoent ENOENT: no such file or directory, open '/home/jeremy/xos/views/package.json'
+15 error enoent ENOENT: no such file or directory, open '/home/jeremy/xos/views/package.json'
+15 error enoent This is most likely not a problem with npm itself
+15 error enoent and is related to npm not being able to find a file.
+16 verbose exit [ -2, true ]
diff --git a/xos/api/README.md b/xos/api/README.md
index c0244f0..eb2bd28 100644
--- a/xos/api/README.md
+++ b/xos/api/README.md
@@ -1,13 +1,24 @@
## XOS REST API
-The XOS API importer is automatic and will search this subdirectory and its hierarchy of children for valid API methods. API methods that are descendents of the django View class are discovered automatically. This should include django_rest_framework based Views and Viewsets. This processing is handled by import_methods.py.
+Source for the XOS REST API lives in directory `xos/api`. An importer
+tool, `import_methods.py`, auto-generates the REST API by searching
+this directory (and sub-directories) for valid API methods. These
+methods are descendents of the Django View class. This should include
+django_rest_framework based Views and Viewsets.
-A convention is established for locating API methods within the XOS hierarchy. The root of the api will automatically be /api/. Under that are the following paths:
+We establish a convention for locating API methods within the XOS
+hierarchy. The root of the api is automatically `/api/`. Under that
+are the following paths:
* `/api/service` ... API endpoints that are service-wide
* `/api/tenant` ... API endpoints that are relative to a tenant within a service
-For example, `/api/tenant/cord/subscriber/` contains the Subscriber API for the CORD service.
+For example, `/api/tenant/cord/subscriber/` contains the Subscriber
+API for the CORD service.
-The API importer will automatically construct REST paths based on where files are placed within the directory hierarchy. For example, the files in `xos/api/tenant/cord/` will automatically appear at the API endpoint `http://server_name/api/tenant/cord/`.
-The directory `examples` contains examples that demonstrate using the API from the Linux command line.
+The API importer automatically constructs REST paths based on
+where files are placed within the directory hierarchy. For example,
+the files in `xos/api/tenant/cord/` will automatically appear at the
+API endpoint `http://server_name/api/tenant/cord/`. The directory
+`examples` contains examples that demonstrate using the API from the
+Linux command line.
diff --git a/xos/api/examples/misc/add_slice.sh b/xos/api/examples/misc/add_slice.sh
new file mode 100755
index 0000000..b2d7adc
--- /dev/null
+++ b/xos/api/examples/misc/add_slice.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+source ./config.sh
+
+SITE_ID=1
+USER_ID=1
+
+DATA=$(cat <<EOF
+{"name": "mysite_test1",
+ "site": $SITE_ID,
+ "creator": $USER_ID
+}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X POST -d "$DATA" $HOST/xos/slices/?no_hyperlinks=1
diff --git a/xos/api/examples/misc/config.sh b/xos/api/examples/misc/config.sh
new file mode 100644
index 0000000..92d703c
--- /dev/null
+++ b/xos/api/examples/misc/config.sh
@@ -0,0 +1,2 @@
+# see config.sh in the parent directory
+source ../config.sh
diff --git a/xos/api/examples/misc/delete_slice.sh b/xos/api/examples/misc/delete_slice.sh
new file mode 100755
index 0000000..1113d4f
--- /dev/null
+++ b/xos/api/examples/misc/delete_slice.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+SLICE_NAME=mysite_test1
+
+SLICE_ID=$(lookup_slice_id $SLICE_NAME)
+if [[ $? != 0 ]]; then
+ exit -1
+fi
+
+curl -u $AUTH -X DELETE $HOST/xos/slices/$SLICE_ID/
diff --git a/xos/api/examples/misc/get_sshkeys.sh b/xos/api/examples/misc/get_sshkeys.sh
new file mode 100755
index 0000000..0ac14c0
--- /dev/null
+++ b/xos/api/examples/misc/get_sshkeys.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+curl -H "Accept: application/json; indent=4" -u $AUTH -X GET $HOST/api/utility/sshkeys/
diff --git a/xos/api/examples/misc/update_slice.sh b/xos/api/examples/misc/update_slice.sh
new file mode 100755
index 0000000..94e82cd
--- /dev/null
+++ b/xos/api/examples/misc/update_slice.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+source ./config.sh
+source ./util.sh
+
+SLICE_NAME=mysite_test1
+
+SLICE_ID=$(lookup_slice_id $SLICE_NAME)
+if [[ $? != 0 ]]; then
+ exit -1
+fi
+
+DATA=$(cat <<EOF
+{"description": "foo"}
+EOF
+)
+
+curl -H "Accept: application/json; indent=4" -H "Content-Type: application/json" -u $AUTH -X PATCH -d "$DATA" $HOST/xos/slices/$SLICE_ID/
diff --git a/xos/api/examples/misc/util.sh b/xos/api/examples/misc/util.sh
new file mode 100644
index 0000000..7b66903
--- /dev/null
+++ b/xos/api/examples/misc/util.sh
@@ -0,0 +1 @@
+source ../util.sh
diff --git a/xos/api/examples/util.sh b/xos/api/examples/util.sh
index b3d3060..73cc039 100644
--- a/xos/api/examples/util.sh
+++ b/xos/api/examples/util.sh
@@ -12,6 +12,19 @@
echo $ID
}
+function lookup_slice_id {
+ JSON=`curl -f -s -u $AUTH -X GET $HOST/xos/slices/?name=$1`
+ if [[ $? != 0 ]]; then
+ echo "function lookup_slice_id with arguments $1 failed" >&2
+ echo "See CURL output below:" >&2
+ curl -s -u $AUTH -X GET $HOST/xos/slices/?name=$1 >&2
+ exit -1
+ fi
+ ID=`echo $JSON | python -c "import json,sys; print json.load(sys.stdin)[0].get('id','')"`
+ #echo "(mapped slice_name to id $ID)" >&2
+ echo $ID
+}
+
function lookup_subscriber_volt {
JSON=`curl -f -s -u $AUTH -X GET $HOST/api/tenant/cord/subscriber/$1/`
if [[ $? != 0 ]]; then
@@ -41,7 +54,7 @@
fi
ID=`echo $JSON | python -c "import json,sys; print json.load(sys.stdin)['related'].get('vsg_id','')"`
if [[ $ID == "" ]]; then
- echo "there is no volt for this subscriber" >&2
+ echo "there is no vsg for this subscriber" >&2
exit -1
fi
diff --git a/xos/api/import_methods.py b/xos/api/import_methods.py
index 1b5e3ca..3702f8a 100644
--- a/xos/api/import_methods.py
+++ b/xos/api/import_methods.py
@@ -30,7 +30,16 @@
def import_module_by_dotted_name(name):
print "import", name
- module = __import__(name)
+ try:
+ module = __import__(name)
+ except:
+ # django will eat the exception, and then fail later with
+ # 'conflicting models in application'
+ # when it tries to import the module a second time.
+ print "exception in import_model_by_dotted_name"
+ import traceback
+ traceback.print_exc()
+ raise
for part in name.split(".")[1:]:
module = getattr(module, part)
return module
diff --git a/xos/api/service/vtn.py b/xos/api/service/vtn.py
new file mode 100644
index 0000000..6b02616
--- /dev/null
+++ b/xos/api/service/vtn.py
@@ -0,0 +1,96 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework import viewsets
+from rest_framework.decorators import detail_route, list_route
+from rest_framework.views import APIView
+from core.models import *
+from services.vtn.models import VTNService
+from django.forms import widgets
+from django.conf.urls import patterns, url
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+from django.shortcuts import get_object_or_404
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from xos.exceptions import *
+import json
+import subprocess
+
+class VTNServiceSerializer(PlusModelSerializer):
+ id = ReadOnlyField()
+
+ privateGatewayMac = serializers.CharField(required=False)
+ localManagementIp = serializers.CharField(required=False)
+ ovsdbPort = serializers.IntegerField(required=False)
+ sshPort = serializers.IntegerField(required=False)
+ sshUser = serializers.CharField(required=False)
+ sshKeyFile = serializers.CharField(required=False)
+ mgmtSubnetBits = serializers.IntegerField(required=False)
+ xosEndpoint = serializers.CharField(required=False)
+ xosUser = serializers.CharField(required=False)
+ xosPassword = serializers.CharField(required=False)
+
+
+ humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+ class Meta:
+ model = VTNService
+ fields = ('humanReadableName', 'id', 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile',
+ 'mgmtSubnetBits', 'xosEndpoint', 'xosUser', 'xosPassword')
+
+ def getHumanReadableName(self, obj):
+ return obj.__unicode__()
+
+class VTNViewSet(XOSViewSet):
+ base_name = "vtn"
+ method_name = "vtn"
+ method_kind = "viewset"
+
+ # these are just because ViewSet needs some queryset and model, even if we don't use the
+ # default endpoints
+ queryset = VTNService.get_service_objects().all()
+ model = VTNService
+ serializer_class = VTNServiceSerializer
+
+ @classmethod
+ def get_urlpatterns(self, api_path="^"):
+ patterns = []
+
+ patterns.append( self.detail_url("services/$", {"get": "get_services"}, "services") )
+ patterns.append( self.detail_url("services_names/$", {"get": "get_services_names"}, "services") )
+ patterns.append( self.detail_url("services/(?P<service>[a-zA-Z0-9\-_]+)/$", {"get": "get_service"}, "get_service") )
+
+ # Not as RESTful as it could be, but maintain these endpoints for compability
+ patterns.append( self.list_url("services/$", {"get": "get_services"}, "rootvtn_services") )
+ patterns.append( self.list_url("services_names/$", {"get": "get_services_names"}, "rootvtn_services") )
+ patterns.append( self.list_url("services/(?P<service>[a-zA-Z0-9\-_]+)/$", {"get": "get_service"}, "rootvtn_get_service") )
+
+ patterns = patterns + super(VTNViewSet,self).get_urlpatterns(api_path)
+
+ return patterns
+
+ def get_services_names(self, request, pk=None):
+ result = {}
+ for service in Service.objects.all():
+ for id in service.get_vtn_src_names():
+ dependencies = service.get_vtn_dependencies_names()
+ if dependencies:
+ result[id] = dependencies
+ return Response(result)
+
+ def get_services(self, request, pk=None):
+ result = {}
+ for service in Service.objects.all():
+ for id in service.get_vtn_src_ids():
+ dependencies = service.get_vtn_dependencies_ids()
+ if dependencies:
+ result[id] = dependencies
+ return Response(result)
+
+ def get_service(self, request, pk=None, service=None):
+ for xos_service in Service.objects.all():
+ if service in xos_service.get_vtn_src_ids():
+ return Response(xos_service.get_vtn_dependencies_ids())
+ return Response([])
+
+
diff --git a/xos/api/tenant/cord/vsg.py b/xos/api/tenant/cord/vsg.py
new file mode 100644
index 0000000..6807e98
--- /dev/null
+++ b/xos/api/tenant/cord/vsg.py
@@ -0,0 +1,62 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework import status
+from core.models import *
+from django.forms import widgets
+from services.cord.models import VSGTenant, VSGService, CordSubscriberRoot
+from xos.apibase import XOSListCreateAPIView, XOSRetrieveUpdateDestroyAPIView, XOSPermissionDenied
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+
+def get_default_vsg_service():
+ vsg_services = VSGService.get_service_objects().all()
+ if vsg_services:
+ return vsg_services[0].id
+ return None
+
+class VSGTenantForAPI(VSGTenant):
+ class Meta:
+ proxy = True
+ app_label = "cord"
+
+ @property
+ def related(self):
+ related = {}
+ if self.instance:
+ related["instance_id"] = self.instance.id
+ return related
+
+class VSGTenantSerializer(PlusModelSerializer):
+ id = ReadOnlyField()
+ wan_container_ip = serializers.CharField()
+ wan_container_mac = ReadOnlyField()
+ related = serializers.DictField(required=False)
+
+ humanReadableName = serializers.SerializerMethodField("getHumanReadableName")
+ class Meta:
+ model = VSGTenantForAPI
+ fields = ('humanReadableName', 'id', 'wan_container_ip', 'wan_container_mac', 'related' )
+
+ def getHumanReadableName(self, obj):
+ return obj.__unicode__()
+
+class VSGTenantViewSet(XOSViewSet):
+ base_name = "vsg"
+ method_name = "vsg"
+ method_kind = "viewset"
+ queryset = VSGTenantForAPI.get_tenant_objects().all()
+ serializer_class = VSGTenantSerializer
+
+ @classmethod
+ def get_urlpatterns(self, api_path="^"):
+ patterns = super(VSGTenantViewSet, self).get_urlpatterns(api_path=api_path)
+
+ return patterns
+
+
+
+
+
+
diff --git a/xos/api/tenant/openvpn/__init__.py b/xos/api/tenant/openvpn/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/tenant/openvpn/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/tenant/openvpn/openvpn.py b/xos/api/tenant/openvpn/openvpn.py
new file mode 100644
index 0000000..9cc13f0
--- /dev/null
+++ b/xos/api/tenant/openvpn/openvpn.py
@@ -0,0 +1,78 @@
+import jinja2
+
+from api.xosapi_helpers import PlusModelSerializer, ReadOnlyField, XOSViewSet
+from core.models import TenantPrivilege
+from rest_framework import serializers
+from services.openvpn.models import OpenVPNService, OpenVPNTenant
+
+
+def get_default_openvpn_service():
+ openvpn_services = OpenVPNService.get_service_objects().all()
+ if openvpn_services:
+ return openvpn_services[0].id
+ return None
+
+
+class OpenVPNTenantSerializer(PlusModelSerializer):
+ """A Serializer for the OpenVPNTenant that has the minimum information required for clients.
+
+ Attributes:
+ id (ReadOnlyField): The ID of OpenVPNTenant.
+ 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()
+
+ class Meta:
+ model = OpenVPNTenant
+ 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.openvpn.models.OpenVPNTenant): The OpenVPNTenant to connect to.
+
+ Returns:
+ str: The client script as a str.
+ """
+ env = jinja2.Environment(
+ loader=jinja2.FileSystemLoader("/opt/xos/services/openvpn/templates"))
+ template = env.get_template("connect.vpn.j2")
+ client_name = self.context['request'].user.email + "-" + str(obj.id)
+ remote_ids = list(obj.failover_server_ids)
+ remote_ids.insert(0, obj.id)
+ remotes = OpenVPNTenant.get_tenant_objects().filter(pk__in=remote_ids)
+ pki_dir = OpenVPNService.get_pki_dir(obj)
+ fields = {"client_name": client_name,
+ "remotes": remotes,
+ "is_persistent": obj.is_persistent,
+ "ca_crt": obj.get_ca_crt(pki_dir),
+ "client_crt": obj.get_client_cert(client_name, pki_dir),
+ "client_key": obj.get_client_key(client_name, pki_dir)
+ }
+ return template.render(fields)
+
+
+class OpenVPNTenantViewSet(XOSViewSet):
+ """Class that provides a list of OpenVPNTenants that the user has permission to access."""
+ base_name = "openvpn"
+ method_kind = "viewset"
+ method_name = "list"
+ serializer_class = OpenVPNTenantSerializer
+
+ def get_queryset(self):
+ # Get every privilege for this user
+ tenants_privs = TenantPrivilege.objects.all().filter(
+ user=self.request.user)
+ vpn_tenants = []
+ for priv in tenants_privs:
+ vpn_tenants.append(
+ OpenVPNTenant.get_tenant_objects().filter(pk=priv.tenant.pk)[0])
+ return vpn_tenants
diff --git a/xos/api/utility/__init__.py b/xos/api/utility/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/api/utility/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/api/utility/loginview.py b/xos/api/utility/loginview.py
new file mode 100644
index 0000000..2dc79c6
--- /dev/null
+++ b/xos/api/utility/loginview.py
@@ -0,0 +1,101 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework.views import APIView
+from core.models import *
+from services.hpc.models import *
+from services.requestrouter.models import *
+from django.forms import widgets
+from django.core.exceptions import PermissionDenied
+from django.contrib.contenttypes.models import ContentType
+import json
+import socket
+import time
+import django.middleware.csrf
+from xos.exceptions import *
+from django.contrib.sessions.backends.db import SessionStore
+from django.contrib.sessions.models import Session
+from django.contrib.auth import authenticate
+
+def date_handler(obj):
+ return obj.isoformat() if hasattr(obj, 'isoformat') else obj
+
+def serialize_user(model):
+ serialized = model_to_dict(model)
+ del serialized['timezone']
+ del serialized['password']
+ return json.dumps(serialized, default=date_handler)
+
+class LoginView(APIView):
+ method_kind = "list"
+ method_name = "login"
+
+ def do_login(self, request, username, password):
+ if not username:
+ raise XOSMissingField("No username specified")
+
+ if not password:
+ raise XOSMissingField("No password specified")
+
+ u=authenticate(username=username, password=password)
+ if not u:
+ raise PermissionDenied("Failed to authenticate user %s" % username)
+
+ auth = {"username": username, "password": password}
+ request.session["auth"] = auth
+ request.session['_auth_user_id'] = u.pk
+ request.session['_auth_user_backend'] = u.backend
+ request.session.save()
+
+ return Response({
+ "xoscsrftoken": django.middleware.csrf.get_token(request),
+ "xossessionid": request.session.session_key,
+ "user": serialize_user(u)
+ })
+
+ def get(self, request, format=None):
+ username = request.GET.get("username", None)
+ password = request.GET.get("password", None)
+
+ return self.do_login(request, username, password)
+
+ def post(self, request, format=None):
+ username = request.data.get("username", None)
+ password = request.data.get("password", None)
+
+ return self.do_login(request, username, password)
+
+class LogoutView(APIView):
+ method_kind = "list"
+ method_name = "logout"
+
+ def do_logout(self, request, sessionid):
+ if not sessionid:
+ raise XOSMissingField("No xossessionid specified")
+
+ # Make sure the session exists. This prevents us from accidentally
+ # creating empty sessions with SessionStore()
+ session = Session.objects.filter(session_key=sessionid)
+ if not session:
+ # session doesn't exist
+ raise PermissionDenied("Session does not exist")
+
+ session = SessionStore(session_key=sessionid)
+ if "auth" in session:
+ del session["auth"]
+ session.save()
+ if "_auth_user_id" in session:
+ del session["_auth_user_id"]
+ session.save()
+
+ return Response("Logged Out")
+
+ def get(self, request, format=None):
+ sessionid = request.GET.get("xossessionid", None)
+ return self.do_logout(request, sessionid)
+
+ def post(self, request, format=None):
+ sessionid = request.data.get("xossessionid", None)
+ return self.do_logout(request, sessionid)
\ No newline at end of file
diff --git a/xos/api/utility/portforwarding.py b/xos/api/utility/portforwarding.py
new file mode 100644
index 0000000..0bb7a93
--- /dev/null
+++ b/xos/api/utility/portforwarding.py
@@ -0,0 +1,49 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework.views import APIView
+from core.models import *
+from django.forms import widgets
+from django.core.exceptions import PermissionDenied
+from xos.exceptions import XOSNotFound
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+from django.db.models import Q
+
+class PortForwardingSerializer(serializers.Serializer):
+ id = serializers.IntegerField(read_only=True)
+ ip = serializers.CharField(read_only=True)
+ ports = serializers.CharField(read_only=True, source="network.ports")
+ hostname = serializers.CharField(read_only=True, source="instance.node.name")
+
+ class Meta:
+ model = Port
+ fields = ('id', 'ip', 'ports', 'hostname')
+
+class PortForwardingViewSet(XOSViewSet):
+ base_name = "portforwarding"
+ method_name = "portforwarding"
+ method_kind = "viewset"
+ serializer_class = PortForwardingSerializer
+
+ def get_queryset(self):
+ queryset=Port.objects.exclude(Q(network__isnull=True) |
+ Q(instance__isnull=True) |
+ Q(instance__node__isnull=True) |
+ Q(network__ports__isnull=True) | Q(network__ports__exact='') |
+ Q(ip__isnull=True))
+
+ node_name = self.request.query_params.get('node_name', None)
+ if node_name is not None:
+ queryset = queryset.filter(instance__node__name = node_name)
+
+ if "" in [q.ip for q in list(queryset)]:
+ # Q(ip__exact=='') does not work right, so let's filter the hard way
+ queryset = [q for q in list(queryset) if q.ip!='']
+ queryset = [q.id for q in queryset]
+ queryset = Port.objects.filter(pk__in=queryset)
+
+ return queryset
+
+
diff --git a/xos/api/utility/sshkeys.py b/xos/api/utility/sshkeys.py
new file mode 100644
index 0000000..6ddd1ef
--- /dev/null
+++ b/xos/api/utility/sshkeys.py
@@ -0,0 +1,42 @@
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework import serializers
+from rest_framework import generics
+from rest_framework.views import APIView
+from core.models import *
+from django.forms import widgets
+from django.core.exceptions import PermissionDenied
+from xos.exceptions import XOSNotFound
+from api.xosapi_helpers import PlusModelSerializer, XOSViewSet, ReadOnlyField
+from django.db.models import Q
+
+class SSHKeysSerializer(PlusModelSerializer):
+ id = serializers.CharField(read_only=True, source="instance_id")
+ public_keys = serializers.ListField(read_only=True, source="get_public_keys")
+ node_name = serializers.CharField(read_only=True, source="node.name")
+
+ class Meta:
+ model = Instance
+ fields = ('id', 'public_keys', 'node_name')
+
+class SSHKeysViewSet(XOSViewSet):
+ base_name = "sshkeys"
+ method_name = "sshkeys"
+ method_kind = "viewset"
+ serializer_class = SSHKeysSerializer
+ read_only = True
+
+ lookup_field = "instance_id"
+ lookup_url_kwarg = "pk"
+
+ def get_queryset(self):
+ queryset = queryset=Instance.objects.exclude(Q(instance_id__isnull=True) | Q(instance_id__exact=''))
+
+ node_name = self.request.query_params.get('node_name', None)
+ if node_name is not None:
+ queryset = queryset.filter(node__name = node_name)
+
+ return queryset
+
+
diff --git a/xos/api/xosapi_helpers.py b/xos/api/xosapi_helpers.py
index 4dccb2a..8c737cb 100644
--- a/xos/api/xosapi_helpers.py
+++ b/xos/api/xosapi_helpers.py
@@ -75,6 +75,7 @@
class XOSViewSet(viewsets.ModelViewSet):
api_path=""
+ read_only=False
@classmethod
def get_api_method_path(self):
@@ -85,7 +86,7 @@
@classmethod
def detail_url(self, pattern, viewdict, name):
- return url(self.get_api_method_path() + r'(?P<pk>[a-zA-Z0-9\-]+)/' + pattern,
+ return url(self.get_api_method_path() + r'(?P<pk>[a-zA-Z0-9\-_]+)/' + pattern,
self.as_view(viewdict),
name=self.base_name+"_"+name)
@@ -101,8 +102,12 @@
patterns = []
- patterns.append(url(self.get_api_method_path() + '$', self.as_view({'get': 'list', 'post': 'create'}), name=self.base_name+'_list'))
- patterns.append(url(self.get_api_method_path() + '(?P<pk>[a-zA-Z0-9\-]+)/$', self.as_view({'get': 'retrieve', 'put': 'update', 'post': 'update', 'delete': 'destroy', 'patch': 'partial_update'}), name=self.base_name+'_detail'))
+ if self.read_only:
+ patterns.append(url(self.get_api_method_path() + '$', self.as_view({'get': 'list'}), name=self.base_name+'_list'))
+ patterns.append(url(self.get_api_method_path() + '(?P<pk>[a-zA-Z0-9\-_]+)/$', self.as_view({'get': 'retrieve'}), name=self.base_name+'_detail'))
+ else:
+ patterns.append(url(self.get_api_method_path() + '$', self.as_view({'get': 'list', 'post': 'create'}), name=self.base_name+'_list'))
+ patterns.append(url(self.get_api_method_path() + '(?P<pk>[a-zA-Z0-9\-_]+)/$', self.as_view({'get': 'retrieve', 'put': 'update', 'post': 'update', 'delete': 'destroy', 'patch': 'partial_update'}), name=self.base_name+'_detail'))
return patterns
diff --git a/xos/configurations/acord/Makefile b/xos/configurations/acord/Makefile
index b5b93fa..cfa04f8 100644
--- a/xos/configurations/acord/Makefile
+++ b/xos/configurations/acord/Makefile
@@ -8,8 +8,9 @@
cord:
sudo MYIP=$(MYIP) docker-compose up -d
bash ../common/wait_for_xos.sh
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
acord: cord
diff --git a/xos/configurations/acord/ceilometer.yaml b/xos/configurations/acord/ceilometer.yaml
index ff56579..66d5d32 100644
--- a/xos/configurations/acord/ceilometer.yaml
+++ b/xos/configurations/acord/ceilometer.yaml
@@ -164,6 +164,9 @@
ceilometer-trusty-server-multi-nic:
type: tosca.nodes.Image
+ m1.small:
+ type: tosca.nodes.Flavor
+
mysite_ceilometer:
description: Ceilometer Proxy Slice
type: tosca.nodes.Slice
@@ -177,8 +180,10 @@
- default_image:
node: ceilometer-trusty-server-multi-nic
relationship: tosca.relationships.DefaultImage
+ - m1.small:
+ node: m1.small
+ relationship: tosca.relationships.DefaultFlavor
properties:
- default_flavor: m1.small
max_instances: 2
# mysite_sflow:
diff --git a/xos/configurations/bash/copyin-vtn.sh b/xos/configurations/bash/copyin-vtn.sh
deleted file mode 100644
index ef18704..0000000
--- a/xos/configurations/bash/copyin-vtn.sh
+++ /dev/null
@@ -1,12 +0,0 @@
-#! /bin/bash
-
-export SETUPDIR=/root/setup
-
-# copy in file necessary to setup VTN
-
-cd ../cord
-CONTAINER=$( docker ps|grep "xos"|awk '{print $NF}' )
-make vtn_network_cfg_json
-docker cp $SETUPDIR/vtn-network-cfg.json $CONTAINER:/root/setup/
-docker cp ../common/id_rsa.pub $CONTAINER:/opt/xos/observers/onos/onos_key.pub
-docker cp ../common/id_rsa $CONTAINER:/opt/xos/observers/onos/onos_key
diff --git a/xos/configurations/common/cloudlab-openstack.yaml b/xos/configurations/common/cloudlab-openstack.yaml
new file mode 100644
index 0000000..969f84c
--- /dev/null
+++ b/xos/configurations/common/cloudlab-openstack.yaml
@@ -0,0 +1,89 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+# Note:
+# assumes mydeployment.yaml has already been run, and the following exist:
+# MyDeployment
+# mysite
+# padmin@vicci.org
+# Public Shared IPv4
+# assumes the following have been created and filled with appropriate data:
+# /root/setup/admin_openrc
+# /root/setup/flat_net_name
+# /root/setup/padmin_public_key
+
+description: >
+ * Adds OpenCloud Sites, Deployments, and Controllers.
+
+imports:
+ - custom_types/xos.yaml
+
+topology_template:
+ node_templates:
+ trusty-server-multi-nic:
+ type: tosca.nodes.Image
+ properties:
+ disk_format: QCOW2
+ container_format: BARE
+
+ MyDeployment:
+ type: tosca.nodes.Deployment
+ properties:
+ no-create: True
+ no-delete: True
+ requirements:
+ - image:
+ node: trusty-server-multi-nic
+ relationship: tosca.relationships.SupportsImage
+
+ CloudLab:
+ type: tosca.nodes.Controller
+ requirements:
+ - deployment:
+ node: MyDeployment
+ relationship: tosca.relationships.ControllerDeployment
+ properties:
+ backend_type: OpenStack
+ version: Kilo
+ auth_url: { get_script_env: [ SELF, adminrc, OS_AUTH_URL, LOCAL_FILE] }
+ admin_user: { get_script_env: [ SELF, adminrc, OS_USERNAME, LOCAL_FILE] }
+ admin_password: { get_script_env: [ SELF, adminrc, OS_PASSWORD, LOCAL_FILE] }
+ admin_tenant: { get_script_env: [ SELF, adminrc, OS_TENANT_NAME, LOCAL_FILE] }
+ rabbit_user: { get_script_env: [ SELF, controller_settings, RABBIT_USER, LOCAL_FILE] }
+ rabbit_password: { get_script_env: [ SELF, controller_settings, RABBIT_PASS, LOCAL_FILE] }
+ rabbit_host: { get_script_env: [ SELF, controller_settings, CONTROLLER_FLAT_LAN_IP, LOCAL_FILE] }
+ domain: Default
+ artifacts:
+ adminrc: /root/setup/admin-openrc.sh
+ controller_settings: /root/setup/controller_settings
+
+ mysite:
+ type: tosca.nodes.Site
+ properties:
+ no-create: True
+ no-delete: True
+ requirements:
+ - deployment:
+ node: MyDeployment
+ relationship: tosca.relationships.SiteDeployment
+ requirements:
+ - controller:
+ node: CloudLab
+ relationship: tosca.relationships.UsesController
+
+ Public shared IPv4:
+ type: tosca.nodes.NetworkTemplate
+ properties:
+ no-create: True
+ no-delete: True
+ shared_network_name: { get_artifact: [ SELF, flat_net_name, LOCAL_FILE] }
+ artifacts:
+ flat_net_name: /root/setup/flat_net_name
+
+ padmin@vicci.org:
+ type: tosca.nodes.User
+ properties:
+ no-create: True
+ no-delete: True
+ public_key: { get_artifact: [ SELF, pubkey, LOCAL_FILE ] }
+ artifacts:
+ pubkey: /root/setup/padmin_public_key
diff --git a/xos/configurations/common/fixtures.yaml b/xos/configurations/common/fixtures.yaml
index 6419211..6d9c0e8 100644
--- a/xos/configurations/common/fixtures.yaml
+++ b/xos/configurations/common/fixtures.yaml
@@ -7,6 +7,11 @@
topology_template:
node_templates:
+
+# -----------------------------------------------------------------------------
+# Network Parameter Types
+# -----------------------------------------------------------------------------
+
s_tag:
type: tosca.nodes.NetworkParameterType
@@ -24,3 +29,97 @@
neutron_port_name:
type: tosca.nodes.NetworkParameterType
+
+# ----------------------------------------------------------------------------
+# Roles
+# ----------------------------------------------------------------------------
+
+ siterole#admin:
+ type: tosca.nodes.SiteRole
+
+ siterole#pi:
+ type: tosca.nodes.SiteRole
+
+ siterole#tech:
+ type: tosca.nodes.SiteRole
+
+ tenantrole#admin:
+ type: tosca.nodes.TenantRole
+
+ tenantrole#access:
+ type: tosca.nodes.TenantRole
+
+ deploymentrole#admin:
+ type: tosca.nodes.DeploymentRole
+
+ slicerole#admin:
+ type: tosca.nodes.SliceRole
+
+ slicerole#access:
+ type: tosca.nodes.SliceRole
+
+# -----------------------------------------------------------------------------
+# Flavors
+# -----------------------------------------------------------------------------
+
+ m1.small:
+ type: tosca.nodes.Flavor
+
+ m1.medium:
+ type: tosca.nodes.Flavor
+
+ m1.large:
+ type: tosca.nodes.Flavor
+
+# -----------------------------------------------------------------------------
+# Dashboard Views
+# -----------------------------------------------------------------------------
+
+ xsh:
+ type: tosca.nodes.DashboardView
+ properties:
+ url: template:xsh
+
+ Customize:
+ type: tosca.nodes.DashboardView
+ properties:
+ url: template:customize
+
+ Tenant:
+ type: tosca.nodes.DashboardView
+ properties:
+ url: template:xosTenant
+
+ Developer:
+ type: tosca.nodes.DashboardView
+ properties:
+ url: template:xosDeveloper_datatables
+
+# -----------------------------------------------------------------------------
+# Network Templates
+# -----------------------------------------------------------------------------
+
+ Private:
+ type: tosca.nodes.NetworkTemplate
+ properties:
+ visibility: private
+ translation: none
+
+ Public shared IPv4:
+ type: tosca.nodes.NetworkTemplate
+ properties:
+ visibility: private
+ translation: NAT
+ shared_network_name: nat-net
+
+ Public dedicated IPv4:
+ type: tosca.nodes.NetworkTemplate
+ properties:
+ visibility: public
+ translation: none
+ shared_network_name: ext-net
+
+
+
+
+
diff --git a/xos/configurations/common/mydeployment.yaml b/xos/configurations/common/mydeployment.yaml
new file mode 100644
index 0000000..66bb75d
--- /dev/null
+++ b/xos/configurations/common/mydeployment.yaml
@@ -0,0 +1,69 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Some basic fixtures
+
+imports:
+ - custom_types/xos.yaml
+
+topology_template:
+ node_templates:
+ m1.large:
+ type: tosca.nodes.Flavor
+
+ m1.medium:
+ type: tosca.nodes.Flavor
+
+ m1.small:
+ type: tosca.nodes.Flavor
+
+ MyDeployment:
+ type: tosca.nodes.Deployment
+ requirements:
+ - m1.large:
+ node: m1.large
+ relationship: tosca.relationships.SupportsFlavor
+ - m1.medium:
+ node: m1.medium
+ relationship: tosca.relationships.SupportsFlavor
+ - m1.small:
+ node: m1.small
+ relationship: tosca.relationships.SupportsFlavor
+
+ mysite:
+ type: tosca.nodes.Site
+ properties:
+ display_name: MySite
+ requirements:
+ - deployment:
+ node: MyDeployment
+ relationship: tosca.relationships.SiteDeployment
+
+ # Attach the Tenant view to the MyDeployment deployment
+ Tenant:
+ type: tosca.nodes.DashboardView
+ properties:
+ no-create: true
+ no-delete: true
+ requirements:
+ - deployment:
+ node: MyDeployment
+ relationship: tosca.relationships.SupportsDeployment
+
+ padmin@vicci.org:
+ type: tosca.nodes.User
+ properties:
+ password: letmein
+# encrypted_password: pbkdf2_sha256$12000$Qufx9iqtaYma$xs0YurPOcj9qYQna/Qrb3K+im9Yr2XEVr0J4Kqek7AE=
+ firstname: XOS
+ lastname: admin
+ is_admin: true
+ requirements:
+ - site:
+ node: mysite
+ relationship: tosca.relationships.MemberOfSite
+ - tenant_dashboard:
+ node: Tenant
+ relationship: tosca.relationships.UsesDashboard
+
+
+
diff --git a/xos/configurations/common/wait_for_xos_port.sh b/xos/configurations/common/wait_for_xos_port.sh
index dab6e70..3ed5833 100755
--- a/xos/configurations/common/wait_for_xos_port.sh
+++ b/xos/configurations/common/wait_for_xos_port.sh
@@ -11,7 +11,7 @@
fi
echo "Waiting for XOS to come up"
-until http 0.0.0.0:$1 &> /dev/null
+until curl 0.0.0.0:$1 &> /dev/null
do
sleep 1
RUNNING_CONTAINER=`sudo docker ps|grep "xos"|awk '{print $$NF}'`
diff --git a/xos/configurations/cord-deprecated/Makefile b/xos/configurations/cord-deprecated/Makefile
index 1e53e79..184f2d5 100644
--- a/xos/configurations/cord-deprecated/Makefile
+++ b/xos/configurations/cord-deprecated/Makefile
@@ -8,8 +8,9 @@
cord: virtualbng_json vtn_network_cfg_json
sudo MYIP=$(MYIP) docker-compose up -d
bash ../common/wait_for_xos.sh
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/cord/cord.yaml
diff --git a/xos/configurations/cord-deprecated/ceilometer.yaml b/xos/configurations/cord-deprecated/ceilometer.yaml
index 3724265..464b07b 100644
--- a/xos/configurations/cord-deprecated/ceilometer.yaml
+++ b/xos/configurations/cord-deprecated/ceilometer.yaml
@@ -164,6 +164,9 @@
ceilometer-trusty-server-multi-nic:
type: tosca.nodes.Image
+ m1.small:
+ type: tosca.nodes.Flavor
+
mysite_ceilometer:
description: Ceilometer Proxy Slice
type: tosca.nodes.Slice
@@ -177,8 +180,9 @@
- default_image:
node: ceilometer-trusty-server-multi-nic
relationship: tosca.relationships.DefaultImage
- properties:
- default_flavor: m1.small
+ - default_flavor:
+ node: m1.small
+ relationship: tosca.relationships.DefaultFlavor
# mysite_sflow:
# description: Slice for sFlow service
diff --git a/xos/configurations/cord-pod/Makefile b/xos/configurations/cord-pod/Makefile
index 9296f10..950f758 100644
--- a/xos/configurations/cord-pod/Makefile
+++ b/xos/configurations/cord-pod/Makefile
@@ -1,17 +1,23 @@
.PHONY: xos
-xos: nodes.yaml images.yaml
+xos: up bootstrap
+
+up:
sudo docker-compose up -d
../common/wait_for_xos_port.sh 80
+
+bootstrap: nodes.yaml images.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/setup.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/images.yaml
-vtn:
+vtn: vtn-external.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/vtn-external.yaml
-cord: virtualbng_json
+cord:
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/mgmt-net.yaml
- sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/cord-vtn-vsg.yaml
exampleservice:
@@ -26,6 +32,9 @@
images.yaml:
export SETUPDIR=.; bash ../common/make-images-yaml.sh
+vtn-external.yaml:
+ export SETUPDIR=.; bash ./make-vtn-external-yaml.sh
+
virtualbng_json:
export SETUPDIR=.; bash ./make-virtualbng-json.sh
diff --git a/xos/configurations/cord-pod/README.md b/xos/configurations/cord-pod/README.md
index b1304b7..d5051c9 100644
--- a/xos/configurations/cord-pod/README.md
+++ b/xos/configurations/cord-pod/README.md
@@ -31,7 +31,7 @@
To set up OpenStack, follow the instructions in the
[README.md](https://github.com/open-cloud/openstack-cluster-setup/blob/master/README.md)
file of the [open-cloud/openstack-cluster-setup](https://github.com/open-cloud/openstack-cluster-setup/)
-repository. If you're just getting started with CORD, it's probably best to begin with the
+repository. If you're just getting started with CORD, it's probably best to begin with the
single-node CORD test environment to familiarize yourself with the overall setup.
**NOTE: In order to use the cord-pod configuration, you must set up OpenStack using the above recipe.**
@@ -73,51 +73,82 @@
ubuntu@xos:~$ cd xos/xos/configurations/cord-pod
```
-Next, check that the following files exist in this directory:
+Next, check that the following files exist in this directory
+(they will have been put there for you by the cluster installation scripts):
* *admin-openrc.sh*: Admin credentials for your OpenStack cloud
* *id_rsa[.pub]*: A keypair that will be used by the various services
* *node_key*: A private key that allows root login to the compute nodes
-They will have been put there for you by the cluster installation scripts.
+XOS can then be brought up for CORD by running a few `make` commands.
+First, run:
-**If your setup uses the CORD fabric**, you need to modify the autogenerated VTN
-configuration and node tags, and edit `cord-vtn-vsg.yml` as follows.
-
- 1. The VTN app configuration is autogenerated by XOS. For more information
-about the configuration, see [this page on the ONOS Wiki](https://wiki.onosproject.org/display/ONOS/CORD+VTN),
-under the **ONOS Settings** heading. To see the generated
-configuration, go to http://xos/admin/onos/onosapp/, click on
-*VTN_ONOS_app*, then the *Attributes* tab, and look for the
-`rest_onos/v1/network/configuration/` attribute. You can edit this
-configuration after deleting the `autogenerate` attribute (otherwise XOS will
-overwrite your changes), or you can change the other
-attributes and delete `rest_onos/v1/network/configuration/` in order
-to get XOS to regenerate it.
-
- 2. The process of autoconfiguring VTN also assigns some default values to per-node parameters. Go to
- http://xos/admin/core/node/, select a node, then select the *Tags* tab. Configure the following:
- * `bridgeId` (the ID to set on the node's br-int)
- * `dataPlaneIntf` (the data plane interface for the fabric on the node)
- * `dataPlaneIp` (the IP address for the node on the fabric)
-
- 3. Modify `cord-vtn-vsg.yml` and set these parameters to the
-appropriate values for the fabric:
- * `addresses_vsg:properties:addresses` (IP address block of fabric)
- * `addresses_vsg:properties:gateway_ip`
- * `addresses_vsg:properties:gateway_mac`
-
-If you're not using the fabric then the default values should be OK.
-
-XOS can then be brought up for CORD by running a few `make` commands:
```
ubuntu@xos:~/xos/xos/configurations/cord-pod$ make
+```
+
+After this you will be able to login to the XOS GUI at
+*http://xos/* using username/password `padmin@vicci.org/letmein`.
+Before proceeding, you should verify that objects in XOS are
+being sync'ed with OpenStack. Log into the GUI and select *Users*
+at left. Make sure there is a green check next to `padmin@vicci.org`.
+
+> If you are **not** building the single-node development POD, the next
+> step is to create and edit the VTN configuration. Run `make vtn-external.yaml`
+> then edit the `vtn-external.yml` TOSCA file. The `rest_hostname:`
+> field points to the host where ONOS should run the VTN app. The
+> fields in the `service_vtn` and the objects of type `tosca.nodes.Tag`
+> correspond to the VTN fields listed
+> on [the CORD VTN page on the ONOS Wiki](https://wiki.onosproject.org/display/ONOS/CORD+VTN),
+> under the **ONOS Settings** heading; refer there for the fields'
+> meanings.
+
+Then run:
+
+```
ubuntu@xos:~/xos/xos/configurations/cord-pod$ make vtn
+```
+The above step configures the ONOS VTN app by generating a configuration
+and pushing it to ONOS. You are able to see and modify the configuration
+via the GUI as follows:
+
+* To see the generated configuration, go to *http://xos/admin/onos/onosapp/*, select
+*VTN_ONOS_app*, then the *Attributes* tab, and look for the
+`rest_onos/v1/network/configuration/` attribute.
+
+* To change the VTN configuration, modify the fields of the VTN Service object
+and the Tag objects associated with Nodes. Don't forget to select *Save*.
+
+* After modifying the above fields, delete the `rest_onos/v1/network/configuration/` attribute
+in the *ONOS_VTN_app* and select *Save*. The attribute will be regenerated using the new information.
+
+* Alternatively, if you want to load your own VTN configuration manually, you can delete the
+`autogenerate` attribute from the *ONOS_VTN_app*, edit the configuration in the
+`rest_onos/v1/network/configuration/` attribute, and select *Save*.
+
+Before proceeding, check that the VTN app is controlling Open vSwitch on the compute nodes. Log
+into ONOS and run the `cordvtn-nodes` command:
+
+```
+$ ssh -p 8101 karaf@onos-cord # password is karaf
+onos> cordvtn-nodes
+hostname=nova-compute, hostMgmtIp=192.168.122.177/24, dpIp=192.168.199.1/24, br-int=of:0000000000000001, dpIntf=veth1, init=COMPLETE
+Total 1 nodes
+```
+The important part is the `init=COMPLETE` at the end. If you do not see this, refer to
+[the CORD VTN page on the ONOS Wiki](https://wiki.onosproject.org/display/ONOS/CORD+VTN) for
+help fixing the problem. This must be working to bring up VMs on the POD.
+
+> If you are **not** building the single-node development POD, modify `cord-vtn-vsg.yml`
+> and change `addresses_vsg` so that it contains the IP address block,
+> gateway IP, and gateway MAC of the CORD fabric.
+
+Then run:
+
+```
ubuntu@xos:~/xos/xos/configurations/cord-pod$ make cord
```
-After the first 'make' command above, you will be able to login to XOS at
-*http://xos/* using username/password `padmin@vicci.org/letmein`.
### Inspecting the vSG
diff --git a/xos/configurations/cord-pod/ceilometer.yaml b/xos/configurations/cord-pod/ceilometer.yaml
index 3b32345..d07f2e9 100644
--- a/xos/configurations/cord-pod/ceilometer.yaml
+++ b/xos/configurations/cord-pod/ceilometer.yaml
@@ -171,6 +171,9 @@
ceilometer-trusty-server-multi-nic:
type: tosca.nodes.Image
+ m1.small:
+ type: tosca.nodes.Flavor
+
mysite_ceilometer:
description: Ceilometer Proxy Slice
type: tosca.nodes.Slice
@@ -187,8 +190,9 @@
- management:
node: management
relationship: tosca.relationships.ConnectsToNetwork
- properties:
- default_flavor: m1.small
+ - m1.small:
+ node: m1.small
+ relationship: tosca.relationships.DefaultFlavor
# mysite_sflow:
# description: Slice for sFlow service
diff --git a/xos/configurations/cord-pod/cord-vtn-vsg.yaml b/xos/configurations/cord-pod/cord-vtn-vsg.yaml
index 7de536a..e3f73c6 100644
--- a/xos/configurations/cord-pod/cord-vtn-vsg.yaml
+++ b/xos/configurations/cord-pod/cord-vtn-vsg.yaml
@@ -65,6 +65,11 @@
node: addresses_exampleservice-public
relationship: tosca.relationships.ProvidesAddresses
+ service_fabric:
+ type: tosca.nodes.FabricService
+ properties:
+ view_url: /admin/fabric/fabricservice/$id$/
+
Private:
type: tosca.nodes.NetworkTemplate
diff --git a/xos/configurations/cord-pod/docker-compose.yml b/xos/configurations/cord-pod/docker-compose.yml
index 234fd43..e2a5768 100644
--- a/xos/configurations/cord-pod/docker-compose.yml
+++ b/xos/configurations/cord-pod/docker-compose.yml
@@ -13,7 +13,7 @@
- xos_db
volumes:
- ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
- - xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
+ - ./xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
- .:/root/setup:ro
- ../vtn/files/xos_vtn_config:/opt/xos/xos_configuration/xos_vtn_config:ro
- ./images:/opt/xos/images:ro
@@ -96,7 +96,7 @@
volumes:
- .:/root/setup:ro
- ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
- - xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
+ - ./xos_cord_config:/opt/xos/xos_configuration/xos_cord_config:ro
- ../vtn/files/xos_vtn_config:/opt/xos/xos_configuration/xos_vtn_config:ro
- ./id_rsa.pub:/opt/xos/synchronizers/onos/onos_key.pub:ro
- ./id_rsa.pub:/opt/xos/synchronizers/vcpe/vcpe_public_key:ro
diff --git a/xos/configurations/cord-pod/make-vtn-external-yaml.sh b/xos/configurations/cord-pod/make-vtn-external-yaml.sh
new file mode 100644
index 0000000..4c9e0ee
--- /dev/null
+++ b/xos/configurations/cord-pod/make-vtn-external-yaml.sh
@@ -0,0 +1,107 @@
+FN=$SETUPDIR/vtn-external.yaml
+
+rm -f $FN
+
+cat >> $FN <<EOF
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+imports:
+ - custom_types/xos.yaml
+
+description: autogenerated node tags file for VTN configuration
+
+topology_template:
+ node_templates:
+
+ service_ONOS_CORD:
+ type: tosca.nodes.ONOSService
+ requirements:
+ properties:
+ kind: onos
+ view_url: /admin/onos/onosservice/\$id$/
+ no_container: true
+ rest_hostname: onos-cord
+
+ service_vtn:
+ type: tosca.nodes.VTNService
+ properties:
+ view_url: /admin/vtn/vtnservice/\$id$/
+ privateGatewayMac: 00:00:00:00:00:01
+ localManagementIp: 172.27.0.1/24
+ ovsdbPort: 6641
+ sshUser: root
+ sshKeyFile: /root/node_key
+ sshPort: 22
+ xosEndpoint: http://xos/
+ xosUser: padmin@vicci.org
+ xosPassword: letmein
+
+EOF
+
+NODES=$( bash -c "source $SETUPDIR/admin-openrc.sh ; nova host-list" |grep compute|awk '{print $2}' )
+I=0
+for NODE in $NODES; do
+ echo $NODE
+ cat >> $FN <<EOF
+ $NODE:
+ type: tosca.nodes.Node
+
+ # VTN bridgeId field for node $NODE
+ ${NODE}_bridgeId_tag:
+ type: tosca.nodes.Tag
+ properties:
+ name: bridgeId
+ value: of:0000000000000001
+ requirements:
+ - target:
+ node: $NODE
+ relationship: tosca.relationships.TagsObject
+ - service:
+ node: service_ONOS_CORD
+ relationship: tosca.relationships.MemberOfService
+
+ # VTN dataPlaneIntf field for node $NODE
+ ${NODE}_dataPlaneIntf_tag:
+ type: tosca.nodes.Tag
+ properties:
+ name: dataPlaneIntf
+ value: veth1
+ requirements:
+ - target:
+ node: $NODE
+ relationship: tosca.relationships.TagsObject
+ - service:
+ node: service_ONOS_CORD
+ relationship: tosca.relationships.MemberOfService
+
+ # VTN dataPlaneIp field for node $NODE
+ ${NODE}_dataPlaneIp_tag:
+ type: tosca.nodes.Tag
+ properties:
+ name: dataPlaneIp
+ value: 10.168.0.253/24
+ requirements:
+ - target:
+ node: $NODE
+ relationship: tosca.relationships.TagsObject
+ - service:
+ node: service_ONOS_CORD
+ relationship: tosca.relationships.MemberOfService
+
+EOF
+done
+
+cat >> $FN <<EOF
+ VTN_ONOS_app:
+ type: tosca.nodes.ONOSVTNApp
+ requirements:
+ - onos_tenant:
+ node: service_ONOS_CORD
+ relationship: tosca.relationships.TenantOfService
+ - vtn_service:
+ node: service_vtn
+ relationship: tosca.relationships.UsedByService
+ properties:
+ dependencies: org.onosproject.drivers, org.onosproject.drivers.ovsdb, org.onosproject.openflow-base, org.onosproject.ovsdb-base, org.onosproject.dhcp, org.onosproject.cordvtn, org.onosproject.olt, org.onosproject.igmp, org.onosproject.cordmcast
+ autogenerate: vtn-network-cfg
+EOF
\ No newline at end of file
diff --git a/xos/configurations/cord-pod/vtn-external.yaml b/xos/configurations/cord-pod/vtn-external.yaml
deleted file mode 100644
index 51cca08..0000000
--- a/xos/configurations/cord-pod/vtn-external.yaml
+++ /dev/null
@@ -1,36 +0,0 @@
-tosca_definitions_version: tosca_simple_yaml_1_0
-
-description: Set up ONOS VTN app
-
-imports:
- - custom_types/xos.yaml
-
-topology_template:
- node_templates:
-
- service_ONOS_CORD:
- type: tosca.nodes.ONOSService
- requirements:
- properties:
- kind: onos
- view_url: /admin/onos/onosservice/$id$/
- no_container: true
- rest_hostname: onos-cord
-
- VTN_ONOS_app:
- type: tosca.nodes.ONOSVTNApp
- requirements:
- - onos_tenant:
- node: service_ONOS_CORD
- relationship: tosca.relationships.TenantOfService
- - vtn_service:
- node: service_vtn
- relationship: tosca.relationships.UsedByService
- properties:
- dependencies: org.onosproject.drivers, org.onosproject.drivers.ovsdb, org.onosproject.openflow-base, org.onosproject.ovsdb-base, org.onosproject.dhcp, org.onosproject.cordvtn, org.onosproject.olt, org.onosproject.igmp, org.onosproject.cordmcast
- autogenerate: vtn-network-cfg
-
- service_vtn:
- type: tosca.nodes.VTNService
- properties:
- view_url: /admin/vtn/vtnservice/$id$/
diff --git a/xos/configurations/devel/Makefile b/xos/configurations/devel/Makefile
index 1e650f3..e55f38b 100644
--- a/xos/configurations/devel/Makefile
+++ b/xos/configurations/devel/Makefile
@@ -7,7 +7,9 @@
xos:
sudo MYIP=$(MYIP) docker-compose up -d
bash ../common/wait_for_xos.sh
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
containers:
diff --git a/xos/configurations/frontend/Makefile b/xos/configurations/frontend/Makefile
index d2a1d5c..a8cb042 100644
--- a/xos/configurations/frontend/Makefile
+++ b/xos/configurations/frontend/Makefile
@@ -4,7 +4,12 @@
sudo make -f ../common/Makefile.prereqs
sudo docker-compose up -d
bash ../common/wait_for_xos.sh
+<<<<<<< HEAD
# bash ../mcord/xos/initdb
+=======
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+>>>>>>> upstream/master
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
# sudo docker-compose run xos python manage.py makemigrations mcordservice
# sudo docker-compose run xos python manage.py syncdb
@@ -29,9 +34,8 @@
sudo docker exec -ti frontend_xos_1 bash
mock-cord:
- sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
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 cp /opt/xos/configurations/cord-pod/xos_cord_config /opt/xos/xos_configuration/
sudo docker exec frontend_xos_1 touch /opt/xos/xos/settings.py
mock-mcord:
diff --git a/xos/configurations/frontend/mocks/cord.yaml b/xos/configurations/frontend/mocks/cord.yaml
index 8c84d8f..9d5aeaa 100644
--- a/xos/configurations/frontend/mocks/cord.yaml
+++ b/xos/configurations/frontend/mocks/cord.yaml
@@ -7,6 +7,21 @@
topology_template:
node_templates:
+
+ addresses_vsg:
+ type: tosca.nodes.AddressPool
+ properties:
+ addresses: 10.168.0.0/24
+ gateway_ip: 10.168.0.1
+ gateway_mac: 02:42:0a:a8:00:01
+
+ addresses_exampleservice-public:
+ type: tosca.nodes.AddressPool
+ properties:
+ addresses: 10.168.1.0/24
+ gateway_ip: 10.168.1.1
+ gateway_mac: 02:42:0a:a8:00:01
+
# CORD Services
service_volt:
type: tosca.nodes.Service
@@ -24,11 +39,23 @@
view_url: /admin/cord/voltservice/$id$/
kind: vOLT
+ service_vrouter:
+ type: tosca.nodes.VRouterService
+ properties:
+ view_url: /admin/vrouter/vrouterservice/$id$/
+ requirements:
+ - addresses_vsg:
+ node: addresses_vsg
+ relationship: tosca.relationships.ProvidesAddresses
+ - addresses_service1:
+ node: addresses_exampleservice-public
+ relationship: tosca.relationships.ProvidesAddresses
+
service_vsg:
type: tosca.nodes.VSGService
requirements:
- - vbng_tenant:
- node: service_vbng
+ - vrouter_tenant:
+ node: service_vrouter
relationship: tosca.relationships.TenantOfService
properties:
view_url: /admin/cord/vsgservice/$id$/
diff --git a/xos/configurations/frontend/xos.sql b/xos/configurations/frontend/xos.sql
index 7b13f4d..bbfd15a 100644
--- a/xos/configurations/frontend/xos.sql
+++ b/xos/configurations/frontend/xos.sql
@@ -5163,12 +5163,6 @@
265 Can add vbng tenant 12 add_vbngtenant
266 Can change vbng tenant 12 change_vbngtenant
267 Can delete vbng tenant 12 delete_vbngtenant
-268 Can add Hello World Service 7 add_helloworldservicecomplete
-269 Can change Hello World Service 7 change_helloworldservicecomplete
-270 Can delete Hello World Service 7 delete_helloworldservicecomplete
-271 Can add Hello World Tenant 12 add_helloworldtenantcomplete
-272 Can change Hello World Tenant 12 change_helloworldtenantcomplete
-273 Can delete Hello World Tenant 12 delete_helloworldtenantcomplete
274 Can add ONOS Service 7 add_onosservice
275 Can change ONOS Service 7 change_onosservice
276 Can delete ONOS Service 7 delete_onosservice
@@ -6509,8 +6503,6 @@
87 cord subscriber root cord cordsubscriberroot
88 vOLT Service cord voltservice
89 vSG Service cord vsgservice
-90 Hello World Tenant helloworldservice_complete helloworldtenantcomplete
-91 Hello World Service helloworldservice_complete helloworldservicecomplete
92 ONOS Service onos onosservice
93 onos app onos onosapp
94 s flow tenant ceilometer sflowtenant
@@ -6548,7 +6540,6 @@
4 auth 0001_initial 2016-04-05 17:41:46.384468+00
5 ceilometer 0001_initial 2016-04-05 17:41:46.659809+00
6 cord 0001_initial 2016-04-05 17:41:46.862406+00
-7 helloworldservice_complete 0001_initial 2016-04-05 17:41:47.056651+00
8 hpc 0001_initial 2016-04-05 17:41:50.450946+00
9 onos 0001_initial 2016-04-05 17:41:50.637887+00
10 requestrouter 0001_initial 2016-04-05 17:41:51.319325+00
diff --git a/xos/configurations/opencloud/Makefile b/xos/configurations/opencloud/Makefile
index aef4946..03168ed 100644
--- a/xos/configurations/opencloud/Makefile
+++ b/xos/configurations/opencloud/Makefile
@@ -1,6 +1,7 @@
xos:
sudo docker-compose up -d
bash ./wait_for_xos.sh
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/opencloud/opencloud.yaml
containers:
diff --git a/xos/configurations/openvpn/Makefile b/xos/configurations/openvpn/Makefile
new file mode 100644
index 0000000..aa6f2d5
--- /dev/null
+++ b/xos/configurations/openvpn/Makefile
@@ -0,0 +1,59 @@
+MYIP:=$(shell hostname -i)
+
+cloudlab: common_cloudlab xos
+
+xos:
+ sudo MYIP=$(MYIP) docker-compose up -d
+ bash ../common/wait_for_xos.sh
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
+
+frontend:
+ sudo make -f ../common/Makefile.prereqs
+ sudo docker-compose up -d
+ bash ../common/wait_for_xos.sh
+ sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
+
+containers:
+ cd ../../../containers/xos; make devel
+ cd ../../../containers/synchronizer; make
+ cd ../../../containers/openvpn; make
+
+common_cloudlab:
+ make -C ../common -f Makefile.cloudlab
+
+stop:
+ sudo MYIP=$(MYIP) docker-compose stop
+
+showlogs:
+ sudo MYIP=$(MYIP) docker-compose logs
+
+rm: stop
+ sudo MYIP=$(MYIP) docker-compose rm
+
+ps:
+ sudo MYIP=$(MYIP) docker-compose ps
+
+enter-xos:
+ sudo docker exec -it openvpn_xos_1 bash
+
+enter-synchronizer:
+ sudo docker exec -it openvpn_xos_synchronizer_openvpn_1 bash
+
+upgrade_pkgs:
+ sudo pip install httpie --upgrade
+
+rebuild_xos:
+ make -C ../../../containers/xos devel
+
+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
+ socker 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/openvpn/docker-compose.yml b/xos/configurations/openvpn/docker-compose.yml
new file mode 100644
index 0000000..e609838
--- /dev/null
+++ b/xos/configurations/openvpn/docker-compose.yml
@@ -0,0 +1,62 @@
+xos_db:
+ image: xosproject/xos-postgres
+ expose:
+ - "5432"
+
+xos_synchronizer_openstack:
+ image: xosproject/xos-synchronizer-openstack
+ command: bash -c "sleep 120; python /opt/xos/synchronizers/openstack/xos-synchronizer.py"
+ labels:
+ org.xosproject.kind: synchronizer
+ org.xosproject.target: openstack
+ links:
+ - xos_db
+ extra_hosts:
+ - ctl:${MYIP}
+ volumes:
+ - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
+ - ./images:/opt/xos/images:ro
+
+xos_synchronizer_openvpn:
+ image: xosproject/xos-openvpn
+ command: bash -c "sleep 120 ; python /opt/xos/synchronizers/openvpn/openvpn-synchronizer.py -C /opt/xos/synchronizers/openvpn/openvpn_config"
+ labels:
+ org.xosproject.kind: synchronizer
+ org.xosproject.target: openvpn
+ links:
+ - xos_db
+ extra_hosts:
+ - ctl:${MYIP}
+ volumes:
+ - ../setup/id_rsa:/opt/xos/synchronizers/openvpn/openvpn_private_key:ro # private key
+ volumes_from:
+ - xos_openvpn_data:rw
+
+xos_openvpn_data:
+ image: xosproject/xos-openvpn
+ links:
+ - xos_db
+ extra_hosts:
+ - ctl:${MYIP}
+ volumes:
+ - /opt/openvpn
+
+# FUTURE
+#xos_swarm_synchronizer:
+# image: xosproject/xos-swarm-synchronizer
+# labels:
+# org.xosproject.kind: synchronizer
+# org.xosproject.target: swarm
+
+xos:
+ image: xosproject/xos-openvpn
+ command: python /opt/xos/manage.py runserver 0.0.0.0:8000 --insecure --makemigrations
+ ports:
+ - "9999:8000"
+ links:
+ - xos_db
+ volumes:
+ - ../setup:/root/setup:ro
+ - ../common/xos_common_config:/opt/xos/xos_configuration/xos_common_config:ro
+ volumes_from:
+ - xos_openvpn_data:rw
diff --git a/xos/configurations/syndicate/Makefile b/xos/configurations/syndicate/Makefile
index eb8050e..3b29ee5 100644
--- a/xos/configurations/syndicate/Makefile
+++ b/xos/configurations/syndicate/Makefile
@@ -7,7 +7,9 @@
xos: syndicate_config
sudo MYIP=$(MYIP) docker-compose up -d
bash ../common/wait_for_xos.sh
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
containers:
diff --git a/xos/configurations/test-standalone/Makefile b/xos/configurations/test-standalone/Makefile
index 50f2cc5..7de1598 100644
--- a/xos/configurations/test-standalone/Makefile
+++ b/xos/configurations/test-standalone/Makefile
@@ -16,8 +16,6 @@
export TRUNCATE_FN
prepare: xos
- # INSTALL DEPS
- # RUN ONCE BEFORE RUNNING TESTS
sudo docker exec -i teststandalone_xos_1 bash -c "cd /opt/xos/tests/api; npm install --production"
sudo docker exec teststandalone_xos_1 pip install dredd_hooks
@@ -25,30 +23,27 @@
sudo make -f ../common/Makefile.prereqs
sudo docker-compose up -d
bash ../common/wait_for_xos.sh
- # sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
- # sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
restore-initial-db-status:
- sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "$$TRUNCATE_FN"
- sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT truncate_tables('postgres');"
- sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT setval('core_tenant_id_seq', 1)"
+ sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "$$TRUNCATE_FN" >/dev/null 2>&1
+ sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT truncate_tables('postgres');" >/dev/null 2>&1
+ sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT setval('core_tenant_id_seq', 1)" >/dev/null 2>&1
+ sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT setval('core_deployment_id_seq', 1)" >/dev/null 2>&1
+ sudo docker exec teststandalone_xos_db_1 psql -U postgres -d xos -c "SELECT setval('core_flavor_id_seq', 1)" >/dev/null 2>&1
sudo docker-compose run xos python /opt/xos/manage.py --noobserver --nomodelpolicy loaddata /opt/xos/core/fixtures/core_initial_data.json
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/frontend/sample.yaml
- sudo docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/fixtures.yaml
-
test: restore-initial-db-status
# RUN TESTS
+ sudo docker cp ../../../apiary.apib teststandalone_xos_1:/opt/xos/tests/api/apiary.apib
sudo docker exec -i teststandalone_xos_1 bash -c "cd /opt/xos/tests/api; npm test"
test-tosca:
sudo docker-compose run xos bash -c "cd /opt/xos/tosca/tests; python ./alltests.py"
-test-gui:
- cd ../../../views/ngXosViews/ceilometerDashboard; npm install
- cd ../../../views/ngXosViews/ceilometerDashboard; npm run test:ci
-
base-container:
cd ../../../containers/xos; make devel
diff --git a/xos/configurations/test/Makefile b/xos/configurations/test/Makefile
index adf349a..d4e54fd 100644
--- a/xos/configurations/test/Makefile
+++ b/xos/configurations/test/Makefile
@@ -7,7 +7,9 @@
xos:
sudo MYIP=$(MYIP) docker-compose up -d
bash ../common/wait_for_xos.sh
- sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/base.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/fixtures.yaml
+ sudo docker-compose run xos python /opt/xos/tosca/run.py none /opt/xos/configurations/common/mydeployment.yaml
+ sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /opt/xos/configurations/common/cloudlab-openstack.yaml
sudo MYIP=$(MYIP) docker-compose run xos python /opt/xos/tosca/run.py padmin@vicci.org /root/setup/nodes.yaml
sudo MYIP=$(MYIP) docker-compose run xos bash -c "cd /opt/xos/tosca/tests; python ./alltests.py"
sudo MYIP=$(MYIP) docker-compose run xos bash -c "cd /opt/xos/tosca/tests; python ./allObserverTests.py"
diff --git a/xos/core/admin.py b/xos/core/admin.py
index f14710b..a0fd3cb 100644
--- a/xos/core/admin.py
+++ b/xos/core/admin.py
@@ -977,6 +977,24 @@
)
+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'
+ fields = ['backend_status_icon', 'user', 'role', 'tenant']
+ readonly_fields = ('backend_status_icon', )
+
+ def queryset(self, request):
+ return TenantPrivilege.select_by_user(request.user)
+
+
class ProviderTenantInline(XOSTabularInline):
model = CoarseTenant
fields = ['provider_service', 'subscriber_service', 'connect_method']
@@ -2363,6 +2381,17 @@
return tabs
+class AddressPoolInline(XOSTabularInline):
+ model = AddressPool
+ extra = 0
+ suit_classes = 'suit-tab suit-tab-addresspools'
+ fields = ['cidr', 'gateway_ip', 'gateway_mac']
+ readonly_fields = ['cidr',]
+
+ # disable the add link
+ def has_add_permission(self, request):
+ return False
+
# Now register the new UserAdmin...
admin.site.register(User, UserAdmin)
# ... and, since we're not using Django's builtin permissions,
@@ -2404,5 +2433,6 @@
admin.site.register(Flavor, FlavorAdmin)
admin.site.register(TenantRoot, TenantRootAdmin)
admin.site.register(TenantRootRole, TenantRootRoleAdmin)
+ admin.site.register(TenantRole, TenantRoleAdmin)
admin.site.register(TenantAttribute, TenantAttributeAdmin)
admin.site.register(AddressPool, AddressPoolAdmin)
diff --git a/xos/core/fixtures/core_initial_data.json b/xos/core/fixtures/core_initial_data.json
index 86658bb..01ff999 100644
--- a/xos/core/fixtures/core_initial_data.json
+++ b/xos/core/fixtures/core_initial_data.json
@@ -1,214 +1,6 @@
[
{
"fields": {
- "updated": "2015-02-17T22:06:37.837Z",
- "policed": null,
- "created": "2015-02-17T22:06:37.837Z",
- "deleted": false,
- "site_url": null,
- "enabled": true,
- "longitude": null,
- "name": "MySite",
- "backend_register": "{}",
- "login_base": "mysite",
- "location": "0,0",
- "latitude": null,
- "is_public": true,
- "backend_status": "0 - Provisioning in progress",
- "abbreviated_name": "mysite",
- "enacted": null
- },
- "model": "core.site",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.620Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.620Z",
- "deleted": false,
- "backend_register": "{}",
- "role": "admin",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.siterole",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.670Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.669Z",
- "deleted": false,
- "backend_register": "{}",
- "role": "pi",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.siterole",
- "pk": 2
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.731Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.730Z",
- "deleted": false,
- "backend_register": "{}",
- "role": "tech",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.siterole",
- "pk": 3
-},
-{
- "fields": {
- "accessControl": "allow all",
- "updated": "2015-02-17T22:06:37.789Z",
- "policed": null,
- "created": "2015-02-17T22:06:37.789Z",
- "deleted": false,
- "name": "MyDeployment",
- "backend_register": "{}",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.deployment",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.894Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.894Z",
- "deleted": false,
- "backend_register": "{}",
- "role": "admin",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.deploymentrole",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:37.893Z",
- "policed": null,
- "created": "2015-02-17T22:06:37.893Z",
- "deleted": false,
- "availability_zone": null,
- "site": 1,
- "backend_register": "{}",
- "controller": null,
- "deployment": 1,
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.sitedeployment",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.953Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.953Z",
- "deleted": false,
- "deployments": [],
- "enabled": true,
- "name": "xsh",
- "backend_register": "{}",
- "url": "template:xsh",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.dashboardview",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:39.011Z",
- "policed": null,
- "created": "2015-02-17T22:06:39.011Z",
- "deleted": false,
- "deployments": [],
- "enabled": true,
- "name": "Customize",
- "backend_register": "{}",
- "url": "template:customize",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.dashboardview",
- "pk": 2
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:39.244Z",
- "policed": null,
- "created": "2015-02-17T22:06:39.069Z",
- "deleted": false,
- "deployments": [
- 1
- ],
- "enabled": true,
- "name": "Tenant",
- "backend_register": "{}",
- "url": "template:xosTenant",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.dashboardview",
- "pk": 3
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:39.302Z",
- "policed": null,
- "created": "2015-02-17T22:06:39.302Z",
- "deleted": false,
- "deployments": [],
- "enabled": true,
- "name": "Developer",
- "backend_register": "{}",
- "url": "template:xosDeveloper_datatables",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.dashboardview",
- "pk": 4
-},
-{
- "fields": {
- "policed": null,
- "site": 1,
- "is_staff": true,
- "timezone": "America/New_York",
- "backend_status": "Provisioning in progress",
- "is_registering": false,
- "last_login": "2015-02-17T22:35:17.822Z",
- "email": "padmin@vicci.org",
- "username": "padmin@vicci.org",
- "updated": "2015-02-17T22:06:38.059Z",
- "firstname": "XOS",
- "user_url": null,
- "deleted": false,
- "lastname": "admin",
- "is_active": true,
- "phone": null,
- "is_admin": true,
- "password": "pbkdf2_sha256$12000$Qufx9iqtaYma$xs0YurPOcj9qYQna/Qrb3K+im9Yr2XEVr0J4Kqek7AE=",
- "enacted": null,
- "public_key": null,
- "is_readonly": false,
- "created": "2015-02-17T22:06:38.059Z"
- },
- "model": "core.user",
- "pk": 1
-},
-{
- "fields": {
"updated": "2015-02-17T22:06:39.361Z",
"membershipFee": 0,
"policed": null,
@@ -226,162 +18,5 @@
},
"model": "core.serviceclass",
"pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.236Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.095Z",
- "deleted": false,
- "deployments": [
- 1
- ],
- "description": null,
- "name": "m1.small",
- "backend_register": "{}",
- "default": false,
- "flavor": "m1.small",
- "backend_status": "0 - Provisioning in progress",
- "order": 0,
- "enacted": null
- },
- "model": "core.flavor",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.394Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.287Z",
- "deleted": false,
- "deployments": [
- 1
- ],
- "description": null,
- "name": "m1.medium",
- "backend_register": "{}",
- "default": false,
- "flavor": "m1.medium",
- "backend_status": "0 - Provisioning in progress",
- "order": 0,
- "enacted": null
- },
- "model": "core.flavor",
- "pk": 2
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.561Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.445Z",
- "deleted": false,
- "deployments": [
- 1
- ],
- "description": null,
- "name": "m1.large",
- "backend_register": "{}",
- "default": false,
- "flavor": "m1.large",
- "backend_status": "0 - Provisioning in progress",
- "order": 0,
- "enacted": null
- },
- "model": "core.flavor",
- "pk": 3
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.778Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.778Z",
- "deleted": false,
- "backend_register": "{}",
- "role": "admin",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.slicerole",
- "pk": 1
-},
-{
- "fields": {
- "updated": "2015-02-17T22:06:38.836Z",
- "policed": null,
- "created": "2015-02-17T22:06:38.836Z",
- "deleted": false,
- "backend_register": "{}",
- "role": "access",
- "backend_status": "0 - Provisioning in progress",
- "enacted": null
- },
- "model": "core.slicerole",
- "pk": 2
-},
-{
- "fields": {
- "shared_network_id": null,
- "updated": "2015-02-17T22:06:39.419Z",
- "policed": null,
- "created": "2015-02-17T22:06:39.419Z",
- "deleted": false,
- "description": "A private virtual network",
- "visibility": "private",
- "name": "Private",
- "backend_register": "{}",
- "topology_kind": "bigswitch",
- "guaranteed_bandwidth": 0,
- "translation": "none",
- "backend_status": "0 - Provisioning in progress",
- "shared_network_name": null,
- "controller_kind": null,
- "enacted": null
- },
- "model": "core.networktemplate",
- "pk": 1
-},
-{
- "fields": {
- "shared_network_id": null,
- "updated": "2015-02-17T22:06:39.477Z",
- "policed": null,
- "created": "2015-02-17T22:06:39.477Z",
- "deleted": false,
- "description": "Connect a instance to the public network",
- "visibility": "private",
- "name": "Public shared IPv4",
- "backend_register": "{}",
- "topology_kind": "bigswitch",
- "guaranteed_bandwidth": 0,
- "translation": "NAT",
- "backend_status": "0 - Provisioning in progress",
- "shared_network_name": "nat-net",
- "controller_kind": null,
- "enacted": null
- },
- "model": "core.networktemplate",
- "pk": 2
-},
-{
- "fields": {
- "shared_network_id": null,
- "updated": "2015-02-17T22:06:39.536Z",
- "policed": null,
- "created": "2015-02-17T22:06:39.536Z",
- "deleted": false,
- "description": "Connect a instance to the public network",
- "visibility": "public",
- "name": "Public dedicated IPv4",
- "backend_register": "{}",
- "topology_kind": "bigswitch",
- "guaranteed_bandwidth": 0,
- "translation": "none",
- "backend_status": "0 - Provisioning in progress",
- "shared_network_name": "ext-net",
- "controller_kind": null,
- "enacted": null
- },
- "model": "core.networktemplate",
- "pk": 3
}
]
diff --git a/xos/core/migrations/0001_initial.py b/xos/core/migrations/0001_initial.py
index b2e5d00..c55a8bf 100644
--- a/xos/core/migrations/0001_initial.py
+++ b/xos/core/migrations/0001_initial.py
@@ -1628,6 +1628,52 @@
},
bases=(models.Model,),
),
+ migrations.CreateModel(
+ name='TenantPrivilege',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID',
+ serialize=False, auto_created=True, primary_key=True)),
+ ('created', models.DateTimeField(
+ default=django.utils.timezone.now, auto_now_add=True)),
+ ('updated', models.DateTimeField(
+ default=django.utils.timezone.now, auto_now=True)),
+ ('enacted', models.DateTimeField(
+ default=None, null=True, blank=True)),
+ ('policed', models.DateTimeField(
+ default=None, null=True, blank=True)),
+ ('backend_status', models.CharField(
+ default=b'Provisioning in progress', max_length=140)),
+ ('deleted', models.BooleanField(default=False)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ migrations.CreateModel(
+ name='TenantRole',
+ fields=[
+ ('id', models.AutoField(verbose_name='ID',
+ serialize=False, auto_created=True, primary_key=True)),
+ ('created', models.DateTimeField(
+ default=django.utils.timezone.now, auto_now_add=True)),
+ ('updated', models.DateTimeField(
+ default=django.utils.timezone.now, auto_now=True)),
+ ('enacted', models.DateTimeField(
+ default=None, null=True, blank=True)),
+ ('policed', models.DateTimeField(
+ default=None, null=True, blank=True)),
+ ('backend_status', models.CharField(
+ default=b'Provisioning in progress', max_length=140)),
+ ('deleted', models.BooleanField(default=False)),
+ ('role', models.CharField(unique=True, max_length=30,
+ choices=[(b'admin', b'Admin'), (b'access', b'Access')])),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
migrations.AddField(
model_name='sliceprivilege',
name='role',
diff --git a/xos/core/models/__init__.py b/xos/core/models/__init__.py
index 6fad0f1..5b0ad4b 100644
--- a/xos/core/models/__init__.py
+++ b/xos/core/models/__init__.py
@@ -1,7 +1,7 @@
from .plcorebase import PlCoreBase,PlCoreBaseManager,PlCoreBaseDeletionManager,PlModelMixIn
from .project import Project
from .singletonmodel import SingletonModel
-from .service import Service, Tenant, TenantWithContainer, CoarseTenant, ServicePrivilege, TenantRoot, TenantRootPrivilege, TenantRootRole, Subscriber, Provider
+from .service import Service, Tenant, TenantWithContainer, CoarseTenant, ServicePrivilege, TenantRoot, TenantRootPrivilege, TenantRootRole, TenantPrivilege, TenantRole, Subscriber, Provider
from .service import ServiceAttribute, TenantAttribute, ServiceRole
from .tag import Tag
from .role import Role
diff --git a/xos/core/models/plcorebase.py b/xos/core/models/plcorebase.py
index c8c25a7..060570d 100644
--- a/xos/core/models/plcorebase.py
+++ b/xos/core/models/plcorebase.py
@@ -2,6 +2,7 @@
import json
import os
import sys
+import threading
from django import db
from django.db import models
from django.forms.models import model_to_dict
@@ -198,7 +199,7 @@
# default values for created and updated are only there to keep evolution
# from failing.
created = models.DateTimeField(auto_now_add=True, default=timezone.now)
- updated = models.DateTimeField(auto_now=True, default=timezone.now)
+ updated = models.DateTimeField(default=timezone.now)
enacted = models.DateTimeField(null=True, blank=True, default=None)
policed = models.DateTimeField(null=True, blank=True, default=None)
@@ -270,6 +271,9 @@
if not (field in ["backend_register", "backend_status", "deleted", "enacted", "updated"]):
ignore_composite_key_check=False
+ if 'synchronizer' not in threading.current_thread().name:
+ self.updated = datetime.datetime.now()
+
super(PlCoreBase, self).save(*args, **kwargs)
# This is a no-op if observer_disabled is set
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index f1f8de4..cece4a1 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -67,7 +67,7 @@
kind = StrippedCharField(
max_length=30, help_text="Kind of service", default=KIND)
name = StrippedCharField(max_length=30, help_text="Service Name")
- versionNumber = StrippedCharField(
+ versionNumber = StrippedCharField(blank=True, null=True,
max_length=30, help_text="Version of Service Definition")
published = models.BooleanField(default=True)
view_url = StrippedCharField(blank=True, null=True, max_length=1024)
@@ -908,3 +908,52 @@
[trp.id for trp in cls.objects.filter(tenant_root=priv.tenant_root)])
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")
+ role = models.ForeignKey('TenantRole', related_name="tenantprivileges")
+
+ def __unicode__(self): return u'%s %s %s' % (
+ self.tenant, self.user, self.role)
+
+ def save(self, *args, **kwds):
+ if not self.user.is_active:
+ raise PermissionDenied, "Cannot modify role(s) of a disabled user"
+ super(TenantPrivilege, self).save(*args, **kwds)
+
+ def can_update(self, user):
+ return user.can_update_tenant_privilege(self)
+
+ @classmethod
+ def select_by_user(cls, user):
+ if user.is_admin:
+ return cls.objects.all()
+ else:
+ # User can see his own privilege
+ trp_ids = [trp.id for trp in cls.objects.filter(user=user)]
+
+ # A tenant admin can see the TenantPrivileges for their Tenants
+ for priv in cls.objects.filter(user=user, role__role="admin"):
+ trp_ids.extend(
+ [trp.id for trp in cls.objects.filter(tenant=priv.tenant)])
+
+ return cls.objects.filter(id__in=trp_ids)
diff --git a/xos/core/models/slice.py b/xos/core/models/slice.py
index aefe08c..73fa121 100644
--- a/xos/core/models/slice.py
+++ b/xos/core/models/slice.py
@@ -7,7 +7,7 @@
from core.models import Role
from core.models import Controller,ControllerLinkManager,ControllerLinkDeletionManager
from core.models import ServiceClass
-from core.models.serviceclass import get_default_serviceclass
+#from core.models.serviceclass import get_default_serviceclass
from core.models import Tag
from django.contrib.contenttypes import generic
from core.models import Service
@@ -35,7 +35,7 @@
network = models.CharField(null=True, blank=True, max_length=256, choices=NETWORK_CHOICES)
exposed_ports = models.CharField(null=True, blank=True, max_length=256)
tags = generic.GenericRelation(Tag)
- serviceClass = models.ForeignKey(ServiceClass, related_name = "slices", null=True, default=get_default_serviceclass)
+ serviceClass = models.ForeignKey(ServiceClass, related_name = "slices", null=True, blank=True) # DEPRECATED
creator = models.ForeignKey(User, related_name='slices', blank=True, null=True)
# for tenant view
@@ -64,12 +64,6 @@
if " " in self.name:
raise XOSValidationError('slice name must not contain spaces')
- if self.serviceClass is None:
- # We allowed None=True for serviceClass because Django evolution
- # will fail unless it is allowed. But, we we really don't want it to
- # ever save None, so fix it up here.
- self.serviceClass = ServiceClass.get_default()
-
# set creator on first save
if not self.creator and hasattr(self, 'caller'):
self.creator = self.caller
diff --git a/xos/core/models/user.py b/xos/core/models/user.py
index f1d73d2..715c670 100644
--- a/xos/core/models/user.py
+++ b/xos/core/models/user.py
@@ -356,9 +356,21 @@
return True
return False
+ def can_update_tenant(self, tenant, allow=[]):
+ from core.models.service import Tenant, TenantPrivilege
+ if self.can_update_root():
+ return True
+ if TenantPrivilege.objects.filter(
+ tenant=tenant, user=self, role__role__in=['admin', 'Admin'] + allow):
+ return True
+ return False
+
def can_update_tenant_root_privilege(self, tenant_root_privilege, allow=[]):
return self.can_update_tenant_root(tenant_root_privilege.tenant_root, allow)
+ def can_update_tenant_privilege(self, tenant_privilege, allow=[]):
+ return self.can_update_tenant(tenant_privilege.tenant, allow)
+
def get_readable_objects(self, filter_by=None):
""" Returns a list of objects that the user is allowed to read. """
from core.models import Deployment, Flavor, Image, Network, NetworkTemplate, Node, PlModelMixIn, Site, Slice, SliceTag, Instance, Tag, User, DeploymentPrivilege, SitePrivilege, SlicePrivilege
diff --git a/xos/core/static/.gitignore b/xos/core/static/.gitignore
index de9b24c..c148592 100644
--- a/xos/core/static/.gitignore
+++ b/xos/core/static/.gitignore
@@ -1,3 +1,4 @@
*.css
!xos.css
-!cord.css
\ No newline at end of file
+!cord.css
+!xosNgLib.css
\ No newline at end of file
diff --git a/xos/core/static/xosNgLib.css b/xos/core/static/xosNgLib.css
new file mode 100644
index 0000000..22631da
--- /dev/null
+++ b/xos/core/static/xosNgLib.css
@@ -0,0 +1,155 @@
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible; }
+ to {
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0); }
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0); } }
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); }
+ to {
+ opacity: 1;
+ transform: none; } }
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1; }
+ to {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); } }
+
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible; }
+ to {
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0); }
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0); } }
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); }
+ to {
+ opacity: 1;
+ transform: none; } }
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1; }
+ to {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); } }
+
+xos-table {
+ display: block; }
+ xos-table tr.ng-move,
+ xos-table tr.ng-enter,
+ xos-table tr.ng-leave {
+ transition: all linear 0.5s; }
+ xos-table tr.ng-leave.ng-leave-active,
+ xos-table tr.ng-move,
+ xos-table tr.ng-enter {
+ opacity: 0;
+ animation: 0.5s slideOutRight ease-in-out; }
+ xos-table tr.ng-leave,
+ xos-table tr.ng-move.ng-move-active,
+ xos-table tr.ng-enter.ng-enter-active {
+ opacity: 1;
+ animation: 0.5s slideInRight ease-in-out; }
+
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible; }
+ to {
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0); }
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0); } }
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); }
+ to {
+ opacity: 1;
+ transform: none; } }
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1; }
+ to {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); } }
+
+xos-alert {
+ /* when hiding */
+ /* when showing */ }
+ xos-alert .ng-hide-add {
+ animation: 0.5s fadeOutDown ease-in-out; }
+ xos-alert .ng-hide-remove {
+ animation: 0.5s fadeInUp ease-in-out; }
+
+@keyframes slideInRight {
+ from {
+ transform: translate3d(100%, 0, 0);
+ visibility: visible; }
+ to {
+ transform: translate3d(0, 0, 0); } }
+
+@keyframes slideOutRight {
+ from {
+ transform: translate3d(0, 0, 0); }
+ to {
+ visibility: hidden;
+ transform: translate3d(100%, 0, 0); } }
+
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); }
+ to {
+ opacity: 1;
+ transform: none; } }
+
+@keyframes fadeOutDown {
+ from {
+ opacity: 1; }
+ to {
+ opacity: 0;
+ transform: translate3d(0, 100%, 0); } }
+
+input + xos-validation {
+ margin-top: 15px;
+ display: block; }
+
+xos-smart-table .row + xos-table {
+ margin-top: 15px; }
+
+[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
+ display: none !important; }
+
+.row + .row {
+ margin-top: 20px; }
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoieG9zTmdMaWIuY3NzIiwic291cmNlcyI6WyJtYWluLnNjc3MiLCJhbmltYXRpb25zLnNjc3MiLCIuLi91aV9jb21wb25lbnRzL2R1bWJDb21wb25lbnRzL3RhYmxlL3RhYmxlLnNjc3MiLCIuLi91aV9jb21wb25lbnRzL2R1bWJDb21wb25lbnRzL2FsZXJ0L2FsZXJ0LnNjc3MiLCIuLi91aV9jb21wb25lbnRzL2R1bWJDb21wb25lbnRzL3ZhbGlkYXRpb24vdmFsaWRhdGlvbi5zY3NzIiwiLi4vLi4vLi4vLi4vc3R5bGUvc2Fzcy9ib290c3RyYXAvYm9vdHN0cmFwL192YXJpYWJsZXMuc2NzcyIsIi4uL3VpX2NvbXBvbmVudHMvc21hcnRDb21wb25lbnRzL3NtYXJ0VGFibGUvc21hcnRUYWJsZS5zY3NzIl0sInNvdXJjZXNDb250ZW50IjpbIkBpbXBvcnQgJy4vYW5pbWF0aW9ucy5zY3NzJztcblxuQGltcG9ydCAnLi4vdWlfY29tcG9uZW50cy9kdW1iQ29tcG9uZW50cy90YWJsZS90YWJsZS5zY3NzJztcbkBpbXBvcnQgJy4uL3VpX2NvbXBvbmVudHMvZHVtYkNvbXBvbmVudHMvYWxlcnQvYWxlcnQuc2Nzcyc7XG5AaW1wb3J0ICcuLi91aV9jb21wb25lbnRzL2R1bWJDb21wb25lbnRzL3ZhbGlkYXRpb24vdmFsaWRhdGlvbi5zY3NzJztcblxuQGltcG9ydCAnLi4vdWlfY29tcG9uZW50cy9zbWFydENvbXBvbmVudHMvc21hcnRUYWJsZS9zbWFydFRhYmxlLnNjc3MnO1xuXG5bbmdcXDpjbG9ha10sIFtuZy1jbG9ha10sIFtkYXRhLW5nLWNsb2FrXSwgW3gtbmctY2xvYWtdLCAubmctY2xvYWssIC54LW5nLWNsb2FrIHtcbiAgZGlzcGxheTogbm9uZSAhaW1wb3J0YW50O1xufVxuXG4ucm93ICsgLnJvdyB7XG4gIG1hcmdpbi10b3A6IDIwcHg7XG59IiwiQGtleWZyYW1lcyBzbGlkZUluUmlnaHQge1xuICBmcm9tIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDEwMCUsIDAsIDApO1xuICAgIHZpc2liaWxpdHk6IHZpc2libGU7XG4gIH1cblxuICB0byB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxufVxuXG5Aa2V5ZnJhbWVzIHNsaWRlT3V0UmlnaHQge1xuICBmcm9tIHtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuICB9XG5cbiAgdG8ge1xuICAgIHZpc2liaWxpdHk6IGhpZGRlbjtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDEwMCUsIDAsIDApO1xuICB9XG59XG5cbkBrZXlmcmFtZXMgZmFkZUluVXAge1xuICBmcm9tIHtcbiAgICBvcGFjaXR5OiAwO1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgMTAwJSwgMCk7XG4gIH1cblxuICB0byB7XG4gICAgb3BhY2l0eTogMTtcbiAgICB0cmFuc2Zvcm06IG5vbmU7XG4gIH1cbn1cblxuQGtleWZyYW1lcyBmYWRlT3V0RG93biB7XG4gIGZyb20ge1xuICAgIG9wYWNpdHk6IDE7XG4gIH1cblxuICB0byB7XG4gICAgb3BhY2l0eTogMDtcbiAgICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDEwMCUsIDApO1xuICB9XG59IiwiQGltcG9ydCAnLi4vLi4vLi4vc3R5bGVzL2FuaW1hdGlvbnMuc2Nzcyc7XG5cbnhvcy10YWJsZSB7XG5cbiAgZGlzcGxheTogYmxvY2s7XG5cbiAgdHIubmctbW92ZSxcbiAgdHIubmctZW50ZXIsXG4gIHRyLm5nLWxlYXZlIHtcbiAgICB0cmFuc2l0aW9uOmFsbCBsaW5lYXIgMC41cztcbiAgfVxuXG4gIHRyLm5nLWxlYXZlLm5nLWxlYXZlLWFjdGl2ZSxcbiAgdHIubmctbW92ZSxcbiAgdHIubmctZW50ZXIge1xuICAgIG9wYWNpdHk6MDtcbiAgICBhbmltYXRpb246IDAuNXMgc2xpZGVPdXRSaWdodCBlYXNlLWluLW91dDtcbiAgfVxuXG4gIHRyLm5nLWxlYXZlLFxuICB0ci5uZy1tb3ZlLm5nLW1vdmUtYWN0aXZlLFxuICB0ci5uZy1lbnRlci5uZy1lbnRlci1hY3RpdmUge1xuICAgIG9wYWNpdHk6MTtcbiAgICBhbmltYXRpb246IDAuNXMgc2xpZGVJblJpZ2h0IGVhc2UtaW4tb3V0O1xuICB9XG59IiwiQGltcG9ydCAnLi4vLi4vLi4vc3R5bGVzL2FuaW1hdGlvbnMuc2Nzcyc7XG5cbnhvcy1hbGVydCB7XG5cbiAgLyogd2hlbiBoaWRpbmcgKi9cbiAgLm5nLWhpZGUtYWRkICAgICAgICAgeyBhbmltYXRpb246MC41cyBmYWRlT3V0RG93biBlYXNlLWluLW91dDsgfVxuXG4gIC8qIHdoZW4gc2hvd2luZyAqL1xuICAubmctaGlkZS1yZW1vdmUgICAgICB7IGFuaW1hdGlvbjowLjVzIGZhZGVJblVwIGVhc2UtaW4tb3V0OyB9XG59IiwiQGltcG9ydCAnLi4vLi4vLi4vc3R5bGVzL2FuaW1hdGlvbnMuc2Nzcyc7XG5AaW1wb3J0ICcuLi8uLi8uLi8uLi8uLi8uLi9zdHlsZS9zYXNzL2Jvb3RzdHJhcC9ib290c3RyYXAvX3ZhcmlhYmxlcy5zY3NzJztcblxuaW5wdXQgKyB4b3MtdmFsaWRhdGlvbiB7XG4gIG1hcmdpbi10b3A6ICRmb3JtLWdyb3VwLW1hcmdpbi1ib3R0b207XG4gIGRpc3BsYXk6IGJsb2NrO1xufSIsIiRib290c3RyYXAtc2Fzcy1hc3NldC1oZWxwZXI6IGZhbHNlICFkZWZhdWx0O1xuLy9cbi8vIFZhcmlhYmxlc1xuLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS1cblxuXG4vLz09IENvbG9yc1xuLy9cbi8vIyMgR3JheSBhbmQgYnJhbmQgY29sb3JzIGZvciB1c2UgYWNyb3NzIEJvb3RzdHJhcC5cblxuJGdyYXktYmFzZTogICAgICAgICAgICAgICMwMDAgIWRlZmF1bHQ7XG4kZ3JheS1kYXJrZXI6ICAgICAgICAgICAgbGlnaHRlbigkZ3JheS1iYXNlLCAxMy41JSkgIWRlZmF1bHQ7IC8vICMyMjJcbiRncmF5LWRhcms6ICAgICAgICAgICAgICBsaWdodGVuKCRncmF5LWJhc2UsIDIwJSkgIWRlZmF1bHQ7ICAgLy8gIzMzM1xuJGdyYXk6ICAgICAgICAgICAgICAgICAgIGxpZ2h0ZW4oJGdyYXktYmFzZSwgMzMuNSUpICFkZWZhdWx0OyAvLyAjNTU1XG4kZ3JheS1saWdodDogICAgICAgICAgICAgbGlnaHRlbigkZ3JheS1iYXNlLCA0Ni43JSkgIWRlZmF1bHQ7IC8vICM3NzdcbiRncmF5LWxpZ2h0ZXI6ICAgICAgICAgICBsaWdodGVuKCRncmF5LWJhc2UsIDkzLjUlKSAhZGVmYXVsdDsgLy8gI2VlZVxuXG4kYnJhbmQtcHJpbWFyeTogICAgICAgICBkYXJrZW4oIzQyOGJjYSwgNi41JSkgIWRlZmF1bHQ7IC8vICMzMzdhYjdcbiRicmFuZC1zdWNjZXNzOiAgICAgICAgICM1Y2I4NWMgIWRlZmF1bHQ7XG4kYnJhbmQtaW5mbzogICAgICAgICAgICAjNWJjMGRlICFkZWZhdWx0O1xuJGJyYW5kLXdhcm5pbmc6ICAgICAgICAgI2YwYWQ0ZSAhZGVmYXVsdDtcbiRicmFuZC1kYW5nZXI6ICAgICAgICAgICNkOTUzNGYgIWRlZmF1bHQ7XG5cblxuLy89PSBTY2FmZm9sZGluZ1xuLy9cbi8vIyMgU2V0dGluZ3MgZm9yIHNvbWUgb2YgdGhlIG1vc3QgZ2xvYmFsIHN0eWxlcy5cblxuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIGZvciBgPGJvZHk+YC5cbiRib2R5LWJnOiAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4vLyoqIEdsb2JhbCB0ZXh0IGNvbG9yIG9uIGA8Ym9keT5gLlxuJHRleHQtY29sb3I6ICAgICAgICAgICAgJGdyYXktZGFyayAhZGVmYXVsdDtcblxuLy8qKiBHbG9iYWwgdGV4dHVhbCBsaW5rIGNvbG9yLlxuJGxpbmstY29sb3I6ICAgICAgICAgICAgJGJyYW5kLXByaW1hcnkgIWRlZmF1bHQ7XG4vLyoqIExpbmsgaG92ZXIgY29sb3Igc2V0IHZpYSBgZGFya2VuKClgIGZ1bmN0aW9uLlxuJGxpbmstaG92ZXItY29sb3I6ICAgICAgZGFya2VuKCRsaW5rLWNvbG9yLCAxNSUpICFkZWZhdWx0O1xuLy8qKiBMaW5rIGhvdmVyIGRlY29yYXRpb24uXG4kbGluay1ob3Zlci1kZWNvcmF0aW9uOiB1bmRlcmxpbmUgIWRlZmF1bHQ7XG5cblxuLy89PSBUeXBvZ3JhcGh5XG4vL1xuLy8jIyBGb250LCBsaW5lLWhlaWdodCwgYW5kIGNvbG9yIGZvciBib2R5IHRleHQsIGhlYWRpbmdzLCBhbmQgbW9yZS5cblxuJGZvbnQtZmFtaWx5LXNhbnMtc2VyaWY6ICBcIkhlbHZldGljYSBOZXVlXCIsIEhlbHZldGljYSwgQXJpYWwsIHNhbnMtc2VyaWYgIWRlZmF1bHQ7XG4kZm9udC1mYW1pbHktc2VyaWY6ICAgICAgIEdlb3JnaWEsIFwiVGltZXMgTmV3IFJvbWFuXCIsIFRpbWVzLCBzZXJpZiAhZGVmYXVsdDtcbi8vKiogRGVmYXVsdCBtb25vc3BhY2UgZm9udHMgZm9yIGA8Y29kZT5gLCBgPGtiZD5gLCBhbmQgYDxwcmU+YC5cbiRmb250LWZhbWlseS1tb25vc3BhY2U6ICAgTWVubG8sIE1vbmFjbywgQ29uc29sYXMsIFwiQ291cmllciBOZXdcIiwgbW9ub3NwYWNlICFkZWZhdWx0O1xuJGZvbnQtZmFtaWx5LWJhc2U6ICAgICAgICAkZm9udC1mYW1pbHktc2Fucy1zZXJpZiAhZGVmYXVsdDtcblxuJGZvbnQtc2l6ZS1iYXNlOiAgICAgICAgICAxNHB4ICFkZWZhdWx0O1xuJGZvbnQtc2l6ZS1sYXJnZTogICAgICAgICBjZWlsKCgkZm9udC1zaXplLWJhc2UgKiAxLjI1KSkgIWRlZmF1bHQ7IC8vIH4xOHB4XG4kZm9udC1zaXplLXNtYWxsOiAgICAgICAgIGNlaWwoKCRmb250LXNpemUtYmFzZSAqIDAuODUpKSAhZGVmYXVsdDsgLy8gfjEycHhcblxuJGZvbnQtc2l6ZS1oMTogICAgICAgICAgICBmbG9vcigoJGZvbnQtc2l6ZS1iYXNlICogMi42KSkgIWRlZmF1bHQ7IC8vIH4zNnB4XG4kZm9udC1zaXplLWgyOiAgICAgICAgICAgIGZsb29yKCgkZm9udC1zaXplLWJhc2UgKiAyLjE1KSkgIWRlZmF1bHQ7IC8vIH4zMHB4XG4kZm9udC1zaXplLWgzOiAgICAgICAgICAgIGNlaWwoKCRmb250LXNpemUtYmFzZSAqIDEuNykpICFkZWZhdWx0OyAvLyB+MjRweFxuJGZvbnQtc2l6ZS1oNDogICAgICAgICAgICBjZWlsKCgkZm9udC1zaXplLWJhc2UgKiAxLjI1KSkgIWRlZmF1bHQ7IC8vIH4xOHB4XG4kZm9udC1zaXplLWg1OiAgICAgICAgICAgICRmb250LXNpemUtYmFzZSAhZGVmYXVsdDtcbiRmb250LXNpemUtaDY6ICAgICAgICAgICAgY2VpbCgoJGZvbnQtc2l6ZS1iYXNlICogMC44NSkpICFkZWZhdWx0OyAvLyB+MTJweFxuXG4vLyoqIFVuaXQtbGVzcyBgbGluZS1oZWlnaHRgIGZvciB1c2UgaW4gY29tcG9uZW50cyBsaWtlIGJ1dHRvbnMuXG4kbGluZS1oZWlnaHQtYmFzZTogICAgICAgIDEuNDI4NTcxNDI5ICFkZWZhdWx0OyAvLyAyMC8xNFxuLy8qKiBDb21wdXRlZCBcImxpbmUtaGVpZ2h0XCIgKGBmb250LXNpemVgICogYGxpbmUtaGVpZ2h0YCkgZm9yIHVzZSB3aXRoIGBtYXJnaW5gLCBgcGFkZGluZ2AsIGV0Yy5cbiRsaW5lLWhlaWdodC1jb21wdXRlZDogICAgZmxvb3IoKCRmb250LXNpemUtYmFzZSAqICRsaW5lLWhlaWdodC1iYXNlKSkgIWRlZmF1bHQ7IC8vIH4yMHB4XG5cbi8vKiogQnkgZGVmYXVsdCwgdGhpcyBpbmhlcml0cyBmcm9tIHRoZSBgPGJvZHk+YC5cbiRoZWFkaW5ncy1mb250LWZhbWlseTogICAgaW5oZXJpdCAhZGVmYXVsdDtcbiRoZWFkaW5ncy1mb250LXdlaWdodDogICAgNTAwICFkZWZhdWx0O1xuJGhlYWRpbmdzLWxpbmUtaGVpZ2h0OiAgICAxLjEgIWRlZmF1bHQ7XG4kaGVhZGluZ3MtY29sb3I6ICAgICAgICAgIGluaGVyaXQgIWRlZmF1bHQ7XG5cblxuLy89PSBJY29ub2dyYXBoeVxuLy9cbi8vIyMgU3BlY2lmeSBjdXN0b20gbG9jYXRpb24gYW5kIGZpbGVuYW1lIG9mIHRoZSBpbmNsdWRlZCBHbHlwaGljb25zIGljb24gZm9udC4gVXNlZnVsIGZvciB0aG9zZSBpbmNsdWRpbmcgQm9vdHN0cmFwIHZpYSBCb3dlci5cblxuLy8qKiBMb2FkIGZvbnRzIGZyb20gdGhpcyBkaXJlY3RvcnkuXG5cbi8vIFtjb252ZXJ0ZXJdIElmICRib290c3RyYXAtc2Fzcy1hc3NldC1oZWxwZXIgaWYgdXNlZCwgcHJvdmlkZSBwYXRoIHJlbGF0aXZlIHRvIHRoZSBhc3NldHMgbG9hZCBwYXRoLlxuLy8gW2NvbnZlcnRlcl0gVGhpcyBpcyBiZWNhdXNlIHNvbWUgYXNzZXQgaGVscGVycywgc3VjaCBhcyBTcHJvY2tldHMsIGRvIG5vdCB3b3JrIHdpdGggZmlsZS1yZWxhdGl2ZSBwYXRocy5cbiRpY29uLWZvbnQtcGF0aDogaWYoJGJvb3RzdHJhcC1zYXNzLWFzc2V0LWhlbHBlciwgXCJib290c3RyYXAvXCIsIFwiLi4vZm9udHMvYm9vdHN0cmFwL1wiKSAhZGVmYXVsdDtcblxuLy8qKiBGaWxlIG5hbWUgZm9yIGFsbCBmb250IGZpbGVzLlxuJGljb24tZm9udC1uYW1lOiAgICAgICAgICBcImdseXBoaWNvbnMtaGFsZmxpbmdzLXJlZ3VsYXJcIiAhZGVmYXVsdDtcbi8vKiogRWxlbWVudCBJRCB3aXRoaW4gU1ZHIGljb24gZmlsZS5cbiRpY29uLWZvbnQtc3ZnLWlkOiAgICAgICAgXCJnbHlwaGljb25zX2hhbGZsaW5nc3JlZ3VsYXJcIiAhZGVmYXVsdDtcblxuXG4vLz09IENvbXBvbmVudHNcbi8vXG4vLyMjIERlZmluZSBjb21tb24gcGFkZGluZyBhbmQgYm9yZGVyIHJhZGl1cyBzaXplcyBhbmQgbW9yZS4gVmFsdWVzIGJhc2VkIG9uIDE0cHggdGV4dCBhbmQgMS40MjggbGluZS1oZWlnaHQgKH4yMHB4IHRvIHN0YXJ0KS5cblxuJHBhZGRpbmctYmFzZS12ZXJ0aWNhbDogICAgIDZweCAhZGVmYXVsdDtcbiRwYWRkaW5nLWJhc2UtaG9yaXpvbnRhbDogICAxMnB4ICFkZWZhdWx0O1xuXG4kcGFkZGluZy1sYXJnZS12ZXJ0aWNhbDogICAgMTBweCAhZGVmYXVsdDtcbiRwYWRkaW5nLWxhcmdlLWhvcml6b250YWw6ICAxNnB4ICFkZWZhdWx0O1xuXG4kcGFkZGluZy1zbWFsbC12ZXJ0aWNhbDogICAgNXB4ICFkZWZhdWx0O1xuJHBhZGRpbmctc21hbGwtaG9yaXpvbnRhbDogIDEwcHggIWRlZmF1bHQ7XG5cbiRwYWRkaW5nLXhzLXZlcnRpY2FsOiAgICAgICAxcHggIWRlZmF1bHQ7XG4kcGFkZGluZy14cy1ob3Jpem9udGFsOiAgICAgNXB4ICFkZWZhdWx0O1xuXG4kbGluZS1oZWlnaHQtbGFyZ2U6ICAgICAgICAgMS4zMzMzMzMzICFkZWZhdWx0OyAvLyBleHRyYSBkZWNpbWFscyBmb3IgV2luIDguMSBDaHJvbWVcbiRsaW5lLWhlaWdodC1zbWFsbDogICAgICAgICAxLjUgIWRlZmF1bHQ7XG5cbiRib3JkZXItcmFkaXVzLWJhc2U6ICAgICAgICA0cHggIWRlZmF1bHQ7XG4kYm9yZGVyLXJhZGl1cy1sYXJnZTogICAgICAgNnB4ICFkZWZhdWx0O1xuJGJvcmRlci1yYWRpdXMtc21hbGw6ICAgICAgIDNweCAhZGVmYXVsdDtcblxuLy8qKiBHbG9iYWwgY29sb3IgZm9yIGFjdGl2ZSBpdGVtcyAoZS5nLiwgbmF2cyBvciBkcm9wZG93bnMpLlxuJGNvbXBvbmVudC1hY3RpdmUtY29sb3I6ICAgICNmZmYgIWRlZmF1bHQ7XG4vLyoqIEdsb2JhbCBiYWNrZ3JvdW5kIGNvbG9yIGZvciBhY3RpdmUgaXRlbXMgKGUuZy4sIG5hdnMgb3IgZHJvcGRvd25zKS5cbiRjb21wb25lbnQtYWN0aXZlLWJnOiAgICAgICAkYnJhbmQtcHJpbWFyeSAhZGVmYXVsdDtcblxuLy8qKiBXaWR0aCBvZiB0aGUgYGJvcmRlcmAgZm9yIGdlbmVyYXRpbmcgY2FyZXRzIHRoYXQgaW5kaWNhdG9yIGRyb3Bkb3ducy5cbiRjYXJldC13aWR0aC1iYXNlOiAgICAgICAgICA0cHggIWRlZmF1bHQ7XG4vLyoqIENhcmV0cyBpbmNyZWFzZSBzbGlnaHRseSBpbiBzaXplIGZvciBsYXJnZXIgY29tcG9uZW50cy5cbiRjYXJldC13aWR0aC1sYXJnZTogICAgICAgICA1cHggIWRlZmF1bHQ7XG5cblxuLy89PSBUYWJsZXNcbi8vXG4vLyMjIEN1c3RvbWl6ZXMgdGhlIGAudGFibGVgIGNvbXBvbmVudCB3aXRoIGJhc2ljIHZhbHVlcywgZWFjaCB1c2VkIGFjcm9zcyBhbGwgdGFibGUgdmFyaWF0aW9ucy5cblxuLy8qKiBQYWRkaW5nIGZvciBgPHRoPmBzIGFuZCBgPHRkPmBzLlxuJHRhYmxlLWNlbGwtcGFkZGluZzogICAgICAgICAgICA4cHggIWRlZmF1bHQ7XG4vLyoqIFBhZGRpbmcgZm9yIGNlbGxzIGluIGAudGFibGUtY29uZGVuc2VkYC5cbiR0YWJsZS1jb25kZW5zZWQtY2VsbC1wYWRkaW5nOiAgNXB4ICFkZWZhdWx0O1xuXG4vLyoqIERlZmF1bHQgYmFja2dyb3VuZCBjb2xvciB1c2VkIGZvciBhbGwgdGFibGVzLlxuJHRhYmxlLWJnOiAgICAgICAgICAgICAgICAgICAgICB0cmFuc3BhcmVudCAhZGVmYXVsdDtcbi8vKiogQmFja2dyb3VuZCBjb2xvciB1c2VkIGZvciBgLnRhYmxlLXN0cmlwZWRgLlxuJHRhYmxlLWJnLWFjY2VudDogICAgICAgICAgICAgICAjZjlmOWY5ICFkZWZhdWx0O1xuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIHVzZWQgZm9yIGAudGFibGUtaG92ZXJgLlxuJHRhYmxlLWJnLWhvdmVyOiAgICAgICAgICAgICAgICAjZjVmNWY1ICFkZWZhdWx0O1xuJHRhYmxlLWJnLWFjdGl2ZTogICAgICAgICAgICAgICAkdGFibGUtYmctaG92ZXIgIWRlZmF1bHQ7XG5cbi8vKiogQm9yZGVyIGNvbG9yIGZvciB0YWJsZSBhbmQgY2VsbCBib3JkZXJzLlxuJHRhYmxlLWJvcmRlci1jb2xvcjogICAgICAgICAgICAjZGRkICFkZWZhdWx0O1xuXG5cbi8vPT0gQnV0dG9uc1xuLy9cbi8vIyMgRm9yIGVhY2ggb2YgQm9vdHN0cmFwJ3MgYnV0dG9ucywgZGVmaW5lIHRleHQsIGJhY2tncm91bmQgYW5kIGJvcmRlciBjb2xvci5cblxuJGJ0bi1mb250LXdlaWdodDogICAgICAgICAgICAgICAgbm9ybWFsICFkZWZhdWx0O1xuXG4kYnRuLWRlZmF1bHQtY29sb3I6ICAgICAgICAgICAgICAjMzMzICFkZWZhdWx0O1xuJGJ0bi1kZWZhdWx0LWJnOiAgICAgICAgICAgICAgICAgI2ZmZiAhZGVmYXVsdDtcbiRidG4tZGVmYXVsdC1ib3JkZXI6ICAgICAgICAgICAgICNjY2MgIWRlZmF1bHQ7XG5cbiRidG4tcHJpbWFyeS1jb2xvcjogICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kYnRuLXByaW1hcnktYmc6ICAgICAgICAgICAgICAgICAkYnJhbmQtcHJpbWFyeSAhZGVmYXVsdDtcbiRidG4tcHJpbWFyeS1ib3JkZXI6ICAgICAgICAgICAgIGRhcmtlbigkYnRuLXByaW1hcnktYmcsIDUlKSAhZGVmYXVsdDtcblxuJGJ0bi1zdWNjZXNzLWNvbG9yOiAgICAgICAgICAgICAgI2ZmZiAhZGVmYXVsdDtcbiRidG4tc3VjY2Vzcy1iZzogICAgICAgICAgICAgICAgICRicmFuZC1zdWNjZXNzICFkZWZhdWx0O1xuJGJ0bi1zdWNjZXNzLWJvcmRlcjogICAgICAgICAgICAgZGFya2VuKCRidG4tc3VjY2Vzcy1iZywgNSUpICFkZWZhdWx0O1xuXG4kYnRuLWluZm8tY29sb3I6ICAgICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuJGJ0bi1pbmZvLWJnOiAgICAgICAgICAgICAgICAgICAgJGJyYW5kLWluZm8gIWRlZmF1bHQ7XG4kYnRuLWluZm8tYm9yZGVyOiAgICAgICAgICAgICAgICBkYXJrZW4oJGJ0bi1pbmZvLWJnLCA1JSkgIWRlZmF1bHQ7XG5cbiRidG4td2FybmluZy1jb2xvcjogICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kYnRuLXdhcm5pbmctYmc6ICAgICAgICAgICAgICAgICAkYnJhbmQtd2FybmluZyAhZGVmYXVsdDtcbiRidG4td2FybmluZy1ib3JkZXI6ICAgICAgICAgICAgIGRhcmtlbigkYnRuLXdhcm5pbmctYmcsIDUlKSAhZGVmYXVsdDtcblxuJGJ0bi1kYW5nZXItY29sb3I6ICAgICAgICAgICAgICAgI2ZmZiAhZGVmYXVsdDtcbiRidG4tZGFuZ2VyLWJnOiAgICAgICAgICAgICAgICAgICRicmFuZC1kYW5nZXIgIWRlZmF1bHQ7XG4kYnRuLWRhbmdlci1ib3JkZXI6ICAgICAgICAgICAgICBkYXJrZW4oJGJ0bi1kYW5nZXItYmcsIDUlKSAhZGVmYXVsdDtcblxuJGJ0bi1saW5rLWRpc2FibGVkLWNvbG9yOiAgICAgICAgJGdyYXktbGlnaHQgIWRlZmF1bHQ7XG5cbi8vIEFsbG93cyBmb3IgY3VzdG9taXppbmcgYnV0dG9uIHJhZGl1cyBpbmRlcGVuZGVudGx5IGZyb20gZ2xvYmFsIGJvcmRlciByYWRpdXNcbiRidG4tYm9yZGVyLXJhZGl1cy1iYXNlOiAgICAgICAgICRib3JkZXItcmFkaXVzLWJhc2UgIWRlZmF1bHQ7XG4kYnRuLWJvcmRlci1yYWRpdXMtbGFyZ2U6ICAgICAgICAkYm9yZGVyLXJhZGl1cy1sYXJnZSAhZGVmYXVsdDtcbiRidG4tYm9yZGVyLXJhZGl1cy1zbWFsbDogICAgICAgICRib3JkZXItcmFkaXVzLXNtYWxsICFkZWZhdWx0O1xuXG5cbi8vPT0gRm9ybXNcbi8vXG4vLyMjXG5cbi8vKiogYDxpbnB1dD5gIGJhY2tncm91bmQgY29sb3JcbiRpbnB1dC1iZzogICAgICAgICAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4vLyoqIGA8aW5wdXQgZGlzYWJsZWQ+YCBiYWNrZ3JvdW5kIGNvbG9yXG4kaW5wdXQtYmctZGlzYWJsZWQ6ICAgICAgICAgICAgICAkZ3JheS1saWdodGVyICFkZWZhdWx0O1xuXG4vLyoqIFRleHQgY29sb3IgZm9yIGA8aW5wdXQ+YHNcbiRpbnB1dC1jb2xvcjogICAgICAgICAgICAgICAgICAgICRncmF5ICFkZWZhdWx0O1xuLy8qKiBgPGlucHV0PmAgYm9yZGVyIGNvbG9yXG4kaW5wdXQtYm9yZGVyOiAgICAgICAgICAgICAgICAgICAjY2NjICFkZWZhdWx0O1xuXG4vLyBUT0RPOiBSZW5hbWUgYCRpbnB1dC1ib3JkZXItcmFkaXVzYCB0byBgJGlucHV0LWJvcmRlci1yYWRpdXMtYmFzZWAgaW4gdjRcbi8vKiogRGVmYXVsdCBgLmZvcm0tY29udHJvbGAgYm9yZGVyIHJhZGl1c1xuLy8gVGhpcyBoYXMgbm8gZWZmZWN0IG9uIGA8c2VsZWN0PmBzIGluIHNvbWUgYnJvd3NlcnMsIGR1ZSB0byB0aGUgbGltaXRlZCBzdHlsYWJpbGl0eSBvZiBgPHNlbGVjdD5gcyBpbiBDU1MuXG4kaW5wdXQtYm9yZGVyLXJhZGl1czogICAgICAgICAgICAkYm9yZGVyLXJhZGl1cy1iYXNlICFkZWZhdWx0O1xuLy8qKiBMYXJnZSBgLmZvcm0tY29udHJvbGAgYm9yZGVyIHJhZGl1c1xuJGlucHV0LWJvcmRlci1yYWRpdXMtbGFyZ2U6ICAgICAgJGJvcmRlci1yYWRpdXMtbGFyZ2UgIWRlZmF1bHQ7XG4vLyoqIFNtYWxsIGAuZm9ybS1jb250cm9sYCBib3JkZXIgcmFkaXVzXG4kaW5wdXQtYm9yZGVyLXJhZGl1cy1zbWFsbDogICAgICAkYm9yZGVyLXJhZGl1cy1zbWFsbCAhZGVmYXVsdDtcblxuLy8qKiBCb3JkZXIgY29sb3IgZm9yIGlucHV0cyBvbiBmb2N1c1xuJGlucHV0LWJvcmRlci1mb2N1czogICAgICAgICAgICAgIzY2YWZlOSAhZGVmYXVsdDtcblxuLy8qKiBQbGFjZWhvbGRlciB0ZXh0IGNvbG9yXG4kaW5wdXQtY29sb3ItcGxhY2Vob2xkZXI6ICAgICAgICAjOTk5ICFkZWZhdWx0O1xuXG4vLyoqIERlZmF1bHQgYC5mb3JtLWNvbnRyb2xgIGhlaWdodFxuJGlucHV0LWhlaWdodC1iYXNlOiAgICAgICAgICAgICAgKCRsaW5lLWhlaWdodC1jb21wdXRlZCArICgkcGFkZGluZy1iYXNlLXZlcnRpY2FsICogMikgKyAyKSAhZGVmYXVsdDtcbi8vKiogTGFyZ2UgYC5mb3JtLWNvbnRyb2xgIGhlaWdodFxuJGlucHV0LWhlaWdodC1sYXJnZTogICAgICAgICAgICAgKGNlaWwoJGZvbnQtc2l6ZS1sYXJnZSAqICRsaW5lLWhlaWdodC1sYXJnZSkgKyAoJHBhZGRpbmctbGFyZ2UtdmVydGljYWwgKiAyKSArIDIpICFkZWZhdWx0O1xuLy8qKiBTbWFsbCBgLmZvcm0tY29udHJvbGAgaGVpZ2h0XG4kaW5wdXQtaGVpZ2h0LXNtYWxsOiAgICAgICAgICAgICAoZmxvb3IoJGZvbnQtc2l6ZS1zbWFsbCAqICRsaW5lLWhlaWdodC1zbWFsbCkgKyAoJHBhZGRpbmctc21hbGwtdmVydGljYWwgKiAyKSArIDIpICFkZWZhdWx0O1xuXG4vLyoqIGAuZm9ybS1ncm91cGAgbWFyZ2luXG4kZm9ybS1ncm91cC1tYXJnaW4tYm90dG9tOiAgICAgICAxNXB4ICFkZWZhdWx0O1xuXG4kbGVnZW5kLWNvbG9yOiAgICAgICAgICAgICAgICAgICAkZ3JheS1kYXJrICFkZWZhdWx0O1xuJGxlZ2VuZC1ib3JkZXItY29sb3I6ICAgICAgICAgICAgI2U1ZTVlNSAhZGVmYXVsdDtcblxuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIGZvciB0ZXh0dWFsIGlucHV0IGFkZG9uc1xuJGlucHV0LWdyb3VwLWFkZG9uLWJnOiAgICAgICAgICAgJGdyYXktbGlnaHRlciAhZGVmYXVsdDtcbi8vKiogQm9yZGVyIGNvbG9yIGZvciB0ZXh0dWFsIGlucHV0IGFkZG9uc1xuJGlucHV0LWdyb3VwLWFkZG9uLWJvcmRlci1jb2xvcjogJGlucHV0LWJvcmRlciAhZGVmYXVsdDtcblxuLy8qKiBEaXNhYmxlZCBjdXJzb3IgZm9yIGZvcm0gY29udHJvbHMgYW5kIGJ1dHRvbnMuXG4kY3Vyc29yLWRpc2FibGVkOiAgICAgICAgICAgICAgICBub3QtYWxsb3dlZCAhZGVmYXVsdDtcblxuXG4vLz09IERyb3Bkb3duc1xuLy9cbi8vIyMgRHJvcGRvd24gbWVudSBjb250YWluZXIgYW5kIGNvbnRlbnRzLlxuXG4vLyoqIEJhY2tncm91bmQgZm9yIHRoZSBkcm9wZG93biBtZW51LlxuJGRyb3Bkb3duLWJnOiAgICAgICAgICAgICAgICAgICAgI2ZmZiAhZGVmYXVsdDtcbi8vKiogRHJvcGRvd24gbWVudSBgYm9yZGVyLWNvbG9yYC5cbiRkcm9wZG93bi1ib3JkZXI6ICAgICAgICAgICAgICAgIHJnYmEoMCwwLDAsLjE1KSAhZGVmYXVsdDtcbi8vKiogRHJvcGRvd24gbWVudSBgYm9yZGVyLWNvbG9yYCAqKmZvciBJRTgqKi5cbiRkcm9wZG93bi1mYWxsYmFjay1ib3JkZXI6ICAgICAgICNjY2MgIWRlZmF1bHQ7XG4vLyoqIERpdmlkZXIgY29sb3IgZm9yIGJldHdlZW4gZHJvcGRvd24gaXRlbXMuXG4kZHJvcGRvd24tZGl2aWRlci1iZzogICAgICAgICAgICAjZTVlNWU1ICFkZWZhdWx0O1xuXG4vLyoqIERyb3Bkb3duIGxpbmsgdGV4dCBjb2xvci5cbiRkcm9wZG93bi1saW5rLWNvbG9yOiAgICAgICAgICAgICRncmF5LWRhcmsgIWRlZmF1bHQ7XG4vLyoqIEhvdmVyIGNvbG9yIGZvciBkcm9wZG93biBsaW5rcy5cbiRkcm9wZG93bi1saW5rLWhvdmVyLWNvbG9yOiAgICAgIGRhcmtlbigkZ3JheS1kYXJrLCA1JSkgIWRlZmF1bHQ7XG4vLyoqIEhvdmVyIGJhY2tncm91bmQgZm9yIGRyb3Bkb3duIGxpbmtzLlxuJGRyb3Bkb3duLWxpbmstaG92ZXItYmc6ICAgICAgICAgI2Y1ZjVmNSAhZGVmYXVsdDtcblxuLy8qKiBBY3RpdmUgZHJvcGRvd24gbWVudSBpdGVtIHRleHQgY29sb3IuXG4kZHJvcGRvd24tbGluay1hY3RpdmUtY29sb3I6ICAgICAkY29tcG9uZW50LWFjdGl2ZS1jb2xvciAhZGVmYXVsdDtcbi8vKiogQWN0aXZlIGRyb3Bkb3duIG1lbnUgaXRlbSBiYWNrZ3JvdW5kIGNvbG9yLlxuJGRyb3Bkb3duLWxpbmstYWN0aXZlLWJnOiAgICAgICAgJGNvbXBvbmVudC1hY3RpdmUtYmcgIWRlZmF1bHQ7XG5cbi8vKiogRGlzYWJsZWQgZHJvcGRvd24gbWVudSBpdGVtIGJhY2tncm91bmQgY29sb3IuXG4kZHJvcGRvd24tbGluay1kaXNhYmxlZC1jb2xvcjogICAkZ3JheS1saWdodCAhZGVmYXVsdDtcblxuLy8qKiBUZXh0IGNvbG9yIGZvciBoZWFkZXJzIHdpdGhpbiBkcm9wZG93biBtZW51cy5cbiRkcm9wZG93bi1oZWFkZXItY29sb3I6ICAgICAgICAgICRncmF5LWxpZ2h0ICFkZWZhdWx0O1xuXG4vLyoqIERlcHJlY2F0ZWQgYCRkcm9wZG93bi1jYXJldC1jb2xvcmAgYXMgb2YgdjMuMS4wXG4kZHJvcGRvd24tY2FyZXQtY29sb3I6ICAgICAgICAgICAjMDAwICFkZWZhdWx0O1xuXG5cbi8vLS0gWi1pbmRleCBtYXN0ZXIgbGlzdFxuLy9cbi8vIFdhcm5pbmc6IEF2b2lkIGN1c3RvbWl6aW5nIHRoZXNlIHZhbHVlcy4gVGhleSdyZSB1c2VkIGZvciBhIGJpcmQncyBleWUgdmlld1xuLy8gb2YgY29tcG9uZW50cyBkZXBlbmRlbnQgb24gdGhlIHotYXhpcyBhbmQgYXJlIGRlc2lnbmVkIHRvIGFsbCB3b3JrIHRvZ2V0aGVyLlxuLy9cbi8vIE5vdGU6IFRoZXNlIHZhcmlhYmxlcyBhcmUgbm90IGdlbmVyYXRlZCBpbnRvIHRoZSBDdXN0b21pemVyLlxuXG4kemluZGV4LW5hdmJhcjogICAgICAgICAgICAxMDAwICFkZWZhdWx0O1xuJHppbmRleC1kcm9wZG93bjogICAgICAgICAgMTAwMCAhZGVmYXVsdDtcbiR6aW5kZXgtcG9wb3ZlcjogICAgICAgICAgIDEwNjAgIWRlZmF1bHQ7XG4kemluZGV4LXRvb2x0aXA6ICAgICAgICAgICAxMDcwICFkZWZhdWx0O1xuJHppbmRleC1uYXZiYXItZml4ZWQ6ICAgICAgMTAzMCAhZGVmYXVsdDtcbiR6aW5kZXgtbW9kYWwtYmFja2dyb3VuZDogIDEwNDAgIWRlZmF1bHQ7XG4kemluZGV4LW1vZGFsOiAgICAgICAgICAgICAxMDUwICFkZWZhdWx0O1xuXG5cbi8vPT0gTWVkaWEgcXVlcmllcyBicmVha3BvaW50c1xuLy9cbi8vIyMgRGVmaW5lIHRoZSBicmVha3BvaW50cyBhdCB3aGljaCB5b3VyIGxheW91dCB3aWxsIGNoYW5nZSwgYWRhcHRpbmcgdG8gZGlmZmVyZW50IHNjcmVlbiBzaXplcy5cblxuLy8gRXh0cmEgc21hbGwgc2NyZWVuIC8gcGhvbmVcbi8vKiogRGVwcmVjYXRlZCBgJHNjcmVlbi14c2AgYXMgb2YgdjMuMC4xXG4kc2NyZWVuLXhzOiAgICAgICAgICAgICAgICAgIDQ4MHB4ICFkZWZhdWx0O1xuLy8qKiBEZXByZWNhdGVkIGAkc2NyZWVuLXhzLW1pbmAgYXMgb2YgdjMuMi4wXG4kc2NyZWVuLXhzLW1pbjogICAgICAgICAgICAgICRzY3JlZW4teHMgIWRlZmF1bHQ7XG4vLyoqIERlcHJlY2F0ZWQgYCRzY3JlZW4tcGhvbmVgIGFzIG9mIHYzLjAuMVxuJHNjcmVlbi1waG9uZTogICAgICAgICAgICAgICAkc2NyZWVuLXhzLW1pbiAhZGVmYXVsdDtcblxuLy8gU21hbGwgc2NyZWVuIC8gdGFibGV0XG4vLyoqIERlcHJlY2F0ZWQgYCRzY3JlZW4tc21gIGFzIG9mIHYzLjAuMVxuJHNjcmVlbi1zbTogICAgICAgICAgICAgICAgICA3NjhweCAhZGVmYXVsdDtcbiRzY3JlZW4tc20tbWluOiAgICAgICAgICAgICAgJHNjcmVlbi1zbSAhZGVmYXVsdDtcbi8vKiogRGVwcmVjYXRlZCBgJHNjcmVlbi10YWJsZXRgIGFzIG9mIHYzLjAuMVxuJHNjcmVlbi10YWJsZXQ6ICAgICAgICAgICAgICAkc2NyZWVuLXNtLW1pbiAhZGVmYXVsdDtcblxuLy8gTWVkaXVtIHNjcmVlbiAvIGRlc2t0b3Bcbi8vKiogRGVwcmVjYXRlZCBgJHNjcmVlbi1tZGAgYXMgb2YgdjMuMC4xXG4kc2NyZWVuLW1kOiAgICAgICAgICAgICAgICAgIDk5MnB4ICFkZWZhdWx0O1xuJHNjcmVlbi1tZC1taW46ICAgICAgICAgICAgICAkc2NyZWVuLW1kICFkZWZhdWx0O1xuLy8qKiBEZXByZWNhdGVkIGAkc2NyZWVuLWRlc2t0b3BgIGFzIG9mIHYzLjAuMVxuJHNjcmVlbi1kZXNrdG9wOiAgICAgICAgICAgICAkc2NyZWVuLW1kLW1pbiAhZGVmYXVsdDtcblxuLy8gTGFyZ2Ugc2NyZWVuIC8gd2lkZSBkZXNrdG9wXG4vLyoqIERlcHJlY2F0ZWQgYCRzY3JlZW4tbGdgIGFzIG9mIHYzLjAuMVxuJHNjcmVlbi1sZzogICAgICAgICAgICAgICAgICAxMjAwcHggIWRlZmF1bHQ7XG4kc2NyZWVuLWxnLW1pbjogICAgICAgICAgICAgICRzY3JlZW4tbGcgIWRlZmF1bHQ7XG4vLyoqIERlcHJlY2F0ZWQgYCRzY3JlZW4tbGctZGVza3RvcGAgYXMgb2YgdjMuMC4xXG4kc2NyZWVuLWxnLWRlc2t0b3A6ICAgICAgICAgICRzY3JlZW4tbGctbWluICFkZWZhdWx0O1xuXG4vLyBTbyBtZWRpYSBxdWVyaWVzIGRvbid0IG92ZXJsYXAgd2hlbiByZXF1aXJlZCwgcHJvdmlkZSBhIG1heGltdW1cbiRzY3JlZW4teHMtbWF4OiAgICAgICAgICAgICAgKCRzY3JlZW4tc20tbWluIC0gMSkgIWRlZmF1bHQ7XG4kc2NyZWVuLXNtLW1heDogICAgICAgICAgICAgICgkc2NyZWVuLW1kLW1pbiAtIDEpICFkZWZhdWx0O1xuJHNjcmVlbi1tZC1tYXg6ICAgICAgICAgICAgICAoJHNjcmVlbi1sZy1taW4gLSAxKSAhZGVmYXVsdDtcblxuXG4vLz09IEdyaWQgc3lzdGVtXG4vL1xuLy8jIyBEZWZpbmUgeW91ciBjdXN0b20gcmVzcG9uc2l2ZSBncmlkLlxuXG4vLyoqIE51bWJlciBvZiBjb2x1bW5zIGluIHRoZSBncmlkLlxuJGdyaWQtY29sdW1uczogICAgICAgICAgICAgIDEyICFkZWZhdWx0O1xuLy8qKiBQYWRkaW5nIGJldHdlZW4gY29sdW1ucy4gR2V0cyBkaXZpZGVkIGluIGhhbGYgZm9yIHRoZSBsZWZ0IGFuZCByaWdodC5cbiRncmlkLWd1dHRlci13aWR0aDogICAgICAgICAzMHB4ICFkZWZhdWx0O1xuLy8gTmF2YmFyIGNvbGxhcHNlXG4vLyoqIFBvaW50IGF0IHdoaWNoIHRoZSBuYXZiYXIgYmVjb21lcyB1bmNvbGxhcHNlZC5cbiRncmlkLWZsb2F0LWJyZWFrcG9pbnQ6ICAgICAkc2NyZWVuLXNtLW1pbiAhZGVmYXVsdDtcbi8vKiogUG9pbnQgYXQgd2hpY2ggdGhlIG5hdmJhciBiZWdpbnMgY29sbGFwc2luZy5cbiRncmlkLWZsb2F0LWJyZWFrcG9pbnQtbWF4OiAoJGdyaWQtZmxvYXQtYnJlYWtwb2ludCAtIDEpICFkZWZhdWx0O1xuXG5cbi8vPT0gQ29udGFpbmVyIHNpemVzXG4vL1xuLy8jIyBEZWZpbmUgdGhlIG1heGltdW0gd2lkdGggb2YgYC5jb250YWluZXJgIGZvciBkaWZmZXJlbnQgc2NyZWVuIHNpemVzLlxuXG4vLyBTbWFsbCBzY3JlZW4gLyB0YWJsZXRcbiRjb250YWluZXItdGFibGV0OiAgICAgICAgICAgICAoNzIwcHggKyAkZ3JpZC1ndXR0ZXItd2lkdGgpICFkZWZhdWx0O1xuLy8qKiBGb3IgYCRzY3JlZW4tc20tbWluYCBhbmQgdXAuXG4kY29udGFpbmVyLXNtOiAgICAgICAgICAgICAgICAgJGNvbnRhaW5lci10YWJsZXQgIWRlZmF1bHQ7XG5cbi8vIE1lZGl1bSBzY3JlZW4gLyBkZXNrdG9wXG4kY29udGFpbmVyLWRlc2t0b3A6ICAgICAgICAgICAgKDk0MHB4ICsgJGdyaWQtZ3V0dGVyLXdpZHRoKSAhZGVmYXVsdDtcbi8vKiogRm9yIGAkc2NyZWVuLW1kLW1pbmAgYW5kIHVwLlxuJGNvbnRhaW5lci1tZDogICAgICAgICAgICAgICAgICRjb250YWluZXItZGVza3RvcCAhZGVmYXVsdDtcblxuLy8gTGFyZ2Ugc2NyZWVuIC8gd2lkZSBkZXNrdG9wXG4kY29udGFpbmVyLWxhcmdlLWRlc2t0b3A6ICAgICAgKDExNDBweCArICRncmlkLWd1dHRlci13aWR0aCkgIWRlZmF1bHQ7XG4vLyoqIEZvciBgJHNjcmVlbi1sZy1taW5gIGFuZCB1cC5cbiRjb250YWluZXItbGc6ICAgICAgICAgICAgICAgICAkY29udGFpbmVyLWxhcmdlLWRlc2t0b3AgIWRlZmF1bHQ7XG5cblxuLy89PSBOYXZiYXJcbi8vXG4vLyMjXG5cbi8vIEJhc2ljcyBvZiBhIG5hdmJhclxuJG5hdmJhci1oZWlnaHQ6ICAgICAgICAgICAgICAgICAgICA1MHB4ICFkZWZhdWx0O1xuJG5hdmJhci1tYXJnaW4tYm90dG9tOiAgICAgICAgICAgICAkbGluZS1oZWlnaHQtY29tcHV0ZWQgIWRlZmF1bHQ7XG4kbmF2YmFyLWJvcmRlci1yYWRpdXM6ICAgICAgICAgICAgICRib3JkZXItcmFkaXVzLWJhc2UgIWRlZmF1bHQ7XG4kbmF2YmFyLXBhZGRpbmctaG9yaXpvbnRhbDogICAgICAgIGZsb29yKCgkZ3JpZC1ndXR0ZXItd2lkdGggLyAyKSkgIWRlZmF1bHQ7XG4kbmF2YmFyLXBhZGRpbmctdmVydGljYWw6ICAgICAgICAgICgoJG5hdmJhci1oZWlnaHQgLSAkbGluZS1oZWlnaHQtY29tcHV0ZWQpIC8gMikgIWRlZmF1bHQ7XG4kbmF2YmFyLWNvbGxhcHNlLW1heC1oZWlnaHQ6ICAgICAgIDM0MHB4ICFkZWZhdWx0O1xuXG4kbmF2YmFyLWRlZmF1bHQtY29sb3I6ICAgICAgICAgICAgICM3NzcgIWRlZmF1bHQ7XG4kbmF2YmFyLWRlZmF1bHQtYmc6ICAgICAgICAgICAgICAgICNmOGY4ZjggIWRlZmF1bHQ7XG4kbmF2YmFyLWRlZmF1bHQtYm9yZGVyOiAgICAgICAgICAgIGRhcmtlbigkbmF2YmFyLWRlZmF1bHQtYmcsIDYuNSUpICFkZWZhdWx0O1xuXG4vLyBOYXZiYXIgbGlua3NcbiRuYXZiYXItZGVmYXVsdC1saW5rLWNvbG9yOiAgICAgICAgICAgICAgICAjNzc3ICFkZWZhdWx0O1xuJG5hdmJhci1kZWZhdWx0LWxpbmstaG92ZXItY29sb3I6ICAgICAgICAgICMzMzMgIWRlZmF1bHQ7XG4kbmF2YmFyLWRlZmF1bHQtbGluay1ob3Zlci1iZzogICAgICAgICAgICAgdHJhbnNwYXJlbnQgIWRlZmF1bHQ7XG4kbmF2YmFyLWRlZmF1bHQtbGluay1hY3RpdmUtY29sb3I6ICAgICAgICAgIzU1NSAhZGVmYXVsdDtcbiRuYXZiYXItZGVmYXVsdC1saW5rLWFjdGl2ZS1iZzogICAgICAgICAgICBkYXJrZW4oJG5hdmJhci1kZWZhdWx0LWJnLCA2LjUlKSAhZGVmYXVsdDtcbiRuYXZiYXItZGVmYXVsdC1saW5rLWRpc2FibGVkLWNvbG9yOiAgICAgICAjY2NjICFkZWZhdWx0O1xuJG5hdmJhci1kZWZhdWx0LWxpbmstZGlzYWJsZWQtYmc6ICAgICAgICAgIHRyYW5zcGFyZW50ICFkZWZhdWx0O1xuXG4vLyBOYXZiYXIgYnJhbmQgbGFiZWxcbiRuYXZiYXItZGVmYXVsdC1icmFuZC1jb2xvcjogICAgICAgICAgICAgICAkbmF2YmFyLWRlZmF1bHQtbGluay1jb2xvciAhZGVmYXVsdDtcbiRuYXZiYXItZGVmYXVsdC1icmFuZC1ob3Zlci1jb2xvcjogICAgICAgICBkYXJrZW4oJG5hdmJhci1kZWZhdWx0LWJyYW5kLWNvbG9yLCAxMCUpICFkZWZhdWx0O1xuJG5hdmJhci1kZWZhdWx0LWJyYW5kLWhvdmVyLWJnOiAgICAgICAgICAgIHRyYW5zcGFyZW50ICFkZWZhdWx0O1xuXG4vLyBOYXZiYXIgdG9nZ2xlXG4kbmF2YmFyLWRlZmF1bHQtdG9nZ2xlLWhvdmVyLWJnOiAgICAgICAgICAgI2RkZCAhZGVmYXVsdDtcbiRuYXZiYXItZGVmYXVsdC10b2dnbGUtaWNvbi1iYXItYmc6ICAgICAgICAjODg4ICFkZWZhdWx0O1xuJG5hdmJhci1kZWZhdWx0LXRvZ2dsZS1ib3JkZXItY29sb3I6ICAgICAgICNkZGQgIWRlZmF1bHQ7XG5cblxuLy89PT0gSW52ZXJ0ZWQgbmF2YmFyXG4vLyBSZXNldCBpbnZlcnRlZCBuYXZiYXIgYmFzaWNzXG4kbmF2YmFyLWludmVyc2UtY29sb3I6ICAgICAgICAgICAgICAgICAgICAgIGxpZ2h0ZW4oJGdyYXktbGlnaHQsIDE1JSkgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtYmc6ICAgICAgICAgICAgICAgICAgICAgICAgICMyMjIgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtYm9yZGVyOiAgICAgICAgICAgICAgICAgICAgIGRhcmtlbigkbmF2YmFyLWludmVyc2UtYmcsIDEwJSkgIWRlZmF1bHQ7XG5cbi8vIEludmVydGVkIG5hdmJhciBsaW5rc1xuJG5hdmJhci1pbnZlcnNlLWxpbmstY29sb3I6ICAgICAgICAgICAgICAgICBsaWdodGVuKCRncmF5LWxpZ2h0LCAxNSUpICFkZWZhdWx0O1xuJG5hdmJhci1pbnZlcnNlLWxpbmstaG92ZXItY29sb3I6ICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuJG5hdmJhci1pbnZlcnNlLWxpbmstaG92ZXItYmc6ICAgICAgICAgICAgICB0cmFuc3BhcmVudCAhZGVmYXVsdDtcbiRuYXZiYXItaW52ZXJzZS1saW5rLWFjdGl2ZS1jb2xvcjogICAgICAgICAgJG5hdmJhci1pbnZlcnNlLWxpbmstaG92ZXItY29sb3IgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtbGluay1hY3RpdmUtYmc6ICAgICAgICAgICAgIGRhcmtlbigkbmF2YmFyLWludmVyc2UtYmcsIDEwJSkgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtbGluay1kaXNhYmxlZC1jb2xvcjogICAgICAgICM0NDQgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtbGluay1kaXNhYmxlZC1iZzogICAgICAgICAgIHRyYW5zcGFyZW50ICFkZWZhdWx0O1xuXG4vLyBJbnZlcnRlZCBuYXZiYXIgYnJhbmQgbGFiZWxcbiRuYXZiYXItaW52ZXJzZS1icmFuZC1jb2xvcjogICAgICAgICAgICAgICAgJG5hdmJhci1pbnZlcnNlLWxpbmstY29sb3IgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtYnJhbmQtaG92ZXItY29sb3I6ICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtYnJhbmQtaG92ZXItYmc6ICAgICAgICAgICAgIHRyYW5zcGFyZW50ICFkZWZhdWx0O1xuXG4vLyBJbnZlcnRlZCBuYXZiYXIgdG9nZ2xlXG4kbmF2YmFyLWludmVyc2UtdG9nZ2xlLWhvdmVyLWJnOiAgICAgICAgICAgICMzMzMgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtdG9nZ2xlLWljb24tYmFyLWJnOiAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kbmF2YmFyLWludmVyc2UtdG9nZ2xlLWJvcmRlci1jb2xvcjogICAgICAgICMzMzMgIWRlZmF1bHQ7XG5cblxuLy89PSBOYXZzXG4vL1xuLy8jI1xuXG4vLz09PSBTaGFyZWQgbmF2IHN0eWxlc1xuJG5hdi1saW5rLXBhZGRpbmc6ICAgICAgICAgICAgICAgICAgICAgICAgICAxMHB4IDE1cHggIWRlZmF1bHQ7XG4kbmF2LWxpbmstaG92ZXItYmc6ICAgICAgICAgICAgICAgICAgICAgICAgICRncmF5LWxpZ2h0ZXIgIWRlZmF1bHQ7XG5cbiRuYXYtZGlzYWJsZWQtbGluay1jb2xvcjogICAgICAgICAgICAgICAgICAgJGdyYXktbGlnaHQgIWRlZmF1bHQ7XG4kbmF2LWRpc2FibGVkLWxpbmstaG92ZXItY29sb3I6ICAgICAgICAgICAgICRncmF5LWxpZ2h0ICFkZWZhdWx0O1xuXG4vLz09IFRhYnNcbiRuYXYtdGFicy1ib3JkZXItY29sb3I6ICAgICAgICAgICAgICAgICAgICAgI2RkZCAhZGVmYXVsdDtcblxuJG5hdi10YWJzLWxpbmstaG92ZXItYm9yZGVyLWNvbG9yOiAgICAgICAgICAkZ3JheS1saWdodGVyICFkZWZhdWx0O1xuXG4kbmF2LXRhYnMtYWN0aXZlLWxpbmstaG92ZXItYmc6ICAgICAgICAgICAgICRib2R5LWJnICFkZWZhdWx0O1xuJG5hdi10YWJzLWFjdGl2ZS1saW5rLWhvdmVyLWNvbG9yOiAgICAgICAgICAkZ3JheSAhZGVmYXVsdDtcbiRuYXYtdGFicy1hY3RpdmUtbGluay1ob3Zlci1ib3JkZXItY29sb3I6ICAgI2RkZCAhZGVmYXVsdDtcblxuJG5hdi10YWJzLWp1c3RpZmllZC1saW5rLWJvcmRlci1jb2xvcjogICAgICAgICAgICAjZGRkICFkZWZhdWx0O1xuJG5hdi10YWJzLWp1c3RpZmllZC1hY3RpdmUtbGluay1ib3JkZXItY29sb3I6ICAgICAkYm9keS1iZyAhZGVmYXVsdDtcblxuLy89PSBQaWxsc1xuJG5hdi1waWxscy1ib3JkZXItcmFkaXVzOiAgICAgICAgICAgICAgICAgICAkYm9yZGVyLXJhZGl1cy1iYXNlICFkZWZhdWx0O1xuJG5hdi1waWxscy1hY3RpdmUtbGluay1ob3Zlci1iZzogICAgICAgICAgICAkY29tcG9uZW50LWFjdGl2ZS1iZyAhZGVmYXVsdDtcbiRuYXYtcGlsbHMtYWN0aXZlLWxpbmstaG92ZXItY29sb3I6ICAgICAgICAgJGNvbXBvbmVudC1hY3RpdmUtY29sb3IgIWRlZmF1bHQ7XG5cblxuLy89PSBQYWdpbmF0aW9uXG4vL1xuLy8jI1xuXG4kcGFnaW5hdGlvbi1jb2xvcjogICAgICAgICAgICAgICAgICAgICAkbGluay1jb2xvciAhZGVmYXVsdDtcbiRwYWdpbmF0aW9uLWJnOiAgICAgICAgICAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kcGFnaW5hdGlvbi1ib3JkZXI6ICAgICAgICAgICAgICAgICAgICAjZGRkICFkZWZhdWx0O1xuXG4kcGFnaW5hdGlvbi1ob3Zlci1jb2xvcjogICAgICAgICAgICAgICAkbGluay1ob3Zlci1jb2xvciAhZGVmYXVsdDtcbiRwYWdpbmF0aW9uLWhvdmVyLWJnOiAgICAgICAgICAgICAgICAgICRncmF5LWxpZ2h0ZXIgIWRlZmF1bHQ7XG4kcGFnaW5hdGlvbi1ob3Zlci1ib3JkZXI6ICAgICAgICAgICAgICAjZGRkICFkZWZhdWx0O1xuXG4kcGFnaW5hdGlvbi1hY3RpdmUtY29sb3I6ICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuJHBhZ2luYXRpb24tYWN0aXZlLWJnOiAgICAgICAgICAgICAgICAgJGJyYW5kLXByaW1hcnkgIWRlZmF1bHQ7XG4kcGFnaW5hdGlvbi1hY3RpdmUtYm9yZGVyOiAgICAgICAgICAgICAkYnJhbmQtcHJpbWFyeSAhZGVmYXVsdDtcblxuJHBhZ2luYXRpb24tZGlzYWJsZWQtY29sb3I6ICAgICAgICAgICAgJGdyYXktbGlnaHQgIWRlZmF1bHQ7XG4kcGFnaW5hdGlvbi1kaXNhYmxlZC1iZzogICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuJHBhZ2luYXRpb24tZGlzYWJsZWQtYm9yZGVyOiAgICAgICAgICAgI2RkZCAhZGVmYXVsdDtcblxuXG4vLz09IFBhZ2VyXG4vL1xuLy8jI1xuXG4kcGFnZXItYmc6ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAkcGFnaW5hdGlvbi1iZyAhZGVmYXVsdDtcbiRwYWdlci1ib3JkZXI6ICAgICAgICAgICAgICAgICAgICAgICAgICRwYWdpbmF0aW9uLWJvcmRlciAhZGVmYXVsdDtcbiRwYWdlci1ib3JkZXItcmFkaXVzOiAgICAgICAgICAgICAgICAgIDE1cHggIWRlZmF1bHQ7XG5cbiRwYWdlci1ob3Zlci1iZzogICAgICAgICAgICAgICAgICAgICAgICRwYWdpbmF0aW9uLWhvdmVyLWJnICFkZWZhdWx0O1xuXG4kcGFnZXItYWN0aXZlLWJnOiAgICAgICAgICAgICAgICAgICAgICAkcGFnaW5hdGlvbi1hY3RpdmUtYmcgIWRlZmF1bHQ7XG4kcGFnZXItYWN0aXZlLWNvbG9yOiAgICAgICAgICAgICAgICAgICAkcGFnaW5hdGlvbi1hY3RpdmUtY29sb3IgIWRlZmF1bHQ7XG5cbiRwYWdlci1kaXNhYmxlZC1jb2xvcjogICAgICAgICAgICAgICAgICRwYWdpbmF0aW9uLWRpc2FibGVkLWNvbG9yICFkZWZhdWx0O1xuXG5cbi8vPT0gSnVtYm90cm9uXG4vL1xuLy8jI1xuXG4kanVtYm90cm9uLXBhZGRpbmc6ICAgICAgICAgICAgICAzMHB4ICFkZWZhdWx0O1xuJGp1bWJvdHJvbi1jb2xvcjogICAgICAgICAgICAgICAgaW5oZXJpdCAhZGVmYXVsdDtcbiRqdW1ib3Ryb24tYmc6ICAgICAgICAgICAgICAgICAgICRncmF5LWxpZ2h0ZXIgIWRlZmF1bHQ7XG4kanVtYm90cm9uLWhlYWRpbmctY29sb3I6ICAgICAgICBpbmhlcml0ICFkZWZhdWx0O1xuJGp1bWJvdHJvbi1mb250LXNpemU6ICAgICAgICAgICAgY2VpbCgoJGZvbnQtc2l6ZS1iYXNlICogMS41KSkgIWRlZmF1bHQ7XG4kanVtYm90cm9uLWhlYWRpbmctZm9udC1zaXplOiAgICBjZWlsKCgkZm9udC1zaXplLWJhc2UgKiA0LjUpKSAhZGVmYXVsdDtcblxuXG4vLz09IEZvcm0gc3RhdGVzIGFuZCBhbGVydHNcbi8vXG4vLyMjIERlZmluZSBjb2xvcnMgZm9yIGZvcm0gZmVlZGJhY2sgc3RhdGVzIGFuZCwgYnkgZGVmYXVsdCwgYWxlcnRzLlxuXG4kc3RhdGUtc3VjY2Vzcy10ZXh0OiAgICAgICAgICAgICAjM2M3NjNkICFkZWZhdWx0O1xuJHN0YXRlLXN1Y2Nlc3MtYmc6ICAgICAgICAgICAgICAgI2RmZjBkOCAhZGVmYXVsdDtcbiRzdGF0ZS1zdWNjZXNzLWJvcmRlcjogICAgICAgICAgIGRhcmtlbihhZGp1c3QtaHVlKCRzdGF0ZS1zdWNjZXNzLWJnLCAtMTApLCA1JSkgIWRlZmF1bHQ7XG5cbiRzdGF0ZS1pbmZvLXRleHQ6ICAgICAgICAgICAgICAgICMzMTcwOGYgIWRlZmF1bHQ7XG4kc3RhdGUtaW5mby1iZzogICAgICAgICAgICAgICAgICAjZDllZGY3ICFkZWZhdWx0O1xuJHN0YXRlLWluZm8tYm9yZGVyOiAgICAgICAgICAgICAgZGFya2VuKGFkanVzdC1odWUoJHN0YXRlLWluZm8tYmcsIC0xMCksIDclKSAhZGVmYXVsdDtcblxuJHN0YXRlLXdhcm5pbmctdGV4dDogICAgICAgICAgICAgIzhhNmQzYiAhZGVmYXVsdDtcbiRzdGF0ZS13YXJuaW5nLWJnOiAgICAgICAgICAgICAgICNmY2Y4ZTMgIWRlZmF1bHQ7XG4kc3RhdGUtd2FybmluZy1ib3JkZXI6ICAgICAgICAgICBkYXJrZW4oYWRqdXN0LWh1ZSgkc3RhdGUtd2FybmluZy1iZywgLTEwKSwgNSUpICFkZWZhdWx0O1xuXG4kc3RhdGUtZGFuZ2VyLXRleHQ6ICAgICAgICAgICAgICAjYTk0NDQyICFkZWZhdWx0O1xuJHN0YXRlLWRhbmdlci1iZzogICAgICAgICAgICAgICAgI2YyZGVkZSAhZGVmYXVsdDtcbiRzdGF0ZS1kYW5nZXItYm9yZGVyOiAgICAgICAgICAgIGRhcmtlbihhZGp1c3QtaHVlKCRzdGF0ZS1kYW5nZXItYmcsIC0xMCksIDUlKSAhZGVmYXVsdDtcblxuXG4vLz09IFRvb2x0aXBzXG4vL1xuLy8jI1xuXG4vLyoqIFRvb2x0aXAgbWF4IHdpZHRoXG4kdG9vbHRpcC1tYXgtd2lkdGg6ICAgICAgICAgICAyMDBweCAhZGVmYXVsdDtcbi8vKiogVG9vbHRpcCB0ZXh0IGNvbG9yXG4kdG9vbHRpcC1jb2xvcjogICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuLy8qKiBUb29sdGlwIGJhY2tncm91bmQgY29sb3JcbiR0b29sdGlwLWJnOiAgICAgICAgICAgICAgICAgICMwMDAgIWRlZmF1bHQ7XG4kdG9vbHRpcC1vcGFjaXR5OiAgICAgICAgICAgICAuOSAhZGVmYXVsdDtcblxuLy8qKiBUb29sdGlwIGFycm93IHdpZHRoXG4kdG9vbHRpcC1hcnJvdy13aWR0aDogICAgICAgICA1cHggIWRlZmF1bHQ7XG4vLyoqIFRvb2x0aXAgYXJyb3cgY29sb3JcbiR0b29sdGlwLWFycm93LWNvbG9yOiAgICAgICAgICR0b29sdGlwLWJnICFkZWZhdWx0O1xuXG5cbi8vPT0gUG9wb3ZlcnNcbi8vXG4vLyMjXG5cbi8vKiogUG9wb3ZlciBib2R5IGJhY2tncm91bmQgY29sb3JcbiRwb3BvdmVyLWJnOiAgICAgICAgICAgICAgICAgICAgICAgICAgI2ZmZiAhZGVmYXVsdDtcbi8vKiogUG9wb3ZlciBtYXhpbXVtIHdpZHRoXG4kcG9wb3Zlci1tYXgtd2lkdGg6ICAgICAgICAgICAgICAgICAgIDI3NnB4ICFkZWZhdWx0O1xuLy8qKiBQb3BvdmVyIGJvcmRlciBjb2xvclxuJHBvcG92ZXItYm9yZGVyLWNvbG9yOiAgICAgICAgICAgICAgICByZ2JhKDAsMCwwLC4yKSAhZGVmYXVsdDtcbi8vKiogUG9wb3ZlciBmYWxsYmFjayBib3JkZXIgY29sb3JcbiRwb3BvdmVyLWZhbGxiYWNrLWJvcmRlci1jb2xvcjogICAgICAgI2NjYyAhZGVmYXVsdDtcblxuLy8qKiBQb3BvdmVyIHRpdGxlIGJhY2tncm91bmQgY29sb3JcbiRwb3BvdmVyLXRpdGxlLWJnOiAgICAgICAgICAgICAgICAgICAgZGFya2VuKCRwb3BvdmVyLWJnLCAzJSkgIWRlZmF1bHQ7XG5cbi8vKiogUG9wb3ZlciBhcnJvdyB3aWR0aFxuJHBvcG92ZXItYXJyb3ctd2lkdGg6ICAgICAgICAgICAgICAgICAxMHB4ICFkZWZhdWx0O1xuLy8qKiBQb3BvdmVyIGFycm93IGNvbG9yXG4kcG9wb3Zlci1hcnJvdy1jb2xvcjogICAgICAgICAgICAgICAgICRwb3BvdmVyLWJnICFkZWZhdWx0O1xuXG4vLyoqIFBvcG92ZXIgb3V0ZXIgYXJyb3cgd2lkdGhcbiRwb3BvdmVyLWFycm93LW91dGVyLXdpZHRoOiAgICAgICAgICAgKCRwb3BvdmVyLWFycm93LXdpZHRoICsgMSkgIWRlZmF1bHQ7XG4vLyoqIFBvcG92ZXIgb3V0ZXIgYXJyb3cgY29sb3JcbiRwb3BvdmVyLWFycm93LW91dGVyLWNvbG9yOiAgICAgICAgICAgZmFkZV9pbigkcG9wb3Zlci1ib3JkZXItY29sb3IsIDAuMDUpICFkZWZhdWx0O1xuLy8qKiBQb3BvdmVyIG91dGVyIGFycm93IGZhbGxiYWNrIGNvbG9yXG4kcG9wb3Zlci1hcnJvdy1vdXRlci1mYWxsYmFjay1jb2xvcjogIGRhcmtlbigkcG9wb3Zlci1mYWxsYmFjay1ib3JkZXItY29sb3IsIDIwJSkgIWRlZmF1bHQ7XG5cblxuLy89PSBMYWJlbHNcbi8vXG4vLyMjXG5cbi8vKiogRGVmYXVsdCBsYWJlbCBiYWNrZ3JvdW5kIGNvbG9yXG4kbGFiZWwtZGVmYXVsdC1iZzogICAgICAgICAgICAkZ3JheS1saWdodCAhZGVmYXVsdDtcbi8vKiogUHJpbWFyeSBsYWJlbCBiYWNrZ3JvdW5kIGNvbG9yXG4kbGFiZWwtcHJpbWFyeS1iZzogICAgICAgICAgICAkYnJhbmQtcHJpbWFyeSAhZGVmYXVsdDtcbi8vKiogU3VjY2VzcyBsYWJlbCBiYWNrZ3JvdW5kIGNvbG9yXG4kbGFiZWwtc3VjY2Vzcy1iZzogICAgICAgICAgICAkYnJhbmQtc3VjY2VzcyAhZGVmYXVsdDtcbi8vKiogSW5mbyBsYWJlbCBiYWNrZ3JvdW5kIGNvbG9yXG4kbGFiZWwtaW5mby1iZzogICAgICAgICAgICAgICAkYnJhbmQtaW5mbyAhZGVmYXVsdDtcbi8vKiogV2FybmluZyBsYWJlbCBiYWNrZ3JvdW5kIGNvbG9yXG4kbGFiZWwtd2FybmluZy1iZzogICAgICAgICAgICAkYnJhbmQtd2FybmluZyAhZGVmYXVsdDtcbi8vKiogRGFuZ2VyIGxhYmVsIGJhY2tncm91bmQgY29sb3JcbiRsYWJlbC1kYW5nZXItYmc6ICAgICAgICAgICAgICRicmFuZC1kYW5nZXIgIWRlZmF1bHQ7XG5cbi8vKiogRGVmYXVsdCBsYWJlbCB0ZXh0IGNvbG9yXG4kbGFiZWwtY29sb3I6ICAgICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuLy8qKiBEZWZhdWx0IHRleHQgY29sb3Igb2YgYSBsaW5rZWQgbGFiZWxcbiRsYWJlbC1saW5rLWhvdmVyLWNvbG9yOiAgICAgICNmZmYgIWRlZmF1bHQ7XG5cblxuLy89PSBNb2RhbHNcbi8vXG4vLyMjXG5cbi8vKiogUGFkZGluZyBhcHBsaWVkIHRvIHRoZSBtb2RhbCBib2R5XG4kbW9kYWwtaW5uZXItcGFkZGluZzogICAgICAgICAxNXB4ICFkZWZhdWx0O1xuXG4vLyoqIFBhZGRpbmcgYXBwbGllZCB0byB0aGUgbW9kYWwgdGl0bGVcbiRtb2RhbC10aXRsZS1wYWRkaW5nOiAgICAgICAgIDE1cHggIWRlZmF1bHQ7XG4vLyoqIE1vZGFsIHRpdGxlIGxpbmUtaGVpZ2h0XG4kbW9kYWwtdGl0bGUtbGluZS1oZWlnaHQ6ICAgICAkbGluZS1oZWlnaHQtYmFzZSAhZGVmYXVsdDtcblxuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIG9mIG1vZGFsIGNvbnRlbnQgYXJlYVxuJG1vZGFsLWNvbnRlbnQtYmc6ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuLy8qKiBNb2RhbCBjb250ZW50IGJvcmRlciBjb2xvclxuJG1vZGFsLWNvbnRlbnQtYm9yZGVyLWNvbG9yOiAgICAgICAgICAgICAgICAgICByZ2JhKDAsMCwwLC4yKSAhZGVmYXVsdDtcbi8vKiogTW9kYWwgY29udGVudCBib3JkZXIgY29sb3IgKipmb3IgSUU4KipcbiRtb2RhbC1jb250ZW50LWZhbGxiYWNrLWJvcmRlci1jb2xvcjogICAgICAgICAgIzk5OSAhZGVmYXVsdDtcblxuLy8qKiBNb2RhbCBiYWNrZHJvcCBiYWNrZ3JvdW5kIGNvbG9yXG4kbW9kYWwtYmFja2Ryb3AtYmc6ICAgICAgICAgICAjMDAwICFkZWZhdWx0O1xuLy8qKiBNb2RhbCBiYWNrZHJvcCBvcGFjaXR5XG4kbW9kYWwtYmFja2Ryb3Atb3BhY2l0eTogICAgICAuNSAhZGVmYXVsdDtcbi8vKiogTW9kYWwgaGVhZGVyIGJvcmRlciBjb2xvclxuJG1vZGFsLWhlYWRlci1ib3JkZXItY29sb3I6ICAgI2U1ZTVlNSAhZGVmYXVsdDtcbi8vKiogTW9kYWwgZm9vdGVyIGJvcmRlciBjb2xvclxuJG1vZGFsLWZvb3Rlci1ib3JkZXItY29sb3I6ICAgJG1vZGFsLWhlYWRlci1ib3JkZXItY29sb3IgIWRlZmF1bHQ7XG5cbiRtb2RhbC1sZzogICAgICAgICAgICAgICAgICAgIDkwMHB4ICFkZWZhdWx0O1xuJG1vZGFsLW1kOiAgICAgICAgICAgICAgICAgICAgNjAwcHggIWRlZmF1bHQ7XG4kbW9kYWwtc206ICAgICAgICAgICAgICAgICAgICAzMDBweCAhZGVmYXVsdDtcblxuXG4vLz09IEFsZXJ0c1xuLy9cbi8vIyMgRGVmaW5lIGFsZXJ0IGNvbG9ycywgYm9yZGVyIHJhZGl1cywgYW5kIHBhZGRpbmcuXG5cbiRhbGVydC1wYWRkaW5nOiAgICAgICAgICAgICAgIDE1cHggIWRlZmF1bHQ7XG4kYWxlcnQtYm9yZGVyLXJhZGl1czogICAgICAgICAkYm9yZGVyLXJhZGl1cy1iYXNlICFkZWZhdWx0O1xuJGFsZXJ0LWxpbmstZm9udC13ZWlnaHQ6ICAgICAgYm9sZCAhZGVmYXVsdDtcblxuJGFsZXJ0LXN1Y2Nlc3MtYmc6ICAgICAgICAgICAgJHN0YXRlLXN1Y2Nlc3MtYmcgIWRlZmF1bHQ7XG4kYWxlcnQtc3VjY2Vzcy10ZXh0OiAgICAgICAgICAkc3RhdGUtc3VjY2Vzcy10ZXh0ICFkZWZhdWx0O1xuJGFsZXJ0LXN1Y2Nlc3MtYm9yZGVyOiAgICAgICAgJHN0YXRlLXN1Y2Nlc3MtYm9yZGVyICFkZWZhdWx0O1xuXG4kYWxlcnQtaW5mby1iZzogICAgICAgICAgICAgICAkc3RhdGUtaW5mby1iZyAhZGVmYXVsdDtcbiRhbGVydC1pbmZvLXRleHQ6ICAgICAgICAgICAgICRzdGF0ZS1pbmZvLXRleHQgIWRlZmF1bHQ7XG4kYWxlcnQtaW5mby1ib3JkZXI6ICAgICAgICAgICAkc3RhdGUtaW5mby1ib3JkZXIgIWRlZmF1bHQ7XG5cbiRhbGVydC13YXJuaW5nLWJnOiAgICAgICAgICAgICRzdGF0ZS13YXJuaW5nLWJnICFkZWZhdWx0O1xuJGFsZXJ0LXdhcm5pbmctdGV4dDogICAgICAgICAgJHN0YXRlLXdhcm5pbmctdGV4dCAhZGVmYXVsdDtcbiRhbGVydC13YXJuaW5nLWJvcmRlcjogICAgICAgICRzdGF0ZS13YXJuaW5nLWJvcmRlciAhZGVmYXVsdDtcblxuJGFsZXJ0LWRhbmdlci1iZzogICAgICAgICAgICAgJHN0YXRlLWRhbmdlci1iZyAhZGVmYXVsdDtcbiRhbGVydC1kYW5nZXItdGV4dDogICAgICAgICAgICRzdGF0ZS1kYW5nZXItdGV4dCAhZGVmYXVsdDtcbiRhbGVydC1kYW5nZXItYm9yZGVyOiAgICAgICAgICRzdGF0ZS1kYW5nZXItYm9yZGVyICFkZWZhdWx0O1xuXG5cbi8vPT0gUHJvZ3Jlc3MgYmFyc1xuLy9cbi8vIyNcblxuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIG9mIHRoZSB3aG9sZSBwcm9ncmVzcyBjb21wb25lbnRcbiRwcm9ncmVzcy1iZzogICAgICAgICAgICAgICAgICNmNWY1ZjUgIWRlZmF1bHQ7XG4vLyoqIFByb2dyZXNzIGJhciB0ZXh0IGNvbG9yXG4kcHJvZ3Jlc3MtYmFyLWNvbG9yOiAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuLy8qKiBWYXJpYWJsZSBmb3Igc2V0dGluZyByb3VuZGVkIGNvcm5lcnMgb24gcHJvZ3Jlc3MgYmFyLlxuJHByb2dyZXNzLWJvcmRlci1yYWRpdXM6ICAgICAgJGJvcmRlci1yYWRpdXMtYmFzZSAhZGVmYXVsdDtcblxuLy8qKiBEZWZhdWx0IHByb2dyZXNzIGJhciBjb2xvclxuJHByb2dyZXNzLWJhci1iZzogICAgICAgICAgICAgJGJyYW5kLXByaW1hcnkgIWRlZmF1bHQ7XG4vLyoqIFN1Y2Nlc3MgcHJvZ3Jlc3MgYmFyIGNvbG9yXG4kcHJvZ3Jlc3MtYmFyLXN1Y2Nlc3MtYmc6ICAgICAkYnJhbmQtc3VjY2VzcyAhZGVmYXVsdDtcbi8vKiogV2FybmluZyBwcm9ncmVzcyBiYXIgY29sb3JcbiRwcm9ncmVzcy1iYXItd2FybmluZy1iZzogICAgICRicmFuZC13YXJuaW5nICFkZWZhdWx0O1xuLy8qKiBEYW5nZXIgcHJvZ3Jlc3MgYmFyIGNvbG9yXG4kcHJvZ3Jlc3MtYmFyLWRhbmdlci1iZzogICAgICAkYnJhbmQtZGFuZ2VyICFkZWZhdWx0O1xuLy8qKiBJbmZvIHByb2dyZXNzIGJhciBjb2xvclxuJHByb2dyZXNzLWJhci1pbmZvLWJnOiAgICAgICAgJGJyYW5kLWluZm8gIWRlZmF1bHQ7XG5cblxuLy89PSBMaXN0IGdyb3VwXG4vL1xuLy8jI1xuXG4vLyoqIEJhY2tncm91bmQgY29sb3Igb24gYC5saXN0LWdyb3VwLWl0ZW1gXG4kbGlzdC1ncm91cC1iZzogICAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4vLyoqIGAubGlzdC1ncm91cC1pdGVtYCBib3JkZXIgY29sb3JcbiRsaXN0LWdyb3VwLWJvcmRlcjogICAgICAgICAgICAgI2RkZCAhZGVmYXVsdDtcbi8vKiogTGlzdCBncm91cCBib3JkZXIgcmFkaXVzXG4kbGlzdC1ncm91cC1ib3JkZXItcmFkaXVzOiAgICAgICRib3JkZXItcmFkaXVzLWJhc2UgIWRlZmF1bHQ7XG5cbi8vKiogQmFja2dyb3VuZCBjb2xvciBvZiBzaW5nbGUgbGlzdCBpdGVtcyBvbiBob3ZlclxuJGxpc3QtZ3JvdXAtaG92ZXItYmc6ICAgICAgICAgICAjZjVmNWY1ICFkZWZhdWx0O1xuLy8qKiBUZXh0IGNvbG9yIG9mIGFjdGl2ZSBsaXN0IGl0ZW1zXG4kbGlzdC1ncm91cC1hY3RpdmUtY29sb3I6ICAgICAgICRjb21wb25lbnQtYWN0aXZlLWNvbG9yICFkZWZhdWx0O1xuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIG9mIGFjdGl2ZSBsaXN0IGl0ZW1zXG4kbGlzdC1ncm91cC1hY3RpdmUtYmc6ICAgICAgICAgICRjb21wb25lbnQtYWN0aXZlLWJnICFkZWZhdWx0O1xuLy8qKiBCb3JkZXIgY29sb3Igb2YgYWN0aXZlIGxpc3QgZWxlbWVudHNcbiRsaXN0LWdyb3VwLWFjdGl2ZS1ib3JkZXI6ICAgICAgJGxpc3QtZ3JvdXAtYWN0aXZlLWJnICFkZWZhdWx0O1xuLy8qKiBUZXh0IGNvbG9yIGZvciBjb250ZW50IHdpdGhpbiBhY3RpdmUgbGlzdCBpdGVtc1xuJGxpc3QtZ3JvdXAtYWN0aXZlLXRleHQtY29sb3I6ICBsaWdodGVuKCRsaXN0LWdyb3VwLWFjdGl2ZS1iZywgNDAlKSAhZGVmYXVsdDtcblxuLy8qKiBUZXh0IGNvbG9yIG9mIGRpc2FibGVkIGxpc3QgaXRlbXNcbiRsaXN0LWdyb3VwLWRpc2FibGVkLWNvbG9yOiAgICAgICRncmF5LWxpZ2h0ICFkZWZhdWx0O1xuLy8qKiBCYWNrZ3JvdW5kIGNvbG9yIG9mIGRpc2FibGVkIGxpc3QgaXRlbXNcbiRsaXN0LWdyb3VwLWRpc2FibGVkLWJnOiAgICAgICAgICRncmF5LWxpZ2h0ZXIgIWRlZmF1bHQ7XG4vLyoqIFRleHQgY29sb3IgZm9yIGNvbnRlbnQgd2l0aGluIGRpc2FibGVkIGxpc3QgaXRlbXNcbiRsaXN0LWdyb3VwLWRpc2FibGVkLXRleHQtY29sb3I6ICRsaXN0LWdyb3VwLWRpc2FibGVkLWNvbG9yICFkZWZhdWx0O1xuXG4kbGlzdC1ncm91cC1saW5rLWNvbG9yOiAgICAgICAgICM1NTUgIWRlZmF1bHQ7XG4kbGlzdC1ncm91cC1saW5rLWhvdmVyLWNvbG9yOiAgICRsaXN0LWdyb3VwLWxpbmstY29sb3IgIWRlZmF1bHQ7XG4kbGlzdC1ncm91cC1saW5rLWhlYWRpbmctY29sb3I6ICMzMzMgIWRlZmF1bHQ7XG5cblxuLy89PSBQYW5lbHNcbi8vXG4vLyMjXG5cbiRwYW5lbC1iZzogICAgICAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kcGFuZWwtYm9keS1wYWRkaW5nOiAgICAgICAgICAxNXB4ICFkZWZhdWx0O1xuJHBhbmVsLWhlYWRpbmctcGFkZGluZzogICAgICAgMTBweCAxNXB4ICFkZWZhdWx0O1xuJHBhbmVsLWZvb3Rlci1wYWRkaW5nOiAgICAgICAgJHBhbmVsLWhlYWRpbmctcGFkZGluZyAhZGVmYXVsdDtcbiRwYW5lbC1ib3JkZXItcmFkaXVzOiAgICAgICAgICRib3JkZXItcmFkaXVzLWJhc2UgIWRlZmF1bHQ7XG5cbi8vKiogQm9yZGVyIGNvbG9yIGZvciBlbGVtZW50cyB3aXRoaW4gcGFuZWxzXG4kcGFuZWwtaW5uZXItYm9yZGVyOiAgICAgICAgICAjZGRkICFkZWZhdWx0O1xuJHBhbmVsLWZvb3Rlci1iZzogICAgICAgICAgICAgI2Y1ZjVmNSAhZGVmYXVsdDtcblxuJHBhbmVsLWRlZmF1bHQtdGV4dDogICAgICAgICAgJGdyYXktZGFyayAhZGVmYXVsdDtcbiRwYW5lbC1kZWZhdWx0LWJvcmRlcjogICAgICAgICNkZGQgIWRlZmF1bHQ7XG4kcGFuZWwtZGVmYXVsdC1oZWFkaW5nLWJnOiAgICAjZjVmNWY1ICFkZWZhdWx0O1xuXG4kcGFuZWwtcHJpbWFyeS10ZXh0OiAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuJHBhbmVsLXByaW1hcnktYm9yZGVyOiAgICAgICAgJGJyYW5kLXByaW1hcnkgIWRlZmF1bHQ7XG4kcGFuZWwtcHJpbWFyeS1oZWFkaW5nLWJnOiAgICAkYnJhbmQtcHJpbWFyeSAhZGVmYXVsdDtcblxuJHBhbmVsLXN1Y2Nlc3MtdGV4dDogICAgICAgICAgJHN0YXRlLXN1Y2Nlc3MtdGV4dCAhZGVmYXVsdDtcbiRwYW5lbC1zdWNjZXNzLWJvcmRlcjogICAgICAgICRzdGF0ZS1zdWNjZXNzLWJvcmRlciAhZGVmYXVsdDtcbiRwYW5lbC1zdWNjZXNzLWhlYWRpbmctYmc6ICAgICRzdGF0ZS1zdWNjZXNzLWJnICFkZWZhdWx0O1xuXG4kcGFuZWwtaW5mby10ZXh0OiAgICAgICAgICAgICAkc3RhdGUtaW5mby10ZXh0ICFkZWZhdWx0O1xuJHBhbmVsLWluZm8tYm9yZGVyOiAgICAgICAgICAgJHN0YXRlLWluZm8tYm9yZGVyICFkZWZhdWx0O1xuJHBhbmVsLWluZm8taGVhZGluZy1iZzogICAgICAgJHN0YXRlLWluZm8tYmcgIWRlZmF1bHQ7XG5cbiRwYW5lbC13YXJuaW5nLXRleHQ6ICAgICAgICAgICRzdGF0ZS13YXJuaW5nLXRleHQgIWRlZmF1bHQ7XG4kcGFuZWwtd2FybmluZy1ib3JkZXI6ICAgICAgICAkc3RhdGUtd2FybmluZy1ib3JkZXIgIWRlZmF1bHQ7XG4kcGFuZWwtd2FybmluZy1oZWFkaW5nLWJnOiAgICAkc3RhdGUtd2FybmluZy1iZyAhZGVmYXVsdDtcblxuJHBhbmVsLWRhbmdlci10ZXh0OiAgICAgICAgICAgJHN0YXRlLWRhbmdlci10ZXh0ICFkZWZhdWx0O1xuJHBhbmVsLWRhbmdlci1ib3JkZXI6ICAgICAgICAgJHN0YXRlLWRhbmdlci1ib3JkZXIgIWRlZmF1bHQ7XG4kcGFuZWwtZGFuZ2VyLWhlYWRpbmctYmc6ICAgICAkc3RhdGUtZGFuZ2VyLWJnICFkZWZhdWx0O1xuXG5cbi8vPT0gVGh1bWJuYWlsc1xuLy9cbi8vIyNcblxuLy8qKiBQYWRkaW5nIGFyb3VuZCB0aGUgdGh1bWJuYWlsIGltYWdlXG4kdGh1bWJuYWlsLXBhZGRpbmc6ICAgICAgICAgICA0cHggIWRlZmF1bHQ7XG4vLyoqIFRodW1ibmFpbCBiYWNrZ3JvdW5kIGNvbG9yXG4kdGh1bWJuYWlsLWJnOiAgICAgICAgICAgICAgICAkYm9keS1iZyAhZGVmYXVsdDtcbi8vKiogVGh1bWJuYWlsIGJvcmRlciBjb2xvclxuJHRodW1ibmFpbC1ib3JkZXI6ICAgICAgICAgICAgI2RkZCAhZGVmYXVsdDtcbi8vKiogVGh1bWJuYWlsIGJvcmRlciByYWRpdXNcbiR0aHVtYm5haWwtYm9yZGVyLXJhZGl1czogICAgICRib3JkZXItcmFkaXVzLWJhc2UgIWRlZmF1bHQ7XG5cbi8vKiogQ3VzdG9tIHRleHQgY29sb3IgZm9yIHRodW1ibmFpbCBjYXB0aW9uc1xuJHRodW1ibmFpbC1jYXB0aW9uLWNvbG9yOiAgICAgJHRleHQtY29sb3IgIWRlZmF1bHQ7XG4vLyoqIFBhZGRpbmcgYXJvdW5kIHRoZSB0aHVtYm5haWwgY2FwdGlvblxuJHRodW1ibmFpbC1jYXB0aW9uLXBhZGRpbmc6ICAgOXB4ICFkZWZhdWx0O1xuXG5cbi8vPT0gV2VsbHNcbi8vXG4vLyMjXG5cbiR3ZWxsLWJnOiAgICAgICAgICAgICAgICAgICAgICNmNWY1ZjUgIWRlZmF1bHQ7XG4kd2VsbC1ib3JkZXI6ICAgICAgICAgICAgICAgICBkYXJrZW4oJHdlbGwtYmcsIDclKSAhZGVmYXVsdDtcblxuXG4vLz09IEJhZGdlc1xuLy9cbi8vIyNcblxuJGJhZGdlLWNvbG9yOiAgICAgICAgICAgICAgICAgI2ZmZiAhZGVmYXVsdDtcbi8vKiogTGlua2VkIGJhZGdlIHRleHQgY29sb3Igb24gaG92ZXJcbiRiYWRnZS1saW5rLWhvdmVyLWNvbG9yOiAgICAgICNmZmYgIWRlZmF1bHQ7XG4kYmFkZ2UtYmc6ICAgICAgICAgICAgICAgICAgICAkZ3JheS1saWdodCAhZGVmYXVsdDtcblxuLy8qKiBCYWRnZSB0ZXh0IGNvbG9yIGluIGFjdGl2ZSBuYXYgbGlua1xuJGJhZGdlLWFjdGl2ZS1jb2xvcjogICAgICAgICAgJGxpbmstY29sb3IgIWRlZmF1bHQ7XG4vLyoqIEJhZGdlIGJhY2tncm91bmQgY29sb3IgaW4gYWN0aXZlIG5hdiBsaW5rXG4kYmFkZ2UtYWN0aXZlLWJnOiAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuXG4kYmFkZ2UtZm9udC13ZWlnaHQ6ICAgICAgICAgICBib2xkICFkZWZhdWx0O1xuJGJhZGdlLWxpbmUtaGVpZ2h0OiAgICAgICAgICAgMSAhZGVmYXVsdDtcbiRiYWRnZS1ib3JkZXItcmFkaXVzOiAgICAgICAgIDEwcHggIWRlZmF1bHQ7XG5cblxuLy89PSBCcmVhZGNydW1ic1xuLy9cbi8vIyNcblxuJGJyZWFkY3J1bWItcGFkZGluZy12ZXJ0aWNhbDogICA4cHggIWRlZmF1bHQ7XG4kYnJlYWRjcnVtYi1wYWRkaW5nLWhvcml6b250YWw6IDE1cHggIWRlZmF1bHQ7XG4vLyoqIEJyZWFkY3J1bWIgYmFja2dyb3VuZCBjb2xvclxuJGJyZWFkY3J1bWItYmc6ICAgICAgICAgICAgICAgICAjZjVmNWY1ICFkZWZhdWx0O1xuLy8qKiBCcmVhZGNydW1iIHRleHQgY29sb3JcbiRicmVhZGNydW1iLWNvbG9yOiAgICAgICAgICAgICAgI2NjYyAhZGVmYXVsdDtcbi8vKiogVGV4dCBjb2xvciBvZiBjdXJyZW50IHBhZ2UgaW4gdGhlIGJyZWFkY3J1bWJcbiRicmVhZGNydW1iLWFjdGl2ZS1jb2xvcjogICAgICAgJGdyYXktbGlnaHQgIWRlZmF1bHQ7XG4vLyoqIFRleHR1YWwgc2VwYXJhdG9yIGZvciBiZXR3ZWVuIGJyZWFkY3J1bWIgZWxlbWVudHNcbiRicmVhZGNydW1iLXNlcGFyYXRvcjogICAgICAgICAgXCIvXCIgIWRlZmF1bHQ7XG5cblxuLy89PSBDYXJvdXNlbFxuLy9cbi8vIyNcblxuJGNhcm91c2VsLXRleHQtc2hhZG93OiAgICAgICAgICAgICAgICAgICAgICAgIDAgMXB4IDJweCByZ2JhKDAsMCwwLC42KSAhZGVmYXVsdDtcblxuJGNhcm91c2VsLWNvbnRyb2wtY29sb3I6ICAgICAgICAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4kY2Fyb3VzZWwtY29udHJvbC13aWR0aDogICAgICAgICAgICAgICAgICAgICAgMTUlICFkZWZhdWx0O1xuJGNhcm91c2VsLWNvbnRyb2wtb3BhY2l0eTogICAgICAgICAgICAgICAgICAgIC41ICFkZWZhdWx0O1xuJGNhcm91c2VsLWNvbnRyb2wtZm9udC1zaXplOiAgICAgICAgICAgICAgICAgIDIwcHggIWRlZmF1bHQ7XG5cbiRjYXJvdXNlbC1pbmRpY2F0b3ItYWN0aXZlLWJnOiAgICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuJGNhcm91c2VsLWluZGljYXRvci1ib3JkZXItY29sb3I6ICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG5cbiRjYXJvdXNlbC1jYXB0aW9uLWNvbG9yOiAgICAgICAgICAgICAgICAgICAgICAjZmZmICFkZWZhdWx0O1xuXG5cbi8vPT0gQ2xvc2Vcbi8vXG4vLyMjXG5cbiRjbG9zZS1mb250LXdlaWdodDogICAgICAgICAgIGJvbGQgIWRlZmF1bHQ7XG4kY2xvc2UtY29sb3I6ICAgICAgICAgICAgICAgICAjMDAwICFkZWZhdWx0O1xuJGNsb3NlLXRleHQtc2hhZG93OiAgICAgICAgICAgMCAxcHggMCAjZmZmICFkZWZhdWx0O1xuXG5cbi8vPT0gQ29kZVxuLy9cbi8vIyNcblxuJGNvZGUtY29sb3I6ICAgICAgICAgICAgICAgICAgI2M3MjU0ZSAhZGVmYXVsdDtcbiRjb2RlLWJnOiAgICAgICAgICAgICAgICAgICAgICNmOWYyZjQgIWRlZmF1bHQ7XG5cbiRrYmQtY29sb3I6ICAgICAgICAgICAgICAgICAgICNmZmYgIWRlZmF1bHQ7XG4ka2JkLWJnOiAgICAgICAgICAgICAgICAgICAgICAjMzMzICFkZWZhdWx0O1xuXG4kcHJlLWJnOiAgICAgICAgICAgICAgICAgICAgICAjZjVmNWY1ICFkZWZhdWx0O1xuJHByZS1jb2xvcjogICAgICAgICAgICAgICAgICAgJGdyYXktZGFyayAhZGVmYXVsdDtcbiRwcmUtYm9yZGVyLWNvbG9yOiAgICAgICAgICAgICNjY2MgIWRlZmF1bHQ7XG4kcHJlLXNjcm9sbGFibGUtbWF4LWhlaWdodDogICAzNDBweCAhZGVmYXVsdDtcblxuXG4vLz09IFR5cGVcbi8vXG4vLyMjXG5cbi8vKiogSG9yaXpvbnRhbCBvZmZzZXQgZm9yIGZvcm1zIGFuZCBsaXN0cy5cbiRjb21wb25lbnQtb2Zmc2V0LWhvcml6b250YWw6IDE4MHB4ICFkZWZhdWx0O1xuLy8qKiBUZXh0IG11dGVkIGNvbG9yXG4kdGV4dC1tdXRlZDogICAgICAgICAgICAgICAgICAkZ3JheS1saWdodCAhZGVmYXVsdDtcbi8vKiogQWJicmV2aWF0aW9ucyBhbmQgYWNyb255bXMgYm9yZGVyIGNvbG9yXG4kYWJici1ib3JkZXItY29sb3I6ICAgICAgICAgICAkZ3JheS1saWdodCAhZGVmYXVsdDtcbi8vKiogSGVhZGluZ3Mgc21hbGwgY29sb3JcbiRoZWFkaW5ncy1zbWFsbC1jb2xvcjogICAgICAgICRncmF5LWxpZ2h0ICFkZWZhdWx0O1xuLy8qKiBCbG9ja3F1b3RlIHNtYWxsIGNvbG9yXG4kYmxvY2txdW90ZS1zbWFsbC1jb2xvcjogICAgICAkZ3JheS1saWdodCAhZGVmYXVsdDtcbi8vKiogQmxvY2txdW90ZSBmb250IHNpemVcbiRibG9ja3F1b3RlLWZvbnQtc2l6ZTogICAgICAgICgkZm9udC1zaXplLWJhc2UgKiAxLjI1KSAhZGVmYXVsdDtcbi8vKiogQmxvY2txdW90ZSBib3JkZXIgY29sb3JcbiRibG9ja3F1b3RlLWJvcmRlci1jb2xvcjogICAgICRncmF5LWxpZ2h0ZXIgIWRlZmF1bHQ7XG4vLyoqIFBhZ2UgaGVhZGVyIGJvcmRlciBjb2xvclxuJHBhZ2UtaGVhZGVyLWJvcmRlci1jb2xvcjogICAgJGdyYXktbGlnaHRlciAhZGVmYXVsdDtcbi8vKiogV2lkdGggb2YgaG9yaXpvbnRhbCBkZXNjcmlwdGlvbiBsaXN0IHRpdGxlc1xuJGRsLWhvcml6b250YWwtb2Zmc2V0OiAgICAgICAgJGNvbXBvbmVudC1vZmZzZXQtaG9yaXpvbnRhbCAhZGVmYXVsdDtcbi8vKiogUG9pbnQgYXQgd2hpY2ggLmRsLWhvcml6b250YWwgYmVjb21lcyBob3Jpem9udGFsXG4kZGwtaG9yaXpvbnRhbC1icmVha3BvaW50OiAgICAkZ3JpZC1mbG9hdC1icmVha3BvaW50ICFkZWZhdWx0O1xuLy8qKiBIb3Jpem9udGFsIGxpbmUgY29sb3IuXG4kaHItYm9yZGVyOiAgICAgICAgICAgICAgICAgICAkZ3JheS1saWdodGVyICFkZWZhdWx0O1xuIiwieG9zLXNtYXJ0LXRhYmxle1xuICAucm93ICsgeG9zLXRhYmxlIHtcbiAgICBtYXJnaW4tdG9wOiAxNXB4O1xuICB9XG59Il0sIm1hcHBpbmdzIjoiQUNBQSxVQUFVLENBQUMsQUFBQSxZQUFZO0VBQ3JCLEFBQUEsSUFBSTtJQUNGLFNBQVMsRUFBRSx1QkFBVztJQUN0QixVQUFVLEVBQUUsT0FBUTtFQUd0QixBQUFBLEVBQUU7SUFDQSxTQUFTLEVBQUUsb0JBQVc7O0FBSTFCLFVBQVUsQ0FBQyxBQUFBLGFBQWE7RUFDdEIsQUFBQSxJQUFJO0lBQ0YsU0FBUyxFQUFFLG9CQUFXO0VBR3hCLEFBQUEsRUFBRTtJQUNBLFVBQVUsRUFBRSxNQUFPO0lBQ25CLFNBQVMsRUFBRSx1QkFBVzs7QUFJMUIsVUFBVSxDQUFDLEFBQUEsUUFBUTtFQUNqQixBQUFBLElBQUk7SUFDRixPQUFPLEVBQUUsQ0FBRTtJQUNYLFNBQVMsRUFBRSx1QkFBVztFQUd4QixBQUFBLEVBQUU7SUFDQSxPQUFPLEVBQUUsQ0FBRTtJQUNYLFNBQVMsRUFBRSxJQUFLOztBQUlwQixVQUFVLENBQUMsQUFBQSxXQUFXO0VBQ3BCLEFBQUEsSUFBSTtJQUNGLE9BQU8sRUFBRSxDQUFFO0VBR2IsQUFBQSxFQUFFO0lBQ0EsT0FBTyxFQUFFLENBQUU7SUFDWCxTQUFTLEVBQUUsdUJBQVc7O0FBekMxQixVQUFVLENBQUMsQUFBQSxZQUFZO0VBQ3JCLEFBQUEsSUFBSTtJQUNGLFNBQVMsRUFBRSx1QkFBVztJQUN0QixVQUFVLEVBQUUsT0FBUTtFQUd0QixBQUFBLEVBQUU7SUFDQSxTQUFTLEVBQUUsb0JBQVc7O0FBSTFCLFVBQVUsQ0FBQyxBQUFBLGFBQWE7RUFDdEIsQUFBQSxJQUFJO0lBQ0YsU0FBUyxFQUFFLG9CQUFXO0VBR3hCLEFBQUEsRUFBRTtJQUNBLFVBQVUsRUFBRSxNQUFPO0lBQ25CLFNBQVMsRUFBRSx1QkFBVzs7QUFJMUIsVUFBVSxDQUFDLEFBQUEsUUFBUTtFQUNqQixBQUFBLElBQUk7SUFDRixPQUFPLEVBQUUsQ0FBRTtJQUNYLFNBQVMsRUFBRSx1QkFBVztFQUd4QixBQUFBLEVBQUU7SUFDQSxPQUFPLEVBQUUsQ0FBRTtJQUNYLFNBQVMsRUFBRSxJQUFLOztBQUlwQixVQUFVLENBQUMsQUFBQSxXQUFXO0VBQ3BCLEFBQUEsSUFBSTtJQUNGLE9BQU8sRUFBRSxDQUFFO0VBR2IsQUFBQSxFQUFFO0lBQ0EsT0FBTyxFQUFFLENBQUU7SUFDWCxTQUFTLEVBQUUsdUJBQVc7O0FDdkMxQixBQUFBLFNBQVMsQ0FBQztFQUVSLE9BQU8sRUFBRSxLQUFNLEdBcUJoQjtFQXZCRCxBQUlJLFNBSkssQ0FJUCxFQUFFLEFBQUEsUUFBUTtFQUpaLEFBS0ksU0FMSyxDQUtQLEVBQUUsQUFBQSxTQUFTO0VBTGIsQUFNSSxTQU5LLENBTVAsRUFBRSxBQUFBLFNBQVMsQ0FBQztJQUNWLFVBQVUsRUFBQyxlQUFnQixHQUM1QjtFQVJILEFBVWEsU0FWSixDQVVQLEVBQUUsQUFBQSxTQUFTLEFBQUEsZ0JBQWdCO0VBVjdCLEFBV0ksU0FYSyxDQVdQLEVBQUUsQUFBQSxRQUFRO0VBWFosQUFZSSxTQVpLLENBWVAsRUFBRSxBQUFBLFNBQVMsQ0FBQztJQUNWLE9BQU8sRUFBQyxDQUFFO0lBQ1YsU0FBUyxFQUFFLDhCQUErQixHQUMzQztFQWZILEFBaUJJLFNBakJLLENBaUJQLEVBQUUsQUFBQSxTQUFTO0VBakJiLEFBa0JZLFNBbEJILENBa0JQLEVBQUUsQUFBQSxRQUFRLEFBQUEsZUFBZTtFQWxCM0IsQUFtQmEsU0FuQkosQ0FtQlAsRUFBRSxBQUFBLFNBQVMsQUFBQSxnQkFBZ0IsQ0FBQztJQUMxQixPQUFPLEVBQUMsQ0FBRTtJQUNWLFNBQVMsRUFBRSw2QkFBOEIsR0FDMUM7O0FEeEJILFVBQVUsQ0FBQyxBQUFBLFlBQVk7RUFDckIsQUFBQSxJQUFJO0lBQ0YsU0FBUyxFQUFFLHVCQUFXO0lBQ3RCLFVBQVUsRUFBRSxPQUFRO0VBR3RCLEFBQUEsRUFBRTtJQUNBLFNBQVMsRUFBRSxvQkFBVzs7QUFJMUIsVUFBVSxDQUFDLEFBQUEsYUFBYTtFQUN0QixBQUFBLElBQUk7SUFDRixTQUFTLEVBQUUsb0JBQVc7RUFHeEIsQUFBQSxFQUFFO0lBQ0EsVUFBVSxFQUFFLE1BQU87SUFDbkIsU0FBUyxFQUFFLHVCQUFXOztBQUkxQixVQUFVLENBQUMsQUFBQSxRQUFRO0VBQ2pCLEFBQUEsSUFBSTtJQUNGLE9BQU8sRUFBRSxDQUFFO0lBQ1gsU0FBUyxFQUFFLHVCQUFXO0VBR3hCLEFBQUEsRUFBRTtJQUNBLE9BQU8sRUFBRSxDQUFFO0lBQ1gsU0FBUyxFQUFFLElBQUs7O0FBSXBCLFVBQVUsQ0FBQyxBQUFBLFdBQVc7RUFDcEIsQUFBQSxJQUFJO0lBQ0YsT0FBTyxFQUFFLENBQUU7RUFHYixBQUFBLEVBQUU7SUFDQSxPQUFPLEVBQUUsQ0FBRTtJQUNYLFNBQVMsRUFBRSx1QkFBVzs7QUV2QzFCLEFBQUEsU0FBUyxDQUFDO0VBRVIsaUJBQWlCO0VBR2pCLGtCQUFrQixFQUVuQjtFQVBELEFBR0UsU0FITyxDQUdQLFlBQVksQ0FBUztJQUFFLFNBQVMsRUFBQyw0QkFBNkIsR0FBSTtFQUhwRSxBQU1FLFNBTk8sQ0FNUCxlQUFlLENBQU07SUFBRSxTQUFTLEVBQUMseUJBQTBCLEdBQUk7O0FGUmpFLFVBQVUsQ0FBQyxBQUFBLFlBQVk7RUFDckIsQUFBQSxJQUFJO0lBQ0YsU0FBUyxFQUFFLHVCQUFXO0lBQ3RCLFVBQVUsRUFBRSxPQUFRO0VBR3RCLEFBQUEsRUFBRTtJQUNBLFNBQVMsRUFBRSxvQkFBVzs7QUFJMUIsVUFBVSxDQUFDLEFBQUEsYUFBYTtFQUN0QixBQUFBLElBQUk7SUFDRixTQUFTLEVBQUUsb0JBQVc7RUFHeEIsQUFBQSxFQUFFO0lBQ0EsVUFBVSxFQUFFLE1BQU87SUFDbkIsU0FBUyxFQUFFLHVCQUFXOztBQUkxQixVQUFVLENBQUMsQUFBQSxRQUFRO0VBQ2pCLEFBQUEsSUFBSTtJQUNGLE9BQU8sRUFBRSxDQUFFO0lBQ1gsU0FBUyxFQUFFLHVCQUFXO0VBR3hCLEFBQUEsRUFBRTtJQUNBLE9BQU8sRUFBRSxDQUFFO0lBQ1gsU0FBUyxFQUFFLElBQUs7O0FBSXBCLFVBQVUsQ0FBQyxBQUFBLFdBQVc7RUFDcEIsQUFBQSxJQUFJO0lBQ0YsT0FBTyxFQUFFLENBQUU7RUFHYixBQUFBLEVBQUU7SUFDQSxPQUFPLEVBQUUsQ0FBRTtJQUNYLFNBQVMsRUFBRSx1QkFBVzs7QUd0QzFCLEFBQVEsS0FBSCxHQUFHLGNBQWMsQ0FBQztFQUNyQixVQUFVLEVDd05xQixJQUFJO0VEdk5uQyxPQUFPLEVBQUUsS0FBTSxHQUNoQjs7QUVORCxBQUNTLGVBRE0sQ0FDYixJQUFJLEdBQUcsU0FBUyxDQUFDO0VBQ2YsVUFBVSxFQUFFLElBQUssR0FDbEI7O0NOS0gsQUFBQSxBQUFVLFNBQVQsQUFBQSxJQUFZLEFBQUEsQUFBUyxRQUFSLEFBQUEsSUFBVyxBQUFBLEFBQWMsYUFBYixBQUFBLElBQWdCLEFBQUEsQUFBVyxVQUFWLEFBQUEsR0FBYSxBQUFBLFNBQVMsRUFBRSxBQUFBLFdBQVcsQ0FBQztFQUM3RSxPQUFPLEVBQUUsZUFBZ0IsR0FDMUI7O0FBRUQsQUFBTyxJQUFILEdBQUcsSUFBSSxDQUFDO0VBQ1YsVUFBVSxFQUFFLElBQUssR0FDbEIiLCJuYW1lcyI6W10sInNvdXJjZVJvb3QiOiIvc291cmNlLyJ9 */
diff --git a/xos/core/xoslib/dashboards/helloworld.html b/xos/core/xoslib/dashboards/helloworld.html
deleted file mode 100644
index 91dde39..0000000
--- a/xos/core/xoslib/dashboards/helloworld.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!-- /opt/xos/templates/admin/dashboard/helloworld.html -->
-<div>Hello, {{ user.firstname }} {{ user.lastname }}.</div>
-<div>This is the hello world view. The value of foobar is {{ foobar }}.</div>
-<div id="dynamicTableOfInterestingThings"></div>
-<p>Type a new description for the the first slice in the collection:</p>
-<input type="text" name="newDescription" id="newDescription">
-<input type="button" name="submitNewDescription" value="submit new description" id="submitNewDescription">
-
-<script src="{{ STATIC_URL }}/js/vendor/underscore-min.js"></script>
-<script src="{{ STATIC_URL }}/js/vendor/backbone.js"></script>
-<script src="{{ STATIC_URL }}/js/xoslib/xos-defaults.js"></script>
-<script src="{{ STATIC_URL }}/js/xoslib/xos-backbone.js"></script>
-
-<script src="{{ STATIC_URL }}/js/helloworld.js"></script>
diff --git a/xos/core/xoslib/dashboards/xosOpenVPNDashboard.html b/xos/core/xoslib/dashboards/xosOpenVPNDashboard.html
new file mode 100644
index 0000000..eb1c9c6
--- /dev/null
+++ b/xos/core/xoslib/dashboards/xosOpenVPNDashboard.html
@@ -0,0 +1,14 @@
+<!-- browserSync -->
+
+<!-- inject:css -->
+<link rel="stylesheet" href="/static/css/xosOpenVPNDashboard.css">
+<!-- endinject -->
+
+<div id="xosOpenVPNDashboard">
+ <div ui-view></div>
+</div>
+
+
+<!-- inject:js -->
+<script src="/static/js/xosOpenVPNDashboard.js"></script>
+<!-- endinject -->
diff --git a/xos/core/xoslib/methods/loginview.py b/xos/core/xoslib/methods/loginview.py
old mode 100755
new mode 100644
index 69ee289..2dc79c6
--- a/xos/core/xoslib/methods/loginview.py
+++ b/xos/core/xoslib/methods/loginview.py
@@ -62,8 +62,8 @@
return self.do_login(request, username, password)
def post(self, request, format=None):
- username = request.DATA.get("username", None)
- password = request.DATA.get("password", None)
+ username = request.data.get("username", None)
+ password = request.data.get("password", None)
return self.do_login(request, username, password)
@@ -97,5 +97,5 @@
return self.do_logout(request, sessionid)
def post(self, request, format=None):
- sessionid = request.DATA.get("xossessionid", None)
+ sessionid = request.data.get("xossessionid", None)
return self.do_logout(request, sessionid)
\ No newline at end of file
diff --git a/xos/core/xoslib/methods/portforwarding.py b/xos/core/xoslib/methods/portforwarding.py
index 0d24423..c98b1bd 100644
--- a/xos/core/xoslib/methods/portforwarding.py
+++ b/xos/core/xoslib/methods/portforwarding.py
@@ -27,7 +27,7 @@
def get_queryset(self):
queryset = queryset=Port.objects.all()
- node_name = self.request.QUERY_PARAMS.get('node_name', None)
+ node_name = self.request.query_params.get('node_name', None)
if node_name is not None:
queryset = queryset.filter(instance__node__name = node_name)
@@ -51,7 +51,7 @@
def get_queryset(self):
queryset = queryset=Port.objects.all()
- node_name = self.request.QUERY_PARAMS.get('node_name', None)
+ node_name = self.request.query_params.get('node_name', None)
if node_name is not None:
queryset = queryset.filter(instance__node__name = node_name)
diff --git a/xos/core/xoslib/methods/sshkeys.py b/xos/core/xoslib/methods/sshkeys.py
index 6223540..705a968 100644
--- a/xos/core/xoslib/methods/sshkeys.py
+++ b/xos/core/xoslib/methods/sshkeys.py
@@ -26,7 +26,7 @@
def get_queryset(self):
queryset = queryset=Instance.objects.all()
- node_name = self.request.QUERY_PARAMS.get('node_name', None)
+ node_name = self.request.query_params.get('node_name', None)
if node_name is not None:
queryset = queryset.filter(node__name = node_name)
@@ -47,7 +47,7 @@
def get_queryset(self):
queryset = queryset=Instance.objects.all()
- node_name = self.request.QUERY_PARAMS.get('node_name', None)
+ node_name = self.request.query_params.get('node_name', None)
if node_name is not None:
queryset = queryset.filter(node__name = node_name)
diff --git a/xos/core/xoslib/methods/vtn.py b/xos/core/xoslib/methods/vtn.py
index 1a2af53..c9dbf3a 100644
--- a/xos/core/xoslib/methods/vtn.py
+++ b/xos/core/xoslib/methods/vtn.py
@@ -57,7 +57,8 @@
for xos_service in Service.objects.all():
if service in xos_service.get_vtn_src_ids():
return Response(xos_service.get_vtn_dependencies_ids())
- raise DoesNotExist()
+ return Response([])
+ # raise DoesNotExist()
def list(self, request):
raise Exception("Not Implemented")
diff --git a/xos/core/xoslib/static/css/xosOpenVPNDashboard.css b/xos/core/xoslib/static/css/xosOpenVPNDashboard.css
new file mode 100644
index 0000000..d9d966e
--- /dev/null
+++ b/xos/core/xoslib/static/css/xosOpenVPNDashboard.css
@@ -0,0 +1 @@
+#xosOpenVPNDashboard{width:70%;margin:auto}.vpn-row{display:table-row}.vpn-cell{display:table-cell;padding:5px}.vpn-header{font-weight:700}
\ No newline at end of file
diff --git a/xos/core/xoslib/static/js/helloworld.js b/xos/core/xoslib/static/js/helloworld.js
deleted file mode 100644
index 166d183..0000000
--- a/xos/core/xoslib/static/js/helloworld.js
+++ /dev/null
@@ -1,38 +0,0 @@
-/* eslint-disable guard-for-in, space-before-blocks */
-
-// helloworld.js
-function updateHelloWorldData() {
- var html = '<table class="table table-bordered table-striped">';
-
- for (var slicekey in xos.slices.models) {
- var slice = xos.slices.models[slicekey];
-
- html = html + '<tr><td>' + slice.get('name') + '</td><td>' + slice.get('description') + '</td></tr>';
- }
- html = html + '</table>';
- $('#dynamicTableOfInterestingThings').html(html);
-}
-
-$(document).ready(function() {
- xos.slices.on('change', function() {
- updateHelloWorldData();
- });
- xos.slices.on('remove', function() {
- updateHelloWorldData();
- });
- xos.slices.on('sort', function() {
- updateHelloWorldData();
- });
-
- xos.slices.startPolling();
-});
-
-// helloworld.js
-$(document).ready(function() {
- $('#submitNewDescription').bind('click', function() {
- var newDescription = $('#newDescription').val();
-
- xos.slices.models[0].set('description', newDescription);
- xos.slices.models[0].save();
- });
-});
diff --git a/xos/core/xoslib/static/js/vendor/ngXosHelpers.js b/xos/core/xoslib/static/js/vendor/ngXosHelpers.js
index f981551..fe5cc8f 100644
--- a/xos/core/xoslib/static/js/vendor/ngXosHelpers.js
+++ b/xos/core/xoslib/static/js/vendor/ngXosHelpers.js
@@ -1 +1,1477 @@
-"use strict";!function(){angular.module("xos.uiComponents",[])}(),function(){angular.module("xos.uiComponents").directive("xosTable",function(){return{restrict:"E",scope:{data:"=",config:"="},template:'\n <div ng-show="vm.data.length > 0">\n <div class="row" ng-if="vm.config.filter == \'fulltext\'">\n <div class="col-xs-12">\n <input\n class="form-control"\n placeholder="Type to search.."\n type="text"\n ng-model="vm.query"/>\n </div>\n </div>\n <table ng-class="vm.classes" ng-show="vm.data.length > 0">\n <thead>\n <tr>\n <th ng-repeat="col in vm.columns">\n {{col.label}}\n <span ng-if="vm.config.order">\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = false">\n <i class="glyphicon glyphicon-chevron-up"></i>\n </a>\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = true">\n <i class="glyphicon glyphicon-chevron-down"></i>\n </a>\n </span>\n </th>\n <th ng-if="vm.config.actions">Actions</th>\n </tr>\n </thead>\n <tbody ng-if="vm.config.filter == \'field\'">\n <tr>\n <td ng-repeat="col in vm.columns">\n <input\n class="form-control"\n placeholder="Type to search by {{col.label}}"\n type="text"\n ng-model="vm.query[col.prop]"/>\n </td>\n <td ng-if="vm.config.actions"></td>\n </tr>\n </tbody>\n <tbody>\n <tr ng-repeat="item in vm.data | filter:vm.query | orderBy:vm.orderBy:vm.reverse | pagination:vm.currentPage * vm.config.pagination.pageSize | limitTo: (vm.config.pagination.pageSize || vm.data.length) track by $index">\n <td ng-repeat="col in vm.columns">{{item[col.prop]}}</td>\n <td ng-if="vm.config.actions">\n <a href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(item)"\n title="{{action.label}}">\n <i\n class="glyphicon glyphicon-{{action.icon}}"\n style="color: {{action.color}};"></i>\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n <xos-pagination\n ng-if="vm.config.pagination"\n page-size="vm.config.pagination.pageSize"\n total-elements="vm.data.length"\n change="vm.goToPage">\n </xos-pagination>\n </div>\n <div ng-show="vm.data.length == 0 || !vm.data">\n <div class="alert alert-info">\n No data to show.\n </div>\n </div>\n ',bindToController:!0,controllerAs:"vm",controller:function(){var n=this;if(!this.config)throw new Error('[xosTable] Please provide a configuration via the "config" attribute');if(!this.config.columns)throw new Error("[xosTable] Please provide a columns list in the configuration");this.columns=this.config.columns,this.classes=this.config.classes||"table table-striped table-bordered",this.config.actions,this.config.pagination&&(this.currentPage=0,this.goToPage=function(e){n.currentPage=e})}}})}(),function(){angular.module("xos.uiComponents").directive("xosPagination",function(){return{restrict:"E",scope:{pageSize:"=",totalElements:"=",change:"="},template:'\n <div class="row" ng-if="vm.pageList.length > 1">\n <div class="col-xs-12 text-center">\n <ul class="pagination">\n <li\n ng-click="vm.goToPage(vm.currentPage - 1)"\n ng-class="{disabled: vm.currentPage == 0}">\n <a href="" aria-label="Previous">\n <span aria-hidden="true">«</span>\n </a>\n </li>\n <li ng-repeat="i in vm.pageList" ng-class="{active: i === vm.currentPage}">\n <a href="" ng-click="vm.goToPage(i)">{{i + 1}}</a>\n </li>\n <li\n ng-click="vm.goToPage(vm.currentPage + 1)"\n ng-class="{disabled: vm.currentPage == vm.pages - 1}">\n <a href="" aria-label="Next">\n <span aria-hidden="true">»</span>\n </a>\n </li>\n </ul>\n </div>\n </div>\n ',bindToController:!0,controllerAs:"vm",controller:["$scope",function(n){var e=this;this.currentPage=0,this.goToPage=function(n){0>n||n===e.pages||(e.currentPage=n,e.change(n))},this.createPages=function(n){for(var e=[],r=0;n>r;r++)e.push(r);return e},n.$watch(function(){return e.totalElements},function(){e.totalElements&&(e.pages=Math.ceil(e.totalElements/e.pageSize),e.pageList=e.createPages(e.pages))})}]}}).filter("pagination",function(){return function(n,e){return n&&angular.isArray(n)?(e=parseInt(e,10),n.slice(e)):n}})}(),function(){angular.module("xos.uiComponents").directive("xosAlert",function(){return{restrict:"E",scope:{config:"=",show:"=?"},template:'\n <div class="alert alert-{{vm.config.type}}" ng-show="vm.show">\n <button type="button" class="close" ng-if="vm.config.closeBtn" ng-click="vm.dismiss()">\n <span aria-hidden="true">×</span>\n </button>\n <p ng-transclude></p>\n </div>\n ',transclude:!0,bindToController:!0,controllerAs:"vm",controller:["$timeout",function(n){var e=this;if(!this.config)throw new Error('[xosAlert] Please provide a configuration via the "config" attribute');this.show=this.show!==!1,this.dismiss=function(){e.show=!1},this.config.autoHide&&!function(){var r=n(function(){e.dismiss(),n.cancel(r)},e.config.autoHide)}()}]}})}(),function(){function n(n,e,r){n.interceptors.push("SetCSRFToken"),e.startSymbol("{$"),e.endSymbol("$}"),r.defaults.stripTrailingSlashes=!1}n.$inject=["$httpProvider","$interpolateProvider","$resourceProvider"],angular.module("bugSnag",[]).factory("$exceptionHandler",function(){return function(n,e){window.Bugsnag?Bugsnag.notifyException(n,{diagnostics:{cause:e}}):console.error(n,e,n.stack)}}),angular.module("xos.helpers",["ngCookies","ngResource","bugSnag","xos.uiComponents"]).config(n)}(),function(){angular.module("xos.helpers").service("vSG-Collection",["$resource",function(n){return n("/api/service/vsg/")}])}(),function(){angular.module("xos.helpers").service("vOLT-Collection",["$resource",function(n){return n("/api/tenant/cord/volt/:volt_id/",{volt_id:"@id"})}])}(),function(){angular.module("xos.helpers").service("Users",["$resource",function(n){return n("/api/core/users/")}])}(),function(){angular.module("xos.helpers").service("Truckroll-Collection",["$resource",function(n){return n("/api/tenant/truckroll/:truckroll_id/",{truckroll_id:"@id"})}])}(),function(){angular.module("xos.helpers").service("Subscribers",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/",{subscriber_id:"@id"})}]).service("Subscriber-features",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/features/",{subscriber_id:"@id"})}]).service("Subscriber-features-uplink_speed",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/features/uplink_speed/",{subscriber_id:"@id"})}]).service("Subscriber-features-downlink_speed",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/features/downlink_speed/",{subscriber_id:"@id"})}]).service("Subscriber-features-cdn",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/features/cdn/",{subscriber_id:"@id"})}]).service("Subscriber-features-uverse",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/features/uverse/",{subscriber_id:"@id"})}]).service("Subscriber-features-status",["$resource",function(n){return n("/api/tenant/cord/subscriber/:subscriber_id/features/status/",{subscriber_id:"@id"})}])}(),function(){angular.module("xos.helpers").service("ONOS-Services-Collection",["$resource",function(n){return n("/api/service/onos/")}])}(),function(){angular.module("xos.helpers").service("ONOS-App-Collection",["$resource",function(n){return n("/api/tenant/onos/app/")}])}(),function(){function n(){return{request:function(n){return-1===n.url.indexOf(".html")&&(n.url+="?no_hyperlinks=1"),n}}}angular.module("xos.helpers").factory("NoHyperlinks",n)}(),function(){function n(n){return{request:function(e){return"GET"!==e.method&&(e.headers["X-CSRFToken"]=n.get("xoscsrftoken")),e}}}n.$inject=["$cookies"],angular.module("xos.helpers").factory("SetCSRFToken",n)}();
\ No newline at end of file
+'use strict';
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+ 'use strict';
+
+ /**
+ * @ngdoc overview
+ * @name xos.uiComponents
+ * @description
+ * A collection of UI components useful for Dashboard development.
+ * Currently available components are:
+ * - [xosAlert](/#/module/xos.uiComponents.directive:xosAlert)
+ * - [xosForm](/#/module/xos.uiComponents.directive:xosForm)
+ * - [xosPagination](/#/module/xos.uiComponents.directive:xosPagination)
+ * - [xosSmartTable](/#/module/xos.uiComponents.directive:xosSmartTable)
+ * - [xosTable](/#/module/xos.uiComponents.directive:xosTable)
+ * - [xosValidation](/#/module/xos.uiComponents.directive:xosValidation)
+ **/
+
+ angular.module('xos.uiComponents', []);
+})();
+//# sourceMappingURL=../maps/ui_components/ui-components.module.js.map
+
+'use strict';
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosSmartTable
+ * @link xos.uiComponents.directive:xosTable xosTable
+ * @link xos.uiComponents.directive:xosForm xosForm
+ * @restrict E
+ * @description The xos-table directive
+ * @param {Object} config The configuration for the component,
+ * it is composed by the name of an angular [$resource](https://docs.angularjs.org/api/ngResource/service/$resource)
+ * and an array of fields that shouldn't be printed.
+ * ```
+ * {
+ resource: 'Users',
+ hiddenFields: []
+ }
+ * ```
+ * @scope
+ * @example
+ <example module="sampleSmartTable">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm">
+ <xos-smart-table config="vm.config"></xos-smart-table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleSmartTable', ['xos.uiComponents', 'ngResource', 'ngMockE2E'])
+ // This is only for documentation purpose
+ .run(function($httpBackend, _){
+ let datas = [{id: 1, name: 'Jhon', surname: 'Doe'}];
+ let count = 1;
+ let paramsUrl = new RegExp(/\/test\/(.+)/);
+ $httpBackend.whenDELETE(paramsUrl, undefined, ['id']).respond((method, url, data, headers, params) => {
+ data = angular.fromJson(data);
+ let id = url.match(paramsUrl)[1];
+ _.remove(datas, (d) => {
+ return d.id === parseInt(id);
+ })
+ return [204];
+ });
+ $httpBackend.whenGET('/test').respond(200, datas)
+ $httpBackend.whenPOST('/test').respond((method, url, data) => {
+ data = angular.fromJson(data);
+ data.id = ++count;
+ datas.push(data);
+ return [201, data, {}];
+ });
+ })
+ .factory('_', function($window){
+ return $window._;
+ })
+ .service('SampleResource', function($resource){
+ return $resource('/test/:id', {id: '@id'});
+ })
+ // End of documentation purpose, example start
+ .controller('SampleCtrl', function(){
+ this.config = {
+ resource: 'SampleResource'
+ };
+ });
+ </file>
+ </example>
+ */
+
+ .directive('xosSmartTable', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ config: '='
+ },
+ template: '\n <div class="row" ng-show="vm.data.length > 0">\n <div class="col-xs-12 text-right">\n <a href="" class="btn btn-success" ng-click="vm.createItem()">\n Add\n </a>\n </div>\n </div>\n <div class="row">\n <div class="col-xs-12 table-responsive">\n <xos-table config="vm.tableConfig" data="vm.data"></xos-table>\n </div>\n </div>\n <div class="panel panel-default" ng-show="vm.detailedItem">\n <div class="panel-heading">\n <div class="row">\n <div class="col-xs-11">\n <h3 class="panel-title" ng-show="vm.detailedItem.id">Update {{vm.config.resource}} {{vm.detailedItem.id}}</h3>\n <h3 class="panel-title" ng-show="!vm.detailedItem.id">Create {{vm.config.resource}} item</h3>\n </div>\n <div class="col-xs-1">\n <a href="" ng-click="vm.cleanForm()">\n <i class="glyphicon glyphicon-remove pull-right"></i>\n </a>\n </div>\n </div>\n </div>\n <div class="panel-body">\n <xos-form config="vm.formConfig" ng-model="vm.detailedItem"></xos-form>\n </div>\n </div>\n <xos-alert config="{type: \'success\', closeBtn: true}" show="vm.responseMsg">{{vm.responseMsg}}</xos-alert>\n <xos-alert config="{type: \'danger\', closeBtn: true}" show="vm.responseErr">{{vm.responseErr}}</xos-alert>\n ',
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: ["$injector", "LabelFormatter", "_", "XosFormHelpers", function controller($injector, LabelFormatter, _, XosFormHelpers) {
+ var _this = this;
+
+ // TODO
+ // - Validate the config (what if resource does not exist?)
+
+ // NOTE
+ // Corner case
+ // - if response is empty, how can we generate a form ?
+
+ this.responseMsg = false;
+ this.responseErr = false;
+
+ this.tableConfig = {
+ columns: [],
+ actions: [{
+ label: 'delete',
+ icon: 'remove',
+ cb: function cb(item) {
+ console.log(item);
+ _this.Resource.delete({ id: item.id }).$promise.then(function () {
+ _.remove(_this.data, function (d) {
+ return d.id === item.id;
+ });
+ _this.responseMsg = _this.config.resource + ' with id ' + item.id + ' successfully deleted';
+ }).catch(function (err) {
+ _this.responseErr = err.data.detail || 'Error while deleting ' + _this.config.resource + ' with id ' + item.id;
+ });
+ },
+ color: 'red'
+ }, {
+ label: 'details',
+ icon: 'search',
+ cb: function cb(item) {
+ _this.detailedItem = item;
+ }
+ }],
+ classes: 'table table-striped table-bordered table-responsive',
+ filter: 'field',
+ order: true,
+ pagination: {
+ pageSize: 10
+ }
+ };
+
+ this.formConfig = {
+ exclude: this.config.hiddenFields,
+ fields: {},
+ formName: this.config.resource + 'Form',
+ actions: [{
+ label: 'Save',
+ icon: 'ok',
+ cb: function cb(item) {
+ var p = void 0;
+ var isNew = true;
+
+ if (item.id) {
+ p = item.$update();
+ isNew = false;
+ } else {
+ p = item.$save();
+ }
+
+ p.then(function (res) {
+ if (isNew) {
+ _this.data.push(angular.copy(res));
+ }
+ delete _this.detailedItem;
+ _this.responseMsg = _this.config.resource + ' with id ' + item.id + ' successfully saved';
+ }).catch(function (err) {
+ _this.responseErr = err.data.detail || 'Error while saving ' + _this.config.resource + ' with id ' + item.id;
+ });
+ },
+ class: 'success'
+ }]
+ };
+
+ this.cleanForm = function () {
+ delete _this.detailedItem;
+ };
+
+ this.createItem = function () {
+ _this.detailedItem = new _this.Resource();
+ };
+
+ this.Resource = $injector.get(this.config.resource);
+
+ var getData = function getData() {
+ _this.Resource.query().$promise.then(function (res) {
+
+ if (!res[0]) {
+ return;
+ }
+
+ var item = res[0];
+ var props = Object.keys(item);
+
+ _.remove(props, function (p) {
+ return p == 'id' || p == 'validators';
+ });
+
+ // TODO move out cb
+ if (angular.isArray(_this.config.hiddenFields)) {
+ props = _.difference(props, _this.config.hiddenFields);
+ }
+
+ var labels = props.map(function (p) {
+ return LabelFormatter.format(p);
+ });
+
+ props.forEach(function (p, i) {
+ _this.tableConfig.columns.push({
+ label: labels[i],
+ prop: p
+ });
+ });
+
+ // build form structure
+ props.forEach(function (p, i) {
+ _this.formConfig.fields[p] = {
+ label: LabelFormatter.format(labels[i]).replace(':', ''),
+ type: XosFormHelpers._getFieldFormat(item[p])
+ };
+ });
+
+ _this.data = res;
+ });
+ };
+
+ getData();
+ }]
+ };
+ });
+})();
+//# sourceMappingURL=../../../maps/ui_components/smartComponents/smartTable/smartTable.component.js.map
+
+'use strict';
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosValidation
+ * @restrict E
+ * @description The xos-validation directive
+ * @param {Object} errors The error object
+ * @element ANY
+ * @scope
+ * @example
+ <example module="sampleValidation">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm">
+ <div class="row">
+ <div class="col-xs-12">
+ <label>Set an error type:</label>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.required = !vm.errors.required"
+ ng-class="{'btn-default': !vm.errors.required, 'btn-success': vm.errors.required}">
+ Required
+ </a>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.email = !vm.errors.email"
+ ng-class="{'btn-default': !vm.errors.email, 'btn-success': vm.errors.email}">
+ Email
+ </a>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.minlength = !vm.errors.minlength"
+ ng-class="{'btn-default': !vm.errors.minlength, 'btn-success': vm.errors.minlength}">
+ Min Length
+ </a>
+ </div>
+ <div class="col-xs-2">
+ <a class="btn"
+ ng-click="vm.errors.maxlength = !vm.errors.maxlength"
+ ng-class="{'btn-default': !vm.errors.maxlength, 'btn-success': vm.errors.maxlength}">
+ Max Length
+ </a>
+ </div>
+ </div>
+ <xos-validation errors="vm.errors"></xos-validation>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleValidation', ['xos.uiComponents'])
+ .controller('SampleCtrl', function(){
+ this.errors = {
+ email: false
+ }
+ });
+ </file>
+ </example>
+ */
+
+ .directive('xosValidation', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ errors: '='
+ },
+ template: '\n <div ng-cloak>\n <!-- <pre>{{vm.errors.email | json}}</pre> -->\n <xos-alert config="vm.config" show="vm.errors.required !== undefined && vm.errors.required !== false">\n Field required\n </xos-alert>\n <xos-alert config="vm.config" show="vm.errors.email !== undefined && vm.errors.email !== false">\n This is not a valid email\n </xos-alert>\n <xos-alert config="vm.config" show="vm.errors.minlength !== undefined && vm.errors.minlength !== false">\n Too short\n </xos-alert>\n <xos-alert config="vm.config" show="vm.errors.maxlength !== undefined && vm.errors.maxlength !== false">\n Too long\n </xos-alert>\n <xos-alert config="vm.config" show="vm.errors.custom !== undefined && vm.errors.custom !== false">\n Field invalid\n </xos-alert>\n </div>\n ',
+ transclude: true,
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: function controller() {
+ this.config = {
+ type: 'danger'
+ };
+ }
+ };
+ });
+})();
+//# sourceMappingURL=../../../maps/ui_components/dumbComponents/validation/validation.component.js.map
+
+'use strict';
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 3/24/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosTable
+ * @restrict E
+ * @description The xos-table directive
+ * @param {Object} config The configuration for the component.
+ * ```
+ * {
+ * columns: [
+ * {
+ * label: 'Human readable name',
+ * prop: 'Property to read in the model object'
+ * }
+ * ],
+ * classes: 'table table-striped table-bordered',
+ * actions: [ // if defined add an action column
+ {
+ label: 'delete',
+ icon: 'remove', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ color: 'red'
+ }
+ ],
+ filter: 'field', // can be by `field` or `fulltext`
+ order: true // whether to show ordering arrows
+ * }
+ * ```
+ * @param {Array} data The data that should be rendered
+ * @element ANY
+ * @scope
+ * @example
+ <example module="sampleTable1">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl1 as vm">
+ <xos-table data="vm.data" config="vm.config"></xos-table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleTable1', ['xos.uiComponents'])
+ .controller('SampleCtrl1', function(){
+ this.config = {
+ columns: [
+ {
+ label: 'First Name', // column title
+ prop: 'name' // property to read in the data array
+ },
+ {
+ label: 'Last Name',
+ prop: 'lastname'
+ }
+ ]
+ };
+ this.data = [
+ {
+ name: 'John',
+ lastname: 'Doe'
+ },
+ {
+ name: 'Gili',
+ lastname: 'Fereydoun'
+ }
+ ]
+ });
+ </file>
+ </example>
+ <example module="sampleTable2" animations="true">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl2 as vm">
+ <xos-table data="vm.data" config="vm.config"></xos-table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleTable2', ['xos.uiComponents', 'ngAnimate'])
+ .controller('SampleCtrl2', function(){
+ this.config = {
+ columns: [
+ {
+ label: 'First Name', // column title
+ prop: 'name' // property to read in the data array
+ },
+ {
+ label: 'Last Name',
+ prop: 'lastname'
+ }
+ ],
+ classes: 'table table-striped table-condensed', // table classes, default to `table table-striped table-bordered`
+ actions: [ // if defined add an action column
+ {
+ label: 'delete', // label
+ icon: 'remove', // icons, refers to bootstraps glyphicon
+ cb: (user) => { // callback, get feeded with the full object
+ console.log(user);
+ },
+ color: 'red' // icon color
+ }
+ ],
+ filter: 'field', // can be by `field` or `fulltext`
+ order: true
+ };
+ this.data = [
+ {
+ name: 'John',
+ lastname: 'Doe'
+ },
+ {
+ name: 'Gili',
+ lastname: 'Fereydoun'
+ }
+ ]
+ });
+ </file>
+ </example>
+ <example module="sampleTable3">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl3 as vm">
+ <xos-table data="vm.data" config="vm.config"></xos-table>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleTable3', ['xos.uiComponents'])
+ .controller('SampleCtrl3', function(){
+ this.config = {
+ columns: [
+ {
+ label: 'First Name', // column title
+ prop: 'name' // property to read in the data array
+ },
+ {
+ label: 'Last Name',
+ prop: 'lastname'
+ }
+ ],
+ pagination: {
+ pageSize: 2
+ }
+ };
+ this.data = [
+ {
+ name: 'John',
+ lastname: 'Doe'
+ },
+ {
+ name: 'Gili',
+ lastname: 'Fereydoun'
+ },
+ {
+ name: 'Lucky',
+ lastname: 'Clarkson'
+ },
+ {
+ name: 'Tate',
+ lastname: 'Spalding'
+ }
+ ]
+ });
+ </file>
+ </example>
+ **/
+
+ .directive('xosTable', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ data: '=',
+ config: '='
+ },
+ template: '\n <div ng-show="vm.data.length > 0">\n <div class="row" ng-if="vm.config.filter == \'fulltext\'">\n <div class="col-xs-12">\n <input\n class="form-control"\n placeholder="Type to search.."\n type="text"\n ng-model="vm.query"/>\n </div>\n </div>\n <table ng-class="vm.classes" ng-hide="vm.data.length == 0">\n <thead>\n <tr>\n <th ng-repeat="col in vm.columns">\n {{col.label}}\n <span ng-if="vm.config.order">\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = false">\n <i class="glyphicon glyphicon-chevron-up"></i>\n </a>\n <a href="" ng-click="vm.orderBy = col.prop; vm.reverse = true">\n <i class="glyphicon glyphicon-chevron-down"></i>\n </a>\n </span>\n </th>\n <th ng-if="vm.config.actions">Actions:</th>\n </tr>\n </thead>\n <tbody ng-if="vm.config.filter == \'field\'">\n <tr>\n <td ng-repeat="col in vm.columns">\n <input\n class="form-control"\n placeholder="Type to search by {{col.label}}"\n type="text"\n ng-model="vm.query[col.prop]"/>\n </td>\n <td ng-if="vm.config.actions"></td>\n </tr>\n </tbody>\n <tbody>\n <tr ng-repeat="item in vm.data | filter:vm.query | orderBy:vm.orderBy:vm.reverse | pagination:vm.currentPage * vm.config.pagination.pageSize | limitTo: (vm.config.pagination.pageSize || vm.data.length) track by $index">\n <td ng-repeat="col in vm.columns">{{item[col.prop]}}</td>\n <td ng-if="vm.config.actions">\n <a href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(item)"\n title="{{action.label}}">\n <i\n class="glyphicon glyphicon-{{action.icon}}"\n style="color: {{action.color}};"></i>\n </a>\n </td>\n </tr>\n </tbody>\n </table>\n <xos-pagination\n ng-if="vm.config.pagination"\n page-size="vm.config.pagination.pageSize"\n total-elements="vm.data.length"\n change="vm.goToPage">\n </xos-pagination>\n </div>\n <div ng-show="vm.data.length == 0 || !vm.data">\n <xos-alert config="{type: \'info\'}">\n No data to show.\n </xos-alert>\n </div>\n ',
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: function controller() {
+ var _this = this;
+
+ if (!this.config) {
+ throw new Error('[xosTable] Please provide a configuration via the "config" attribute');
+ }
+
+ if (!this.config.columns) {
+ throw new Error('[xosTable] Please provide a columns list in the configuration');
+ }
+
+ this.columns = this.config.columns;
+ this.classes = this.config.classes || 'table table-striped table-bordered';
+
+ if (this.config.actions) {
+ // TODO validate action format
+ }
+ if (this.config.pagination) {
+ this.currentPage = 0;
+ this.goToPage = function (n) {
+ _this.currentPage = n;
+ };
+ }
+ }
+ };
+ });
+})();
+//# sourceMappingURL=../../../maps/ui_components/dumbComponents/table/table.component.js.map
+
+'use strict';
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosPagination
+ * @restrict E
+ * @description The xos-table directive
+ * @param {Number} pageSize Number of elements per page
+ * @param {Number} totalElements Number of total elements in the collection
+ * @param {Function} change The callback to be triggered on page change.
+ * * @element ANY
+ * @scope
+ * @example
+ <example module="samplePagination">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl1 as vm">
+ <xos-pagination
+ page-size="vm.pageSize"
+ total-elements="vm.totalElements"
+ change="vm.change">
+ </xos-pagination>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('samplePagination', ['xos.uiComponents'])
+ .controller('SampleCtrl1', function(){
+ this.pageSize = 10;
+ this.totalElements = 35;
+ this.change = (pageNumber) => {
+ console.log(pageNumber);
+ }
+ });
+ </file>
+ </example>
+ **/
+
+ .directive('xosPagination', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ pageSize: '=',
+ totalElements: '=',
+ change: '='
+ },
+ template: '\n <div class="row" ng-if="vm.pageList.length > 1">\n <div class="col-xs-12 text-center">\n <ul class="pagination">\n <li\n ng-click="vm.goToPage(vm.currentPage - 1)"\n ng-class="{disabled: vm.currentPage == 0}">\n <a href="" aria-label="Previous">\n <span aria-hidden="true">«</span>\n </a>\n </li>\n <li ng-repeat="i in vm.pageList" ng-class="{active: i === vm.currentPage}">\n <a href="" ng-click="vm.goToPage(i)">{{i + 1}}</a>\n </li>\n <li\n ng-click="vm.goToPage(vm.currentPage + 1)"\n ng-class="{disabled: vm.currentPage == vm.pages - 1}">\n <a href="" aria-label="Next">\n <span aria-hidden="true">»</span>\n </a>\n </li>\n </ul>\n </div>\n </div>\n ',
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: ["$scope", function controller($scope) {
+ var _this = this;
+
+ this.currentPage = 0;
+
+ this.goToPage = function (n) {
+ if (n < 0 || n === _this.pages) {
+ return;
+ }
+ _this.currentPage = n;
+ _this.change(n);
+ };
+
+ this.createPages = function (pages) {
+ var arr = [];
+ for (var i = 0; i < pages; i++) {
+ arr.push(i);
+ }
+ return arr;
+ };
+
+ // watch for data changes
+ $scope.$watch(function () {
+ return _this.totalElements;
+ }, function () {
+ if (_this.totalElements) {
+ _this.pages = Math.ceil(_this.totalElements / _this.pageSize);
+ _this.pageList = _this.createPages(_this.pages);
+ }
+ });
+ }]
+ };
+ }).filter('pagination', function () {
+ return function (input, start) {
+ if (!input || !angular.isArray(input)) {
+ return input;
+ }
+ start = parseInt(start, 10);
+ return input.slice(start);
+ };
+ });
+})();
+//# sourceMappingURL=../../../maps/ui_components/dumbComponents/pagination/pagination.component.js.map
+
+'use strict';
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 4/18/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosForm
+ * @restrict E
+ * @description The xos-form directive.
+ * This components have two usage, given a model it is able to autogenerate a form or it can be configured to create a custom form.
+ * @param {Object} config The configuration object
+ * ```
+ * {
+ * exclude: ['id', 'validators', 'created', 'updated', 'deleted'], //field to be skipped in the form, the provide values are concatenated
+ * actions: [ // define the form buttons with related callback
+ * {
+ label: 'save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ class: 'success'
+ }
+ * ],
+ * fields: {
+ * field_name: {
+ * label: 'Field Label',
+ * type: 'string' // options are: [date, boolean, number, email, string],
+ * validators: {
+ * minlength: number,
+ maxlength: number,
+ required: boolean,
+ min: number,
+ max: number
+ * }
+ * }
+ * }
+ * }
+ * ```
+ * @element ANY
+ * @scope
+ * @example
+
+ Autogenerated form
+ <example module="sampleForm">
+ <file name="script.js">
+ angular.module('sampleForm', ['xos.uiComponents'])
+ .factory('_', function($window){
+ return $window._;
+ })
+ .controller('SampleCtrl', function(){
+ this.model = {
+ first_name: 'Jhon',
+ last_name: 'Doe',
+ email: 'jhon.doe@sample.com',
+ active: true,
+ birthDate: '2015-02-17T22:06:38.059000Z'
+ }
+ this.config = {
+ exclude: ['password', 'last_login'],
+ formName: 'sampleForm',
+ actions: [
+ {
+ label: 'Save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ class: 'success'
+ }
+ ]
+ };
+ });
+ </file>
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm">
+ <xos-form ng-model="vm.model" config="vm.config"></xos-form>
+ </div>
+ </file>
+ </example>
+ Configuration defined form
+ <example module="sampleForm1">
+ <file name="script.js">
+ angular.module('sampleForm1', ['xos.uiComponents'])
+ .factory('_', function($window){
+ return $window._;
+ })
+ .controller('SampleCtrl1', function(){
+ this.model = {
+ };
+ this.config = {
+ exclude: ['password', 'last_login'],
+ formName: 'sampleForm1',
+ actions: [
+ {
+ label: 'Save',
+ icon: 'ok', // refers to bootstraps glyphicon
+ cb: (user) => { // receive the model
+ console.log(user);
+ },
+ class: 'success'
+ }
+ ],
+ fields: {
+ first_name: {
+ type: 'string',
+ validators: {
+ required: true
+ }
+ },
+ last_name: {
+ label: 'Surname',
+ type: 'string',
+ validators: {
+ required: true,
+ minlength: 10
+ }
+ },
+ age: {
+ type: 'number',
+ validators: {
+ required: true,
+ min: 21
+ }
+ },
+ }
+ };
+ });
+ </file>
+ <file name="index.html">
+ <div ng-controller="SampleCtrl1 as vm">
+ <xos-form ng-model="vm.model" config="vm.config"></xos-form>
+ </div>
+ </file>
+ </example>
+ **/
+
+ .directive('xosForm', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ config: '=',
+ ngModel: '='
+ },
+ template: '\n <ng-form name="vm.{{vm.config.formName || \'form\'}}">\n <div class="form-group" ng-repeat="(name, field) in vm.formField">\n <label>{{field.label}}</label>\n <input\n ng-if="field.type !== \'boolean\'"\n type="{{field.type}}"\n name="{{name}}"\n class="form-control"\n ng-model="vm.ngModel[name]"\n ng-minlength="field.validators.minlength || 0"\n ng-maxlength="field.validators.maxlength || 2000"\n ng-required="field.validators.required || false" />\n <span class="boolean-field" ng-if="field.type === \'boolean\'">\n <button\n class="btn btn-success"\n ng-show="vm.ngModel[name]"\n ng-click="vm.ngModel[name] = false">\n <i class="glyphicon glyphicon-ok"></i>\n </button>\n <button\n class="btn btn-danger"\n ng-show="!vm.ngModel[name]"\n ng-click="vm.ngModel[name] = true">\n <i class="glyphicon glyphicon-remove"></i>\n </button>\n </span>\n <!-- <pre>{{vm[vm.config.formName][name].$error | json}}</pre> -->\n <xos-validation errors="vm[vm.config.formName || \'form\'][name].$error"></xos-validation>\n </div>\n <div class="form-group" ng-if="vm.config.actions">\n <button role="button" href=""\n ng-repeat="action in vm.config.actions"\n ng-click="action.cb(vm.ngModel)"\n class="btn btn-{{action.class}}"\n title="{{action.label}}">\n <i class="glyphicon glyphicon-{{action.icon}}"></i>\n {{action.label}}\n </button>\n </div>\n </ng-form>\n ',
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: ["$scope", "$log", "_", "XosFormHelpers", function controller($scope, $log, _, XosFormHelpers) {
+ var _this = this;
+
+ if (!this.config) {
+ throw new Error('[xosForm] Please provide a configuration via the "config" attribute');
+ }
+
+ if (!this.config.actions) {
+ throw new Error('[xosForm] Please provide an action list in the configuration');
+ }
+
+ this.excludedField = ['id', 'validators', 'created', 'updated', 'deleted', 'backend_status'];
+ if (this.config && this.config.exclude) {
+ this.excludedField = this.excludedField.concat(this.config.exclude);
+ }
+
+ this.formField = [];
+ $scope.$watch(function () {
+ return _this.ngModel;
+ }, function (model) {
+
+ // empty from old stuff
+ _this.formField = {};
+
+ if (!model) {
+ return;
+ }
+
+ var diff = _.difference(Object.keys(model), _this.excludedField);
+ var modelField = XosFormHelpers.parseModelField(diff);
+ _this.formField = XosFormHelpers.buildFormStructure(modelField, _this.config.fields, model);
+ });
+ }]
+ };
+ }).service('XosFormHelpers', ["_", "LabelFormatter", function (_, LabelFormatter) {
+ var _this2 = this;
+
+ this._isEmail = function (text) {
+ var re = /(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
+ return re.test(text);
+ };
+
+ this._getFieldFormat = function (value) {
+
+ // check if is date
+ if (_.isDate(value) || !Number.isNaN(Date.parse(value)) && new Date(value).getTime() > 631180800000) {
+ return 'date';
+ }
+
+ // check if is boolean
+ // isNaN(false) = false, false is a number (0), true is a number (1)
+ if (typeof value === 'boolean') {
+ return 'boolean';
+ }
+
+ // check if a string is a number
+ if (!isNaN(value) && value !== null) {
+ return 'number';
+ }
+
+ // check if a string is an email
+ if (_this2._isEmail(value)) {
+ return 'email';
+ }
+
+ // if null return string
+ if (value === null) {
+ return 'string';
+ }
+
+ return typeof value === 'undefined' ? 'undefined' : _typeof(value);
+ };
+
+ this.buildFormStructure = function (modelField, customField, model) {
+
+ modelField = Object.keys(modelField).length > 0 ? modelField : customField; //if no model field are provided, check custom
+ customField = customField || {};
+
+ return _.reduce(Object.keys(modelField), function (form, f) {
+
+ form[f] = {
+ label: customField[f] && customField[f].label ? customField[f].label + ':' : LabelFormatter.format(f),
+ type: customField[f] && customField[f].type ? customField[f].type : _this2._getFieldFormat(model[f]),
+ validators: customField[f] && customField[f].validators ? customField[f].validators : {}
+ };
+
+ if (form[f].type === 'date') {
+ model[f] = new Date(model[f]);
+ }
+
+ if (form[f].type === 'number') {
+ model[f] = parseInt(model[f], 10);
+ }
+
+ return form;
+ }, {});
+ };
+
+ this.parseModelField = function (fields) {
+ return _.reduce(fields, function (form, f) {
+ form[f] = {};
+ return form;
+ }, {});
+ };
+ }]);
+})();
+//# sourceMappingURL=../../../maps/ui_components/dumbComponents/form/form.component.js.map
+
+'use strict';
+
+/**
+ * © OpenCORD
+ *
+ * Visit http://guide.xosproject.org/devguide/addview/ for more information
+ *
+ * Created by teone on 4/15/16.
+ */
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.uiComponents')
+
+ /**
+ * @ngdoc directive
+ * @name xos.uiComponents.directive:xosAlert
+ * @restrict E
+ * @description The xos-alert directive
+ * @param {Object} config The configuration object
+ * ```
+ * {
+ * type: 'danger', //info, success, warning
+ * closeBtn: true, //default false
+ * autoHide: 3000 //delay to automatically hide the alert
+ * }
+ * ```
+ * @param {Boolean=} show Binding to show and hide the alert, default to true
+ * @element ANY
+ * @scope
+ * @example
+ <example module="sampleAlert1">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl1 as vm">
+ <xos-alert config="vm.config1">
+ A sample alert message
+ </xos-alert>
+ <xos-alert config="vm.config2">
+ A sample alert message (with close button)
+ </xos-alert>
+ <xos-alert config="vm.config3">
+ A sample info message
+ </xos-alert>
+ <xos-alert config="vm.config4">
+ A sample success message
+ </xos-alert>
+ <xos-alert config="vm.config5">
+ A sample warning message
+ </xos-alert>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleAlert1', ['xos.uiComponents'])
+ .controller('SampleCtrl1', function(){
+ this.config1 = {
+ type: 'danger'
+ };
+ this.config2 = {
+ type: 'danger',
+ closeBtn: true
+ };
+ this.config3 = {
+ type: 'info'
+ };
+ this.config4 = {
+ type: 'success'
+ };
+ this.config5 = {
+ type: 'warning'
+ };
+ });
+ </file>
+ </example>
+ <example module="sampleAlert2" animations="true">
+ <file name="index.html">
+ <div ng-controller="SampleCtrl as vm" class="row">
+ <div class="col-sm-4">
+ <a class="btn btn-default btn-block" ng-show="!vm.show" ng-click="vm.show = true">Show Alert</a>
+ <a class="btn btn-default btn-block" ng-show="vm.show" ng-click="vm.show = false">Hide Alert</a>
+ </div>
+ <div class="col-sm-8">
+ <xos-alert config="vm.config1" show="vm.show">
+ A sample alert message, not displayed by default.
+ </xos-alert>
+ </div>
+ </div>
+ </file>
+ <file name="script.js">
+ angular.module('sampleAlert2', ['xos.uiComponents', 'ngAnimate'])
+ .controller('SampleCtrl', function(){
+ this.config1 = {
+ type: 'success'
+ };
+ this.show = false;
+ });
+ </file>
+ </example>
+ **/
+
+ .directive('xosAlert', function () {
+ return {
+ restrict: 'E',
+ scope: {
+ config: '=',
+ show: '=?'
+ },
+ template: '\n <div ng-cloak class="alert alert-{{vm.config.type}}" ng-hide="!vm.show">\n <button type="button" class="close" ng-if="vm.config.closeBtn" ng-click="vm.dismiss()">\n <span aria-hidden="true">×</span>\n </button>\n <p ng-transclude></p>\n </div>\n ',
+ transclude: true,
+ bindToController: true,
+ controllerAs: 'vm',
+ controller: ["$timeout", function controller($timeout) {
+ var _this = this;
+
+ if (!this.config) {
+ throw new Error('[xosAlert] Please provide a configuration via the "config" attribute');
+ }
+
+ // default the value to true
+ this.show = this.show !== false;
+
+ this.dismiss = function () {
+ _this.show = false;
+ };
+
+ if (this.config.autoHide) {
+ (function () {
+ var to = $timeout(function () {
+ _this.dismiss();
+ $timeout.cancel(to);
+ }, _this.config.autoHide);
+ })();
+ }
+ }]
+ };
+ });
+})();
+//# sourceMappingURL=../../../maps/ui_components/dumbComponents/alert/alert.component.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ config.$inject = ["$httpProvider", "$interpolateProvider", "$resourceProvider"];
+ angular.module('bugSnag', []).factory('$exceptionHandler', function () {
+ return function (exception, cause) {
+ if (window.Bugsnag) {
+ Bugsnag.notifyException(exception, { diagnostics: { cause: cause } });
+ } else {
+ console.error(exception, cause, exception.stack);
+ }
+ };
+ });
+
+ /**
+ * @ngdoc overview
+ * @name xos.helpers
+ * @description this is the module that group all the helpers service and components for XOS
+ **/
+
+ angular.module('xos.helpers', ['ngCookies', 'ngResource', 'ngAnimate', 'bugSnag', 'xos.uiComponents']).config(config).factory('_', ["$window", function ($window) {
+ return $window._;
+ }]);
+
+ function config($httpProvider, $interpolateProvider, $resourceProvider) {
+ $httpProvider.interceptors.push('SetCSRFToken');
+
+ $interpolateProvider.startSymbol('{$');
+ $interpolateProvider.endSymbol('$}');
+
+ // NOTE http://www.masnun.com/2013/09/18/django-rest-framework-angularjs-resource-trailing-slash-problem.html
+ $resourceProvider.defaults.stripTrailingSlashes = false;
+ }
+})();
+//# sourceMappingURL=maps/xosHelpers.module.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.vSG-Collection
+ * @description Angular resource to fetch /api/service/vsg/
+ **/
+ .service('vSG-Collection', ["$resource", function ($resource) {
+ return $resource('/api/service/vsg/');
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/vSG.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.vOLT-Collection
+ * @description Angular resource to fetch /api/tenant/cord/volt/:volt_id/
+ **/
+ .service('vOLT-Collection', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/volt/:volt_id/', { volt_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/vOLT.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Users
+ * @description Angular resource to fetch /api/core/users/:id/
+ **/
+ .service('Users', ["$resource", function ($resource) {
+ return $resource('/api/core/users/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/Users.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Truckroll-Collection
+ * @description Angular resource to fetch /api/tenant/truckroll/:truckroll_id/
+ **/
+ .service('Truckroll-Collection', ["$resource", function ($resource) {
+ return $resource('/api/tenant/truckroll/:truckroll_id/', { truckroll_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/Truckroll.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscribers
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/
+ **/
+ .service('Subscribers', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }])
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscriber-features
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/
+ **/
+ .service('Subscriber-features', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }])
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscriber-features-uplink_speed
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/uplink_speed/
+ **/
+ .service('Subscriber-features-uplink_speed', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/uplink_speed/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }])
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscriber-features-downlink_speed
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/downlink_speed/
+ **/
+ .service('Subscriber-features-downlink_speed', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/downlink_speed/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }])
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscriber-features-cdn
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/cdn/
+ **/
+ .service('Subscriber-features-cdn', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/cdn/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }])
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscriber-features-uverse
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/uverse/
+ **/
+ .service('Subscriber-features-uverse', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/uverse/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }])
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Subscriber-features-status
+ * @description Angular resource to fetch /api/tenant/cord/subscriber/:subscriber_id/features/status/
+ **/
+ .service('Subscriber-features-status', ["$resource", function ($resource) {
+ return $resource('/api/tenant/cord/subscriber/:subscriber_id/features/status/', { subscriber_id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/Subscribers.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.ONOS-Services-Collection
+ * @description Angular resource to fetch /api/service/onos/
+ **/
+ .service('ONOS-Services-Collection', ["$resource", function ($resource) {
+ return $resource('/api/service/onos/');
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/ONOS-Services.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.ONOS-App-Collection
+ * @description Angular resource to fetch /api/tenant/onos/app/
+ **/
+ .service('ONOS-App-Collection', ["$resource", function ($resource) {
+ return $resource('/api/tenant/onos/app/');
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/ONOS-Apps.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Instances
+ * @description Angular resource to fetch /api/core/instances/:id/
+ **/
+ .service('Instances', ["$resource", function ($resource) {
+ return $resource('/api/core/instances/:id/', { id: '@id' }, {
+ update: { method: 'PUT' }
+ });
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/Instances.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ angular.module('xos.helpers')
+ /**
+ * @ngdoc service
+ * @name xos.helpers.Example-Services-Collection
+ * @description Angular resource to fetch /api/service/exampleservice/
+ **/
+ .service('Example-Services-Collection', ["$resource", function ($resource) {
+ return $resource('/api/service/exampleservice/');
+ }]);
+})();
+//# sourceMappingURL=../../maps/services/rest/Example.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name xos.helpers.NoHyperlinks
+ * @description This factory is automatically loaded trough xos.helpers and will add an $http interceptor that will add ?no_hyperlinks=1 to your api request, that is required by django
+ **/
+
+ angular.module('xos.helpers').factory('NoHyperlinks', noHyperlinks);
+
+ function noHyperlinks() {
+ return {
+ request: function request(_request) {
+ if (_request.url.indexOf('.html') === -1) {
+ _request.url += '?no_hyperlinks=1';
+ }
+ return _request;
+ }
+ };
+ }
+})();
+//# sourceMappingURL=../maps/services/noHyperlinks.interceptor.js.map
+
+'use strict';
+
+// TODO write tests for log
+
+angular.module('xos.helpers').config(['$provide', function ($provide) {
+ // Use the `decorator` solution to substitute or attach behaviors to
+ // original service instance; @see angular-mocks for more examples....
+
+ $provide.decorator('$log', ['$delegate', function ($delegate) {
+
+ var isLogEnabled = function isLogEnabled() {
+ return window.location.href.indexOf('debug=true') >= 0;
+ };
+ // Save the original $log.debug()
+ var debugFn = $delegate.info;
+
+ // create the replacement function
+ var replacement = function replacement(fn) {
+ return function () {
+ if (!isLogEnabled()) {
+ console.log('logging is disabled');
+ return;
+ }
+ var args = [].slice.call(arguments);
+ var now = new Date();
+
+ // Prepend timestamp
+ args[0] = '[' + now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds() + '] ' + args[0];
+
+ // Call the original with the output prepended with formatted timestamp
+ fn.apply(null, args);
+ };
+ };
+
+ $delegate.info = replacement(debugFn);
+
+ return $delegate;
+ }]);
+}]);
+//# sourceMappingURL=../maps/services/log.decorator.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name xos.helpers.LabelFormatter
+ * @description This factory define a set of helper function to format label started from an object property
+ **/
+
+ angular.module('xos.uiComponents').factory('LabelFormatter', labelFormatter);
+
+ function labelFormatter() {
+
+ var _formatByUnderscore = function _formatByUnderscore(string) {
+ return string.split('_').join(' ').trim();
+ };
+
+ var _formatByUppercase = function _formatByUppercase(string) {
+ return string.split(/(?=[A-Z])/).map(function (w) {
+ return w.toLowerCase();
+ }).join(' ');
+ };
+
+ var _capitalize = function _capitalize(string) {
+ return string.slice(0, 1).toUpperCase() + string.slice(1);
+ };
+
+ var format = function format(string) {
+ string = _formatByUnderscore(string);
+ string = _formatByUppercase(string);
+
+ string = _capitalize(string).replace(/\s\s+/g, ' ') + ':';
+ return string.replace('::', ':');
+ };
+
+ return {
+ // test export
+ _formatByUnderscore: _formatByUnderscore,
+ _formatByUppercase: _formatByUppercase,
+ _capitalize: _capitalize,
+ // export to use
+ format: format
+ };
+ }
+})();
+//# sourceMappingURL=../maps/services/label_formatter.service.js.map
+
+'use strict';
+
+(function () {
+ 'use strict';
+
+ /**
+ * @ngdoc service
+ * @name xos.helpers.SetCSRFToken
+ * @description This factory is automatically loaded trough xos.helpers and will add an $http interceptor that will the CSRF-Token to your request headers
+ **/
+
+ setCSRFToken.$inject = ["$cookies"];
+ angular.module('xos.helpers').factory('SetCSRFToken', setCSRFToken);
+
+ function setCSRFToken($cookies) {
+ return {
+ request: function request(_request) {
+ if (_request.method !== 'GET') {
+ _request.headers['X-CSRFToken'] = $cookies.get('xoscsrftoken');
+ }
+ return _request;
+ }
+ };
+ }
+})();
+//# sourceMappingURL=../maps/services/csrfToken.interceptor.js.map
diff --git a/xos/core/xoslib/static/js/vendor/ngXosVendor.js b/xos/core/xoslib/static/js/vendor/ngXosVendor.js
index bf3a62d..23bcaae 100644
--- a/xos/core/xoslib/static/js/vendor/ngXosVendor.js
+++ b/xos/core/xoslib/static/js/vendor/ngXosVendor.js
@@ -65492,6 +65492,7 @@
root._ = _;
}
}.call(this));
+<<<<<<< HEAD
/**
* @license AngularJS v1.5.5
@@ -66215,3 +66216,5 @@
})(window, window.angular);
+=======
+>>>>>>> upstream/master
diff --git a/xos/core/xoslib/static/js/xosOpenVPNDashboard.js b/xos/core/xoslib/static/js/xosOpenVPNDashboard.js
new file mode 100644
index 0000000..b28322f
--- /dev/null
+++ b/xos/core/xoslib/static/js/xosOpenVPNDashboard.js
@@ -0,0 +1 @@
+"use strict";angular.module("xos.openVPNDashboard",["ngResource","ngCookies","ngLodash","ui.router","xos.helpers"]).config(["$stateProvider",function(n){n.state("openVPNList",{url:"/",template:"<vpn-list></vpn-list>"})}]).config(["$compileProvider",function(n){n.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|tel|file|blob):/)}]).service("Vpn",["$http","$q",function(n,e){this.getOpenVpnTenants=function(){var t=e.defer();return n.get("/api/tenant/openvpn/list/").then(function(n){t.resolve(n.data)})["catch"](function(n){t.reject(n)}),t.promise}}]).config(["$httpProvider",function(n){n.interceptors.push("NoHyperlinks")}]).directive("vpnList",function(){return{restrict:"E",scope:{},bindToController:!0,controllerAs:"vm",templateUrl:"templates/openvpn-list.tpl.html",controller:["Vpn",function(n){var e=this;n.getOpenVpnTenants().then(function(n){e.vpns=n;for(var t=0;t<e.vpns.length;t++){var i=new Blob([e.vpns[t].script_text],{type:"text/plain"});e.vpns[t].script_text=(window.URL||window.webkitURL).createObjectURL(i)}})["catch"](function(n){throw new Error(n)})}]}}),angular.module("xos.openVPNDashboard").run(["$templateCache",function(n){n.put("templates/openvpn-list.tpl.html",'<div style="display: table;">\n <div class="vpn-row">\n <h1 class="vpn-cell">VPN List</h1>\n </div>\n <div class="vpn-row">\n <div class="vpn-cell vpn-header">ID</div>\n <div class="vpn-cell vpn-header">VPN Network</div>\n <div class="vpn-cell vpn-header">VPN Subnet</div>\n <div class="vpn-cell vpn-header">Script Link</div>\n </div>\n <div class="vpn-row" ng-repeat="vpn in vm.vpns">\n <div class="vpn-cell">{{ vpn.id }}</div>\n <div class="vpn-cell">{{ vpn.server_network }}</div>\n <div class="vpn-cell">{{ vpn.vpn_subnet }}</div>\n <div class="vpn-cell">\n <a download="connect-{{ vpn.id }}.vpn" ng-href="{{ vpn.script_text }}">Script</a>\n </div>\n </div>\n</div>\n')}]),angular.module("xos.openVPNDashboard").run(["$location",function(n){n.path("/")}]),angular.bootstrap(angular.element("#xosOpenVPNDashboard"),["xos.openVPNDashboard"]);
\ No newline at end of file
diff --git a/xos/services/fabric/__init__.py b/xos/services/fabric/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/xos/services/fabric/__init__.py
@@ -0,0 +1 @@
+
diff --git a/xos/services/fabric/admin.py b/xos/services/fabric/admin.py
new file mode 100644
index 0000000..5dc5923
--- /dev/null
+++ b/xos/services/fabric/admin.py
@@ -0,0 +1,63 @@
+from django.contrib import admin
+
+from services.fabric.models import *
+from django import forms
+from django.utils.safestring import mark_safe
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.admin.widgets import FilteredSelectMultiple
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
+from django.contrib.auth.signals import user_logged_in
+from django.utils import timezone
+from django.contrib.contenttypes import generic
+from suit.widgets import LinkedSelect
+from core.models import AddressPool
+from core.admin import ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline, ServicePrivilegeInline, TenantRootTenantInline, TenantRootPrivilegeInline
+from core.middleware import get_request
+
+from functools import update_wrapper
+from django.contrib.admin.views.main import ChangeList
+from django.core.urlresolvers import reverse
+from django.contrib.admin.utils import quote
+
+class FabricServiceForm(forms.ModelForm):
+ def __init__(self,*args,**kwargs):
+ super (FabricServiceForm,self ).__init__(*args,**kwargs)
+
+ def save(self, commit=True):
+ return super(FabricServiceForm, self).save(commit=commit)
+
+ class Meta:
+ model = FabricService
+
+class FabricServiceAdmin(ReadOnlyAwareAdmin):
+ model = FabricService
+ verbose_name = "Fabric Service"
+ verbose_name_plural = "Fabric Services"
+ list_display = ("backend_status_icon", "name", "enabled")
+ list_display_links = ('backend_status_icon', 'name', )
+ fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description', "view_url", "icon_url", ],
+ 'classes':['suit-tab suit-tab-general']})]
+ readonly_fields = ('backend_status_text', )
+ inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
+ form = FabricServiceForm
+
+ extracontext_registered_admins = True
+
+ user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
+
+ suit_form_tabs =(('general', 'Fabric Service Details'),
+ ('administration', 'Administration'),
+ #('tools', 'Tools'),
+ ('slices','Slices'),
+ ('serviceattrs','Additional Attributes'),
+ ('serviceprivileges','Privileges'),
+ )
+
+ suit_form_includes = (('fabricadmin.html', 'top', 'administration'),
+ )
+
+ def queryset(self, request):
+ return FabricService.get_service_objects_by_user(request.user)
+
+admin.site.register(FabricService, FabricServiceAdmin)
+
diff --git a/xos/services/fabric/models.py b/xos/services/fabric/models.py
new file mode 100644
index 0000000..0bf6a14
--- /dev/null
+++ b/xos/services/fabric/models.py
@@ -0,0 +1,30 @@
+from django.db import models
+from core.models import Service, PlCoreBase, Slice, Instance, Tenant, TenantWithContainer, Node, Image, User, Flavor, Subscriber, NetworkParameter, NetworkParameterType, Port, AddressPool
+from core.models.plcorebase import StrippedCharField
+import os
+from django.db import models, transaction
+from django.forms.models import model_to_dict
+from django.db.models import Q
+from operator import itemgetter, attrgetter, methodcaller
+from core.models import Tag
+from core.models.service import LeastLoadedNodeScheduler
+import traceback
+from xos.exceptions import *
+from xos.config import Config
+
+class ConfigurationError(Exception):
+ pass
+
+FABRIC_KIND = "fabric"
+
+CORD_USE_VTN = getattr(Config(), "networking_use_vtn", False)
+
+class FabricService(Service):
+ KIND = FABRIC_KIND
+
+ class Meta:
+ app_label = "fabric"
+ verbose_name = "Fabric Service"
+ proxy = True
+
+
diff --git a/xos/services/helloworld/models.py b/xos/services/helloworld/models.py
deleted file mode 100644
index 9bb343e..0000000
--- a/xos/services/helloworld/models.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from django.db import models
-from core.models import User, Service, SingletonModel, PlCoreBase, Instance
-from core.models.plcorebase import StrippedCharField
-import os
-from django.db import models
-from django.forms.models import model_to_dict
-from django.db.models import Q
-
-
-# Create your models here.
-
-class Hello(PlCoreBase):
- name = models.CharField(max_length=254,help_text="Salutation e.g. Hello or Bonjour")
- instance_backref = models.ForeignKey(Instance,related_name="hellos")
-
-class World(PlCoreBase):
- name = models.CharField(max_length=254,help_text="Name of planet")
- hello = models.ForeignKey(Hello)
diff --git a/xos/services/helloworld/view.py b/xos/services/helloworld/view.py
deleted file mode 100644
index 6ad9ae1..0000000
--- a/xos/services/helloworld/view.py
+++ /dev/null
@@ -1,57 +0,0 @@
-from django.http import HttpResponse
-from django.views.generic import TemplateView, View
-from django import template
-from core.models import *
-from services.helloworld.models import *
-import json
-import os
-import time
-import tempfile
-
-class HelloWorldView(TemplateView):
- head_template = r"""{% extends "admin/dashboard/dashboard_base.html" %}
- {% load admin_static %}
- {% block content %}
- """
-
- tail_template = r"{% endblock %}"
-
- def get(self, request, name="root", *args, **kwargs):
- head_template = self.head_template
- tail_template = self.tail_template
-
- try:
- hello_name = request.GET['hello_name']
- world_name = request.GET['world_name']
- instance_id_str = request.GET['instance_id']
- instance_id = int(instance_id_str)
-
- i = Instance.objects.get(pk=instance_id)
- i.pk=None
- i.userData=None
- i.instance_id=None
- i.instance_name=None
- i.enacted=None
- i.save()
- h = Hello(name=hello_name,instance_backref=i)
- h.save()
- w = World(hello=h,name=world_name)
- w.save()
-
- t = template.Template(head_template + 'Done. New instance id: %r'%i.pk + self.tail_template)
- except KeyError:
- html = """<form>
- Hello string: <input type="text" name="hello_name" placeholder="Planet"><br>
- World string: <input type="text" name="world_name" placeholder="Earth"><br>
- Id of instance to copy: <input type="text" name="instance_id" placeholder="3"><br>
- <input type="submit" value="Submit">
- </form>"""
-
- t = template.Template(head_template + html + self.tail_template)
-
- response_kwargs = {}
- response_kwargs.setdefault('content_type', self.content_type)
- return self.response_class(
- request = request,
- template = t,
- **response_kwargs)
diff --git a/xos/services/helloworldservice_complete/__init__.py b/xos/services/helloworldservice_complete/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/xos/services/helloworldservice_complete/__init__.py
+++ /dev/null
diff --git a/xos/services/helloworldservice_complete/admin.py b/xos/services/helloworldservice_complete/admin.py
deleted file mode 100644
index 3c8e793..0000000
--- a/xos/services/helloworldservice_complete/admin.py
+++ /dev/null
@@ -1,141 +0,0 @@
-
-from core.admin import ReadOnlyAwareAdmin, SliceInline
-from core.middleware import get_request
-from core.models import User
-from django import forms
-from django.contrib import admin
-from services.helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete, HELLO_WORLD_KIND
-
-# The class to provide an admin interface on the web for the service.
-# We do only configuration here and don't change any logic because the logic
-# is taken care of for us by ReadOnlyAwareAdmin
-class HelloWorldServiceCompleteAdmin(ReadOnlyAwareAdmin):
- # We must set the model so that the admin knows what fields to use
- model = HelloWorldServiceComplete
- verbose_name = "Hello World Service"
- verbose_name_plural = "Hello World Services"
-
- # Setting list_display creates columns on the admin page, each value here
- # is a column, the column is populated for every instance of the model.
- list_display = ("backend_status_icon", "name", "enabled")
-
- # Used to indicate which values in the columns of the admin form are links.
- list_display_links = ('backend_status_icon', 'name', )
-
- # Denotes the sections of the form, the fields in the section, and the
- # CSS classes used to style them. We represent this as a set of tuples, each
- # tuple as a name (or None) and a set of fields and classes.
- # Here the first section does not have a name so we use none. That first
- # section has several fields indicated in the 'fields' attribute, and styled
- # by the classes indicated in the 'classes' attribute. The classes given
- # here are important for rendering the tabs on the form. To give the tabs
- # we must assign the classes suit-tab and suit-tab-<name> where
- # where <name> will be used later.
- fieldsets = [(None, {'fields': ['backend_status_text', 'name', 'enabled',
- 'versionNumber', 'description', "view_url"],
- 'classes':['suit-tab suit-tab-general']})]
-
- # Denotes the fields that are readonly and cannot be changed.
- readonly_fields = ('backend_status_text', )
-
- # Inlines are used to denote other models that can be edited on the same
- # form as this one. In this case the service form also allows changes
- # to slices.
- inlines = [SliceInline]
-
- extracontext_registered_admins = True
-
- # Denotes the fields that can be changed by an admin but not be all users
- user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
-
- # Associates fieldsets from this form and from the inlines.
- # The format here are tuples, of (<name>, tab title). <name> comes from the
- # <name> in the fieldsets.
- suit_form_tabs = (('general', 'Hello World Service Details'),
- ('administration', 'Tenants'),
- ('slices', 'Slices'),)
-
- # Used to include a template for a tab. Here we include the
- # helloworldserviceadmin template in the top position for the administration
- # tab.
- suit_form_includes = (('helloworldserviceadmin.html',
- 'top',
- 'administration'),)
-
- # Used to get the objects for this model that are associated with the
- # requesting user.
- def queryset(self, request):
- return HelloWorldServiceComplete.get_service_objects_by_user(request.user)
-
-# Class to represent the form to add and edit tenants.
-# We need to define this instead of just using an admin like we did for the
-# service because tenants vary more than services and there isn't a common form.
-# This allows us to change the python behavior for the admin form to save extra
-# fields and control defaults.
-class HelloWorldTenantCompleteForm(forms.ModelForm):
- # Defines a field for the creator of this service. It is a dropdown which
- # is populated with all of the users.
- creator = forms.ModelChoiceField(queryset=User.objects.all())
- # Defines a text field for the display message, it is not required.
- display_message = forms.CharField(required=False)
-
- def __init__(self, *args, **kwargs):
- super(HelloWorldTenantCompleteForm, self).__init__(*args, **kwargs)
- # Set the kind field to readonly
- self.fields['kind'].widget.attrs['readonly'] = True
- # Define the logic for obtaining the objects for the provider_service
- # dropdown of the tenant form.
- self.fields[
- 'provider_service'].queryset = HelloWorldServiceComplete.get_service_objects().all()
- # Set the initial kind to HELLO_WORLD_KIND for this tenant.
- self.fields['kind'].initial = HELLO_WORLD_KIND
- # If there is an instance of this model then we can set the initial
- # form values to the existing values.
- if self.instance:
- self.fields['creator'].initial = self.instance.creator
- self.fields[
- 'display_message'].initial = self.instance.display_message
-
- # If there is not an instance then we need to set initial values.
- if (not self.instance) or (not self.instance.pk):
- self.fields['creator'].initial = get_request().user
- if HelloWorldServiceComplete.get_service_objects().exists():
- self.fields["provider_service"].initial = HelloWorldServiceComplete.get_service_objects().all()[0]
-
- # This function describes what happens when the save button is pressed on
- # the tenant form. In this case we set the values for the instance that were
- # entered.
- def save(self, commit=True):
- self.instance.creator = self.cleaned_data.get("creator")
- self.instance.display_message = self.cleaned_data.get(
- "display_message")
- return super(HelloWorldTenantCompleteForm, self).save(commit=commit)
-
- class Meta:
- model = HelloWorldTenantComplete
-
-# Define the admin form for the tenant. This uses a similar structure as the
-# service but uses HelloWorldTenantCompleteForm to change the python behavior.
-
-
-class HelloWorldTenantCompleteAdmin(ReadOnlyAwareAdmin):
- verbose_name = "Hello World Tenant"
- verbose_name_plural = "Hello World Tenants"
- list_display = ('id', 'backend_status_icon', 'instance', 'display_message')
- list_display_links = ('backend_status_icon', 'instance', 'display_message',
- 'id')
- fieldsets = [(None, {'fields': ['backend_status_text', 'kind',
- 'provider_service', 'instance', 'creator',
- 'display_message'],
- 'classes': ['suit-tab suit-tab-general']})]
- readonly_fields = ('backend_status_text', 'instance',)
- form = HelloWorldTenantCompleteForm
-
- suit_form_tabs = (('general', 'Details'),)
-
- def queryset(self, request):
- return HelloWorldTenantComplete.get_tenant_objects_by_user(request.user)
-
-# Associate the admin forms with the models.
-admin.site.register(HelloWorldServiceComplete, HelloWorldServiceCompleteAdmin)
-admin.site.register(HelloWorldTenantComplete, HelloWorldTenantCompleteAdmin)
diff --git a/xos/services/helloworldservice_complete/models.py b/xos/services/helloworldservice_complete/models.py
deleted file mode 100644
index 8a4ce59..0000000
--- a/xos/services/helloworldservice_complete/models.py
+++ /dev/null
@@ -1,113 +0,0 @@
-from core.models import Service, TenantWithContainer
-from django.db import transaction
-
-HELLO_WORLD_KIND = "helloworldservice_complete"
-
-# The class to represent the service. Most of the service logic is given for us
-# in the Service class but, we have some configuration that is specific for
-# this example.
-class HelloWorldServiceComplete(Service):
- KIND = HELLO_WORLD_KIND
-
- class Meta:
- # When the proxy field is set to True the model is represented as
- # it's superclass in the database, but we can still change the python
- # behavior. In this case HelloWorldServiceComplete is a Service in the
- # database.
- proxy = True
- # The name used to find this service, all directories are named this
- app_label = "helloworldservice_complete"
- verbose_name = "Hello World Service"
-
-# This is the class to represent the tenant. Most of the logic is given to use
-# in TenantWithContainer, however there is some configuration and logic that
-# we need to define for this example.
-class HelloWorldTenantComplete(TenantWithContainer):
-
- class Meta:
- # Same as a above, HelloWorldTenantComplete is represented as a
- # TenantWithContainer, but we change the python behavior.
- proxy = True
- verbose_name = "Hello World Tenant"
-
- # The kind of the service is used on forms to differentiate this service
- # from the other services.
- KIND = HELLO_WORLD_KIND
-
- # Ansible requires that the sync_attributes field contain nat_ip and nat_mac
- # these will be used to determine where to SSH to for ansible.
- # Getters must be defined for every attribute specified here.
- sync_attributes = ("nat_ip", "nat_mac",)
-
- # default_attributes is used cleanly indicate what the default values for
- # the fields are.
- default_attributes = {'display_message': 'Hello World!'}
-
- def __init__(self, *args, **kwargs):
- helloworld_services = HelloWorldServiceComplete.get_service_objects().all()
- # When the tenant is created the default service in the form is set
- # to be the first created HelloWorldServiceComplete
- if helloworld_services:
- self._meta.get_field(
- "provider_service").default = helloworld_services[0].id
- super(HelloWorldTenantComplete, self).__init__(*args, **kwargs)
-
- def save(self, *args, **kwargs):
- super(HelloWorldTenantComplete, self).save(*args, **kwargs)
- # This call needs to happen so that an instance is created for this
- # tenant is created in the slice. One instance is created per tenant.
- model_policy_helloworld_tenant(self.pk)
-
- def delete(self, *args, **kwargs):
- # Delete the instance that was created for this tenant
- self.cleanup_container()
- super(HelloWorldTenantComplete, self).delete(*args, **kwargs)
-
- # Getter for the message that will appear on the webpage
- # By default it is "Hello World!"
- @property
- def display_message(self):
- return self.get_attribute(
- "display_message",
- self.default_attributes['display_message'])
-
- # Setter for the message that will appear on the webpage
- @display_message.setter
- def display_message(self, value):
- self.set_attribute("display_message", value)
-
- @property
- def addresses(self):
- if (not self.id) or (not self.instance):
- return {}
-
- addresses = {}
- # The ports field refers to networks for the instance.
- # This loop stores the details for the NAT network that will be
- # necessary for ansible.
- for ns in self.instance.ports.all():
- if "nat" in ns.network.name.lower():
- addresses["nat"] = (ns.ip, ns.mac)
- return addresses
-
- # This getter is necessary because nat_ip is a sync_attribute
- @property
- def nat_ip(self):
- return self.addresses.get("nat", (None, None))[0]
-
- # This getter is necessary because nat_mac is a sync_attribute
- @property
- def nat_mac(self):
- return self.addresses.get("nat", (None, None))[1]
-
-
-def model_policy_helloworld_tenant(pk):
- # This section of code is atomic to prevent race conditions
- with transaction.atomic():
- # We find all of the tenants that are waiting to update
- tenant = HelloWorldTenantComplete.objects.select_for_update().filter(pk=pk)
- if not tenant:
- return
- # Since this code is atomic it is safe to always use the first tenant
- tenant = tenant[0]
- tenant.manage_container()
diff --git a/xos/services/helloworldservice_complete/templates/helloworldserviceadmin.html b/xos/services/helloworldservice_complete/templates/helloworldserviceadmin.html
deleted file mode 100644
index ba418ee..0000000
--- a/xos/services/helloworldservice_complete/templates/helloworldserviceadmin.html
+++ /dev/null
@@ -1,10 +0,0 @@
-<!-- Template used to for the button leading to the HelloWorldTenantComplete form. -->
-<div class = "left-nav">
- <ul>
- <li>
- <a href="/admin/helloworldservice_complete/helloworldtenantcomplete/">
- Hello World Tenants
- </a>
- </li>
- </ul>
-</div>
diff --git a/xos/services/helloworld/__init__.py b/xos/services/openvpn/__init__.py
similarity index 100%
rename from xos/services/helloworld/__init__.py
rename to xos/services/openvpn/__init__.py
diff --git a/xos/services/openvpn/admin.py b/xos/services/openvpn/admin.py
new file mode 100644
index 0000000..28e778d
--- /dev/null
+++ b/xos/services/openvpn/admin.py
@@ -0,0 +1,229 @@
+from django import forms
+from django.contrib import admin
+
+from core.admin import ReadOnlyAwareAdmin, SliceInline, TenantPrivilegeInline
+from core.middleware import get_request
+from core.models import User
+from services.openvpn.models import OPENVPN_KIND, OpenVPNService, OpenVPNTenant
+from xos.exceptions import XOSValidationError
+
+
+class OpenVPNServiceForm(forms.ModelForm):
+
+ exposed_ports = forms.CharField(required=True)
+
+ def __init__(self, *args, **kwargs):
+ super(OpenVPNServiceForm, self).__init__(*args, **kwargs)
+
+ if self.instance:
+ self.fields['exposed_ports'].initial = (
+ self.instance.exposed_ports_str)
+
+ def save(self, commit=True):
+ self.instance.exposed_ports = self.cleaned_data['exposed_ports']
+ return super(OpenVPNServiceForm, self).save(commit=commit)
+
+ def clean_exposed_ports(self):
+ exposed_ports = self.cleaned_data['exposed_ports']
+ self.instance.exposed_ports_str = exposed_ports
+ port_mapping = {"udp": [], "tcp": []}
+ parts = exposed_ports.split(",")
+ for part in parts:
+ part = part.strip()
+ if "/" in part:
+ (protocol, ports) = part.split("/", 1)
+ elif " " in part:
+ (protocol, ports) = part.split(None, 1)
+ else:
+ raise XOSValidationError(
+ 'malformed port specifier %s, format example: ' +
+ '"tcp 123, tcp 201:206, udp 333"' % part)
+
+ protocol = protocol.strip()
+ ports = ports.strip()
+
+ if not (protocol in ["udp", "tcp"]):
+ raise XOSValidationError('unknown protocol %s' % protocol)
+
+ if "-" in ports:
+ port_mapping[protocol].extend(
+ self.parse_port_range(ports, "-"))
+ elif ":" in ports:
+ port_mapping[protocol].extend(
+ self.parse_port_range(ports, ":"))
+ else:
+ port_mapping[protocol].append(int(ports))
+
+ return port_mapping
+
+ def parse_port_range(self, port_str, split_str):
+ (first, last) = port_str.split(split_str)
+ first = int(first.strip())
+ last = int(last.strip())
+ return list(range(first, last))
+
+ class Meta:
+ model = OpenVPNService
+
+
+class OpenVPNServiceAdmin(ReadOnlyAwareAdmin):
+ """Defines the admin for the OpenVPNService."""
+ model = OpenVPNService
+ form = OpenVPNServiceForm
+ verbose_name = "OpenVPN Service"
+
+ list_display = ("backend_status_icon", "name", "enabled")
+
+ list_display_links = ('backend_status_icon', 'name', )
+
+ fieldsets = [(None, {'fields': ['backend_status_text', 'name', 'enabled',
+ 'versionNumber', 'description', "view_url",
+ 'exposed_ports'],
+ 'classes':['suit-tab suit-tab-general']})]
+
+ readonly_fields = ('backend_status_text', )
+
+ inlines = [SliceInline]
+
+ extracontext_registered_admins = True
+
+ user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
+
+ suit_form_tabs = (('general', 'VPN Service Details'),
+ ('slices', 'Slices'),)
+
+ def queryset(self, request):
+ return OpenVPNService.get_service_objects_by_user(request.user)
+
+
+class OpenVPNTenantForm(forms.ModelForm):
+ """The form used to create and edit a OpenVPNTenant.
+
+ Attributes:
+ creator (forms.ModelChoiceField): The XOS user that created this
+ tenant.
+ 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 OpenVPNTenants to use as failover
+ servers.
+ protocol (forms.ChoiceField): The protocol to use.
+ use_ca_from (forms.ModelChoiceField): Another OpenVPNTenant 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(
+ protocol="IPv4", required=True)
+ vpn_subnet = forms.GenericIPAddressField(protocol="IPv4", required=True)
+ is_persistent = forms.BooleanField(required=False)
+ clients_can_see_each_other = forms.BooleanField(required=False)
+ failover_servers = forms.ModelMultipleChoiceField(
+ required=False, queryset=OpenVPNTenant.get_tenant_objects())
+ protocol = forms.ChoiceField(required=True, choices=[
+ ("tcp", "tcp"), ("udp", "udp")])
+ use_ca_from = forms.ModelChoiceField(
+ queryset=OpenVPNTenant.get_tenant_objects(), required=False)
+
+ def __init__(self, *args, **kwargs):
+ super(OpenVPNTenantForm, self).__init__(*args, **kwargs)
+ self.fields['kind'].widget.attrs['readonly'] = True
+ self.fields['failover_servers'].widget.attrs['rows'] = 300
+ self.fields[
+ 'provider_service'].queryset = (
+ OpenVPNService.get_service_objects().all())
+
+ self.fields['kind'].initial = OPENVPN_KIND
+
+ if self.instance:
+ self.fields['creator'].initial = self.instance.creator
+ self.fields['vpn_subnet'].initial = self.instance.vpn_subnet
+ self.fields[
+ 'server_network'].initial = self.instance.server_network
+ self.fields[
+ 'clients_can_see_each_other'].initial = (
+ self.instance.clients_can_see_each_other)
+ self.fields['is_persistent'].initial = self.instance.is_persistent
+ self.initial['protocol'] = self.instance.protocol
+ self.fields['failover_servers'].queryset = (
+ OpenVPNTenant.get_tenant_objects().exclude(pk=self.instance.pk))
+ self.initial['failover_servers'] = OpenVPNTenant.get_tenant_objects().filter(
+ pk__in=self.instance.failover_server_ids)
+ self.fields['use_ca_from'].queryset = (
+ OpenVPNTenant.get_tenant_objects().exclude(pk=self.instance.pk))
+ if (self.instance.use_ca_from_id):
+ self.initial['use_ca_from'] = (
+ OpenVPNTenant.get_tenant_objects().filter(pk=self.instance.use_ca_from_id)[0])
+
+ if (not self.instance) or (not self.instance.pk):
+ self.fields['creator'].initial = get_request().user
+ self.fields['vpn_subnet'].initial = "255.255.255.0"
+ self.fields['server_network'].initial = "10.66.77.0"
+ self.fields['clients_can_see_each_other'].initial = True
+ self.fields['is_persistent'].initial = True
+ self.fields['failover_servers'].queryset = (
+ OpenVPNTenant.get_tenant_objects())
+ if OpenVPNService.get_service_objects().exists():
+ self.fields["provider_service"].initial = (
+ OpenVPNService.get_service_objects().all()[0])
+
+ def save(self, commit=True):
+ self.instance.creator = self.cleaned_data.get("creator")
+ self.instance.is_persistent = self.cleaned_data.get('is_persistent')
+ self.instance.vpn_subnet = self.cleaned_data.get("vpn_subnet")
+ self.instance.server_network = self.cleaned_data.get('server_network')
+ self.instance.clients_can_see_each_other = self.cleaned_data.get(
+ 'clients_can_see_each_other')
+
+ self.instance.failover_server_ids = [
+ tenant.id for tenant in self.cleaned_data.get('failover_servers')]
+
+ # Do not aquire a new port number if the protocol hasn't changed
+ if ((not self.instance.protocol) or
+ (self.instance.protocol != self.cleaned_data.get("protocol"))):
+ self.instance.protocol = self.cleaned_data.get("protocol")
+ self.instance.port_number = (
+ self.instance.provider_service.get_next_available_port(
+ self.instance.protocol))
+
+ if (self.cleaned_data.get('use_ca_from')):
+ self.instance.use_ca_from_id = self.cleaned_data.get(
+ 'use_ca_from').id
+ else:
+ self.instance.use_ca_from_id = None
+
+ return super(OpenVPNTenantForm, self).save(commit=commit)
+
+ class Meta:
+ model = OpenVPNTenant
+
+
+class OpenVPNTenantAdmin(ReadOnlyAwareAdmin):
+ verbose_name = "OpenVPN Tenant Admin"
+ list_display = ('id', 'backend_status_icon', 'instance',
+ 'server_network', 'vpn_subnet')
+ list_display_links = ('id', 'backend_status_icon',
+ 'instance', 'server_network', 'vpn_subnet')
+ fieldsets = [(None, {'fields': ['backend_status_text', 'kind',
+ 'provider_service', 'instance', 'creator',
+ 'server_network', 'vpn_subnet',
+ 'is_persistent', 'use_ca_from',
+ 'clients_can_see_each_other',
+ 'failover_servers', "protocol"],
+ 'classes': ['suit-tab suit-tab-general']})]
+ readonly_fields = ('backend_status_text', 'instance')
+ form = OpenVPNTenantForm
+ inlines = [TenantPrivilegeInline]
+
+ suit_form_tabs = (('general', 'Details'),
+ ('tenantprivileges', 'Privileges'))
+
+ def queryset(self, request):
+ return OpenVPNTenant.get_tenant_objects_by_user(request.user)
+
+
+# Associate the admin forms with the models.
+admin.site.register(OpenVPNService, OpenVPNServiceAdmin)
+admin.site.register(OpenVPNTenant, OpenVPNTenantAdmin)
diff --git a/xos/services/openvpn/models.py b/xos/services/openvpn/models.py
new file mode 100644
index 0000000..8aaa825
--- /dev/null
+++ b/xos/services/openvpn/models.py
@@ -0,0 +1,316 @@
+from subprocess import PIPE, Popen
+
+from django.db import transaction
+
+from core.models import Service, TenantWithContainer
+from xos.exceptions import XOSConfigurationError, XOSValidationError
+
+OPENVPN_KIND = "openvpn"
+
+
+class OpenVPNService(Service):
+ """Defines the Service for creating VPN servers."""
+ KIND = OPENVPN_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"
+ """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 = (
+ OpenVPNService.EASYRSA_COMMAND_PREFIX + " --pki-dir=" +
+ pki_dir + " " + command)
+ proc = Popen(
+ full_command, shell=True, stdout=PIPE, stderr=PIPE
+ )
+ (stdout, stderr) = proc.communicate()
+ if (proc.returncode != 0):
+ raise XOSConfigurationError(
+ full_command + " failed with standard out:" + str(stdout) +
+ " and stderr: " + str(stderr))
+
+ @classmethod
+ def get_pki_dir(cls, tenant):
+ """Gets the directory of the PKI for the given tenant.
+
+ Parameters:
+ tenant (services.openvpn.models.OpenVPNTenant): The tenant to get the PKI directory for.
+
+ Returns:
+ str: The pki directory for the tenant.
+ """
+ return OpenVPNService.SERVER_PREFIX + str(tenant.id)
+
+ class Meta:
+ proxy = True
+ # The name used to find this service, all directories are named this
+ app_label = "openvpn"
+ verbose_name = "OpenVPN Service"
+
+ default_attributes = {'exposed_ports': None,
+ 'exposed_ports_str': None}
+
+ @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"])
+
+ @exposed_ports.setter
+ def exposed_ports(self, value):
+ self.set_attribute("exposed_ports", value)
+
+ @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"])
+
+ @exposed_ports_str.setter
+ def exposed_ports_str(self, value):
+ 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]:
+ raise XOSValidationError(
+ "No availble ports for protocol: " + protocol)
+ tenants = [
+ tenant for tenant in OpenVPNTenant.get_tenant_objects().all()
+ if tenant.protocol == protocol]
+ port_numbers = self.exposed_ports[protocol]
+ for port_number in port_numbers:
+ if (
+ len([
+ tenant for tenant in tenants
+ if tenant.port_number == port_number]) == 0):
+ return port_number
+
+
+class OpenVPNTenant(TenantWithContainer):
+ """Defines the Tenant for creating VPN servers."""
+
+ class Meta:
+ proxy = True
+ verbose_name = "OpenVPN Tenant"
+
+ KIND = OPENVPN_KIND
+
+ sync_attributes = ("nat_ip", "nat_mac",)
+
+ default_attributes = {'vpn_subnet': None,
+ 'server_network': None,
+ 'clients_can_see_each_other': True,
+ 'is_persistent': True,
+ 'port': None,
+ 'use_ca_from_id': None,
+ 'failover_server_ids': list(),
+ 'protocol': None}
+
+ def __init__(self, *args, **kwargs):
+ vpn_services = OpenVPNService.get_service_objects().all()
+ if vpn_services:
+ self._meta.get_field(
+ "provider_service").default = vpn_services[0].id
+ super(OpenVPNTenant, self).__init__(*args, **kwargs)
+
+ def save(self, *args, **kwargs):
+ super(OpenVPNTenant, self).save(*args, **kwargs)
+ model_policy_vpn_tenant(self.pk)
+
+ def delete(self, *args, **kwargs):
+ self.cleanup_container()
+ super(OpenVPNTenant, self).delete(*args, **kwargs)
+
+ @property
+ def protocol(self):
+ """str: The protocol that this tenant is listening on."""
+ return self.get_attribute(
+ "protocol", self.default_attributes["protocol"])
+
+ @protocol.setter
+ def protocol(self, value):
+ self.set_attribute("protocol", value)
+
+ @property
+ def use_ca_from_id(self):
+ """int: The ID of OpenVPNTenant to use to obtain a CA."""
+ return self.get_attribute(
+ "use_ca_from_id", self.default_attributes["use_ca_from_id"])
+
+ @use_ca_from_id.setter
+ def use_ca_from_id(self, value):
+ self.set_attribute("use_ca_from_id", value)
+
+ @property
+ def addresses(self):
+ """Mapping[str, str]: The ip, mac address, and subnet of the NAT
+ network of this Tenant."""
+ if (not self.id) or (not self.instance):
+ return {}
+
+ addresses = {}
+ for ns in self.instance.ports.all():
+ if "nat" in ns.network.name.lower():
+ addresses["ip"] = ns.ip
+ addresses["mac"] = ns.mac
+ break
+
+ return addresses
+
+ # This getter is necessary because nat_ip is a sync_attribute
+ @property
+ def nat_ip(self):
+ """str: The IP of this Tenant on the NAT network."""
+ return self.addresses.get("ip", None)
+
+ # This getter is necessary because nat_mac is a sync_attribute
+ @property
+ def nat_mac(self):
+ """str: The MAC address of this Tenant on the NAT network."""
+ return self.addresses.get("mac", None)
+
+ @property
+ def server_network(self):
+ """str: The IP address of the server on the VPN."""
+ return self.get_attribute(
+ 'server_network',
+ self.default_attributes['server_network'])
+
+ @server_network.setter
+ def server_network(self, value):
+ self.set_attribute("server_network", value)
+
+ @property
+ def vpn_subnet(self):
+ """str: The IP address of the client on the VPN."""
+ return self.get_attribute(
+ 'vpn_subnet',
+ self.default_attributes['vpn_subnet'])
+
+ @vpn_subnet.setter
+ def vpn_subnet(self, value):
+ self.set_attribute("vpn_subnet", value)
+
+ @property
+ def is_persistent(self):
+ """bool: True if the VPN connection is persistence, false otherwise."""
+ return self.get_attribute(
+ "is_persistent",
+ self.default_attributes['is_persistent'])
+
+ @is_persistent.setter
+ def is_persistent(self, value):
+ self.set_attribute("is_persistent", value)
+
+ @property
+ def failover_server_ids(self):
+ """list(int): The IDs of the OpenVPNTenants to use as failover servers."""
+ return self.get_attribute(
+ "failover_server_ids", self.default_attributes["failover_server_ids"])
+
+ @failover_server_ids.setter
+ def failover_server_ids(self, value):
+ self.set_attribute("failover_server_ids", value)
+
+ @property
+ def clients_can_see_each_other(self):
+ """bool: True if the client can see the subnet of the server, false
+ otherwise."""
+ return self.get_attribute(
+ "clients_can_see_each_other",
+ self.default_attributes['clients_can_see_each_other'])
+
+ @clients_can_see_each_other.setter
+ def clients_can_see_each_other(self, value):
+ self.set_attribute("clients_can_see_each_other", value)
+
+ @property
+ def port_number(self):
+ """int: the integer representing the port number for this server"""
+ return self.get_attribute("port", self.default_attributes['port'])
+
+ @port_number.setter
+ def port_number(self, value):
+ self.set_attribute("port", value)
+
+ def get_ca_crt(self, pki_dir):
+ """Gets the lines fo the ca.crt file for this OpenVPNTenant.
+
+ Parameters:
+ pki_dir (str): The PKI directory to look in.
+
+ Returns:
+ list(str): The lines of the ca.crt file for this OpenVPNTenant.
+ """
+ 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 model_policy_vpn_tenant(pk):
+ """Manages the container for the VPN Tenant.
+
+ Parameters
+ pk (int): The ID of this OpenVPNTenant.
+ """
+ # This section of code is atomic to prevent race conditions
+ with transaction.atomic():
+ # We find all of the tenants that are waiting to update
+ tenant = OpenVPNTenant.objects.select_for_update().filter(pk=pk)
+ if not tenant:
+ return
+ # Since this code is atomic it is safe to always use the first tenant
+ tenant = tenant[0]
+ tenant.manage_container()
diff --git a/xos/services/openvpn/templates/connect.vpn.j2 b/xos/services/openvpn/templates/connect.vpn.j2
new file mode 100644
index 0000000..2028cd9
--- /dev/null
+++ b/xos/services/openvpn/templates/connect.vpn.j2
@@ -0,0 +1,24 @@
+#! /bin/bash
+# This file autogenerated by OpenVPNTenant.
+# It contains a script used to generate the OPENVPN client files.
+printf "%b" "client
+dev tun
+remote-cert-tls server
+resolv-retry 60
+nobind
+ca ca.crt
+cert {{ client_name }}.crt
+key {{ client_name }}.key
+verb 3
+{% for tenant in remotes %}remote {{ tenant.nat_ip }} {{ tenant.port_number }} {{ tenant.protocol }}{% endfor %}
+{% if is_persistent %}
+persist-tun
+persist-key
+{% endif %}
+" > client.conf
+printf "%b" "{% for line in ca_crt %}{{ line }}{% endfor %}" > ca.crt
+printf "%b" "{% for line in client_crt %}{{ line }}{% endfor %}" > {{ client_name }}.crt
+printf "%b" "{% for line in client_key %}{{ line }}{% endfor %}" > {{ client_name }}.key
+apt-get update
+apt-get install openvpn -y
+openvpn client.conf
diff --git a/xos/services/vrouter/admin.py b/xos/services/vrouter/admin.py
index 318b3dc..4bd99b6 100644
--- a/xos/services/vrouter/admin.py
+++ b/xos/services/vrouter/admin.py
@@ -11,7 +11,7 @@
from django.contrib.contenttypes import generic
from suit.widgets import LinkedSelect
from core.models import AddressPool
-from core.admin import ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline, ServicePrivilegeInline, TenantRootTenantInline, TenantRootPrivilegeInline
+from core.admin import ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline, ServicePrivilegeInline, AddressPoolInline
from core.middleware import get_request
from functools import update_wrapper
@@ -38,7 +38,7 @@
fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description', "view_url", "icon_url", ],
'classes':['suit-tab suit-tab-general']})]
readonly_fields = ('backend_status_text', )
- inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
+ inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline,AddressPoolInline]
form = VRouterServiceForm
extracontext_registered_admins = True
@@ -47,6 +47,7 @@
suit_form_tabs =(('general', 'vRouter Service Details'),
('administration', 'Administration'),
+ ('addresspools', 'Addresses'),
#('tools', 'Tools'),
('slices','Slices'),
('serviceattrs','Additional Attributes'),
diff --git a/xos/services/vtn/admin.py b/xos/services/vtn/admin.py
index c64e054..03ff5cd 100644
--- a/xos/services/vtn/admin.py
+++ b/xos/services/vtn/admin.py
@@ -29,6 +29,9 @@
sshUser = forms.CharField(required=False)
sshKeyFile = forms.CharField(required=False)
mgmtSubnetBits = forms.CharField(required=False)
+ xosEndpoint = forms.CharField(required=False)
+ xosUser = forms.CharField(required=False)
+ xosPassword = forms.CharField(required=False)
def __init__(self,*args,**kwargs):
super (VTNServiceForm,self ).__init__(*args,**kwargs)
@@ -40,6 +43,9 @@
self.fields['sshUser'].initial = self.instance.sshUser
self.fields['sshKeyFile'].initial = self.instance.sshKeyFile
self.fields['mgmtSubnetBits'].initial = self.instance.mgmtSubnetBits
+ self.fields['xosEndpoint'].initial = self.instance.xosEndpoint
+ self.fields['xosUser'].initial = self.instance.xosUser
+ self.fields['xosPassword'].initial = self.instance.xosPassword
def save(self, commit=True):
self.instance.privateGatewayMac = self.cleaned_data.get("privateGatewayMac")
@@ -49,6 +55,9 @@
self.instance.sshUser = self.cleaned_data.get("sshUser")
self.instance.sshKeyFile = self.cleaned_data.get("sshKeyFile")
self.instance.mgmtSubnetBits = self.cleaned_data.get("mgmtSubnetBits")
+ self.instance.xosEndpoint = self.cleaned_data.get("xosEndpoint")
+ self.instance.xosUser = self.cleaned_data.get("xosUser")
+ self.instance.xosPassword = self.cleaned_data.get("xosPassword")
return super(VTNServiceForm, self).save(commit=commit)
class Meta:
@@ -62,7 +71,7 @@
list_display = ("backend_status_icon", "name", "enabled")
list_display_links = ('backend_status_icon', 'name', )
fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber','description',"view_url","icon_url",
- 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits' ], 'classes':['suit-tab suit-tab-general']})]
+ 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits', 'xosEndpoint', 'xosUser', 'xosPassword' ], 'classes':['suit-tab suit-tab-general']})]
readonly_fields = ('backend_status_text', )
inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
@@ -84,4 +93,3 @@
return VTNService.get_service_objects_by_user(request.user)
admin.site.register(VTNService, VTNServiceAdmin)
-
diff --git a/xos/services/vtn/models.py b/xos/services/vtn/models.py
index cb254d7..52b1633 100644
--- a/xos/services/vtn/models.py
+++ b/xos/services/vtn/models.py
@@ -37,8 +37,10 @@
("sshUser", "root"),
("sshKeyFile", "/root/node_key") ,
("mgmtSubnetBits", "24"),
+ ("xosEndpoint", "http://xos/"),
+ ("xosUser", "padmin@vicci.org"),
+ ("xosPassword", "letmein"),
)
VTNService.setup_simple_attributes()
-
diff --git a/xos/synchronizers/base/backend.py b/xos/synchronizers/base/backend.py
index 5f11d46..d7307fa 100644
--- a/xos/synchronizers/base/backend.py
+++ b/xos/synchronizers/base/backend.py
@@ -15,7 +15,7 @@
def run(self):
# start the openstack observer
observer = XOSObserver()
- observer_thread = threading.Thread(target=observer.run)
+ observer_thread = threading.Thread(target=observer.run,name='synchronizer')
observer_thread.start()
# start model policies thread
diff --git a/xos/synchronizers/base/event_loop.py b/xos/synchronizers/base/event_loop.py
index f224380..4f7d436 100644
--- a/xos/synchronizers/base/event_loop.py
+++ b/xos/synchronizers/base/event_loop.py
@@ -521,7 +521,12 @@
loop_end = time.time()
- diag = Diag.objects.filter(name=Config().observer_name).first()
+ try:
+ observer_name = Config().observer_name
+ except:
+ observer_name = ''
+
+ diag = Diag.objects.filter(name=observer_name).first()
if (diag):
br_str = diag.backend_register
br = json.loads(br_str)
diff --git a/xos/synchronizers/helloworld/helloworld-synchronizer.py b/xos/synchronizers/helloworld/helloworld-synchronizer.py
deleted file mode 100755
index 84bec4f..0000000
--- a/xos/synchronizers/helloworld/helloworld-synchronizer.py
+++ /dev/null
@@ -1,11 +0,0 @@
-#!/usr/bin/env python
-
-# This imports and runs ../../xos-observer.py
-
-import importlib
-import os
-import sys
-observer_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../../synchronizers/base")
-sys.path.append(observer_path)
-mod = importlib.import_module("xos-synchronizer")
-mod.main()
diff --git a/xos/synchronizers/helloworld/helloworld_config b/xos/synchronizers/helloworld/helloworld_config
deleted file mode 100644
index 1f67242..0000000
--- a/xos/synchronizers/helloworld/helloworld_config
+++ /dev/null
@@ -1,47 +0,0 @@
-[plc]
-name=plc
-deployment=plc
-
-[db]
-name=xos
-user=postgres
-password=password
-host=localhost
-port=5432
-
-[api]
-host=localhost
-port=8000
-ssl_key=None
-ssl_cert=None
-ca_ssl_cert=None
-ratelimit_enabled=0
-omf_enabled=0
-mail_support_address=support@localhost
-nova_enabled=True
-logfile=/var/log/xos.log
-
-[nova]
-admin_user=admin@domain.com
-admin_password=admin
-admin_tenant=admin
-url=http://localhost:5000/v2.0/
-default_image=None
-default_flavor=m1.small
-default_security_group=default
-ca_ssl_cert=/etc/ssl/certs/ca-certificates.crt
-
-[observer]
-pretend=False
-backoff_disabled=False
-images_directory=/opt/xos/images
-dependency_graph=/opt/xos/model-deps
-logfile=/var/log/xos_backend.log
-steps_dir=/opt/xos/synchronizers/helloworld/steps
-applist=helloworld
-
-[gui]
-disable_minidashboard=True
-#branding_name=CORD
-#branding_css=/static/cord.css
-#branding_icon=/static/onos-logo.png
diff --git a/xos/synchronizers/helloworld/model-deps b/xos/synchronizers/helloworld/model-deps
deleted file mode 100644
index 63188f0..0000000
--- a/xos/synchronizers/helloworld/model-deps
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "OriginServer": [
- "ContentProvider"
- ],
- "ContentProvider": [
- "ServiceProvider"
- ],
- "CDNPrefix": [
- "ContentProvider"
- ],
- "AccessMap": [
- "ContentProvider"
- ],
- "SiteMap": [
- "ContentProvider",
- "ServiceProvider",
- "CDNPrefix"
- ]
-}
diff --git a/xos/synchronizers/helloworld/nohup.out b/xos/synchronizers/helloworld/nohup.out
deleted file mode 100644
index 74072c6..0000000
--- a/xos/synchronizers/helloworld/nohup.out
+++ /dev/null
@@ -1 +0,0 @@
-python: can't open file 'helloworld-observer.py': [Errno 2] No such file or directory
diff --git a/xos/synchronizers/helloworld/run.sh b/xos/synchronizers/helloworld/run.sh
deleted file mode 100755
index 1b9d834..0000000
--- a/xos/synchronizers/helloworld/run.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#if [[ ! -e ./hpc-backend.py ]]; then
-# ln -s ../xos-observer.py hpc-backend.py
-#fi
-
-export XOS_DIR=/opt/xos
-python helloworld-synchronizer.py -C $XOS_DIR/synchronizers/helloworld/helloworld_config
diff --git a/xos/synchronizers/helloworld/start.sh b/xos/synchronizers/helloworld/start.sh
deleted file mode 100755
index 7945db3..0000000
--- a/xos/synchronizers/helloworld/start.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-export XOS_DIR=/opt/xos
-
-echo $XOS_DIR/synchronizers/helloworld/helloworld_config
-python helloworld-synchronizer.py -C $XOS_DIR/synchronizers/helloworld/helloworld_config
diff --git a/xos/synchronizers/helloworld/steps/sync_hello.py b/xos/synchronizers/helloworld/steps/sync_hello.py
deleted file mode 100644
index 55d318a..0000000
--- a/xos/synchronizers/helloworld/steps/sync_hello.py
+++ /dev/null
@@ -1,26 +0,0 @@
-import os
-import sys
-import base64
-from django.db.models import F, Q
-from xos.config import Config
-from synchronizers.base.syncstep import SyncStep
-from services.helloworld.models import Hello,World
-from xos.logger import Logger, logging
-
-parentdir = os.path.join(os.path.dirname(__file__),"..")
-sys.path.insert(0,parentdir)
-
-logger = Logger(level=logging.INFO)
-
-class SyncHello(SyncStep):
- provides=[Hello]
- observes=Hello
- requested_interval=0
-
- def sync_record(self, record):
- instance = record.instance_backref
- instance.userData="packages:\n - apache2\nruncmd:\n - update-rc.d apache2 enable\n - service apache2 start\nwrite_files:\n- content: Hello %s\n path: /var/www/html/hello.txt"%record.name
- instance.save()
-
- def delete_record(self, m):
- return
diff --git a/xos/synchronizers/helloworld/steps/sync_world.py b/xos/synchronizers/helloworld/steps/sync_world.py
deleted file mode 100644
index a4e7e3c..0000000
--- a/xos/synchronizers/helloworld/steps/sync_world.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import os
-import sys
-import base64
-from django.db.models import F, Q
-from xos.config import Config
-from synchronizers.base.syncstep import SyncStep
-from services.helloworld.models import Hello,World
-from xos.logger import Logger, logging
-
-# hpclibrary will be in steps/..
-parentdir = os.path.join(os.path.dirname(__file__),"..")
-sys.path.insert(0,parentdir)
-
-logger = Logger(level=logging.INFO)
-
-class SyncWorld(SyncStep):
- provides=[World]
- observes=World
- requested_interval=0
-
- def sync_record(self, record):
- open('/tmp/hello-synchronizer','w').write(record.name)
-
- def delete_record(self, m):
- return
diff --git a/xos/synchronizers/helloworld/stop.sh b/xos/synchronizers/helloworld/stop.sh
deleted file mode 100755
index a0b4a8e..0000000
--- a/xos/synchronizers/helloworld/stop.sh
+++ /dev/null
@@ -1 +0,0 @@
-pkill -9 -f hpc-observer.py
diff --git a/xos/synchronizers/helloworldservice_complete/helloworldservice_config b/xos/synchronizers/helloworldservice_complete/helloworldservice_config
deleted file mode 100644
index 69894fc..0000000
--- a/xos/synchronizers/helloworldservice_complete/helloworldservice_config
+++ /dev/null
@@ -1,36 +0,0 @@
-# Required by XOS
-[db]
-name=xos
-user=postgres
-password=password
-host=localhost
-port=5432
-
-# Required by XOS
-[api]
-nova_enabled=True
-
-# Sets options for the observer
-[observer]
-# Optional name
-name=helloworldservice
-# This is the location to the dependency graph you generate
-dependency_graph=/opt/xos/synchronizers/helloworldservice_complete/model-deps
-# The location of your SyncSteps
-steps_dir=/opt/xos/synchronizers/helloworldservice_complete/steps
-# A temporary directory that will be used by ansible
-sys_dir=/opt/xos/synchronizers/helloworldservice_complete/sys
-# Location of the file to save logging messages to the backend log is often used
-logfile=/var/log/xos_backend.log
-# If this option is true, then nothing will change, we simply pretend to run
-pretend=False
-# If this is False then XOS will use an exponential backoff when the observer
-# fails, since we will be waiting for an instance, we don't want this.
-backoff_disabled=True
-# We want the output from ansible to be logged
-save_ansible_output=True
-# This determines how we SSH to a client, if this is set to True then we try
-# to ssh using the instance name as a proxy, if this is disabled we ssh using
-# the NAT IP of the instance. On CloudLab the first option will fail so we must
-# set this to False
-proxy_ssh=False
diff --git a/xos/synchronizers/helloworldservice_complete/run.sh b/xos/synchronizers/helloworldservice_complete/run.sh
deleted file mode 100755
index 331f8ae..0000000
--- a/xos/synchronizers/helloworldservice_complete/run.sh
+++ /dev/null
@@ -1,3 +0,0 @@
-# Runs the XOS observer using helloworldservice_config
-export XOS_DIR=/opt/xos
-python helloworldservice-synchronizer.py -C $XOS_DIR/synchronizers/helloworldservice_complete/helloworldservice_config
diff --git a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.py b/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.py
deleted file mode 100644
index 69a08f5..0000000
--- a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import os
-import sys
-from django.db.models import Q, F
-from services.helloworldservice_complete.models import HelloWorldServiceComplete, HelloWorldTenantComplete
-from synchronizers.base.SyncInstanceUsingAnsible import SyncInstanceUsingAnsible
-
-parentdir = os.path.join(os.path.dirname(__file__), "..")
-sys.path.insert(0, parentdir)
-
-# Class to define how we sync a tenant. Using SyncInstanceUsingAnsible we
-# indicate where the find the YAML for ansible, where to find the SSH key,
-# and the logic for determining what tenant needs updating, what additional
-# attributes are needed, and how to delete an instance.
-class SyncHelloWorldTenantComplete(SyncInstanceUsingAnsible):
- # Indicates the position in the data model, this will run when XOS needs to
- # enact a HelloWorldTenantComplete
- provides = [HelloWorldTenantComplete]
- # The actual model being enacted, usually the same as provides.
- observes = HelloWorldTenantComplete
- # Number of milliseconds between interruptions of the observer
- requested_interval = 0
- # The ansible template to run
- template_name = "sync_helloworldtenant.yaml"
- # The location of the SSH private key to use when ansible connects to
- # instances.
- service_key_name = "/opt/xos/synchronizers/helloworldservice_complete/helloworldservice_private_key"
-
- def __init__(self, *args, **kwargs):
- super(SyncHelloWorldTenantComplete, self).__init__(*args, **kwargs)
-
- # Defines the logic for determining what HelloWorldTenantCompletes need to be
- # enacted.
- def fetch_pending(self, deleted):
- # If the update is not a deletion, then we get all of the instnaces that
- # have been updated or have not been enacted.
- if (not deleted):
- objs = HelloWorldTenantComplete.get_tenant_objects().filter(
- Q(enacted__lt=F('updated')) | Q(enacted=None), Q(lazy_blocked=False))
- else:
- # If this is a deletion we get all of the deleted tenants..
- objs = HelloWorldTenantComplete.get_deleted_tenant_objects()
-
- return objs
-
- # Gets the attributes that are used by the Ansible template but are not
- # part of the set of default attributes.
- def get_extra_attributes(self, o):
- return {"display_message": o.display_message}
diff --git a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.yaml b/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
deleted file mode 100644
index 719c75f..0000000
--- a/xos/synchronizers/helloworldservice_complete/steps/sync_helloworldtenant.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
----
-- hosts: {{ instance_name }}
- gather_facts: False
- connection: ssh
- user: ubuntu
- sudo: yes
- tasks:
- - name: install apache
- apt: name=apache2 state=present update_cache=yes
-
- - name: write message
- shell: echo "{{ display_message }}" > /var/www/html/index.html
-
- - name: stop apache
- service: name=apache2 state=stopped
-
- - name: start apache
- service: name=apache2 state=started
diff --git a/xos/synchronizers/helloworldservice_complete/stop.sh b/xos/synchronizers/helloworldservice_complete/stop.sh
deleted file mode 100755
index 76e68d9..0000000
--- a/xos/synchronizers/helloworldservice_complete/stop.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-# Kill the observer
-pkill -9 -f helloworldservice-observer.py
diff --git a/xos/synchronizers/onos/steps/sync_onosapp.py b/xos/synchronizers/onos/steps/sync_onosapp.py
index add749d..7b80641 100644
--- a/xos/synchronizers/onos/steps/sync_onosapp.py
+++ b/xos/synchronizers/onos/steps/sync_onosapp.py
@@ -17,6 +17,7 @@
from services.onos.models import ONOSService, ONOSApp
from xos.logger import Logger, logging
from services.vrouter.models import VRouterService
+from services.vtn.models import VTNService
# hpclibrary will be in steps/..
parentdir = os.path.join(os.path.dirname(__file__),"..")
@@ -117,18 +118,9 @@
raise Exception("Controller user object for %s does not exist" % instance.creator)
return cuser.kuser_id
-
- def node_tag_default(self, o, node, tagname, default):
+ def get_node_tag(self, o, node, tagname):
tags = Tag.select_by_content_object(node).filter(name=tagname)
- if tags:
- value = tags[0].value
- else:
- value = default
- logger.info("node %s: saving default value %s for tag %s" % (node.name, value, tagname))
- service = self.get_onos_service(o)
- tag = Tag(service=service, content_object=node, name=tagname, value=value)
- tag.save()
- return value
+ return tags[0].value
# Scan attrs for attribute name
# If it's not present, save it as a TenantAttribute
@@ -145,16 +137,31 @@
# This function currently assumes a single Deployment and Site
def get_vtn_config(self, o, attrs):
- # The "attrs" argument contains a list of all service and tenant attributes
- # If an attribute is present, use it in the configuration
- # Otherwise save the attriute with a reasonable (for a CORD devel pod) default value
- # The admin will see all possible configuration values and the assigned defaults
- privateGatewayMac = self.attribute_default(o, attrs, "privateGatewayMac", "00:00:00:00:00:01")
- localManagementIp = self.attribute_default(o, attrs, "localManagementIp", "172.27.0.1/24")
- ovsdbPort = self.attribute_default(o, attrs, "ovsdbPort", "6641")
- sshPort = self.attribute_default(o, attrs, "sshPort", "22")
- sshUser = self.attribute_default(o, attrs, "sshUser", "root")
- sshKeyFile = self.attribute_default(o, attrs, "sshKeyFile", "/root/node_key")
+ privateGatewayMac = None
+ localManagementIp = None
+ ovsdbPort = None
+ sshPort = None
+ sshUser = None
+ sshKeyFile = None
+ mgmtSubnetBits = None
+ xosEndpoint = None
+ xosUser = None
+ xosPassword = None
+
+ # VTN-specific configuration from the VTN Service
+ vtns = VTNService.get_service_objects().all()
+ if vtns:
+ vtn = vtns[0]
+ privateGatewayMac = vtn.privateGatewayMac
+ localManagementIp = vtn.localManagementIp
+ ovsdbPort = vtn.ovsdbPort
+ sshPort = vtn.sshPort
+ sshUser = vtn.sshUser
+ sshKeyFile = vtn.sshKeyFile
+ mgmtSubnetBits = vtn.mgmtSubnetBits
+ xosEndpoint = vtn.xosEndpoint
+ xosUser = vtn.xosUser
+ xosPassword = vtn.xosPassword
# OpenStack endpoints and credentials
keystone_server = "http://keystone:5000/v2.0/"
@@ -186,6 +193,11 @@
"user": user_name,
"password": password
},
+ "xos": {
+ "endpoint": xosEndpoint,
+ "user": xosUser,
+ "password": xosPassword
+ },
"publicGateways": [],
"nodes" : []
}
@@ -194,20 +206,14 @@
}
# Generate apps->org.onosproject.cordvtn->cordvtn->nodes
-
- # We need to generate a CIDR address for the physical node's
- # address on the management network
- mgmtSubnetBits = self.attribute_default(o, attrs, "mgmtSubnetBits", "24")
-
nodes = Node.objects.all()
for node in nodes:
nodeip = socket.gethostbyname(node.name)
try:
- bridgeId = self.node_tag_default(o, node, "bridgeId", "of:0000000000000001")
- dataPlaneIntf = self.node_tag_default(o, node, "dataPlaneIntf", "veth1")
- # This should be generated from the AddressPool if not present
- dataPlaneIp = self.node_tag_default(o, node, "dataPlaneIp", "192.168.199.1/24")
+ bridgeId = self.get_node_tag(o, node, "bridgeId")
+ dataPlaneIntf = self.get_node_tag(o, node, "dataPlaneIntf")
+ dataPlaneIp = self.get_node_tag(o, node, "dataPlaneIp")
except:
logger.error("not adding node %s to the VTN configuration" % node.name)
continue
diff --git a/xos/services/helloworld/__init__.py b/xos/synchronizers/openvpn/__init__.py
similarity index 100%
copy from xos/services/helloworld/__init__.py
copy to xos/synchronizers/openvpn/__init__.py
diff --git a/xos/synchronizers/helloworldservice_complete/model-deps b/xos/synchronizers/openvpn/model-deps
similarity index 100%
rename from xos/synchronizers/helloworldservice_complete/model-deps
rename to xos/synchronizers/openvpn/model-deps
diff --git a/xos/synchronizers/helloworldservice_complete/helloworldservice-synchronizer.py b/xos/synchronizers/openvpn/openvpn-synchronizer.py
similarity index 76%
rename from xos/synchronizers/helloworldservice_complete/helloworldservice-synchronizer.py
rename to xos/synchronizers/openvpn/openvpn-synchronizer.py
index 95f4081..3227ed9 100755
--- a/xos/synchronizers/helloworldservice_complete/helloworldservice-synchronizer.py
+++ b/xos/synchronizers/openvpn/openvpn-synchronizer.py
@@ -1,8 +1,5 @@
#!/usr/bin/env python
-# This imports and runs ../../xos-observer.py
-# Runs the standard XOS observer
-
import importlib
import os
import sys
diff --git a/xos/synchronizers/openvpn/openvpn_config b/xos/synchronizers/openvpn/openvpn_config
new file mode 100644
index 0000000..8a58b52
--- /dev/null
+++ b/xos/synchronizers/openvpn/openvpn_config
@@ -0,0 +1,23 @@
+# Required by XOS
+[db]
+name=xos
+user=postgres
+password=password
+host=localhost
+port=5432
+
+# Required by XOS
+[api]
+nova_enabled=True
+
+# Sets options for the synchronizer
+[observer]
+name=openvpn
+dependency_graph=/opt/xos/synchronizers/openvpn/model-deps
+steps_dir=/opt/xos/synchronizers/openvpn/steps
+sys_dir=/opt/xos/synchronizers/openvpn/sys
+logfile=/var/log/xos_backend.log
+pretend=False
+backoff_disabled=True
+save_ansible_output=True
+proxy_ssh=False
diff --git a/xos/synchronizers/openvpn/run.sh b/xos/synchronizers/openvpn/run.sh
new file mode 100755
index 0000000..a5d90c9
--- /dev/null
+++ b/xos/synchronizers/openvpn/run.sh
@@ -0,0 +1,2 @@
+export XOS_DIR=/opt/xos
+python openvpn-synchronizer.py -C $XOS_DIR/synchronizers/openvpn/openvpn_config
diff --git a/xos/services/helloworld/__init__.py b/xos/synchronizers/openvpn/steps/__init__.py
similarity index 100%
copy from xos/services/helloworld/__init__.py
copy to xos/synchronizers/openvpn/steps/__init__.py
diff --git a/xos/synchronizers/openvpn/steps/roles/openvpn/handlers/main.yml b/xos/synchronizers/openvpn/steps/roles/openvpn/handlers/main.yml
new file mode 100644
index 0000000..8725e29
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/roles/openvpn/handlers/main.yml
@@ -0,0 +1,4 @@
+---
+
+- name: restart openvpn
+ shell: (kill -9 $(cat {{ pki_dir }}/pid) || true) && (openvpn {{ pki_dir }}/server.conf &)
diff --git a/xos/synchronizers/openvpn/steps/roles/openvpn/tasks/main.yml b/xos/synchronizers/openvpn/steps/roles/openvpn/tasks/main.yml
new file mode 100644
index 0000000..47093b2
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/roles/openvpn/tasks/main.yml
@@ -0,0 +1,38 @@
+---
+
+- name: install openvpn
+ apt: name=openvpn state=present update_cache=yes
+
+- name: make sure /opt/openvpn exists
+ file: path=/opt/openvpn state=directory
+
+- name: make sure directory for this server exists
+ file: path={{ pki_dir }} state=directory
+
+- name: get server key
+ copy: src={{ pki_dir }}/private/server.key dest={{ pki_dir }}/server.key
+ notify:
+ - restart openvpn
+
+- name: get server crt
+ copy: src={{ pki_dir }}/issued/server.crt dest={{ pki_dir }}/server.crt
+ notify:
+ - restart openvpn
+
+- name: get ca crt
+ copy: src={{ pki_dir }}/ca.crt dest={{ pki_dir }}/ca.crt
+ notify:
+ - restart openvpn
+
+- name: get crl
+ copy: src={{ pki_dir }}/crl.pem dest={{ pki_dir }}/crl.pem
+
+- name: get dh
+ copy: src={{ pki_dir }}/dh.pem dest={{ pki_dir }}/dh.pem
+ notify:
+ - restart openvpn
+
+- name: write config
+ template: src=server.conf.j2 dest={{ pki_dir }}/server.conf owner=root group=root
+ notify:
+ - restart openvpn
diff --git a/xos/synchronizers/openvpn/steps/roles/openvpn/templates/server.conf.j2 b/xos/synchronizers/openvpn/steps/roles/openvpn/templates/server.conf.j2
new file mode 100644
index 0000000..4766e7b
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/roles/openvpn/templates/server.conf.j2
@@ -0,0 +1,24 @@
+# This file autogenerated by OpenVPNTenant synchronizer
+# It contains the OPENVPN config file for the server
+script-security 3 system
+port {{ port_number }}
+proto {{ protocol }}
+dev tun
+writepid {{ pki_dir }}/pid
+ca {{ pki_dir }}/ca.crt
+cert {{ pki_dir }}/server.crt
+key {{ pki_dir }}/server.key
+dh {{ pki_dir }}/dh.pem
+crl-verify {{ pki_dir }}/crl.pem
+server {{ server_network }} {{ vpn_subnet }}
+ifconfig-pool-persist {{ pki_dir }}/ipp.txt
+status {{ pki_dir }}/openvpn-status.log
+verb 3
+{% if is_persistent %}
+keepalive 10 60
+persist-tun
+persist-key
+{% endif %}
+{% if clients_can_see_each_other %}
+client-to-client
+{% endif %}
diff --git a/xos/synchronizers/openvpn/steps/sync_openvpntenant.py b/xos/synchronizers/openvpn/steps/sync_openvpntenant.py
new file mode 100644
index 0000000..b58dd94
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/sync_openvpntenant.py
@@ -0,0 +1,75 @@
+import os
+import shutil
+import sys
+
+from django.db.models import F, Q
+
+from services.openvpn.models import OpenVPNService, OpenVPNTenant
+from synchronizers.base.SyncInstanceUsingAnsible import \
+ SyncInstanceUsingAnsible
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncOpenVPNTenant(SyncInstanceUsingAnsible):
+ """Class for syncing a OpenVPNTenant using Ansible.
+
+ This SyncStep creates any necessary files for the OpenVPNTenant using ESAY RSA and then runs the
+ Ansible template to start the server on an instance.
+ """
+ provides = [OpenVPNTenant]
+ observes = OpenVPNTenant
+ requested_interval = 0
+ template_name = "sync_openvpntenant.yaml"
+ service_key_name = "/opt/xos/synchronizers/openvpn/openvpn_private_key"
+
+ def fetch_pending(self, deleted):
+ if (not deleted):
+ objs = OpenVPNTenant.get_tenant_objects().filter(
+ Q(enacted__lt=F('updated')) |
+ Q(enacted=None), Q(lazy_blocked=False))
+ else:
+ objs = OpenVPNTenant.get_deleted_tenant_objects()
+
+ return objs
+
+ def get_extra_attributes(self, tenant):
+ return {"is_persistent": tenant.is_persistent,
+ "vpn_subnet": tenant.vpn_subnet,
+ "server_network": tenant.server_network,
+ "clients_can_see_each_other": (
+ tenant.clients_can_see_each_other),
+ "port_number": tenant.port_number,
+ "protocol": tenant.protocol,
+ "pki_dir": OpenVPNService.get_pki_dir(tenant)
+ }
+
+ def sync_fields(self, o, fields):
+ pki_dir = OpenVPNService.get_pki_dir(o)
+
+ if (not os.path.isdir(pki_dir)):
+ OpenVPNService.execute_easyrsa_command(pki_dir, "init-pki")
+ OpenVPNService.execute_easyrsa_command(
+ pki_dir, "--req-cn=XOS build-ca nopass")
+
+ # Very hacky way to handle VPNs that need to share CAs
+ if (o.use_ca_from_id):
+ tenant = OpenVPNTenant.get_tenant_objects().filter(
+ pk=o.use_ca_from_id)[0]
+ other_pki_dir = OpenVPNService.get_pki_dir(tenant)
+ shutil.copy2(other_pki_dir + "/ca.crt", pki_dir)
+ shutil.copy2(other_pki_dir + "/private/ca.key",
+ pki_dir + "/private")
+
+ # If the server has to be built then we need to build it
+ if (not os.path.isfile(pki_dir + "/issued/server.crt")):
+ OpenVPNService.execute_easyrsa_command(
+ pki_dir, "build-server-full server nopass")
+ OpenVPNService.execute_easyrsa_command(pki_dir, "gen-dh")
+
+ # Get the most recent list of revoked clients
+ OpenVPNService.execute_easyrsa_command(pki_dir, "gen-crl")
+
+ # Super runs the playbook
+ super(SyncOpenVPNTenant, self).sync_fields(o, fields)
diff --git a/xos/synchronizers/openvpn/steps/sync_openvpntenant.yaml b/xos/synchronizers/openvpn/steps/sync_openvpntenant.yaml
new file mode 100644
index 0000000..e36f51b
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/sync_openvpntenant.yaml
@@ -0,0 +1,17 @@
+---
+- hosts: {{ instance_name }}
+ gather_facts: False
+ connection: ssh
+ user: ubuntu
+ sudo: yes
+ vars:
+ server_network: {{ server_network }}
+ is_persistent: {{ is_persistent }}
+ vpn_subnet: {{ vpn_subnet }}
+ clients_can_see_each_other: {{ clients_can_see_each_other }}
+ port_number: {{ port_number }}
+ protocol: {{ protocol }}
+ pki_dir: {{ pki_dir }}
+
+ roles:
+ - openvpn
diff --git a/xos/synchronizers/openvpn/steps/sync_tenantprivilege.py b/xos/synchronizers/openvpn/steps/sync_tenantprivilege.py
new file mode 100644
index 0000000..51ee6df
--- /dev/null
+++ b/xos/synchronizers/openvpn/steps/sync_tenantprivilege.py
@@ -0,0 +1,79 @@
+import os
+import sys
+
+from core.models import TenantPrivilege
+from services.openvpn.models import OPENVPN_KIND, OpenVPNService, OpenVPNTenant
+from synchronizers.base.syncstep import DeferredException, SyncStep
+
+parentdir = os.path.join(os.path.dirname(__file__), "..")
+sys.path.insert(0, parentdir)
+
+
+class SyncTenantPrivilege(SyncStep):
+ """Class for syncing a TenantPrivilege for a OpenVPNTenant.
+
+ This SyncStep isolates the updated TenantPrivileges that are for OpenVPNTenants 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 OpenVPNTenant. For deleted
+ privileges the client certificate is revoked and the files associated are deleted. In both
+ cases the associated OpenVPNTenant is saved causing the OpenVPNTenant synchronizer to run.
+ """
+ provides = [TenantPrivilege]
+ observes = TenantPrivilege
+ requested_interval = 0
+
+ def fetch_pending(self, deleted):
+ privs = super(SyncTenantPrivilege, self).fetch_pending(deleted)
+ # Get only the TenantPrivileges that relate to OpenVPNTenants
+ privs = [priv for priv in privs if priv.tenant.kind == OPENVPN_KIND]
+ return privs
+
+ def sync_record(self, record):
+ if (not record.tenant.id):
+ raise DeferredException("Privilege waiting on VPN Tenant ID")
+ certificate = self.get_certificate_name(record)
+ tenant = OpenVPNTenant.get_tenant_objects().filter(pk=record.tenant.id)[0]
+ if (not tenant):
+ raise DeferredException("Privilege waiting on VPN Tenant")
+ # Only add a certificate if ones does not yet exist
+ pki_dir = OpenVPNService.get_pki_dir(tenant)
+ if (not os.path.isfile(pki_dir + "/issued/" + certificate + ".crt")):
+ OpenVPNService.execute_easyrsa_command(
+ pki_dir, "build-client-full " + certificate + " nopass")
+ tenant.save()
+ record.save()
+
+ def delete_record(self, record):
+ if (not record.tenant.id):
+ return
+ certificate = self.get_certificate_name(record)
+ tenant = OpenVPNTenant.get_tenant_objects().filter(pk=record.tenant.id)[0]
+ if (not tenant):
+ return
+ # If the client has already been reovked don't do it again
+ pki_dir = OpenVPNService.get_pki_dir(tenant)
+ if (os.path.isfile(pki_dir + "/issued/" + certificate + ".crt")):
+ OpenVPNService.execute_easyrsa_command(
+ pki_dir, "revoke " + certificate)
+ # Revoking a client cert does not delete any of the files
+ # to make sure that we can add this user again we need to
+ # delete all of the files created by easyrsa
+ os.remove(pki_dir + "/issued/" + certificate + ".crt")
+ os.remove(pki_dir + "/private/" + certificate + ".key")
+ os.remove(pki_dir + "/reqs/" + certificate + ".req")
+ tenant.save()
+
+ 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/openvpn/stop.sh b/xos/synchronizers/openvpn/stop.sh
new file mode 100755
index 0000000..4a83aca
--- /dev/null
+++ b/xos/synchronizers/openvpn/stop.sh
@@ -0,0 +1,2 @@
+# Kill the observer
+pkill -9 -f openvpn-synchronizer.py
diff --git a/xos/synchronizers/vtr/steps/sync_vtrtenant.py b/xos/synchronizers/vtr/steps/sync_vtrtenant.py
index 8debadf..c66f19c 100644
--- a/xos/synchronizers/vtr/steps/sync_vtrtenant.py
+++ b/xos/synchronizers/vtr/steps/sync_vtrtenant.py
@@ -95,9 +95,9 @@
# add in the sync_attributes that come from the vSG object
# this will be wan_ip, wan_mac, wan_container_ip, wan_container_mac, ...
- if o.target and o.target.volt and o.target.volt.vsg:
- for attribute_name in o.target.volt.vsg.sync_attributes:
- fields[attribute_name] = getattr(o.target.volt.vsg, attribute_name)
+ if o.target and o.target.volt and o.target.volt.vcpe:
+ for attribute_name in o.target.volt.vcpe.sync_attributes:
+ fields[attribute_name] = getattr(o.target.volt.vcpe, attribute_name)
# add in the sync_attributes that come from the SubscriberRoot object
if o.target and hasattr(o.target, "sync_attributes"):
diff --git a/xos/templates/admin/base.html b/xos/templates/admin/base.html
index b47bf74..ba01201 100644
--- a/xos/templates/admin/base.html
+++ b/xos/templates/admin/base.html
@@ -15,6 +15,7 @@
<!--<link rel="stylesheet" type="text/css" href="{% static 'suit/bootstrap/dist/css/bootstrap.min.css' %}" media="all"/>-->
<link rel="stylesheet" type="text/css" href="{% static 'suit/css/suit.css' %}" media="all">
<link rel="stylesheet" type="text/css" href="{% static 'xos.css' %}" media="all">
+ <link rel="stylesheet" type="text/css" href="{% static 'xosNgLib.css' %}" media="all">
{% if XOS_BRANDING_CSS %}
<link rel="stylesheet" type="text/css" href="{% static 'cord.css' %}" media="all">
<link rel="stylesheet" type="text/css" href="{{ XOS_BRANDING_CSS }}">
diff --git a/xos/tests/api/.gitignore b/xos/tests/api/.gitignore
new file mode 100644
index 0000000..99e7d87
--- /dev/null
+++ b/xos/tests/api/.gitignore
@@ -0,0 +1 @@
+apiary.apib
\ No newline at end of file
diff --git a/xos/tests/api/apiary.apib b/xos/tests/api/apiary.apib
deleted file mode 100644
index f11d612..0000000
--- a/xos/tests/api/apiary.apib
+++ /dev/null
@@ -1,521 +0,0 @@
-FORMAT: 1A
-
-# XOS
-
-
-# Group Users
-
-List of the XOS users
-
-## Users [/api/core/users/]
-
-### List all Users [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "id": 2,
- "password": "pbkdf2_sha256$12000$9gn8DmZuIoz2$YPQkx3AOOV7jZNYr2ddrgUCkiuaPpvb8+aJR7RwLZNA=",
- "last_login": "2016-04-12T18:50:45.880823Z",
- "email": "johndoe@myhouse.com",
- "username": "johndoe@myhouse.com",
- "firstname": "john",
- "lastname": "doe",
- "phone": null,
- "user_url": null,
- "site": "http://xos.dev:9999/api/core/sites/1/",
- "public_key": null,
- "is_active": true,
- "is_admin": false,
- "is_staff": true,
- "is_readonly": false,
- "is_registering": false,
- "is_appuser": false,
- "login_page": null,
- "created": "2016-04-12T18:50:45.912602Z",
- "updated": "2016-04-12T18:50:45.912671Z",
- "enacted": null,
- "policed": null,
- "backend_status": "Provisioning in progress",
- "deleted": false,
- "write_protect": false,
- "timezone": "America/New_York"
- }
- ]
-
-
-
-# Group Example
-
-## Example Services Collection [/api/service/exampleservice/]
-
-### List all Example Services [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "MyExample",
- "id": 1,
- "service_message": "This is the test message"
- }
- ]
-
-
-# Group ONOS Services
-
-List of the active onos services
-
-## ONOS Services Collection [/api/service/onos/]
-
-### List all ONOS Services [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "service_ONOS_vBNG",
- "id": 5,
- "rest_hostname": "",
- "rest_port": "8181",
- "no_container": false,
- "node_key": ""
- }
- ]
-
-
-# Group vSG
-
-## vSG Collection [/api/service/vsg/]
-
-### List all vSGs [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "service_vsg",
- "id": 2,
- "dns_servers": "8.8.8.8",
- "url_filter_kind": null,
- "node_label": null
- }
- ]
-
-
-# Group Subscribers
-
-Resource related to the CORD Subscribers.
-
-## Subscribers [/api/tenant/cord/subscriber/{subscriber_id}/]
-
-### List All Subscribers [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "cordSubscriber-1",
- "id": 1,
- "features": {
- "cdn": false,
- "uplink_speed": 1000000000,
- "downlink_speed": 1000000000,
- "uverse": false,
- "status": "enabled"
- },
- "identity": {
- "account_num": "123",
- "name": "My House"
- },
- "related": {
- "instance_name": "mysite_vcpe",
- "vsg_id": 4,
- "compute_node_name": "node2.opencloud.us",
- "c_tag": "432",
- "instance_id": 1,
- "wan_container_ip": null,
- "volt_id": 3,
- "s_tag": "222"
- }
- }
- ]
-
-
-### View a Subscriber Detail [GET]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-+ Response 200 (application/json)
-
- {
- "humanReadableName": "cordSubscriber-1",
- "id": 1,
- "features": {
- "cdn": false,
- "uplink_speed": 1000000000,
- "downlink_speed": 1000000000,
- "uverse": false,
- "status": "enabled"
- },
- "identity": {
- "account_num": "123",
- "name": "My House"
- },
- "related": {
- "instance_name": "mysite_vcpe",
- "vsg_id": 4,
- "compute_node_name": "node2.opencloud.us",
- "c_tag": "432",
- "instance_id": 1,
- "wan_container_ip": null,
- "volt_id": 3,
- "s_tag": "222"
- }
- }
-
-### Delete a Subscriber [DELETE]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-+ Response 204
-
-### Subscriber features [/api/tenant/cord/subscriber/{subscriber_id}/features/]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-### View a Subscriber Features Detail [GET]
-
-+ Response 200 (application/json)
-
- {
- "cdn": false,
- "uplink_speed": 1000000000,
- "downlink_speed": 1000000000,
- "uverse": true,
- "status": "enabled"
- }
-
-#### Subscriber features uplink_speed [/api/tenant/cord/subscriber/{subscriber_id}/features/uplink_speed/]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-#### Read Subscriber uplink_speed [GET]
-
-+ Response 200 (application/json)
-
- {
- "uplink_speed": 1000000000
- }
-
-#### Update Subscriber uplink_speed [PUT]
-
-+ Request 200 (application/json)
-
- {
- "uplink_speed": 1000000000
- }
-
-+ Response 200 (application/json)
-
- {
- "uplink_speed": 1000000000
- }
-
-#### Subscriber features downlink_speed [/api/tenant/cord/subscriber/{subscriber_id}/features/downlink_speed/]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-#### Read Subscriber downlink_speed [GET]
-
-+ Response 200 (application/json)
-
- {
- "downlink_speed": 1000000000
- }
-
-#### Update Subscriber downlink_speed [PUT]
-
-+ Request 200 (application/json)
-
- {
- "downlink_speed": 1000000000
- }
-
-+ Response 200 (application/json)
-
- {
- "downlink_speed": 1000000000
- }
-
-#### Subscriber features cdn [/api/tenant/cord/subscriber/{subscriber_id}/features/cdn/]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-#### Read Subscriber cdn [GET]
-
-+ Response 200 (application/json)
-
- {
- "cdn": false
- }
-
-#### Update Subscriber cdn [PUT]
-
-+ Request 200 (application/json)
-
- {
- "cdn": false
- }
-
-+ Response 200 (application/json)
-
- {
- "cdn": false
- }
-
-#### Subscriber features uverse [/api/tenant/cord/subscriber/{subscriber_id}/features/uverse/]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-#### Read Subscriber uverse [GET]
-
-+ Response 200 (application/json)
-
- {
- "uverse": false
- }
-
-#### Update Subscriber uverse [PUT]
-
-+ Request 200 (application/json)
-
- {
- "uverse": false
- }
-
-+ Response 200 (application/json)
-
- {
- "uverse": false
- }
-
-#### Subscriber features status [/api/tenant/cord/subscriber/{subscriber_id}/features/status/]
-
-+ Parameters
- + subscriber_id: 1 (number) - ID of the Subscriber in the form of an integer
-
-#### Read Subscriber status [GET]
-
-+ Response 200 (application/json)
-
- {
- "status": "enabled"
- }
-
-#### Update Subscriber status [PUT]
-
-+ Request 200 (application/json)
-
- {
- "status": "enabled"
- }
-
-+ Response 200 (application/json)
-
- {
- "status": "enabled"
- }
-
-
-# Group Truckroll
-
-Virtual Truckroll, enable to perform basic test on user connectivity such as ping, traceroute and tcpdump.
-
-## Truckroll Collection [/api/tenant/truckroll/{truckroll_id}/]
-
-### List all Truckroll [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "vTR-tenant-9",
- "id": 9,
- "provider_service": 6,
- "target_id": 2,
- "scope": "container",
- "test": "ping",
- "argument": "8.8.8.8",
- "result": "",
- "result_code": "",
- "is_synced": false,
- "backend_status": "2 - Exception('Unreachable results in ansible recipe',)"
- }
- ]
-
-### Create a Truckroll [POST]
-
-A virtual truckroll is complete once is_synced equal true
-
-+ Request (application/json)
-
- {
- "target_id": 2,
- "scope": "container",
- "test": "ping",
- "argument": "8.8.8.8"
- }
-
-+ Response 201 (application/json)
-
- {
- "humanReadableName": "vTR-tenant-1",
- "id": 1,
- "provider_service": 6,
- "target_id": 2,
- "scope": "container",
- "test": "ping",
- "argument": "8.8.8.8",
- "result": null,
- "result_code": null,
- "is_synced": false,
- "backend_status": "0 - Provisioning in progress"
- }
-
-
-### View a Truckroll Detail [GET]
-
-+ Parameters
- + truckroll_id: 1 (number) - ID of the Truckroll in the form of an integer
-
-+ Response 200 (application/json)
-
- {
- "humanReadableName": "vTR-tenant-10",
- "id": 10,
- "provider_service": 6,
- "target_id": 2,
- "scope": "container",
- "test": "ping",
- "argument": "8.8.8.8",
- "result": null,
- "result_code": null,
- "is_synced": false,
- "backend_status": "0 - Provisioning in progress"
- }
-
-### Delete a Truckroll Detail [DELETE]
-
-+ Parameters
- + truckroll_id: 1 (number) - ID of the Truckroll in the form of an integer
-
-+ Response 204
-
-
-
-# Group vOLT
-
-OLT devices aggregate a set of subscriber connections
-
-## vOLT Collection [/api/tenant/cord/volt/{volt_id}/]
-
-### List all vOLT [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "vOLT-tenant-1",
- "id": 1,
- "service_specific_id": "123",
- "s_tag": "222",
- "c_tag": "432",
- "subscriber": 1,
- "related": {
- "instance_id": 1,
- "instance_name": "mysite_vcpe",
- "vsg_id": 4,
- "wan_container_ip": null,
- "compute_node_name": "node2.opencloud.us"
- }
- }
- ]
-
-### Create a vOLT [POST]
-
-+ Request (application/json)
-
- {
- "s_tag": "222",
- "c_tag": "432",
- "subscriber": 1
- }
-
-+ Response 201 (application/json)
-
- {
- "humanReadableName": "vOLT-tenant-1",
- "id": 1,
- "service_specific_id": "123",
- "s_tag": "222",
- "c_tag": "432",
- "subscriber": 1,
- "related": {
- "instance_id": 1,
- "instance_name": "mysite_vcpe",
- "vsg_id": 4,
- "wan_container_ip": null,
- "compute_node_name": "node2.opencloud.us"
- }
- }
-
-### View a vOLT Detail [GET]
-
-+ Parameters
- + volt_id: 1 (number) - ID of the vOLT in the form of an integer
-
-+ Response 200 (application/json)
-
- {
- "humanReadableName": "vOLT-tenant-1",
- "id": 1,
- "service_specific_id": "123",
- "s_tag": "222",
- "c_tag": "432",
- "subscriber": 1,
- "related": {
- "instance_id": 1,
- "instance_name": "mysite_vcpe",
- "vsg_id": 4,
- "wan_container_ip": null,
- "compute_node_name": "node2.opencloud.us"
- }
- }
-
-
-
-# Group ONOS Apps
-
-## ONOS App Collection [/api/tenant/onos/app/]
-
-### List all apps [GET]
-
-+ Response 200 (application/json)
-
- [
- {
- "humanReadableName": "onos-tenant-7",
- "id": 7,
- "name": "vBNG_ONOS_app",
- "dependencies": "org.onosproject.proxyarp, org.onosproject.virtualbng, org.onosproject.openflow, org.onosproject.fwd"
- }
- ]
\ No newline at end of file
diff --git a/xos/tests/api/gulpfile.js b/xos/tests/api/gulpfile.js
index 2e82210..988dd8c 100644
--- a/xos/tests/api/gulpfile.js
+++ b/xos/tests/api/gulpfile.js
@@ -12,6 +12,6 @@
'./source/base.md',
'./source/**/*.md'
])
- .pipe(concat('apiary.apib', {newLine: '\n \n \n'}))
+ .pipe(concat('../../../apiary.apib', {newLine: '\n \n \n'}))
.pipe(gulp.dest('./'));
});
diff --git a/xos/tests/api/helpers/before_test.py b/xos/tests/api/helpers/before_test.py
new file mode 100644
index 0000000..e53c323
--- /dev/null
+++ b/xos/tests/api/helpers/before_test.py
@@ -0,0 +1,270 @@
+import dredd_hooks as hooks
+import sys
+
+# HELPERS
+# NOTE move in separated module
+import os
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from core.models import *
+from services.cord.models import *
+from services.vtr.models import *
+import urllib2
+import json
+django.setup()
+
+
+def doLogin(username, password):
+ url = "http://127.0.0.1:8000/xoslib/login?username=%s&password=%s" % (username, password)
+ res = urllib2.urlopen(url).read()
+ parsed = json.loads(res)
+ return {'token': parsed['xoscsrftoken'], 'sessionid': parsed['xossessionid']}
+
+
+def cleanDB():
+ # deleting all subscribers
+ for s in CordSubscriberRoot.objects.all():
+ s.delete(purge=True)
+
+ # deleting all slices
+ for s in Slice.objects.all():
+ s.delete(purge=True)
+
+ # deleting all Services
+ for s in Service.objects.all():
+ s.delete(purge=True)
+
+ # deleting all Tenants
+ for s in Tenant.objects.all():
+ s.delete(purge=True)
+
+ # deleting all Networks
+ for s in Network.objects.all():
+ s.delete(purge=True)
+
+ # deleting all NetworkTemplates
+ for s in NetworkTemplate.objects.all():
+ s.delete(purge=True)
+
+ for s in NetworkSlice.objects.all():
+ s.delete(purge=True)
+
+ for s in AddressPool.objects.all():
+ s.delete(purge=True)
+
+ for s in Flavor.objects.all():
+ s.delete(purge=True)
+
+ for s in Image.objects.all():
+ s.delete(purge=True)
+
+ # print 'DB Cleaned'
+
+
+def createTestSubscriber():
+
+ cleanDB()
+ createFlavors()
+
+ # load user
+ user = User.objects.get(email="padmin@vicci.org")
+
+ # network template
+ private_template = NetworkTemplate()
+ private_template.name = 'Private Network'
+ private_template.save()
+
+ # creating the test subscriber
+ subscriber = CordSubscriberRoot(name='Test Subscriber 1', id=1)
+ subscriber.save()
+
+ # vRouter service
+ vrouter_service = VRouterService()
+ vrouter_service.name = 'service_vrouter'
+ vrouter_service.save()
+
+ # address pools
+ ap_vsg = AddressPool()
+ ap_vsg.service = vrouter_service
+ ap_vsg.name = 'addresses_vsg'
+ ap_vsg.addresses = '10.168.0.0'
+ ap_vsg.gateway_ip = '10.168.0.1'
+ ap_vsg.gateway_mac = '02:42:0a:a8:00:01'
+ ap_vsg.save()
+
+ # print 'vRouter created'
+
+ # Site
+ site = Site.objects.get(name='MySite')
+
+ # vSG service
+ vsg_service = VSGService()
+ vsg_service.name = 'service_vsg'
+
+ # vSG slice
+ vsg_slice = Slice(id=2)
+ vsg_slice.name = site.login_base + "_testVsg"
+ vsg_slice.service = vsg_service.id
+ vsg_slice.site = site
+ vsg_slice.caller = user
+
+ vsg_slice.save()
+
+ vsg_service.save()
+
+ # volt service
+ volt_service = VOLTService()
+ volt_service.name = 'service_volt'
+ volt_service.save()
+
+ # cvpe image
+ createImage('ubuntu-vcpe4')
+
+ # vcpe slice
+ vcpe_slice = Slice(id=3)
+ vcpe_slice.name = site.login_base + "_testVcpe"
+ vcpe_slice.service = Service.objects.get(kind='vCPE')
+ vcpe_slice.site = site
+ vcpe_slice.caller = user
+ vcpe_slice.save()
+
+ # print 'vcpe_slice created'
+
+ # create a lan network
+ lan_net = Network()
+ lan_net.name = 'lan_network'
+ lan_net.owner = vcpe_slice
+ lan_net.template = private_template
+ lan_net.save()
+
+ # print 'lan_network created'
+
+ # add relation between vcpe slice and lan network
+ vcpe_network = NetworkSlice()
+ vcpe_network.network = lan_net
+ vcpe_network.slice = vcpe_slice
+ vcpe_network.save()
+
+ # print 'vcpe network relation added'
+
+ # vbng service
+ vbng_service = VBNGService()
+ vbng_service.name = 'service_vbng'
+ vbng_service.save()
+
+ # print 'vbng_service creater'
+
+ # volt tenant
+ vt = VOLTTenant(subscriber=subscriber.id, id=1)
+ vt.s_tag = "222"
+ vt.c_tag = "432"
+ vt.provider_service_id = volt_service.id
+ vt.caller = user
+ vt.save()
+
+ # print "Subscriber Created"
+
+
+def deleteTruckrolls():
+ for s in VTRTenant.objects.all():
+ s.delete(purge=True)
+
+
+def setUpTruckroll():
+ service_vtr = VTRService()
+ service_vtr.name = 'service_vtr'
+ service_vtr.save()
+
+
+def createTruckroll():
+ setUpTruckroll()
+ tn = VTRTenant(id=1)
+ tn.save()
+
+
+def createFlavors():
+ small = Flavor(id=1)
+ small.name = "m1.small"
+ small.save()
+
+ medium = Flavor(id=2)
+ medium.name = "m1.medium"
+ medium.save()
+
+ large = Flavor(id=3)
+ large.name = "m1.large"
+ large.save()
+
+
+def createSlice():
+ site = Site.objects.get(name='MySite')
+ user = User.objects.get(email="padmin@vicci.org")
+
+ sl = Slice(id=1)
+ sl.name = site.login_base + "_testSlice"
+ sl.site = site
+ sl.caller = user
+ sl.save()
+ return sl
+
+
+def createDeployment():
+ deployment = Deployment(id=1)
+ deployment.name = 'MyTestDeployment'
+ deployment.save()
+ return deployment
+
+
+def createImage(name):
+ img = Image(id=1)
+ img.name = name
+ img.disk_format = 'QCOW2'
+ img.kind = 'vm'
+ img.save()
+ return img
+
+
+def createNode(deployment):
+ site = Site.objects.get(name='MySite')
+
+ site_deployment = SiteDeployment(id=1)
+ site_deployment.site = site
+ site_deployment.deployment = deployment
+ site_deployment.save()
+
+ node = Node(id=1)
+ node.name = 'test-node'
+ node.site = site
+ node.site_deployment = site_deployment
+ node.save()
+ return node
+
+
+def setupInstance():
+ deployment = createDeployment()
+ sl = createSlice()
+ node = createNode(deployment)
+ img = createImage('test-image')
+ print {'image': img.id, 'deployment': deployment.id, 'slice': sl.id}
+ return {'image': img, 'deployment': deployment, 'slice': sl}
+
+
+def createInstance():
+ requirements = setupInstance()
+ user = User.objects.get(email="padmin@vicci.org")
+
+ instance = Instance(id=1)
+ instance.name = 'test-instance'
+ instance.node = Node.objects.all()[0]
+ instance.image = requirements['image']
+ instance.slice = requirements['slice']
+ instance.deployment = requirements['deployment']
+ instance.caller = user
+ instance.save()
+
+setupInstance()
+# createTestSubscriber()
+# createInstance()
+# createNode()
diff --git a/xos/tests/api/helpers/flavors.py b/xos/tests/api/helpers/flavors.py
new file mode 100644
index 0000000..e16d41f
--- /dev/null
+++ b/xos/tests/api/helpers/flavors.py
@@ -0,0 +1,24 @@
+import dredd_hooks as hooks
+import sys
+
+# HELPERS
+# NOTE move in separated module
+import os
+import sys
+sys.path.append("/opt/xos")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xos.settings")
+import django
+from core.models import *
+from services.cord.models import *
+from services.vtr.models import *
+import urllib2
+import json
+django.setup()
+
+def createFlavor():
+ fl = Flavor(id=1)
+ fl.name = 'm1.large'
+ fl.save()
+ print(fl, fl.id)
+
+createFlavor()
\ No newline at end of file
diff --git a/xos/tests/api/hooks.py b/xos/tests/api/hooks.py
index ccf0c6c..1f4a0ad 100644
--- a/xos/tests/api/hooks.py
+++ b/xos/tests/api/hooks.py
@@ -54,12 +54,19 @@
for s in AddressPool.objects.all():
s.delete(purge=True)
+ for s in Flavor.objects.all():
+ s.delete(purge=True)
+
+ for s in Image.objects.all():
+ s.delete(purge=True)
+
# print 'DB Cleaned'
def createTestSubscriber():
cleanDB()
+ createFlavors()
# load user
user = User.objects.get(email="padmin@vicci.org")
@@ -97,7 +104,7 @@
vsg_service.name = 'service_vsg'
# vSG slice
- vsg_slice = Slice()
+ vsg_slice = Slice(id=2)
vsg_slice.name = site.login_base + "_testVsg"
vsg_slice.service = vsg_service.id
vsg_slice.site = site
@@ -112,8 +119,11 @@
volt_service.name = 'service_volt'
volt_service.save()
+ # cvpe image
+ createImage('ubuntu-vcpe4')
+
# vcpe slice
- vcpe_slice = Slice()
+ vcpe_slice = Slice(id=3)
vcpe_slice.name = site.login_base + "_testVcpe"
vcpe_slice.service = Service.objects.get(kind='vCPE')
vcpe_slice.site = site
@@ -123,7 +133,7 @@
# print 'vcpe_slice created'
# create a lan network
- lan_net = Network()
+ lan_net = Network(id=1)
lan_net.name = 'lan_network'
lan_net.owner = vcpe_slice
lan_net.template = private_template
@@ -174,6 +184,87 @@
tn.save()
+def createFlavors():
+ small = Flavor(id=1)
+ small.name = "m1.small"
+ small.save()
+
+ medium = Flavor(id=2)
+ medium.name = "m1.medium"
+ medium.save()
+
+ large = Flavor(id=3)
+ large.name = "m1.large"
+ large.save()
+
+
+def createSlice():
+ site = Site.objects.get(name='MySite')
+ user = User.objects.get(email="padmin@vicci.org")
+
+ sl = Slice(id=1)
+ sl.name = site.login_base + "_testSlice"
+ sl.site = site
+ sl.caller = user
+ sl.save()
+ return sl
+
+
+def createDeployment():
+ deployment = Deployment(id=1)
+ deployment.name = 'MyTestDeployment'
+ deployment.save()
+ return deployment
+
+
+def createImage(name):
+ img = Image(id=1)
+ img.name = name
+ img.disk_format = 'QCOW2'
+ img.kind = 'vm'
+ img.save()
+ return img
+
+
+def createNode(deployment):
+ site = Site.objects.get(name='MySite')
+
+ site_deployment = SiteDeployment(id=1)
+ site_deployment.site = site
+ site_deployment.deployment = deployment
+ site_deployment.save()
+
+ node = Node(id=1)
+ node.name = 'test-node'
+ node.site = site
+ node.site_deployment = site_deployment
+ node.save()
+ return node
+
+
+def setupInstance():
+ deployment = createDeployment()
+ sl = createSlice()
+ node = createNode(deployment)
+ img = createImage('test-image')
+ # print {'image': img.id, 'deployment': deployment.id, 'slice': sl.id}
+ return {'image': img, 'deployment': deployment, 'slice': sl}
+
+
+def createInstance():
+ requirements = setupInstance()
+ user = User.objects.get(email="padmin@vicci.org")
+
+ instance = Instance(id=1)
+ instance.name = 'test-instance'
+ instance.node = Node.objects.all()[0]
+ instance.image = requirements['image']
+ instance.slice = requirements['slice']
+ instance.deployment = requirements['deployment']
+ instance.caller = user
+ instance.save()
+
+
@hooks.before_all
def my_before_all_hook(transactions):
# print "-------------------------------- Before All Hook --------------------------------"
@@ -188,9 +279,15 @@
transaction['request']['headers']['X-CSRFToken'] = auth['token']
transaction['request']['headers']['Cookie'] = "xossessionid=%s; xoscsrftoken=%s" % (auth['sessionid'], auth['token'])
createTestSubscriber()
+ setupInstance()
sys.stdout.flush()
+# @hooks.after_each
+# def my_after_each(transaction):
+# print "-------------------------------- Test end --------------------------------"
+
+
@hooks.before("Truckroll > Truckroll Collection > Create a Truckroll")
def test1(transaction):
setUpTruckroll()
@@ -214,6 +311,50 @@
VOLTTenant.objects.get(kind='vOLT').delete()
+@hooks.before("Flavors > Flavors > View a Flavors Detail")
+def test5(transaction):
+ createFlavors()
+
+
+@hooks.before("Deployments > Deployments > View a Deployment Detail")
+def get_deployments(transaction):
+ createDeployment()
+
+
+@hooks.before("Deployments > Deployments > Delete a Deployment")
+def delete_deployments(transaction):
+ createDeployment()
+
+
+@hooks.before("Instances > Instances Collection > Create an Instance")
+def create_instance(transaction):
+ setupInstance()
+ transaction['skip'] = True
+
+
+@hooks.before("Instances > Instances Detail > Get instance details")
+def get_instance(transaction):
+ createInstance()
+
+
+@hooks.before("Instances > Instances Detail > Delete instance")
+def delete_instance(transaction):
+ createInstance()
+
+
@hooks.before("Example > Example Services Collection > List all Example Services")
def exampleTest(transaction):
transaction['skip'] = True
+
+
+@hooks.before("Utility > Login > Log a user in the system")
+def before_logout_hook(transaction):
+ transaction['skip'] = True
+ # auth = doLogin('padmin@vicci.org', 'letmein')
+ # transaction['request']['body'] = {}
+ # transaction['request']['body']['xossessionid'] = auth['sessionid']
+
+
+@hooks.before("Utility > Logout > Log a user out of the system")
+def skip_for_now(transaction):
+ transaction['skip'] = True
\ No newline at end of file
diff --git a/xos/tests/api/source/core/deployment.md b/xos/tests/api/source/core/deployment.md
new file mode 100644
index 0000000..764a73f
--- /dev/null
+++ b/xos/tests/api/source/core/deployment.md
@@ -0,0 +1,126 @@
+# Group Deployments
+
+List of the XOS deployments
+
+## Deployments [/api/core/deployments/{id}/]
+
+### List all deployments [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "MyDeployment",
+ "id": 1,
+ "created": "2016-04-29T16:19:03.549901Z",
+ "updated": "2016-04-29T16:19:05.624151Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "MyDeployment",
+ "accessControl": "allow all",
+ "images": [
+ "1"
+ ],
+ "sites": [
+ "1"
+ ],
+ "flavors": [
+ "1",
+ "2",
+ "3"
+ ],
+ "dashboardviews": [
+ "1"
+ ]
+ }
+ ]
+
+### Create a deployment [POST]
+
++ Request (application/json)
+
+ {
+ "humanReadableName": "MyDeployment",
+ }
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "MyDeployment",
+ "id": 1,
+ "created": "2016-04-29T16:19:03.549901Z",
+ "updated": "2016-04-29T16:19:05.624151Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "MyDeployment",
+ "accessControl": "allow all",
+ "images": [
+ "1"
+ ],
+ "sites": [
+ "1"
+ ],
+ "flavors": [
+ "1",
+ "2",
+ "3"
+ ],
+ "dashboardviews": [
+ "1"
+ ]
+ }
+
+### View a Deployment Detail [GET]
+
++ Parameters
+ + id: 1 (number) - ID of the Deployment in the form of an integer
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "MyDeployment",
+ "id": 1,
+ "created": "2016-04-27T21:46:57.354544Z",
+ "updated": "2016-04-27T21:47:05.221720Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "MyDeployment",
+ "accessControl": "allow all",
+ "images": [],
+ "sites": [
+ "1"
+ ],
+ "flavors": [
+ "3",
+ "2",
+ "1"
+ ],
+ "dashboardviews": [
+ "3"
+ ]
+ }
+
+### Delete a Deployment [DELETE]
+
++ Parameters
+ + id: 1 (number) - ID of the Deployment in the form of an integer
+
++ Response 204
\ No newline at end of file
diff --git a/xos/tests/api/source/core/flavors.md b/xos/tests/api/source/core/flavors.md
new file mode 100644
index 0000000..aba5a6b
--- /dev/null
+++ b/xos/tests/api/source/core/flavors.md
@@ -0,0 +1,104 @@
+# Group Flavors
+
+List of the XOS flavors
+
+## Flavors [/api/core/flavors/{id}/]
+
+### List all flavors [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "m1.large",
+ "id": 1,
+ "created": "2016-04-29T16:19:01.979548Z",
+ "updated": "2016-04-29T16:19:03.568238Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "m1.large",
+ "description": null,
+ "flavor": "m1.large",
+ "order": 0,
+ "default": false,
+ "deployments": [
+ "1"
+ ]
+ }
+ ]
+
+### Create a Flavor [POST]
+
++ Request (application/json)
+
+ {
+ "humanReadableName": "mq.test",
+ }
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "m1.large",
+ "id": 1,
+ "created": "2016-04-29T16:19:01.979548Z",
+ "updated": "2016-04-29T16:19:03.568238Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "m1.large",
+ "description": null,
+ "flavor": "m1.large",
+ "order": 0,
+ "default": false,
+ "deployments": [
+ "1"
+ ]
+ }
+
+### View a Flavors Detail [GET]
+
++ Parameters
+ + id: 1 (number) - ID of the Flavors in the form of an integer
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "m1.large",
+ "id": 1,
+ "created": "2016-04-29T16:19:01.979548Z",
+ "updated": "2016-04-29T16:19:03.568238Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "m1.large",
+ "description": null,
+ "flavor": "m1.large",
+ "order": 0,
+ "default": false,
+ "deployments": [
+ "1"
+ ]
+ }
+
+### Delete a Flavors Detail [DELETE]
+
++ Parameters
+ + id: 1 (number) - ID of the Flavors in the form of an integer
+
++ Response 204
\ No newline at end of file
diff --git a/xos/tests/api/source/core/instances.md b/xos/tests/api/source/core/instances.md
new file mode 100644
index 0000000..9caf93e
--- /dev/null
+++ b/xos/tests/api/source/core/instances.md
@@ -0,0 +1,149 @@
+# Group Instances
+
+List of the XOS instances
+
+## Instances Collection [/api/core/instances/{?no_hyperlinks}]
+
+ + no_hyperlinks (number, optional) - Wheter to return relation with links or ids
+ + Default: `0`
+
+### List all Instances [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "id": 1,
+ "humanReadableName": "uninstantiated-1",
+ "created": "2016-04-26T00:36:22.465259Z",
+ "updated": "2016-04-26T00:36:22.465288Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "instance_id": null,
+ "instance_uuid": null,
+ "name": "mysite_vcpe",
+ "instance_name": null,
+ "ip": null,
+ "image": "1",
+ "creator": "1",
+ "slice": "1",
+ "deployment": "1",
+ "node": "1",
+ "numberCores": 0,
+ "flavor": "1",
+ "userData": null,
+ "isolation": "vm",
+ "volumes": "/etc/dnsmasq.d,/etc/ufw",
+ "parent": null,
+ "networks": [
+ "1"
+ ]
+ }
+ ]
+
+### Create an Instance [POST]
+
++ Parameters
+ + no_hyperlinks: 1
+
++ Request (application/json)
+
+ {
+ "name": "test-instance",
+ "image": 1,
+ "slice": 1,
+ "deployment": 1,
+ "node": 1
+ }
+
++ Response 200 (application/json)
+
+ {
+ "id": 1,
+ "humanReadableName": "uninstantiated-1",
+ "created": "2016-04-26T00:36:22.465259Z",
+ "updated": "2016-04-26T00:36:22.465288Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "instance_id": null,
+ "instance_uuid": null,
+ "name": "test-instance",
+ "instance_name": null,
+ "ip": null,
+ "image": "1",
+ "creator": "1",
+ "slice": "1",
+ "deployment": "1",
+ "node": "1",
+ "numberCores": 0,
+ "flavor": "1",
+ "userData": null,
+ "isolation": "vm",
+ "volumes": "/etc/dnsmasq.d,/etc/ufw",
+ "parent": null,
+ "networks": [
+ "1"
+ ]
+ }
+
+## Instances Detail [/api/core/instances/{id}/]
+
+### Get instance details [GET]
+
++ Parameters
+ + id: 1 (number) - ID of the Instance in the form of an integer
+
++ Response 200 (application/json)
+
+ {
+ "id": 1,
+ "humanReadableName": "uninstantiated-1",
+ "created": "2016-04-26T00:36:22.465259Z",
+ "updated": "2016-04-26T00:36:22.465288Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "instance_id": null,
+ "instance_uuid": null,
+ "name": "mysite_vcpe",
+ "instance_name": null,
+ "ip": null,
+ "image": "1",
+ "creator": "1",
+ "slice": "1",
+ "deployment": "1",
+ "node": "1",
+ "numberCores": 0,
+ "flavor": "1",
+ "userData": null,
+ "isolation": "vm",
+ "volumes": "/etc/dnsmasq.d,/etc/ufw",
+ "parent": null,
+ "networks": [
+ "1"
+ ]
+ }
+
+### Delete instance [DELETE]
+
++ Parameters
+ + id: 1 (number) - ID of the Instance in the form of an integer
+
++ Response 204
\ No newline at end of file
diff --git a/xos/tests/api/source/core/nodes.md b/xos/tests/api/source/core/nodes.md
new file mode 100644
index 0000000..d9931dc
--- /dev/null
+++ b/xos/tests/api/source/core/nodes.md
@@ -0,0 +1,30 @@
+# Group Nodes
+
+List of the XOS nodes
+
+## Nodes [/api/core/nodes/{id}/]
+
+### List all nodes [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "node2.opencloud.us",
+ "id": 1,
+ "created": "2016-04-29T16:19:05.661567Z",
+ "updated": "2016-04-29T16:19:05.661454Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": true,
+ "name": "node2.opencloud.us",
+ "site_deployment": "1",
+ "site": "1",
+ "nodelabels": []
+ }
+ ]
diff --git a/xos/tests/api/source/core/sites.md b/xos/tests/api/source/core/sites.md
new file mode 100644
index 0000000..c3784db
--- /dev/null
+++ b/xos/tests/api/source/core/sites.md
@@ -0,0 +1,38 @@
+# Group Sites
+
+List of the XOS sites
+
+## Sites [/api/core/sites/{id}/]
+
+### List all sites [GET]
+
++ Response 200 (application/json)
+
+ {
+ "humanReadableName": "MySite",
+ "id": 1,
+ "created": "2016-04-29T16:19:03.587770Z",
+ "updated": "2016-04-29T16:19:05.651933Z",
+ "enacted": null,
+ "policed": null,
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "name": "MySite",
+ "site_url": "http://opencord.us/",
+ "enabled": true,
+ "hosts_nodes": true,
+ "hosts_users": true,
+ "location": null,
+ "longitude": null,
+ "latitude": null,
+ "login_base": "mysite",
+ "is_public": true,
+ "abbreviated_name": "",
+ "deployments": [
+ "1"
+ ]
+ }
\ No newline at end of file
diff --git a/xos/tests/api/source/core/slices.md b/xos/tests/api/source/core/slices.md
new file mode 100644
index 0000000..6ec5c7e
--- /dev/null
+++ b/xos/tests/api/source/core/slices.md
@@ -0,0 +1,46 @@
+# Group Slices
+
+List of the XOS slices
+
+## Slices [/api/core/slices/{id}/]
+
+### List all slices [GET]
+
++ Response 200 (application/json)
+
+ [
+ {
+ "humanReadableName": "mysite_slice",
+ "id": 1,
+ "created": "2016-04-29T16:23:22.505072Z",
+ "updated": "2016-04-29T16:23:22.504691Z",
+ "enacted": null,
+ "policed": "2016-04-29T16:23:22.781298Z",
+ "backend_register": "{}",
+ "backend_status": "0 - Provisioning in progress",
+ "deleted": false,
+ "write_protect": false,
+ "lazy_blocked": false,
+ "no_sync": false,
+ "name": "mysite_slice",
+ "enabled": true,
+ "omf_friendly": false,
+ "description": "",
+ "slice_url": "",
+ "site": "http://apt118.apt.emulab.net/api/core/sites/1/",
+ "max_instances": 10,
+ "service": null,
+ "network": null,
+ "exposed_ports": null,
+ "serviceClass": "http://apt118.apt.emulab.net/api/core/serviceclasses/1/",
+ "creator": "http://apt118.apt.emulab.net/api/core/users/1/",
+ "default_flavor": null,
+ "default_image": null,
+ "mount_data_sets": "GenBank",
+ "default_isolation": "vm",
+ "networks": [
+ "http://apt118.apt.emulab.net/api/core/networks/1/"
+ ]
+ }
+ ]
+
\ No newline at end of file
diff --git a/xos/tests/api/source/core/users.md b/xos/tests/api/source/core/users.md
index c5dcf68..8ccabb5 100644
--- a/xos/tests/api/source/core/users.md
+++ b/xos/tests/api/source/core/users.md
@@ -2,7 +2,7 @@
List of the XOS users
-## Users [/api/core/users/]
+## Users [/api/core/users/{id}/]
### List all Users [GET]
diff --git a/xos/tests/api/source/utility/utility.md b/xos/tests/api/source/utility/utility.md
new file mode 100644
index 0000000..63379be
--- /dev/null
+++ b/xos/tests/api/source/utility/utility.md
@@ -0,0 +1,31 @@
+# Group Utility
+
+List of the XOS Utility API
+
+## Login [/api/utility/login/]
+
+### Log a user in the system [POST]
+
++ Request (application/json)
+
+ {
+ "username": "padmin@vicci.org",
+ "password": "letmein"
+ }
+
++ Response 200 (application/json)
+
+ {
+ "xoscsrftoken":"xuvsRC1jkXAsnrdRlgJvcXpmtthTAqqf",
+ "xossessionid":"7ds5a3wzjlgbjqo4odkd25qsm0j2s6zg",
+ "user": "{\"policed\": null, \"site\": 3, \"is_appuser\": false, \"is_staff\": true, \"backend_status\": \"Provisioning in progress\", \"id\": 3, \"is_registering\": false, \"last_login\": \"2016-04-30T22:51:04.788675+00:00\", \"email\": \"padmin@vicci.org\", \"no_sync\": false, \"username\": \"padmin@vicci.org\", \"dashboards\": [11], \"login_page\": null, \"firstname\": \"XOS\", \"user_url\": null, \"deleted\": false, \"lastname\": \"admin\", \"is_active\": true, \"lazy_blocked\": false, \"phone\": null, \"is_admin\": true, \"enacted\": null, \"public_key\": null, \"is_readonly\": false, \"no_policy\": false, \"write_protect\": false}"
+ }
+
+## Logout [/api/utility/logout/]
+
+### Log a user out of the system [POST]
+
++ Request (application/json)
+ {xossessionid: "sessionId"}
+
++ Response 200 (application/json)
diff --git a/xos/tools/apigen/api.template.py b/xos/tools/apigen/api.template.py
index de733e8..d0852db 100644
--- a/xos/tools/apigen/api.template.py
+++ b/xos/tools/apigen/api.template.py
@@ -68,7 +68,8 @@
# Based on serializers.py
class XOSModelSerializer(serializers.ModelSerializer):
- def save_object(self, obj, **kwargs):
+ # TODO: Rest Framework 3.x doesn't support save_object()
+ def NEED_TO_UPDATE_save_object(self, obj, **kwargs):
""" rest_framework can't deal with ManyToMany relations that have a
through table. In xos, most of the through tables we have
diff --git a/xos/tools/xos-manage b/xos/tools/xos-manage
index 03d27a3..a4705d9 100755
--- a/xos/tools/xos-manage
+++ b/xos/tools/xos-manage
@@ -60,13 +60,13 @@
echo Waiting for postgres to start
sleep 1
sudo -u postgres psql -c '\q'
- done
+ done
}
function db_exists {
- sudo -u postgres psql $DBNAME -c '\q' 2>/dev/null
+ sudo -u postgres psql $DBNAME -c '\q' 2>/dev/null
return $?
-}
+}
function createdb {
wait_postgres
@@ -145,11 +145,12 @@
python ./manage.py makemigrations cord
python ./manage.py makemigrations mcord
python ./manage.py makemigrations ceilometer
- python ./manage.py makemigrations helloworldservice_complete
python ./manage.py makemigrations onos
+ python ./manage.py makemigrations openvpn
python ./manage.py makemigrations vtr
python ./manage.py makemigrations vrouter
python ./manage.py makemigrations vtn
+ python ./manage.py makemigrations fabric
#python ./manage.py makemigrations servcomp
}
diff --git a/xos/tosca/custom_types/xos.m4 b/xos/tosca/custom_types/xos.m4
index 7be82c5..dd548da 100644
--- a/xos/tosca/custom_types/xos.m4
+++ b/xos/tosca/custom_types/xos.m4
@@ -252,6 +252,16 @@
xos_base_props
xos_base_service_props
+ tosca.nodes.FabricService:
+ derived_from: tosca.nodes.Root
+ description: >
+ CORD: The Fabric Service.
+ capabilities:
+ xos_base_service_caps
+ properties:
+ xos_base_props
+ xos_base_service_props
+
tosca.nodes.VTNService:
derived_from: tosca.nodes.Root
description: >
@@ -282,6 +292,16 @@
mgmtSubnetBits:
type: string
required: false
+ xosEndpoint:
+ type: string
+ required: false
+ xosUser:
+ type: string
+ required: false
+ xosPassword:
+ type: string
+ required: false
+
tosca.nodes.CDNService:
derived_from: tosca.nodes.Root
@@ -385,16 +405,17 @@
type: tosca.capabilities.xos.User
properties:
+ xos_base_props
password:
type: string
required: false
firstname:
type: string
- required: true
+ required: false
description: First name of User.
lastname:
type: string
- required: true
+ required: false
description: Last name of User.
phone:
type: string
@@ -410,11 +431,13 @@
description: Public key that will be installed in Instances.
is_active:
type: boolean
- default: true
+ required: false
+ #default: true
description: If True, the user may log in.
is_admin:
type: boolean
- default: false
+ required: false
+ #default: false
description: If True, the user has root admin privileges.
login_page:
type: string
@@ -428,6 +451,9 @@
An XOS network parameter type. May be applied to Networks and/or
Ports.
+ properties:
+ xos_base_props
+
capabilities:
network_parameter_type:
type: tosca.capabilities.xos.NetworkParameterType
@@ -444,13 +470,14 @@
type: tosca.capabilities.xos.NetworkTemplate
properties:
+ xos_base_props
visibility:
type: string
- default: private
+ required: false
description: Indicates whether network is publicly routable.
translation:
type: string
- default: none
+ required: false
description: Indicates whether network uses address translation.
shared_network_name:
type: string
@@ -462,7 +489,7 @@
description: Attaches this template to a specific OpenStack network.
topology_kind:
type: string
- default: BigSwitch
+ required: false
description: Describes the topology of the network.
controller_kind:
type: string
@@ -749,6 +776,7 @@
required: false
description: default isolation to use when bringing up instances (default to 'vm')
network:
+<<<<<<< HEAD
type: string
required: false
description: type of networking to use for this slice
@@ -764,6 +792,8 @@
required: false
description: default flavor to use for slice
default_node:
+=======
+>>>>>>> upstream/master
type: string
required: false
description: default node to use for this slice
@@ -803,6 +833,49 @@
flavor:
type: tosca.capabilities.xos.Flavor
+<<<<<<< HEAD
+=======
+ tosca.nodes.SiteRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Site Role.
+ properties:
+ xos_base_props
+ capabilities:
+ siterole:
+ type: tosca.capabilities.xos.SiteRole
+
+ tosca.nodes.SliceRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Slice Role.
+ properties:
+ xos_base_props
+ capabilities:
+ slicerole:
+ type: tosca.capabilities.xos.SliceRole
+
+ tosca.nodes.TenantRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Tenant Role.
+ properties:
+ xos_base_props
+ capabilities:
+ tenantrole:
+ type: tosca.capabilities.xos.TenantRole
+
+ tosca.nodes.DeploymentRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Deployment Role.
+ properties:
+ xos_base_props
+ capabilities:
+ deploymentrole:
+ type: tosca.capabilities.xos.DeploymentRole
+
+>>>>>>> upstream/master
tosca.nodes.DashboardView:
derived_from: tosca.nodes.Root
description: >
@@ -820,6 +893,21 @@
required: false
description: URL to the dashboard
+ tosca.nodes.Tag:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Tag
+ properties:
+ xos_base_props
+ name:
+ type: string
+ required: true
+ descrption: name of tag
+ value:
+ type: string
+ required: false
+ descrption: value of tag
+
tosca.nodes.Compute.Container:
derived_from: tosca.nodes.Compute
description: >
@@ -956,6 +1044,9 @@
tosca.relationships.DependsOn:
derived_from: tosca.relationships.Root
+ tosca.relationships.TagsObject:
+ derived_from: tosca.relationships.Root
+
tosca.capabilities.xos.Service:
derived_from: tosca.capabilities.Root
description: An XOS Service
@@ -1008,6 +1099,25 @@
derived_from: tosca.capabilities.Root
description: An XOS Flavor
+<<<<<<< HEAD
+=======
+ tosca.capabilities.xos.DeploymentRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS DeploymentRole
+
+ tosca.capabilities.xos.SliceRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS SliceRole
+
+ tosca.capabilities.xos.SiteRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS SiteRole
+
+ tosca.capabilities.xos.TenantRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS TenantRole
+
+>>>>>>> upstream/master
tosca.capabilities.xos.Image:
derived_from: tosca.capabilities.Root
description: An XOS Image
diff --git a/xos/tosca/custom_types/xos.yaml b/xos/tosca/custom_types/xos.yaml
index 4f546f9..9d25554 100644
--- a/xos/tosca/custom_types/xos.yaml
+++ b/xos/tosca/custom_types/xos.yaml
@@ -442,6 +442,60 @@
required: false
description: Version number of Service.
+ tosca.nodes.FabricService:
+ derived_from: tosca.nodes.Root
+ description: >
+ CORD: The Fabric Service.
+ capabilities:
+ scalable:
+ type: tosca.capabilities.Scalable
+ service:
+ type: tosca.capabilities.xos.Service
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ kind:
+ type: string
+ default: generic
+ description: Type of service.
+ view_url:
+ type: string
+ required: false
+ description: URL to follow when icon is clicked in the Service Directory.
+ icon_url:
+ type: string
+ required: false
+ description: ICON to display in the Service Directory.
+ enabled:
+ type: boolean
+ default: true
+ published:
+ type: boolean
+ default: true
+ description: If True then display this Service in the Service Directory.
+ public_key:
+ type: string
+ required: false
+ description: Public key to install into Instances to allows Services to SSH into them.
+ private_key_fn:
+ type: string
+ required: false
+ description: Location of private key file
+ versionNumber:
+ type: string
+ required: false
+ description: Version number of Service.
+
tosca.nodes.VTNService:
derived_from: tosca.nodes.Root
description: >
@@ -516,6 +570,16 @@
mgmtSubnetBits:
type: string
required: false
+ xosEndpoint:
+ type: string
+ required: false
+ xosUser:
+ type: string
+ required: false
+ xosPassword:
+ type: string
+ required: false
+
tosca.nodes.CDNService:
derived_from: tosca.nodes.Root
@@ -730,16 +794,28 @@
type: tosca.capabilities.xos.User
properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
password:
type: string
required: false
firstname:
type: string
- required: true
+ required: false
description: First name of User.
lastname:
type: string
- required: true
+ required: false
description: Last name of User.
phone:
type: string
@@ -755,11 +831,13 @@
description: Public key that will be installed in Instances.
is_active:
type: boolean
- default: true
+ required: false
+ #default: true
description: If True, the user may log in.
is_admin:
type: boolean
- default: false
+ required: false
+ #default: false
description: If True, the user has root admin privileges.
login_page:
type: string
@@ -773,6 +851,20 @@
An XOS network parameter type. May be applied to Networks and/or
Ports.
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+
capabilities:
network_parameter_type:
type: tosca.capabilities.xos.NetworkParameterType
@@ -789,13 +881,25 @@
type: tosca.capabilities.xos.NetworkTemplate
properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
visibility:
type: string
- default: private
+ required: false
description: Indicates whether network is publicly routable.
translation:
type: string
- default: none
+ required: false
description: Indicates whether network uses address translation.
shared_network_name:
type: string
@@ -807,7 +911,7 @@
description: Attaches this template to a specific OpenStack network.
topology_kind:
type: string
- default: BigSwitch
+ required: false
description: Describes the topology of the network.
controller_kind:
type: string
@@ -1164,6 +1268,7 @@
required: false
description: type of networking to use for this slice
exposed_ports:
+<<<<<<< HEAD
type: string
required: false
description: comma-separated list of protocol _space_ port that represent ports the slice should expose
@@ -1175,6 +1280,8 @@
required: false
description: default flavor to use for slice
default_node:
+=======
+>>>>>>> upstream/master
type: string
required: false
description: default node to use for this slice
@@ -1247,6 +1354,93 @@
flavor:
type: tosca.capabilities.xos.Flavor
+<<<<<<< HEAD
+=======
+ tosca.nodes.SiteRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Site Role.
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ capabilities:
+ siterole:
+ type: tosca.capabilities.xos.SiteRole
+
+ tosca.nodes.SliceRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Slice Role.
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ capabilities:
+ slicerole:
+ type: tosca.capabilities.xos.SliceRole
+
+ tosca.nodes.TenantRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Tenant Role.
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ capabilities:
+ tenantrole:
+ type: tosca.capabilities.xos.TenantRole
+
+ tosca.nodes.DeploymentRole:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Deployment Role.
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ capabilities:
+ deploymentrole:
+ type: tosca.capabilities.xos.DeploymentRole
+
+>>>>>>> upstream/master
tosca.nodes.DashboardView:
derived_from: tosca.nodes.Root
description: >
@@ -1275,6 +1469,32 @@
required: false
description: URL to the dashboard
+ tosca.nodes.Tag:
+ derived_from: tosca.nodes.Root
+ description: >
+ An XOS Tag
+ properties:
+ no-delete:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to delete this object
+ no-create:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to create this object
+ no-update:
+ type: boolean
+ default: false
+ description: Do not allow Tosca to update this object
+ name:
+ type: string
+ required: true
+ descrption: name of tag
+ value:
+ type: string
+ required: false
+ descrption: value of tag
+
tosca.nodes.Compute.Container:
derived_from: tosca.nodes.Compute
description: >
@@ -1411,6 +1631,9 @@
tosca.relationships.DependsOn:
derived_from: tosca.relationships.Root
+ tosca.relationships.TagsObject:
+ derived_from: tosca.relationships.Root
+
tosca.capabilities.xos.Service:
derived_from: tosca.capabilities.Root
description: An XOS Service
@@ -1463,6 +1686,25 @@
derived_from: tosca.capabilities.Root
description: An XOS Flavor
+<<<<<<< HEAD
+=======
+ tosca.capabilities.xos.DeploymentRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS DeploymentRole
+
+ tosca.capabilities.xos.SliceRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS SliceRole
+
+ tosca.capabilities.xos.SiteRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS SiteRole
+
+ tosca.capabilities.xos.TenantRole:
+ derived_from: tosca.capabilities.Root
+ description: An XOS TenantRole
+
+>>>>>>> upstream/master
tosca.capabilities.xos.Image:
derived_from: tosca.capabilities.Root
description: An XOS Image
diff --git a/xos/tosca/resources/CORDUser.py b/xos/tosca/resources/CORDUser.py
index 566e205..705a895 100644
--- a/xos/tosca/resources/CORDUser.py
+++ b/xos/tosca/resources/CORDUser.py
@@ -28,12 +28,12 @@
if not sub:
return []
for user in sub.users:
- if user["name"] == self.nodetemplate.name:
+ if user["name"] == self.obj_name:
result.append(user)
return result
def get_xos_args(self):
- args = {"name": self.nodetemplate.name,
+ args = {"name": self.obj_name,
"level": self.get_property("level"),
"mac": self.get_property("mac")}
return args
@@ -46,7 +46,7 @@
sub.create_user(**xos_args)
sub.save()
- self.info("Created CORDUser %s for Subscriber %s" % (self.nodetemplate.name, sub.name))
+ self.info("Created CORDUser %s for Subscriber %s" % (self.obj_name, sub.name))
def update(self, obj):
pass
diff --git a/xos/tosca/resources/cdnprefix.py b/xos/tosca/resources/cdnprefix.py
index 5faaca8..8daf7fb 100644
--- a/xos/tosca/resources/cdnprefix.py
+++ b/xos/tosca/resources/cdnprefix.py
@@ -16,7 +16,7 @@
copyin_props = []
def get_xos_args(self):
- args = {"prefix": self.nodetemplate.name}
+ args = {"prefix": self.obj_name}
cp_name = self.get_requirement("tosca.relationships.MemberOfContentProvider")
if cp_name:
diff --git a/xos/tosca/resources/compute.py b/xos/tosca/resources/compute.py
index 37ba390..2af010a 100644
--- a/xos/tosca/resources/compute.py
+++ b/xos/tosca/resources/compute.py
@@ -39,7 +39,7 @@
nodetemplate = self.nodetemplate
if not name:
- name = nodetemplate.name
+ name = self.obj_name
args = {"name": name}
@@ -105,7 +105,7 @@
if scalable:
default_instances = scalable.get("default_instances",1)
for i in range(0, default_instances):
- name = "%s-%d" % (self.nodetemplate.name, i)
+ name = "%s-%d" % (self.obj_name, i)
existing_instances = Instance.objects.filter(name=name)
if existing_instances:
self.info("%s %s already exists" % (self.xos_model.__name__, name))
@@ -121,7 +121,7 @@
existing_instances = []
max_instances = scalable.get("max_instances",1)
for i in range(0, max_instances):
- name = "%s-%d" % (self.nodetemplate.name, i)
+ name = "%s-%d" % (self.obj_name, i)
existing_instances = existing_instances + list(Instance.objects.filter(name=name))
return existing_instances
else:
diff --git a/xos/tosca/resources/contentprovider.py b/xos/tosca/resources/contentprovider.py
index 06ca02e..66742ea 100644
--- a/xos/tosca/resources/contentprovider.py
+++ b/xos/tosca/resources/contentprovider.py
@@ -17,7 +17,7 @@
def get_xos_args(self):
sp_name = self.get_requirement("tosca.relationships.MemberOfServiceProvider", throw_exception=True)
sp = self.get_xos_object(ServiceProvider, name=sp_name)
- return {"name": self.nodetemplate.name,
+ return {"name": self.obj_name,
"serviceProvider": sp}
def can_delete(self, obj):
diff --git a/xos/tosca/resources/dashboardview.py b/xos/tosca/resources/dashboardview.py
index 9f7687c..3bce58d 100644
--- a/xos/tosca/resources/dashboardview.py
+++ b/xos/tosca/resources/dashboardview.py
@@ -17,6 +17,14 @@
def get_xos_args(self):
return super(XOSDashboardView, self).get_xos_args()
+ def postprocess(self, obj):
+ for deployment_name in self.get_requirements("tosca.relationships.SupportsDeployment"):
+ deployment = self.get_xos_object(Deployment, deployment_name)
+ if not deployment in obj.deployments.all():
+ print "attaching dashboardview %s to deployment %s" % (obj, deployment)
+ obj.deployments.add(deployment)
+ obj.save()
+
def can_delete(self, obj):
return super(XOSDashboardView, self).can_delete(obj)
diff --git a/xos/tosca/resources/deployment.py b/xos/tosca/resources/deployment.py
index ed6734c..e5ab4b1 100644
--- a/xos/tosca/resources/deployment.py
+++ b/xos/tosca/resources/deployment.py
@@ -31,9 +31,8 @@
imageDep = ImageDeployments(deployment=obj, image=image)
imageDep.save()
- # Be a little more lightweight with 'flavors'. Since we install flavors
- # as a fixture rather than using TOSCA, we can just let the user
- # use a comma-separated list.
+ # DEPRECATED - should switch to using a requirement, so tosca can do
+ # the topsort properly
flavors = self.get_property("flavors")
if flavors:
@@ -47,6 +46,15 @@
flavor.deployments.add(obj)
flavor.save()
+ # The new, right way
+ for flavor in self.get_requirements("tosca.relationships.SupportsFlavor"):
+ flavor = self.get_xos_object(Flavor, name=flavor)
+ if not flavor.deployments.filter(id=obj.id).exists():
+ self.info("Attached flavor %s to deployment %s" % (flavor, obj))
+ flavor.deployments.add(obj)
+ flavor.save()
+
+
rolemap = ( ("tosca.relationships.AdminPrivilege", "admin"), )
self.postprocess_privileges(DeploymentRole, DeploymentPrivilege, rolemap, obj, "deployment")
diff --git a/xos/tosca/resources/deploymentrole.py b/xos/tosca/resources/deploymentrole.py
new file mode 100644
index 0000000..4339026
--- /dev/null
+++ b/xos/tosca/resources/deploymentrole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+# /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, DeploymentRole
+
+from xosresource import XOSResource
+
+class XOSDeploymentRole(XOSResource):
+ provides = "tosca.nodes.DeploymentRole"
+ xos_model = DeploymentRole
+ name_field = "role"
+
+ def get_xos_args(self):
+ args = super(XOSDeploymentRole, self).get_xos_args()
+
+ return args
+
+ def delete(self, obj):
+ super(XOSDeploymentRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/fabricservice.py b/xos/tosca/resources/fabricservice.py
new file mode 100644
index 0000000..0c9cfb4
--- /dev/null
+++ b/xos/tosca/resources/fabricservice.py
@@ -0,0 +1,16 @@
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from services.fabric.models import FabricService
+
+from service import XOSService
+
+class FabricService(XOSService):
+ provides = "tosca.nodes.FabricService"
+ xos_model = FabricService
+ copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber"]
+
diff --git a/xos/tosca/resources/node.py b/xos/tosca/resources/node.py
index 99e756f..128aaed 100644
--- a/xos/tosca/resources/node.py
+++ b/xos/tosca/resources/node.py
@@ -14,7 +14,7 @@
xos_model = Node
def get_xos_args(self):
- args = {"name": self.nodetemplate.name}
+ args = {"name": self.obj_name}
site = None
siteName = self.get_requirement("tosca.relationships.MemberOfSite", throw_exception=False)
@@ -44,9 +44,6 @@
obj.save()
def create(self):
- nodetemplate = self.nodetemplate
- sliceName = nodetemplate.name
-
xos_args = self.get_xos_args()
if not xos_args.get("site", None):
diff --git a/xos/tosca/resources/onosapp.py b/xos/tosca/resources/onosapp.py
index 72511b3..dccc8db 100644
--- a/xos/tosca/resources/onosapp.py
+++ b/xos/tosca/resources/onosapp.py
@@ -33,7 +33,7 @@
def get_existing_objs(self):
objs = ONOSApp.get_tenant_objects().all()
- objs = [x for x in objs if x.name == self.nodetemplate.name]
+ objs = [x for x in objs if x.name == self.obj_name]
return objs
def set_tenant_attr(self, obj, prop_name, value):
diff --git a/xos/tosca/resources/originserver.py b/xos/tosca/resources/originserver.py
index 196ce2e..46cf87e 100644
--- a/xos/tosca/resources/originserver.py
+++ b/xos/tosca/resources/originserver.py
@@ -15,18 +15,18 @@
name_field = "url"
copyin_props = []
- def nodetemplate_name_to_url(self):
- url = self.nodetemplate.name
+ def obj_name_to_url(self):
+ url = self.obj_name
if url.startswith("http_"):
url = url[5:]
return url
def get_existing_objs(self):
- url = self.nodetemplate_name_to_url()
+ url = self.obj_name_to_url()
return self.xos_model.objects.filter(**{self.name_field: url})
def get_xos_args(self):
- url = self.nodetemplate_name_to_url()
+ url = self.obj_name_to_url()
cp_name = self.get_requirement("tosca.relationships.MemberOfContentProvider", throw_exception=True)
cp = self.get_xos_object(ContentProvider, name=cp_name)
return {"url": url,
diff --git a/xos/tosca/resources/serviceprovider.py b/xos/tosca/resources/serviceprovider.py
index 8faec6c..2c9a167 100644
--- a/xos/tosca/resources/serviceprovider.py
+++ b/xos/tosca/resources/serviceprovider.py
@@ -17,7 +17,7 @@
def get_xos_args(self):
hpc_service_name = self.get_requirement("tosca.relationships.MemberOfService", throw_exception=True)
hpc_service = self.get_xos_object(HpcService, name=hpc_service_name)
- return {"name": self.nodetemplate.name,
+ return {"name": self.obj_name,
"hpcService": hpc_service}
def can_delete(self, obj):
diff --git a/xos/tosca/resources/siterole.py b/xos/tosca/resources/siterole.py
new file mode 100644
index 0000000..abb1f0d
--- /dev/null
+++ b/xos/tosca/resources/siterole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+# /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, SiteRole
+
+from xosresource import XOSResource
+
+class XOSSiteRole(XOSResource):
+ provides = "tosca.nodes.SiteRole"
+ xos_model = SiteRole
+ name_field = "role"
+
+ def get_xos_args(self):
+ args = super(XOSSiteRole, self).get_xos_args()
+
+ return args
+
+ def delete(self, obj):
+ super(XOSSiteRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/slice.py b/xos/tosca/resources/slice.py
index 724957f..693d6ab 100644
--- a/xos/tosca/resources/slice.py
+++ b/xos/tosca/resources/slice.py
@@ -31,7 +31,7 @@
default_image = self.get_xos_object(Image, name=default_image_name, throw_exception=True)
args["default_image"] = default_image
- default_flavor_name = self.get_property_default("default_flavor", None)
+ default_flavor_name = self.get_requirement("tosca.relationships.DefaultFlavor", throw_exception=False)
if default_flavor_name:
default_flavor = self.get_xos_object(Flavor, name=default_flavor_name, throw_exception=True)
args["default_flavor"] = default_flavor
@@ -55,19 +55,6 @@
("tosca.relationships.PIPrivilege", "pi"), ("tosca.relationships.TechPrivilege", "tech") )
self.postprocess_privileges(SliceRole, SlicePrivilege, rolemap, obj, "slice")
- def create(self):
- nodetemplate = self.nodetemplate
- sliceName = nodetemplate.name
-
- xos_args = self.get_xos_args()
- slice = Slice(**xos_args)
- slice.caller = self.user
- slice.save()
-
- self.postprocess(slice)
-
- self.info("Created Slice '%s' on Site '%s'" % (str(slice), str(slice.site)))
-
def delete(self, obj):
if obj.instances.exists():
self.info("Slice %s has active instances; skipping delete" % obj.name)
diff --git a/xos/tosca/resources/slicerole.py b/xos/tosca/resources/slicerole.py
new file mode 100644
index 0000000..fc7d3f1
--- /dev/null
+++ b/xos/tosca/resources/slicerole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+# /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, SliceRole
+
+from xosresource import XOSResource
+
+class XOSSliceRole(XOSResource):
+ provides = "tosca.nodes.SliceRole"
+ xos_model = SliceRole
+ name_field = "role"
+
+ def get_xos_args(self):
+ args = super(XOSSliceRole, self).get_xos_args()
+
+ return args
+
+ def delete(self, obj):
+ super(XOSSliceRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/tag.py b/xos/tosca/resources/tag.py
new file mode 100644
index 0000000..001cba8
--- /dev/null
+++ b/xos/tosca/resources/tag.py
@@ -0,0 +1,57 @@
+import importlib
+import os
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+from django.contrib.contenttypes.models import ContentType
+
+from core.models import Tag, Service
+
+from xosresource import XOSResource
+
+class XOSTag(XOSResource):
+ provides = "tosca.nodes.Tag"
+ xos_model = Tag
+ name_field = None
+ copyin_props = ("name", "value")
+
+ def get_xos_args(self, throw_exception=True):
+ args = super(XOSTag, self).get_xos_args()
+
+ # Find the Tosca object that this Tag is pointing to, and return its
+ # content_type and object_id, which will be used in the GenericForeignKey
+ # django relation.
+
+ target_name = self.get_requirement("tosca.relationships.TagsObject", throw_exception=throw_exception)
+ if target_name:
+ target_model = self.engine.name_to_xos_model(self.user, target_name)
+ args["content_type"] = ContentType.objects.get_for_model(target_model)
+ args["object_id"] = target_model.id
+
+ service_name = self.get_requirement("tosca.relationships.MemberOfService", throw_exception=throw_exception)
+ if service_name:
+ args["service"] = self.get_xos_object(Service, name=service_name)
+
+ # To uniquely identify a Tag, we must know the object that it is attached
+ # to as well as the name of the Tag.
+
+ if ("content_type" not in args) or ("object_id" not in args) or ("name" not in args):
+ if throw_exception:
+ raise Exception("Tag must specify TagsObject requirement and Name property")
+
+ return args
+
+ def get_existing_objs(self):
+ args = self.get_xos_args(throw_exception=True)
+
+ return Tag.objects.filter(content_type=args["content_type"],
+ object_id=args["object_id"],
+ name=args["name"])
+
+ def postprocess(self, obj):
+ pass
+
+ def can_delete(self, obj):
+ return super(XOSTag, self).can_delete(obj)
+
diff --git a/xos/tosca/resources/tenantrole.py b/xos/tosca/resources/tenantrole.py
new file mode 100644
index 0000000..316a5a3
--- /dev/null
+++ b/xos/tosca/resources/tenantrole.py
@@ -0,0 +1,29 @@
+# note: this module named xossite.py instead of site.py due to conflict with
+# /usr/lib/python2.7/site.py
+
+import os
+import pdb
+import sys
+import tempfile
+sys.path.append("/opt/tosca")
+from translator.toscalib.tosca_template import ToscaTemplate
+
+from core.models import User, Deployment, TenantRole
+
+from xosresource import XOSResource
+
+class XOSTenantRole(XOSResource):
+ provides = "tosca.nodes.TenantRole"
+ xos_model = TenantRole
+ name_field = "role"
+
+ def get_xos_args(self):
+ args = super(XOSTenantRole, self).get_xos_args()
+
+ return args
+
+ def delete(self, obj):
+ super(XOSTenantRole, self).delete(obj)
+
+
+
diff --git a/xos/tosca/resources/user.py b/xos/tosca/resources/user.py
index 8587c89..55c0423 100644
--- a/xos/tosca/resources/user.py
+++ b/xos/tosca/resources/user.py
@@ -25,7 +25,7 @@
return args
def get_existing_objs(self):
- return self.xos_model.objects.filter(email = self.nodetemplate.name)
+ return self.xos_model.objects.filter(email = self.obj_name)
def postprocess(self, obj):
rolemap = ( ("tosca.relationships.AdminPrivilege", "admin"), ("tosca.relationships.AccessPrivilege", "access"),
@@ -62,12 +62,12 @@
udv.save()
def create(self):
- nodetemplate = self.nodetemplate
-
xos_args = self.get_xos_args()
if not xos_args.get("site",None):
raise Exception("Site name must be specified when creating user")
+ if ("firstname" not in xos_args) or ("lastname" not in xos_args):
+ raise Exception("firstname and lastname must be specified when creating user")
user = User(**xos_args)
user.save()
diff --git a/xos/tosca/resources/vtnservice.py b/xos/tosca/resources/vtnservice.py
index f0940d6..2a5738f 100644
--- a/xos/tosca/resources/vtnservice.py
+++ b/xos/tosca/resources/vtnservice.py
@@ -12,5 +12,4 @@
class XOSVTNService(XOSService):
provides = "tosca.nodes.VTNService"
xos_model = VTNService
- copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber", 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits']
-
+ copyin_props = ["view_url", "icon_url", "enabled", "published", "public_key", "versionNumber", 'privateGatewayMac', 'localManagementIp', 'ovsdbPort', 'sshPort', 'sshUser', 'sshKeyFile', 'mgmtSubnetBits', 'xosEndpoint', 'xosUser', 'xosPassword']
diff --git a/xos/tosca/resources/xosresource.py b/xos/tosca/resources/xosresource.py
index cc4672b..82514c9 100644
--- a/xos/tosca/resources/xosresource.py
+++ b/xos/tosca/resources/xosresource.py
@@ -19,6 +19,17 @@
self.nodetemplate = nodetemplate
self.engine = engine
+ @property
+ def full_name(self):
+ return self.nodetemplate.name
+
+ @property
+ def obj_name(self):
+ if "#" in self.nodetemplate.name:
+ return self.nodetemplate.name.split("#",1)[1]
+ else:
+ return self.nodetemplate.name
+
def get_all_required_node_names(self):
results = []
for reqs in self.nodetemplate.requirements:
@@ -38,7 +49,7 @@
results.append(v["node"])
if (not results) and throw_exception:
- raise Exception("Failed to find requirement in %s using relationship %s" % (self.nodetemplate.name, relationship_name))
+ raise Exception("Failed to find requirement in %s using relationship %s" % (self.full_name, relationship_name))
return results
@@ -67,6 +78,11 @@
return default
def get_xos_object(self, cls, throw_exception=True, **kwargs):
+ # do the same parsing that we do for objname
+ for (k,v) in kwargs.items():
+ if (k=="name") and ("#" in v):
+ kwargs[k] = v.split("#",1)[1]
+
objs = cls.objects.filter(**kwargs)
if not objs:
if throw_exception:
@@ -75,7 +91,7 @@
return objs[0]
def get_existing_objs(self):
- return self.xos_model.objects.filter(**{self.name_field: self.nodetemplate.name})
+ return self.xos_model.objects.filter(**{self.name_field: self.obj_name})
def get_model_class_name(self):
return self.xos_model.__name__
@@ -84,19 +100,19 @@
existing_objs = self.get_existing_objs()
if existing_objs:
if self.get_property_default("no-update", False):
- self.info("%s %s already exists. Skipping update due to 'no-update' property" % (self.get_model_class_name(), self.nodetemplate.name))
+ self.info("%s:%s (%s) already exists. Skipping update due to 'no-update' property" % (self.get_model_class_name(), self.obj_name, self.full_name))
else:
- self.info("%s %s already exists" % (self.get_model_class_name(), self.nodetemplate.name))
+ self.info("%s:%s (%s) already exists" % (self.get_model_class_name(), self.obj_name, self.full_name))
self.update(existing_objs[0])
else:
if self.get_property_default("no-create", False):
- self.info("%s %s does not exist, but 'no-create' is specified" % (self.get_model_class_name(), self.nodetemplate.name))
+ self.info("%s:%s (%s) does not exist, but 'no-create' is specified" % (self.get_model_class_name(), self.obj_name, self.full_name))
else:
self.create()
def can_delete(self, obj):
if self.get_property_default("no-delete",False):
- self.info("%s %s is marked 'no-delete'. Skipping delete." % (self.get_model_class_name(), self.nodetemplate.name))
+ self.info("%s:%s %s is marked 'no-delete'. Skipping delete." % (self.get_model_class_name(), self.obj_name, self.full_name))
return False
return True
@@ -170,7 +186,7 @@
args = {}
if self.name_field:
- args[self.name_field] = self.nodetemplate.name
+ args[self.name_field] = self.obj_name
# copy simple string properties from the template into the arguments
for prop in self.copyin_props:
@@ -186,7 +202,8 @@
def create(self):
xos_args = self.get_xos_args()
xos_obj = self.xos_model(**xos_args)
- xos_obj.caller = self.user
+ if self.user:
+ xos_obj.caller = self.user
xos_obj.save()
self.info("Created %s '%s'" % (self.xos_model.__name__,str(xos_obj)))
diff --git a/xos/tosca/resources/xossite.py b/xos/tosca/resources/xossite.py
index 0db2705..9b03bc5 100644
--- a/xos/tosca/resources/xossite.py
+++ b/xos/tosca/resources/xossite.py
@@ -19,9 +19,9 @@
def get_xos_args(self):
display_name = self.get_property("display_name")
if not display_name:
- display_name = self.nodetemplate.name
+ display_name = self.obj_name
- args = {"login_base": self.nodetemplate.name,
+ args = {"login_base": self.obj_name,
"name": display_name}
# copy simple string properties from the template into the arguments
@@ -33,7 +33,7 @@
return args
def get_existing_objs(self):
- return self.xos_model.objects.filter(login_base = self.nodetemplate.name)
+ return self.xos_model.objects.filter(login_base = self.obj_name)
def postprocess(self, obj):
results = []
@@ -44,19 +44,20 @@
deployment = self.get_xos_object(Deployment, name=deployment_name)
controller_name = None
- for sd_req in v["requirements"]:
+ for sd_req in v.get("requirements", []):
for (sd_req_k, sd_req_v) in sd_req.items():
if sd_req_v["relationship"] == "tosca.relationships.UsesController":
controller_name = sd_req_v["node"]
- if not controller_name:
- raise Exception("Controller must be specified in SiteDeployment relationship")
-
- controller = self.get_xos_object(Controller, name=controller_name, throw_exception=True)
+ if controller_name:
+ controller = self.get_xos_object(Controller, name=controller_name, throw_exception=True)
+ else:
+ controller = None
+ # raise Exception("Controller must be specified in SiteDeployment relationship")
existing_sitedeps = SiteDeployment.objects.filter(deployment=deployment, site=obj)
if existing_sitedeps:
sd = existing_sitedeps[0]
- if sd.controller != controller:
+ if (sd.controller != controller) and (controller != None):
sd.controller = controller
sd.save()
self.info("SiteDeployment from %s to %s updated controller" % (str(obj), str(deployment)))
@@ -67,20 +68,6 @@
sitedep.save()
self.info("Created SiteDeployment from %s to %s" % (str(obj), str(deployment)))
- def create(self):
- nodetemplate = self.nodetemplate
- siteName = nodetemplate.name
-
- xos_args = self.get_xos_args()
-
- site = Site(**xos_args)
- site.caller = self.user
- site.save()
-
- self.postprocess(site)
-
- self.info("Created Site '%s'" % (str(site), ))
-
def delete(self, obj):
if obj.slices.exists():
self.info("Site %s has active slices; skipping delete" % obj.name)
diff --git a/xos/tosca/run.py b/xos/tosca/run.py
index 591582b..58dc22b 100644
--- a/xos/tosca/run.py
+++ b/xos/tosca/run.py
@@ -25,7 +25,10 @@
username = sys.argv[1]
template_name = sys.argv[2]
- u = User.objects.get(email=username)
+ if username.lower()=="none":
+ u=None
+ else:
+ u = User.objects.get(email=username)
xt = XOSTosca(file(template_name).read(), parent_dir=currentdir, log_to_console=True)
xt.execute(u)
diff --git a/xos/tosca/samples/helloworld-chain.yaml b/xos/tosca/samples/helloworld-chain.yaml
deleted file mode 100644
index 8b49106..0000000
--- a/xos/tosca/samples/helloworld-chain.yaml
+++ /dev/null
@@ -1,76 +0,0 @@
-tosca_definitions_version: tosca_simple_yaml_1_0
-
-description: Two services "service_one" and "service_two" with a tenancy relationship.
-
-imports:
- - custom_types/xos.yaml
-
-topology_template:
- node_templates:
-
- Private-Indirect:
- type: tosca.nodes.NetworkTemplate
- properties:
- access: indirect
-
- mysite:
- type: tosca.nodes.Site
-
- trusty-server-multi-nic:
- type: tosca.nodes.Image
-
- service_vsg:
- type: tosca.nodes.VSGService
- requirements:
- - helloworld_tenant:
- node: service_helloworld
- relationship: tosca.relationships.TenantOfService
-
- service_helloworld:
- type: tosca.nodes.Service
- properties:
- kind: helloworldservice_complete
- view_url: /admin/helloworldservice_complete/helloworldservicecomplete/$id$/
-
- tenant_helloworld:
- type: tosca.nodes.Tenant
- properties:
- kind: helloworldservice_complete
- service_specific_attribute: "{\"display_message\": \"Hello World from Tosca\"}"
- model: services.helloworldservice_complete.models.HelloWorldTenantComplete
- requirements:
- - provider_service:
- node: service_helloworld
- relationship: tosca.relationships.MemberOfService
-
-
- mysite_helloworld:
- type: tosca.nodes.Slice
- requirements:
- - service:
- node: service_helloworld
- relationship: tosca.relationships.MemberOfService
- - site:
- node: mysite
- relationship: tosca.relationships.MemberOfSite
- - default_image:
- node: trusty-server-multi-nic
- relationship: tosca.relationships.DefaultImage
- properties:
- default_flavor: m1.small
-
- helloworld_access:
- type: tosca.nodes.network.Network
- properties:
- ip_version: 4
- requirements:
- - network_template:
- node: Private-Indirect
- relationship: tosca.relationships.UsesNetworkTemplate
- - owner:
- node: mysite_helloworld
- relationship: tosca.relationships.MemberOfSlice
- - connection:
- node: mysite_helloworld
- relationship: tosca.relationships.ConnectsToSlice
-
diff --git a/xos/tosca/samples/slice_default_image.yaml b/xos/tosca/samples/slice_default_image.yaml
index 91b95c7..ff63373 100644
--- a/xos/tosca/samples/slice_default_image.yaml
+++ b/xos/tosca/samples/slice_default_image.yaml
@@ -16,6 +16,9 @@
trusty-server-multi-nic:
type: tosca.nodes.Image
+ m1.small:
+ type: tosca.nodes.Flavor
+
mysite_test1:
type: tosca.nodes.Slice
requirements:
@@ -25,6 +28,6 @@
- default_image:
node: trusty-server-multi-nic
relationship: tosca.relationships.DefaultImage
- properties:
- default_flavor: m1.small
-
+ -default_flavor:
+ node: m1.small
+ relationship: tosca.relationships.DefaultFlavor
diff --git a/xos/tosca/samples/slicetag.yaml b/xos/tosca/samples/slicetag.yaml
new file mode 100644
index 0000000..ec064e3
--- /dev/null
+++ b/xos/tosca/samples/slicetag.yaml
@@ -0,0 +1,35 @@
+tosca_definitions_version: tosca_simple_yaml_1_0
+
+description: Setup CORD-related services -- vOLT, vCPE, vBNG.
+
+imports:
+ - custom_types/xos.yaml
+
+topology_template:
+ node_templates:
+ mysite_vsg:
+ type: tosca.nodes.Slice
+ properties:
+ no-create: True
+ no-delete: True
+ no-update: True
+
+ service_vsg:
+ type: tosca.nodes.Service
+ properties:
+ no-create: True
+ no-delete: True
+ no-update: True
+
+ mysite_vsg_foobar_tag:
+ type: tosca.nodes.Tag
+ properties:
+ name: foobar
+ value: xyz
+ requirements:
+ - target:
+ node: mysite_vsg
+ relationship: tosca.relationships.TagsObject
+ - service:
+ node: service_vsg
+ relationship: tosca.relationships.MemberOfService
diff --git a/xos/xos/apibase.py b/xos/xos/apibase.py
index 03493ee..addbbe9 100644
--- a/xos/xos/apibase.py
+++ b/xos/xos/apibase.py
@@ -15,32 +15,27 @@
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
- self.object = self.get_object_or_none()
+ self.object = self.get_object()
if self.object is None:
raise XOSProgrammingError("Use the List API for creating objects")
- serializer = self.get_serializer(self.object, data=request.DATA,
- files=request.FILES, partial=partial)
-
- assert(serializer.object is not None)
-
- serializer.object.caller = request.user
+ serializer = self.get_serializer(self.object, data=request.data, partial=partial)
if not serializer.is_valid():
raise XOSValidationError(fields=serializer._errors)
- if not serializer.object.can_update(request.user):
+ # Do the XOS perm check
+
+ assert(serializer.instance is not None)
+ obj = serializer.instance
+ for attr, value in serializer.validated_data.items():
+ setattr(obj, attr, value)
+ obj.caller = request.user
+ if not obj.can_update(request.user):
raise XOSPermissionDenied()
- if (hasattr(self, "pre_save")):
- # rest_framework 2.x
- self.pre_save(serializer.object)
- self.object = serializer.save(force_update=True)
- self.post_save(self.object, created=False)
- else:
- # rest_framework 3.x
- self.perform_update(serializer)
+ self.perform_update(serializer)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -48,11 +43,7 @@
obj = self.get_object()
obj.caller = request.user
if obj.can_update(request.user):
- # this is the guts of DestroyModelMixin, copied here so that we
- # can use the obj with caller set in it,
- self.pre_delete(obj)
- obj.delete()
- self.post_delete(obj)
+ self.perform_destroy(obj)
return Response(status=status.HTTP_204_NO_CONTENT)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
@@ -69,7 +60,7 @@
class XOSListCreateAPIView(generics.ListCreateAPIView):
def create(self, request, *args, **kwargs):
- serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+ serializer = self.get_serializer(data=request.data)
# In rest_framework 3.x: we can pass raise_exception=True instead of
# raising the exception ourselves
@@ -77,22 +68,12 @@
raise XOSValidationError(fields=serializer._errors)
# now do XOS can_update permission checking
-
- obj = serializer.object
+ obj = serializer.Meta.model(**serializer.validated_data)
obj.caller = request.user
if not obj.can_update(request.user):
raise XOSPermissionDenied()
- # stuff below is from generics.ListCreateAPIView
-
- if (hasattr(self, "pre_save")):
- # rest_framework 2.x
- self.pre_save(serializer.object)
- self.object = serializer.save(force_insert=True)
- self.post_save(self.object, created=True)
- else:
- # rest_framework 3.x
- self.perform_create(serializer)
+ self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
diff --git a/xos/xos/hpcapi.py b/xos/xos/hpcapi.py
index d444684..5b97ab9 100644
--- a/xos/xos/hpcapi.py
+++ b/xos/xos/hpcapi.py
@@ -123,7 +123,8 @@
# Based on serializers.py
class XOSModelSerializer(serializers.ModelSerializer):
- def save_object(self, obj, **kwargs):
+ # TODO: Rest Framework 3.x doesn't support save_object()
+ def NEED_TO_UPDATE_save_object(self, obj, **kwargs):
""" rest_framework can't deal with ManyToMany relations that have a
through table. In xos, most of the through tables we have
diff --git a/xos/xos/settings.py b/xos/xos/settings.py
index c4c0f38..c5123da 100644
--- a/xos/xos/settings.py
+++ b/xos/xos/settings.py
@@ -176,15 +176,20 @@
'core',
'services.hpc',
'services.cord',
+<<<<<<< HEAD
'services.mcord',
'services.helloworldservice_complete',
+=======
+>>>>>>> upstream/master
'services.onos',
'services.ceilometer',
'services.requestrouter',
'services.syndicate_storage',
+ 'services.openvpn',
'services.vtr',
'services.vrouter',
'services.vtn',
+ 'services.fabric',
'geoposition',
'rest_framework_swagger',
)
diff --git a/xos/xos/urls.py b/xos/xos/urls.py
index f11aa24..45c0675 100644
--- a/xos/xos/urls.py
+++ b/xos/xos/urls.py
@@ -10,10 +10,13 @@
from core.views.legacyapi import LegacyXMLRPC
from core.views.serviceGraph import ServiceGridView, ServiceGraphView
+<<<<<<< HEAD
from services.helloworld.view import *
from services.mcord.view import *
# from core.views.analytics import AnalyticsAjaxView
+=======
+>>>>>>> upstream/master
from core.models import *
from rest_framework import generics
from core.dashboard.sites import SitePlus
@@ -61,11 +64,15 @@
urlpatterns = patterns(
'',
url(r'^observer', 'core.views.observer.Observer', name='observer'),
+<<<<<<< HEAD
url(r'^helloworld', HelloWorldView.as_view(), name='helloWorld'),
url(r'^mcord', MCordView.as_view(), name='mcord'),
url(r'^serviceGrid', serviceClass(), name='serviceGrid'),
+=======
+ url(r'^serviceGrid', ServiceGridView.as_view(), name='serviceGrid'),
+>>>>>>> upstream/master
url(r'^serviceGraph.png', ServiceGraphView.as_view(), name='serviceGraph'),
url(r'^hpcConfig', 'core.views.hpc_config.HpcConfig', name='hpcConfig'),
diff --git a/xos/xos/wsgi.py b/xos/xos/wsgi.py
index 9b70770..55c6c1a 100644
--- a/xos/xos/wsgi.py
+++ b/xos/xos/wsgi.py
@@ -28,5 +28,3 @@
application = get_wsgi_application()
# Apply WSGI middleware here.
-# from helloworld.wsgi import HelloWorldApplication
-# application = HelloWorldApplication(application)
diff --git a/xos/xos/xosapi.py b/xos/xos/xosapi.py
index d0a9646..7673f28 100644
--- a/xos/xos/xosapi.py
+++ b/xos/xos/xosapi.py
@@ -605,7 +605,8 @@
# Based on serializers.py
class XOSModelSerializer(serializers.ModelSerializer):
- def save_object(self, obj, **kwargs):
+ # TODO: Rest Framework 3.x doesn't support save_object()
+ def NEED_TO_UPDATE_save_object(self, obj, **kwargs):
""" rest_framework can't deal with ManyToMany relations that have a
through table. In xos, most of the through tables we have