blob: 42a0e7fb7ac77780d5d00277eb5c0d2ff991c5aa [file] [log] [blame]
Scott Bakerfab7c9e2021-07-29 17:12:16 -07001.. vim: syntax=rst
2
3Aether ROC Developer Guide
4==========================
5
6Background / Development Environment
7------------------------------------
8
9This document assumes familiarity with Kubernetes and Helm, and that a Kubernetes/Helm development
10environment has already been deployed in the developers work environment.
Zack Williams1ae109e2021-07-27 11:17:04 -070011
12This development environment can use any of a number of potential mechanisms -- including KinD, kubeadm, etc.
13
Scott Bakerfab7c9e2021-07-29 17:12:16 -070014The Aether-in-a-Box script is one potential way to setup a development environment, but not the only way.
15As an alternative to the developers local machine, a remote environment can be set up, for example on
Zack Williams1ae109e2021-07-27 11:17:04 -070016cloud infrastructure such as Cloudlab.
Scott Bakerfab7c9e2021-07-29 17:12:16 -070017
Sean Condoneb95cd62021-08-04 19:44:18 +010018.. note:: When ROC is deployed it is unsecured by default, with no Authentication or Authorization.
19 To secure ROC so that the Authentication and Authorization can be tested, follow the Securing ROC
20 guide below :ref:`securing_roc`
21
Scott Bakerfab7c9e2021-07-29 17:12:16 -070022Installing Prerequisites
23------------------------
24
25Atomix and onos-operator must be installed::
26
27 # create necessary namespaces
28 kubectl create namespace micro-onos
29
Andy Bavier4c425412021-08-27 14:39:38 -070030 # add repos
31 helm repo add atomix https://charts.atomix.io
32 helm repo add onosproject https://charts.onosproject.org
33 helm repo update
34
Scott Bakerfab7c9e2021-07-29 17:12:16 -070035 # install atomix
Sean Condon70dcf702021-08-24 10:57:29 +010036 export ATOMIX_CONTROLLER_VERSION=0.6.8
Sean Condon257687f2021-08-23 11:13:20 +010037 helm -n kube-system install atomix-controller atomix/atomix-controller --version $ATOMIX_CONTROLLER_VERSION
Sean Condon70dcf702021-08-24 10:57:29 +010038 export ATOMIX_RAFT_VERSION=0.1.9
39 helm -n kube-system install atomix-raft-storage atomix/atomix-raft-storage --version $ATOMIX_RAFT_VERSION
Scott Bakerfab7c9e2021-07-29 17:12:16 -070040
41 # install the onos operator
Sean Condon1df1fcf2021-09-20 09:45:39 +010042 ONOS_OPERATOR_VERSION=0.4.10
Sean Condon257687f2021-08-23 11:13:20 +010043 helm install -n kube-system onos-operator onosproject/onos-operator --version $ONOS_OPERATOR_VERSION
Scott Bakerfab7c9e2021-07-29 17:12:16 -070044
Sean Condon257687f2021-08-23 11:13:20 +010045.. note:: The ROC is sensitive to the versions of Atomix and onos-operator installed. The values
Sean Condon1df1fcf2021-09-20 09:45:39 +010046 shown above are correct for the 1.3.x versions of the *aether-roc-umbrella*.
Sean Condon257687f2021-08-23 11:13:20 +010047
48.. list-table:: ROC support component version matrix
Sean Condon70dcf702021-08-24 10:57:29 +010049 :widths: 40 20 20 20
Sean Condon257687f2021-08-23 11:13:20 +010050 :header-rows: 1
51
52 * - ROC Version
53 - Atomix Controller
54 - Atomix Raft
55 - Onos Operator
Sean Condon70dcf702021-08-24 10:57:29 +010056 * - 1.2.25-1.2.45
Sean Condon257687f2021-08-23 11:13:20 +010057 - 0.6.7
58 - 0.1.8
59 - 0.4.8
Sean Condon70dcf702021-08-24 10:57:29 +010060 * - 1.3.0-
61 - 0.6.8
62 - 0.1.9
Sean Condon1df1fcf2021-09-20 09:45:39 +010063 - 0.4.10
Scott Bakerfab7c9e2021-07-29 17:12:16 -070064
65Verify that these services were installed properly.
66You should see pods for *atomix-controller*, *atomix-raft-storage-controller*,
67*onos-operator-config*, and *onos-operator-topo*.
68Execute these commands::
69
Sean Condon257687f2021-08-23 11:13:20 +010070 helm -n kube-system list
Scott Bakerfab7c9e2021-07-29 17:12:16 -070071 kubectl -n kube-system get pods | grep -i atomix
72 kubectl -n kube-system get pods | grep -i onos
73
Scott Bakerfab7c9e2021-07-29 17:12:16 -070074Create a values-override.yaml
75-----------------------------
76
77Youll want to override several of the defaults in the ROC helm charts::
78
79 cat > values-override.yaml <<EOF
80 import:
Scott Bakerb46a6ed2021-08-02 14:03:10 -070081 onos-gui:
82 enabled: true
Scott Bakerfab7c9e2021-07-29 17:12:16 -070083
84 onos-gui:
Scott Bakerb46a6ed2021-08-02 14:03:10 -070085 ingress:
86 enabled: false
Scott Bakerfab7c9e2021-07-29 17:12:16 -070087
88 aether-roc-gui-v3:
Scott Bakerb46a6ed2021-08-02 14:03:10 -070089 ingress:
90 enabled: false
Scott Bakerfab7c9e2021-07-29 17:12:16 -070091 EOF
92
Zack Williams1ae109e2021-07-27 11:17:04 -070093Installing the ``aether-roc-umbrella`` Helm chart
94-------------------------------------------------
Scott Bakerfab7c9e2021-07-29 17:12:16 -070095
96Add the necessary helm repositories::
97
98 # obtain username and password from Michelle and/or ONF infra team
99 export repo_user=<username>
100 export repo_password=<password>
Sean Condon1df1fcf2021-09-20 09:45:39 +0100101 helm repo add aether --username "$repo_user" --password "$repo_password" https://charts.aetherproject.org
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700102
Zack Williams1ae109e2021-07-27 11:17:04 -0700103``aether-roc-umbrella`` will bring up the ROC and its services::
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700104
Sean Condon1df1fcf2021-09-20 09:45:39 +0100105 helm -n micro-onos install aether-roc-umbrella aether/aether-roc-umbrella -f values-override.yaml
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700106
107 kubectl wait pod -n micro-onos --for=condition=Ready -l type=config --timeout=300s
108
109
Sean Condonf918f642021-08-04 14:32:53 +0100110.. _posting-the-mega-patch:
111
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700112Posting the mega-patch
113----------------------
114
115The ROC usually comes up in a blank state -- there are no Enterprises, UEs, or other artifacts present in it.
116The mega-patch is an example patch that populates the ROC with some sample enterprises, UEs, slices, etc.
117Execute the following::
118
119 # launch a port-forward for the API
120 # this will continue to run in the background
121 kubectl -n micro-onos port-forward service/aether-roc-api --address 0.0.0.0 8181:8181 &
122
123 git clone https://github.com/onosproject/aether-roc-api.git
124
125 # execute the mega-patch (it will post via CURL to localhost:8181)
126 bash ~/path/to/aether-roc-api/examples/MEGA_Patch.curl
127
128
129You may wish to customize the mega patch.
Zack Williams1ae109e2021-07-27 11:17:04 -0700130
131For example, by default the patch configures the ``sdcore-adapter`` to push to
132``sdcore-test-dummy``.
133
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700134You could configure it to push to a live aether-in-a-box core by doing something like this::
135
136 sed -i 's^http://aether-roc-umbrella-sdcore-test-dummy/v1/config/5g^http://webui.omec.svc.cluster.local:9089/config^g' MEGA_Patch.curl
137
138 #apply the patch
139 ./MEGA_Patch.curl
140
141(Note that if your Aether-in-a-Box was installed on a different machine that port-forwarding may be necessary)
142
143
144Expected CURL output from a successful mega-patch post will be a UUID.
Zack Williams1ae109e2021-07-27 11:17:04 -0700145
146You can also verify that the mega-patch was successful by going into the
147``aether-roc-gui`` in a browser (see the section on useful port-forwards
148below). The GUI may open to a dashboard that is unpopulated -- you can use the
149dropdown menu (upper-right hand corner of the screen) to select an object such
150as VCS and you will see a list of VCS.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700151
152 |ROCGUI|
153
Zack Williams1ae109e2021-07-27 11:17:04 -0700154Uninstalling the ``aether-roc-umbrella`` Helm chart
155---------------------------------------------------
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700156
157To tear things back down, usually as part of a developer loop prior to redeploying again, do the following::
158
159 helm -n micro-onos del aether-roc-umbrella
160
161If the uninstall hangs or if a subsequent reinstall hangs, it could be an issue with some of the CRDs
162not getting cleaned up. The following may be useful::
163
164 # fix stuck finalizers in operator CRDs
165
166 kubectl -n micro-onos patch entities connectivity-service-v2 --type json --patch='[ { "op": "remove", "path": "/metadata/finalizers" } ]'
167
168 kubectl -n micro-onos patch entities connectivity-service-v3 --type json --patch='[ { "op": "remove", "path": "/metadata/finalizers" } ]'
169
170 kubectl -n micro-onos patch kind aether --type json --patch='[ { "op": "remove", "path": "/metadata/finalizers" } ]'
171
172Useful port forwards
173--------------------
174
175Port forwarding is often necessary to allow access to ports inside of Kubernetes pods that use ClusterIP addressing.
176Note that you typically need to leave a port-forward running (you can put it in the background).
177Also, If you redeploy the ROC and/or if a pod crashes then you might have to restart a port-forward.
178The following port-forwards may be useful::
179
180 # aether-roc-api
181
182 kubectl -n micro-onos port-forward service/aether-roc-api --address 0.0.0.0 8181:8181
183
184 # aether-roc-gui
185
186 kubectl -n micro-onos port-forward service/aether-roc-gui --address 0.0.0.0 8183:80
187
188 # grafana
189
190 kubectl -n micro-onos port-forward service/aether-roc-umbrella-grafana --address 0.0.0.0 8187:80
191
192 # onos gui
193
194 kubectl -n micro-onos port-forward service/onos-gui --address 0.0.0.0 8182:80
195
Zack Williams1ae109e2021-07-27 11:17:04 -0700196``aether-roc-api`` and ``aether-roc-gui`` are in our experience the most useful two port-forwards.
197
198``aether-roc-api`` is useful to be able to POST REST API requests.
199
200``aether-roc-gui`` is useful to be able to interactively browse the current configuration.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700201
Sean Condon257687f2021-08-23 11:13:20 +0100202.. note:: Internally the ``aether-roc-gui`` operates a Reverse Proxy on the ``aether-roc-api``. This
203 means that if you have done a ``port-forward`` to ``aether-roc-gui`` say on port ``8183`` there's no
204 need to do another on the ``aether-roc-api`` instead you can access the API on
205 ``http://localhost:8183/aether-roc-api``
206
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700207Deploying using custom images
208-----------------------------
209
210Custom images may be used by editing the values-override.yaml file.
Zack Williams1ae109e2021-07-27 11:17:04 -0700211For example, to deploy a custom ``sdcore-adapter``::
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700212
213 sdcore-adapter-v3:
214
215 prometheusEnabled: false
216
217 image:
218
219 repository: my-private-repo/sdcore-adapter
220
221 tag: my-tag
222
223 pullPolicy: Always
224
Zack Williams1ae109e2021-07-27 11:17:04 -0700225The above example assumes you have published a docker images at ``my-private-repo/sdcore-adapter:my-tag``.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700226My particular workflow is to deploy a local-docker registry and push my images to that.
227Please do not publish ONF images to a public repository unless the image is intended to be public.
228Several ONF repositories are private, and therefore their docker artifacts should also be private.
229
230There are alternatives to using a private docker repository.
Zack Williams1ae109e2021-07-27 11:17:04 -0700231For example, if you are using kubeadm, then you may be able to simply tag the image locally.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700232If you’re using KinD, then you can push a local image to into the kind cluster::
233
234 kind load docker-image sdcore-adapter:my-tag
235
Scott Bakerabcfc6e2021-09-08 22:37:51 -0700236Developing using a custom onos-config
237-------------------------------------
238
239The onos-operator is responsible for building model plugins at runtime. To do this, it needs source code
240for onos-config that matches the onos-config image that is deployed. One way to do this is to fork the
241onos-config repository and commit your onos-config changes to a personal repository, and then reference
242that personal repository in the values.yaml. For example::
243
244 onos-config:
245 plugin:
246 compiler:
247 target: "github.com/mygithubaccount/onos-config@mytag"
248 image:
249 repository: mydockeraccount/onos-config
250 tag: mytag
251 pullPolicy: Always
252
253In the above example, the operator will pull the image from `mydockeraccount`, and it'll pull the
254onos-config code from `mygithubaccount`. Using a personal docker account is not strictly necessary;
255images can also be built and tagged entirely locally.
256
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700257Inspecting logs
258---------------
259
260Most of the relevant Kubernetes pods are in the micro-onos namespace.
261The names may change from deployment to deployment, so start by getting a list of pods::
262
263 kubectl -n micro-onos get pods
264
265Then you can inspect a specific pod/container::
266
267 kubectl -n micro-onos logs sdcore-adapter-v3-7468cc58dc-ktctz sdcore-adapter-v3
268
Sean Condoneb95cd62021-08-04 19:44:18 +0100269.. _securing_roc:
270
271Securing ROC
272------------
273
Zack Williams1ae109e2021-07-27 11:17:04 -0700274When deploying ROC with the ``aether-roc-umbrella`` chart, secure mode can be enabled by
Sean Condoneb95cd62021-08-04 19:44:18 +0100275specifying an OpenID Connect (OIDC) issuer like::
276
Andy Baviere86261e2021-09-21 11:05:18 -0700277 helm -n micro-onos install aether-roc-umbrella aether/aether-roc-umbrella \
Sean Condoneb95cd62021-08-04 19:44:18 +0100278 --set onos-config.openidc.issuer=http://dex-ldap-umbrella:5556 \
279 --set aether-roc-gui-v3.openidc.issuer=http://dex-ldap-umbrella:5556
280
Zack Williams1ae109e2021-07-27 11:17:04 -0700281The choice of OIDC issuer in this case is ``dex-ldap-umbrella``.
Sean Condoneb95cd62021-08-04 19:44:18 +0100282
Zack Williams1ae109e2021-07-27 11:17:04 -0700283``dex-ldap-umbrella``
284"""""""""""""""""""""
Sean Condoneb95cd62021-08-04 19:44:18 +0100285
286Dex is a cloud native OIDC Issuer than can act as a front end to several authentication systems
287e.g. LDAP, Crowd, Google, GitHub
288
Zack Williams1ae109e2021-07-27 11:17:04 -0700289``dex-ldap-umbrella`` is a Helm chart that combines a Dex server with an LDAP
290installation, and an LDAP administration tool. It can be deployed in to the
291same cluster namespace as ``aether-roc-umbrella``.
Sean Condoneb95cd62021-08-04 19:44:18 +0100292
293Its LDAP server is populated with 7 different users in the 2 example enterprises - *starbucks* and *acme*.
294
295When running it should be available at *http://dex-ldap-umbrella:5556/.well-known/openid-configuration*.
296
297See `dex-ldap-umbrella <https://github.com/onosproject/onos-helm-charts/tree/master/dex-ldap-umbrella#readme>`_
298for more details.
299
300As an alternative there is a public Dex server connected to the ONF Crowd server, that allows
301ONF staff to login with their own credentials:
302See `public dex <https://dex.aetherproject.org/dex/.well-known/openid-configuration>`_ for more details.
303
304.. note:: Your RBAC access to ROC will be limited by the groups you belong to in Crowd.
305
306Role Based Access Control
Zack Williams1ae109e2021-07-27 11:17:04 -0700307"""""""""""""""""""""""""
308
Sean Condoneb95cd62021-08-04 19:44:18 +0100309When secured, access to the configuration in ROC is limited by the **groups** that a user belongs to.
310
311* **AetherROCAdmin** - users in this group have full read **and** write access to all configuration.
312* *<enterprise>* - users in a group the lowercase name of an enterprise, will have **read** access to that enterprise.
313* **EnterpriseAdmin** - users in this group will have read **and** write access the enterprise they belong to.
314
315 For example in *dex-ldap-umbrella* the user *Daisy Duke* belongs to *starbucks* **and**
316 *EnterpriseAdmin* and so has read **and** write access to items linked with *starbucks* enterprise.
317
318 By comparison the user *Elmer Fudd* belongs only to *starbucks* group and so has only **read** access to items
319 linked with the *starbucks* enterprise.
320
321Requests to a Secure System
Zack Williams1ae109e2021-07-27 11:17:04 -0700322"""""""""""""""""""""""""""
323
Sean Condoneb95cd62021-08-04 19:44:18 +0100324When configuration is retrieved or updated through *aether-config*, a Bearer Token in the
Zack Williams1ae109e2021-07-27 11:17:04 -0700325form of a JSON Web Token (JWT) issued by the selected OIDC Issuer server must accompany
Sean Condoneb95cd62021-08-04 19:44:18 +0100326the request as an Authorization Header.
327
Zack Williams1ae109e2021-07-27 11:17:04 -0700328This applies to both the REST interface of ``aether-roc-api`` **and** the *gnmi* interface of
329``aether-rconfig``.
Sean Condoneb95cd62021-08-04 19:44:18 +0100330
331In the Aether ROC, a Bearer Token can be generated by logging in and selecting API Key from the
332menu. This pops up a window with a copy button, where the key can be copied.
333
334The key will expire after 24 hours.
335
336.. image:: images/aether-roc-gui-copy-api-key.png
337 :width: 580
338 :alt: Aether ROC GUI allows copying of API Key to clipboard
339
340Accessing the REST interface from a tool like Postman, should include this Auth token.
341
342.. image:: images/postman-auth-token.png
343 :width: 930
344 :alt: Postman showing Authentication Token pasted in
345
346Logging
Zack Williams1ae109e2021-07-27 11:17:04 -0700347"""""""
348
Sean Condoneb95cd62021-08-04 19:44:18 +0100349The logs of *aether-config* will contain the **username** and **timestamp** of
350any **gnmi** call when security is enabled.
351
352.. image:: images/aether-config-log.png
353 :width: 887
354 :alt: aether-config log message showing username and timestamp
355
Sean Condon435be9a2021-08-06 14:28:37 +0100356Accessing GUI from an external system
Zack Williams1ae109e2021-07-27 11:17:04 -0700357"""""""""""""""""""""""""""""""""""""
358
Sean Condon435be9a2021-08-06 14:28:37 +0100359To access the ROC GUI from a computer outside the Cluster machine using *port-forwarding* then
360it is necessary to:
361
362* Ensure that all *port-forward*'s have **--address=0.0.0.0**
363* Add to the IP address of the cluster machine to the **/etc/hosts** of the outside computer as::
364
365 <ip address of cluster> dex-ldap-umbrella aether-roc-gui
366* Verify that you can access the Dex server by its name *http://dex-ldap-umbrella:5556/.well-known/openid-configuration*
Zack Williams1ae109e2021-07-27 11:17:04 -0700367* Access the GUI through the hostname (rather than ip address) ``http://aether-roc-gui:8183``
Sean Condon435be9a2021-08-06 14:28:37 +0100368
Sean Condoneb95cd62021-08-04 19:44:18 +0100369Troubleshooting Secure Access
Zack Williams1ae109e2021-07-27 11:17:04 -0700370"""""""""""""""""""""""""""""
371
Sean Condoneb95cd62021-08-04 19:44:18 +0100372While every effort has been made to ensure that securing Aether is simple and effective,
373some difficulties may arise.
374
375One of the most important steps is to validate that the OIDC Issuer (Dex server) can be reached
376from the browser. The **well_known** URL should be available and show the important endpoints are correct.
377
378.. image:: images/dex-ldap-umbrella-well-known.png
379 :width: 580
380 :alt: Dex Well Known page
381
382If logged out of the Browser when accessing the Aether ROC GUI, accessing any page of the application should
383redirect to the Dex login page.
384
385.. image:: images/dex-ldap-login-page.png
386 :width: 493
387 :alt: Dex Login page
388
389When logged in the User details can be seen by clicking the User's name in the drop down menu.
390This shows the **groups** that the user belongs to, and can be used to debug RBAC issues.
391
392.. image:: images/aether-roc-gui-user-details.png
393 :width: 700
394 :alt: User Details page
395
396When you sign out of the ROC GUI, if you are not redirected to the Dex Login Page,
397you should check the Developer Console of the browser. The console should show the correct
Zack Williams1ae109e2021-07-27 11:17:04 -0700398OIDC issuer (Dex server), and that Auth is enabled.
Sean Condoneb95cd62021-08-04 19:44:18 +0100399
400.. image:: images/aether-roc-gui-console-loggedin.png
401 :width: 418
402 :alt: Browser Console showing correct configuration
403
Scott Bakerb46a6ed2021-08-02 14:03:10 -0700404ROC Data Model Conventions and Requirements
405-------------------------------------------
406
407The MEGA-Patch described above will bring up a fully compliant sample data model.
408However, it may be useful to bring up your own data model, customized to a different
409site of sites. This subsection documents conventions and requirements for the Aether
Zack Williams1ae109e2021-07-27 11:17:04 -0700410modeling within the ROC.
Scott Bakerb46a6ed2021-08-02 14:03:10 -0700411
412The ROC models must be configured with the following:
413
414* A default enterprise with the id `defaultent`.
415* A default ip-domain with the id `defaultent-defaultip`.
416* A default site with the id `defaultent-defaultsite`.
417 This site should be linked to the `defaultent` enterprise.
418* A default device group with the id `defaultent-defaultsite-default`.
419 This device group should be linked to the `defaultent-defaultip` ip-domain
420 and the `defaultent-defaultsite` site.
421
422Each Enterprise Site must be configured with a default device group and that default
423device group's name must end in the suffix `-default`. For example, `acme-chicago-default`.
424
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700425Some exercises to get familiar
426------------------------------
427
Zack Williams1ae109e2021-07-27 11:17:04 -07004281. Deploy the ROC and POST the mega-patch, go into the ``aether-roc-gui`` and click
429 through the VCS, DeviceGroup, and other objects to see that they were
430 created as expected.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700431
Zack Williams1ae109e2021-07-27 11:17:04 -07004322. Examine the log of the ``sdcore-adapter-v3`` container. It should be
433 attempting to push the mega-patchs changes. If you dont have a core
434 available, it may be failing the push, but you should see the attempts.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700435
Zack Williams1ae109e2021-07-27 11:17:04 -07004363. Change an object in the GUI. Watch the ``sdcore-adapter-v3`` log file and
437 see that the adapter attempts to push the change.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700438
Zack Williams1ae109e2021-07-27 11:17:04 -07004394. Try POSTing a change via the API. Observe the ``sdcore-adapter-v3`` log
440 file and see that the adapter attempts to push the change.
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700441
Andy Bavierf73c3d22021-08-30 10:29:06 -07004425. Deploy a 5G Aether-in-a-Box (See :doc:`Setting Up Aether-in-a-Box
443 <aiab>`), modify the mega-patch to specify the URL for the Aether-in-a-Box
Zack Williams1ae109e2021-07-27 11:17:04 -0700444 ``webui`` container, POST the mega-patch, and observe that the changes were
445 correctly pushed via the ``sdcore-adapter-v3`` into the ``sd-core``s
446 ``webui`` container (``webui`` container log will show configuration as it
447 is received)
Scott Bakerfab7c9e2021-07-29 17:12:16 -0700448
449.. |ROCGUI| image:: images/rocgui.png
Sean Condoneb95cd62021-08-04 19:44:18 +0100450 :width: 945
451 :alt: ROC GUI showing list of VCS