diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/debian/changelog maas-2.6.0-7802-g59416a869/debian/changelog --- maas-2.6.0~beta2-7695-g691e14ea3/debian/changelog 2019-04-27 20:45:32.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/debian/changelog 2019-06-11 17:57:36.000000000 +0000 @@ -1,4 +1,34 @@ -maas (2.6.0~beta2-7695-g691e14ea3-0ubuntu1) eoan; urgency=medium +maas (2.6.0-7802-g59416a869-0ubuntu1) eoan; urgency=medium + + * New upstream release, MAAS 2.6.0. + + -- Andres Rodriguez Tue, 11 Jun 2019 13:57:36 -0400 + +maas (2.6.0~rc2-7802-g59416a869-0ubuntu1) eoan; urgency=medium + + * New upstream release, MAAS 2.6.0 RC 2. + + -- Andres Rodriguez Mon, 03 Jun 2019 10:08:01 -0400 + +maas (2.6.0~rc1-7799-g70b0fe161-0ubuntu1) eoan; urgency=medium + + * New upstream release, MAAS 2.6.0 RC 1. + + -- Andres Rodriguez Thu, 30 May 2019 08:45:21 -0400 + +maas (2.6.0~beta4-7743-g27b04aaf3-0ubuntu1) eoan; urgency=medium + + * New upstream release, MAAS 2.6.0 beta 4. + + -- Andres Rodriguez Fri, 10 May 2019 16:50:45 -0400 + +maas (2.6.0~beta3-7711-gb79157e92-0ubuntu1) eoan; urgency=medium + + * New upstream release, MAAS 2.6.0 beta 3. + + -- Andres Rodriguez Fri, 03 May 2019 17:37:32 -0400 + +maas (2.6.0~beta2-7695-g691e14ea3-0ubuntu1) disco; urgency=medium * New upstream release, MAAS 2.6.0 beta 2. diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/docs/hacking.rst maas-2.6.0-7802-g59416a869/docs/hacking.rst --- maas-2.6.0~beta2-7695-g691e14ea3/docs/hacking.rst 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/docs/hacking.rst 2019-06-01 02:18:13.000000000 +0000 @@ -173,7 +173,7 @@ default browser but any browser supported by Karma can be used to run the tests.:: - $ ./bin/test.js + $ make test-js If you want to run the JavaScript tests in debug mode so you can inspect the code inside of a running browser you can launch Karma_ manually.:: @@ -183,16 +183,43 @@ .. _Karma: http://karma-runner.github.io/ +Frontend development +==================== + +For faster development, Webpack watch mode can be run with:: + + $ make watch-javascript + JavaScript debugging ^^^^^^^^^^^^^^^^^^^^ -Angularjs debugInfo, which provides hooks for browser debugging tools like Batarang, -is disabled by default. To re-enable debugInfo, run ``angular.reloadWithDebugInfo();`` -in the browser console. +Angularjs debugInfo, which provides hooks for browser debugging tools +like Batarang, is disabled by default. To re-enable debugInfo, +run ``angular.reloadWithDebugInfo();`` in the browser console. See https://docs.angularjs.org/guide/production#disabling-debug-data for details. +JavaScript linting and formatting +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JSLint can be run with:: + + $ make lint-js + +This will also run `prettier-check` which will notify you +if there are formatting issues. + +Prettier can be run in write mode to correct formatting with:: + + $ make format + +ESLint is also available (the intention is to eventually replace JSLint), +and can be run with:: + + $ ./bin/yarn lint + + Production MAAS server debugging ================================ diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/.eslintrc.js maas-2.6.0-7802-g59416a869/.eslintrc.js --- maas-2.6.0~beta2-7695-g691e14ea3/.eslintrc.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/.eslintrc.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,20 +6,19 @@ }, "extends": ["angular", "eslint:recommended"], "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly", + "__dirname": false, "angular": false, - "module": false, + "Atomics": "readonly", "inject": false, - "makeName": false, // TODO: export as named function - "makeInteger": false, // TODO: export as named function - "makeFakeResponse": false // TODO: export as named function + "setTimeout": false, + "SharedArrayBuffer": "readonly" }, "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, "rules": { - "angular/di": [2, "function", { "matchNames": true }] + "angular/di": [2, "function", { "matchNames": true }], + "no-unused-vars": [2, { "args": "none" }] } }; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/HACKING.rst maas-2.6.0-7802-g59416a869/HACKING.rst --- maas-2.6.0~beta2-7695-g691e14ea3/HACKING.rst 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/HACKING.rst 2019-06-01 02:18:13.000000000 +0000 @@ -173,7 +173,7 @@ default browser but any browser supported by Karma can be used to run the tests.:: - $ ./bin/test.js + $ make test-js If you want to run the JavaScript tests in debug mode so you can inspect the code inside of a running browser you can launch Karma_ manually.:: @@ -183,16 +183,43 @@ .. _Karma: http://karma-runner.github.io/ +Frontend development +==================== + +For faster development, Webpack watch mode can be run with:: + + $ make watch-javascript + JavaScript debugging ^^^^^^^^^^^^^^^^^^^^ -Angularjs debugInfo, which provides hooks for browser debugging tools like Batarang, -is disabled by default. To re-enable debugInfo, run ``angular.reloadWithDebugInfo();`` -in the browser console. +Angularjs debugInfo, which provides hooks for browser debugging tools +like Batarang, is disabled by default. To re-enable debugInfo, +run ``angular.reloadWithDebugInfo();`` in the browser console. See https://docs.angularjs.org/guide/production#disabling-debug-data for details. +JavaScript linting and formatting +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +JSLint can be run with:: + + $ make lint-js + +This will also run `prettier-check` which will notify you +if there are formatting issues. + +Prettier can be run in write mode to correct formatting with:: + + $ make format + +ESLint is also available (the intention is to eventually replace JSLint), +and can be run with:: + + $ ./bin/yarn lint + + Production MAAS server debugging ================================ diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/Makefile maas-2.6.0-7802-g59416a869/Makefile --- maas-2.6.0~beta2-7695-g691e14ea3/Makefile 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/Makefile 2019-06-01 02:18:13.000000000 +0000 @@ -393,12 +393,14 @@ -not -path '*-min.js' -a \ '(' -name '*.html' -o -name '*.js' ')' -print0 \ | xargs -r0 -n20 -P4 $(pocketlint) + bin/yarn prettier-check -# Apply automated formatting to all Python files. +# Apply automated formatting to all Python, Sass and Javascript files. format: sources = $(wildcard *.py contrib/*.py) src utilities etc -format: +format: bin/yarn @find $(sources) -name '*.py' -print0 | xargs -r0 utilities/format-imports @find src/ -type f -exec file "{}" ";" | grep CRLF | cut -d ':' -f1 | xargs dos2unix + bin/yarn prettier check: clean test diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/package.json maas-2.6.0-7802-g59416a869/package.json --- maas-2.6.0~beta2-7695-g691e14ea3/package.json 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/package.json 2019-06-01 02:18:13.000000000 +0000 @@ -3,6 +3,8 @@ "build": "NODE_ENV=production webpack --mode production --config webpack.config.js", "build-dev": "NODE_ENV=development webpack --config webpack.config.js", "lint": "eslint ./src/maasserver/static/js/angular/", + "prettier": "prettier --write 'src/maasserver/static/**/*.{js,scss}' '!**/build.scss' '!**/*-min.js' '!**/3rdparty/**/*'", + "prettier-check": "prettier --check 'src/maasserver/static/**/*.{js,scss}' '!**/build.scss' '!**/*-min.js' '!**/3rdparty/**/*'", "watch": "NODE_ENV=development webpack --watch" }, "devDependencies": { @@ -29,8 +31,10 @@ "karma-opera-launcher": "^1.0.0", "karma-phantomjs-launcher": "^1.0.4", "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^3.0.5", "node-sass": "^4.7.2", "phantomjs-prebuilt": "^2.1.16", + "prettier": "^1.17.0", "prop-types": "^15.6.1", "protractor": "^5.3.0", "react": "^16.2.0", diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/requirements.txt maas-2.6.0-7802-g59416a869/requirements.txt --- maas-2.6.0~beta2-7695-g691e14ea3/requirements.txt 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/requirements.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -pyvmomi==6.0.0.2016.6 -git+https://github.com/Supervisor/supervisor@master#egg=supervisor -# XXX this is currently needed for RBAC, should be dropped (and -# python3-macaroonbakery added back to snapcraft.yaml) once it's updated in -# bionic -macaroonbakery==1.2.0 -prometheus_client==0.6.0 diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/snap-data/bind/named.conf maas-2.6.0-7802-g59416a869/snap-data/bind/named.conf --- maas-2.6.0~beta2-7695-g691e14ea3/snap-data/bind/named.conf 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/snap-data/bind/named.conf 2019-06-01 02:18:13.000000000 +0000 @@ -4,10 +4,14 @@ // // If you are adding zones, please do so with MAAS. -options { directory "/var/snap/maas/current/bind/cache"; -auth-nxdomain no; -listen-on-v6 { any; }; -include "/var/snap/maas/current/bind/named.conf.options.inside.maas"; +options { + directory "/var/snap/maas/current/bind/cache"; + pid-file "/var/snap/maas/current/bind/named.pid"; + bindkeys-file "/snap/maas/current/etc/bind/bind.keys"; + session-keyfile "/var/snap/maas/current/bind/session.key"; + auth-nxdomain no; + listen-on-v6 { any; }; + include "/var/snap/maas/current/bind/named.conf.options.inside.maas"; }; include "/var/snap/maas/current/bind/named.conf.maas"; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/snap-data/pypi/README maas-2.6.0-7802-g59416a869/snap-data/pypi/README --- maas-2.6.0~beta2-7695-g691e14ea3/snap-data/pypi/README 1970-01-01 00:00:00.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/snap-data/pypi/README 2019-06-01 02:18:13.000000000 +0000 @@ -0,0 +1,9 @@ +This is a place where all the Python libraries can be put, that need to +come from PyPI, rather than from a deb package. + +The package and the required version should be put in requirements.txt, +which the snapcraft.yaml file references. + +Our preference is to pull dependencies from the Ubuntu archive. +Dependencies should only be added to requirements.txt as a stop-gap +while waiting for the dependency to reach the Ubuntu archive. diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/snap-data/pypi/requirements.txt maas-2.6.0-7802-g59416a869/snap-data/pypi/requirements.txt --- maas-2.6.0~beta2-7695-g691e14ea3/snap-data/pypi/requirements.txt 1970-01-01 00:00:00.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/snap-data/pypi/requirements.txt 2019-06-01 02:18:13.000000000 +0000 @@ -0,0 +1,14 @@ +pyvmomi==6.0.0.2016.6 +git+https://github.com/Supervisor/supervisor@master#egg=supervisor + +# XXX CFFI is needed by PyNaCl which is a macaroonbakery dependency. We force the +# same version as currently in Bionic to avoid conflics. This should be dropped +# once we use macaroonbakery from the archive again +cffi==1.11.5 +# XXX this is currently needed for RBAC, should be dropped (and +# python3-macaroonbakery added back to snapcraft.yaml) once it's updated in +# bionic +macaroonbakery==1.2.0 +# XXX this is currently needed to make multiprocess collector setup work +# correctly +prometheus_client==0.6.0 diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maascli/snappy.py maas-2.6.0-7802-g59416a869/src/maascli/snappy.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maascli/snappy.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maascli/snappy.py 2019-06-01 02:18:13.000000000 +0000 @@ -724,7 +724,7 @@ 'none': [], } - # Requried flags that are in .conf. + # Required flags that are in .conf. setting_flags = ( 'maas_url', 'database_host', 'database_name', @@ -737,22 +737,22 @@ 'config': 'num_workers', }, 'enable_debug': { - 'type': 'bool', + 'type': 'store_true', 'set_value': True, 'config': 'debug', }, 'disable_debug': { - 'type': 'bool', + 'type': 'store_true', 'set_value': False, 'config': 'debug', }, 'enable_debug_queries': { - 'type': 'bool', + 'type': 'store_true', 'set_value': True, 'config': 'debug_queries', }, 'disable_debug_queries': { - 'type': 'bool', + 'type': 'store_true', 'set_value': False, 'config': 'debug_queries', }, @@ -935,7 +935,7 @@ flag_value is not None and current_config.get(flag_key) != flag_value) if should_update: - config_manager.update({flag_key, flag_value}) + config_manager.update({flag_key: flag_value}) restart_required = True elif flag_value: flag_key = flag_info['config'] diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/devices.py maas-2.6.0-7802-g59416a869/src/maasserver/api/devices.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/devices.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/devices.py 2019-06-01 02:18:13.000000000 +0000 @@ -6,6 +6,7 @@ "DevicesHandler", ] +from django.core.exceptions import PermissionDenied from maasserver.api.interfaces import DISPLAYED_INTERFACE_FIELDS from maasserver.api.logger import maaslog from maasserver.api.nodes import ( @@ -234,6 +235,8 @@ parameters. """ form = DeviceWithMACsForm(data=request.data, request=request) + if not form.has_perm(request.user): + raise PermissionDenied() if form.is_valid(): device = form.save() parent = device.parent diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/dnsresources.py maas-2.6.0-7802-g59416a869/src/maasserver/api/dnsresources.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/dnsresources.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/dnsresources.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2016 Canonical Ltd. This software is licensed under the +# Copyright 2016-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """API handlers: `DNSResource`.""" @@ -184,7 +184,8 @@ in this zone. @param (string) "ip_addresses" [required=false] Address (ip or id) to - assign to the dnsresource. + assign to the dnsresource. This creates an A or AAAA record, + for each of the supplied ip_addresses, IPv4 or IPv6, respectively. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing the new DNS @@ -288,10 +289,20 @@ @param (int) "{id}" [required=true] The DNS resource id. @param (string) "fqdn" [required=false] Hostname (with domain) for the - dnsresource. + dnsresource. Either ``fqdn`` or ``name`` and ``domain`` must be + specified. ``fqdn`` is ignored if either ``name`` or ``domain`` is + given. + + @param (string) "name" [required=false] Hostname (without domain). + + @param (string) "domain" [required=false] Domain (name or id). + + @param (string) "address_ttl" [required=false] Default TTL for entries + in this zone. - @param (string) "ip_address" [required=false] Address to assign to the - dnsresource. + @param (string) "ip_addresses" [required=false] Address (ip or id) to + assign to the dnsresource. This creates an A or AAAA record, + for each of the supplied ip_addresses, IPv4 or IPv6, respectively. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing the updated DNS @@ -308,10 +319,32 @@ @error-example "not-found" Not Found """ + data = request.data.copy() + fqdn = data.get('fqdn', None) + name = data.get('name', None) + domainname = data.get('domain', None) + # If the user gave us fqdn and did not give us name/domain, expand + # fqdn. + if domainname is None and name is None and fqdn is not None: + # Assume that we're working with an address, since we ignore + # rrtype and rrdata. + (name, domainname) = separate_fqdn(fqdn, 'A') + data['domain'] = domainname + data['name'] = name + # If the domain is a name, make it an id. + if domainname is not None: + if domainname.isdigit(): + domain = Domain.objects.get_domain_or_404( + domainname, user=request.user, perm=NodePermission.view) + else: + domain = Domain.objects.get_domain_or_404( + "name:%s" % domainname, user=request.user, + perm=NodePermission.view) + data['domain'] = domain.id dnsresource = DNSResource.objects.get_dnsresource_or_404( id, request.user, NodePermission.admin) form = DNSResourceForm( - instance=dnsresource, data=request.data, request=request) + instance=dnsresource, data=data, request=request) if form.is_valid(): return form.save() else: diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/machines.py maas-2.6.0-7802-g59416a869/src/maasserver/api/machines.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/machines.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/machines.py 2019-06-01 02:18:13.000000000 +0000 @@ -684,6 +684,21 @@ system_id=system_id, user=request.user, perm=NodePermission.edit) options = get_allocation_options(request) + # Deploying a node requires re-checking for EDIT permissions. + if not request.user.has_perm(NodePermission.edit, machine): + raise PermissionDenied() + # Deploying with 'install_rackd' requires ADMIN permissions. + if (options.install_rackd and not + request.user.has_perm(NodePermission.admin, machine)): + raise PermissionDenied() + # Deploying with 'install_kvm' requires ADMIN permissions. + if (options.install_kvm and not + request.user.has_perm(NodePermission.admin, machine)): + raise PermissionDenied() + if options.install_kvm and ( + machine.ephemeral_deployment or options.ephemeral_deploy): + raise MAASAPIBadRequest( + "Cannot install KVM host for ephemeral deployments.") if machine.status == NODE_STATUS.READY: with locks.node_acquire: if machine.owner is not None and machine.owner != request.user: @@ -701,21 +716,6 @@ raise NodeStateViolation( "Can't deploy a machine that is in the '{}' state".format( NODE_STATUS_CHOICES_DICT[machine.status])) - # Deploying a node requires re-checking for EDIT permissions. - if not request.user.has_perm(NodePermission.edit, machine): - raise PermissionDenied() - # Deploying with 'install_rackd' requires ADMIN permissions. - if (options.install_rackd and not - request.user.has_perm(NodePermission.admin, machine)): - raise PermissionDenied() - # Deploying with 'install_kvm' requires ADMIN permissions. - if (options.install_kvm and not - request.user.has_perm(NodePermission.admin, machine)): - raise PermissionDenied() - if options.install_kvm and ( - machine.ephemeral_deployment or options.ephemeral_deploy): - raise MAASAPIBadRequest( - "Cannot install KVM host for ephemeral deployments.") if not machine.distro_series and not series: series = Config.objects.get_config('default_distro_series') Form = get_machine_edit_form(request.user) @@ -728,8 +728,6 @@ form.set_hwe_kernel(hwe_kernel=hwe_kernel) if options.install_rackd: form.set_install_rackd(install_rackd=options.install_rackd) - if options.install_kvm: - form.set_install_kvm(install_kvm=options.install_kvm) if options.ephemeral_deploy: form.set_ephemeral_deploy( ephemeral_deploy=options.ephemeral_deploy) @@ -2568,6 +2566,12 @@ @param (string) "destination" [required=true] A list of system_ids to clone the configuration to. + @param (boolean) "interfaces" [required=True] Whether to clone + interface configuration. Defaults to False. + + @param (boolean) "storage" [required=True] Whether to clone storage + configuration. Defaults to False. + @success (http-status-code) "204" 204 @error (http-status-code) "400" 400 diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/nodes.py maas-2.6.0-7802-g59416a869/src/maasserver/api/nodes.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/nodes.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/nodes.py 2019-06-01 02:18:13.000000000 +0000 @@ -16,6 +16,10 @@ from django.db.models import Prefetch from django.http import HttpResponse from django.shortcuts import get_object_or_404 +from formencode.validators import ( + Int, + StringBool, +) from maasserver.api.support import ( admin_method, AnonymousOperationsHandler, @@ -54,6 +58,7 @@ VirtualBlockDevice, ) from maasserver.models.nodeprobeddetails import get_single_probed_details +from maasserver.node_constraint_filter_forms import ReadNodesForm from maasserver.permissions import NodePermission from maasserver.utils.orm import prefetch_queryset from metadataserver.enum import ( @@ -670,6 +675,12 @@ node with the matching hostname will be returned. This can be specified multiple times to see multiple nodes. + @param (int) "cpu_count" [required=false] Only nodes with the specified + minimum number of CPUs will be included. + + @param (string) "mem" [required=false] Only nodes with the specified + minimum amount of RAM (in MiB) will be included. + @param (string) "mac_address" [required=false] Only nodes relating to the node owning the specified MAC address will be returned. This can be specified multiple times to see multiple nodes. @@ -689,12 +700,46 @@ @param (string) "agent_name" [required=false] Only nodes relating to the nodes with matching agent names will be returned. + @param (string) "fabrics" [required=false] Only nodes with interfaces + in specified fabrics will be returned. + + @param (string) "not_fabrics" [required=false] Only nodes with + interfaces not in specified fabrics will be returned. + + @param (string) "vlans" [required=false] Only nodes with interfaces in + specified VLANs will be returned. + + @param (string) "not_vlans" [required=false] Only nodes with interfaces + not in specified VLANs will be returned. + + @param (string) "subnets" [required=false] Only nodes with interfaces + in specified subnets will be returned. + + @param (string) "not_subnets" [required=false] Only nodes with + interfaces not in specified subnets will be returned. + + @param (string) "status" [required=false] Only nodes with specified + status will be returned. + + @param (string) "pod": [required=false] Only nodes that belong to a + specified pod will be returned. + + @param (string) "not_pod": [required=false] Only nodes that don't + belong to a specified pod will be returned. + + @param (string) "pod_type": [required=false] Only nodes that belong to + a pod of the specified type will be returned. + + @param (string) "not_pod_type": [required=false] Only nodes that don't + belong a pod of the specified type will be returned. + @success (http-status-code) "200" 200 @success (json) "success_json" A JSON object containing a list of node objects. @success-example "success_json" [exkey=read-visible-nodes] placeholder text + """ if self.base_model == Node: @@ -715,7 +760,12 @@ )) return nodes else: - nodes = filtered_nodes_list_from_request(request, self.base_model) + form = ReadNodesForm(data=request.GET) + if not form.is_valid(): + raise MAASAPIValidationError(form.errors) + nodes = self.base_model.objects.get_nodes( + request.user, NodePermission.view) + nodes, _, _ = form.filter_nodes(nodes) nodes = nodes.select_related(*NODES_SELECT_RELATED) nodes = prefetch_queryset( nodes, NODES_PREFETCH).order_by('id') @@ -905,7 +955,21 @@ if user_data is not None: user_data = b64decode(user_data) try: - node.start(request.user, user_data=user_data, comment=comment) + # These parameters are passed in the request from + # maasserver.api.machines.deploy when powering on + # the node for deployment. + install_kvm = get_optional_param( + request.POST, 'install_kvm', + default=False, validator=StringBool) + bridge_stp = get_optional_param( + request.POST, 'bridge_stp', default=None, + validator=StringBool) + bridge_fd = get_optional_param( + request.POST, 'bridge_fd', default=None, validator=Int) + node.start( + request.user, user_data=user_data, comment=comment, + install_kvm=install_kvm, bridge_stp=bridge_stp, + bridge_fd=bridge_fd) except StaticIPAddressExhaustion: # The API response should contain error text with the # system_id in it, as that is the primary API key to a node. diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/rackcontrollers.py maas-2.6.0-7802-g59416a869/src/maasserver/api/rackcontrollers.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/rackcontrollers.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/rackcontrollers.py 2019-06-01 02:18:13.000000000 +0000 @@ -157,6 +157,9 @@ @param (string) "zone" [required=false] The name of a valid zone in which to place the given rack controller. + @param (string) "domain" [required=false] The domain for this + controller. If not given the default domain is used. + @success (http-status-code) "200" 200 @success (content) "success-json" A JSON object containing the updated rack-controller object. diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_devices.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_devices.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_devices.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_devices.py 2019-06-01 02:18:13.000000000 +0000 @@ -268,6 +268,15 @@ ], list(parsed_result[0])) + def test_create_no_permission(self): + self.patch(auth, 'validate_user_external_auth').return_value = True + self.useFixture(RBACEnabled()) + self.become_non_local() + response = self.client.post( + reverse('devices_handler'), + {'mac_addresses': ['aa:bb:cc:dd:ee:ff']}) + self.assertEqual(response.status_code, http.client.FORBIDDEN) + def get_device_uri(device): """Return a device's URI on the API.""" diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_dnsresources.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_dnsresources.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_dnsresources.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_dnsresources.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2016 Canonical Ltd. This software is licensed under the +# Copyright 2016-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Tests for DNSResource API.""" @@ -349,6 +349,103 @@ self.assertEqual( http.client.FORBIDDEN, response.status_code, response.content) + def test_update_by_name_domain__id(self): + self.become_admin() + dnsresource = factory.make_DNSResource() + new_name = factory.make_name("dnsresource") + domain = factory.make_Domain() + fqdn = "%s.%s" % (new_name, domain.name) + sip = factory.make_StaticIPAddress() + uri = get_dnsresource_uri(dnsresource) + response = self.client.put(uri, { + "name": new_name, + "domain": domain.id, + "ip_addresses": str(sip.ip), + }) + self.assertEqual( + http.client.OK, response.status_code, response.content) + self.assertEqual( + fqdn, + json.loads( + response.content.decode(settings.DEFAULT_CHARSET))['fqdn']) + self.assertEqual( + sip.ip, + json.loads( + response.content.decode( + settings.DEFAULT_CHARSET))['ip_addresses'][0]['ip']) + + def test_update_by_name_domain__name(self): + self.become_admin() + dnsresource = factory.make_DNSResource() + new_name = factory.make_name("dnsresource") + domain = factory.make_Domain() + fqdn = "%s.%s" % (new_name, domain.name) + sip = factory.make_StaticIPAddress() + uri = get_dnsresource_uri(dnsresource) + response = self.client.put(uri, { + "name": new_name, + "domain": domain.name, + "ip_addresses": str(sip.ip), + }) + self.assertEqual( + http.client.OK, response.status_code, response.content) + self.assertEqual( + fqdn, + json.loads( + response.content.decode(settings.DEFAULT_CHARSET))['fqdn']) + self.assertEqual( + sip.ip, + json.loads( + response.content.decode( + settings.DEFAULT_CHARSET))['ip_addresses'][0]['ip']) + + def test_update_by_fqdn(self): + self.become_admin() + dnsresource = factory.make_DNSResource() + new_name = factory.make_name("dnsresource") + domain = factory.make_Domain() + fqdn = "%s.%s" % (new_name, domain.name) + sip = factory.make_StaticIPAddress() + uri = get_dnsresource_uri(dnsresource) + response = self.client.put(uri, { + "fqdn": fqdn, + "ip_addresses": str(sip.ip), + }) + self.assertEqual( + http.client.OK, response.status_code, response.content) + self.assertEqual( + fqdn, + json.loads( + response.content.decode(settings.DEFAULT_CHARSET))['fqdn']) + self.assertEqual( + sip.ip, + json.loads( + response.content.decode( + settings.DEFAULT_CHARSET))['ip_addresses'][0]['ip']) + + def test_update_multiple_ips(self): + self.become_admin() + dnsresource = factory.make_DNSResource() + new_name = factory.make_name("dnsresource") + domain = factory.make_Domain() + fqdn = "%s.%s" % (new_name, domain.name) + ips = [factory.make_StaticIPAddress() for _ in range(2)] + uri = get_dnsresource_uri(dnsresource) + response = self.client.put(uri, { + "name": new_name, + "domain": domain.id, + "ip_addresses": " ".join([str(ip.ip) for ip in ips]), + }) + self.assertEqual( + http.client.OK, response.status_code, response.content) + self.assertEqual( + fqdn, + json.loads( + response.content.decode(settings.DEFAULT_CHARSET))['fqdn']) + result = json.loads(response.content.decode( + settings.DEFAULT_CHARSET))['ip_addresses'] + self.assertEqual([ip.ip for ip in ips], [ip['ip'] for ip in result]) + def test_delete_deletes_dnsresource(self): self.become_admin() dnsresource = factory.make_DNSResource() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_machine.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_machine.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_machine.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_machine.py 2019-06-01 02:18:13.000000000 +0000 @@ -678,7 +678,8 @@ 'comment': comment, }) self.assertThat(machine_start, MockCalledOnceWith( - self.user, user_data=ANY, comment=comment)) + self.user, user_data=ANY, comment=comment, + install_kvm=ANY, bridge_stp=ANY, bridge_fd=ANY)) def test_POST_deploy_handles_missing_comment(self): machine = factory.make_Node( @@ -698,7 +699,8 @@ 'distro_series': distro_series, }) self.assertThat(machine_start, MockCalledOnceWith( - self.user, user_data=ANY, comment=None)) + self.user, user_data=ANY, comment=None, + install_kvm=ANY, bridge_stp=ANY, bridge_fd=ANY)) def test_POST_deploy_doesnt_reset_power_options_bug_1569102(self): self.become_admin() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_machines.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_machines.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_machines.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_machines.py 2019-06-01 02:18:13.000000000 +0000 @@ -426,7 +426,7 @@ # `default_gateways`, `health_status`, 'special_filesystems' and # 'resource_pool' the number of queries is not the same but it is # proportional to the number of machines. - DEFAULT_NUM = 62 + DEFAULT_NUM = 63 self.assertEqual(DEFAULT_NUM + (10 * 6), num_queries1) self.assertEqual(DEFAULT_NUM + (20 * 6), num_queries2) @@ -538,9 +538,12 @@ 'mac_address': [bad_mac1, bad_mac2, ok_mac], }) self.assertEqual(http.client.BAD_REQUEST, response.status_code) - self.assertIn( - "Invalid MAC address(es): 00:E0:81:DD:D1:ZZ, 00:E0:81:DD:D1:XX", + parsed_result = json.loads( response.content.decode(settings.DEFAULT_CHARSET)) + self.assertEqual( + parsed_result, + {'mac_address': [ + "'00:E0:81:DD:D1:ZZ' is not a valid MAC address."]}) def test_GET_with_agent_name_filters_by_agent_name(self): non_listed_machine = factory.make_Node( @@ -762,7 +765,7 @@ machines_module, 'ComposeMachineForPodsForm', FakeComposer) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} response = self.client.post( reverse('machines_handler'), {'op': 'allocate'}) self.assertEqual(http.client.OK, response.status_code) @@ -780,7 +783,7 @@ machine = factory.make_Node( status=available_status, owner=None, with_boot_disk=True) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -816,7 +819,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -889,7 +892,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -927,7 +930,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -964,7 +967,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -1000,7 +1003,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -1036,7 +1039,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -1071,7 +1074,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -1113,7 +1116,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine response = self.client.post( @@ -1152,7 +1155,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = machine space = factory.make_Space() @@ -1192,7 +1195,7 @@ mock_list_all_usable_architectures.return_value = sorted( pod.architectures) mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') mock_compose.return_value = None response = self.client.post( @@ -1334,8 +1337,7 @@ parsed_result = json.loads( response.content.decode(settings.DEFAULT_CHARSET)) self.assertEqual( - {unknown_constraint: - ["Unable to allocate a machine. No such constraint."]}, + {unknown_constraint: ["No such constraint."]}, parsed_result) def test_POST_allocate_allocates_machine_by_name(self): @@ -1492,7 +1494,7 @@ ])] pod.save() mock_filter_nodes = self.patch(AcquireNodeForm, 'filter_nodes') - mock_filter_nodes.return_value = [], {}, {} + mock_filter_nodes.return_value = Node.objects.none(), {}, {} mock_compose = self.patch(ComposeMachineForPodsForm, 'compose') factory.make_Tag('fast') factory.make_Tag('stable') diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_node.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_node.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_node.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_node.py 2019-06-01 02:18:13.000000000 +0000 @@ -57,6 +57,7 @@ LSHW_OUTPUT_NAME, ) from provisioningserver.rpc.exceptions import PowerActionAlreadyInProgress +from twisted.internet.defer import succeed class NodeAnonAPITest(MAASServerTestCase): @@ -707,6 +708,8 @@ factory.make_Script( script_type=SCRIPT_TYPE.TESTING, tags=['commissioning']) self.patch(node_module.Node, "_power_cycle").return_value = None + self.patch(node_module.Node, "_power_control_node").return_value = ( + succeed(None)) node = factory.make_Node( status=NODE_STATUS.DEPLOYED, owner=factory.make_User()) self.become_admin() @@ -716,6 +719,8 @@ def test_POST_test_tests_machine_with_options(self): self.patch(node_module.Node, "_power_cycle").return_value = None + self.patch(node_module.Node, "_power_control_node").return_value = ( + succeed(None)) node = factory.make_Node( status=NODE_STATUS.DEPLOYED, owner=factory.make_User()) self.become_admin() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_nodes.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_nodes.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_nodes.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_nodes.py 2019-06-01 02:18:13.000000000 +0000 @@ -530,9 +530,12 @@ 'mac_address': [bad_mac1, bad_mac2, ok_mac], }) self.assertEqual(http.client.BAD_REQUEST, response.status_code) - self.assertIn( - "Invalid MAC address(es): 00:E0:81:DD:D1:ZZ, 00:E0:81:DD:D1:XX", + parsed_result = json.loads( response.content.decode(settings.DEFAULT_CHARSET)) + self.assertEqual( + parsed_result, + {'mac_address': [ + "'00:E0:81:DD:D1:ZZ' is not a valid MAC address."]}) def test_GET_with_agent_name_filters_by_agent_name(self): non_listed_node = factory.make_Node( diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_utils.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_utils.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_utils.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_utils.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2012-2016 Canonical Ltd. This software is licensed under the +# Copyright 2012-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Tests for API helpers.""" @@ -138,6 +138,13 @@ extract_oauth_key_from_auth_header( factory.make_oauth_header(oauth_token=token))) + def test_extract_oauth_key_from_auth_header_no_whitespace_rtns_key(self): + token = factory.make_string(18) + auth_header = factory.make_oauth_header(oauth_token=token) + auth_header = auth_header.replace(", ", ",") + self.assertEqual(token, extract_oauth_key_from_auth_header( + auth_header)) + def test_extract_oauth_key_from_auth_header_returns_None_if_missing(self): self.assertIs(None, extract_oauth_key_from_auth_header('')) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_vmfs_datastores.py maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_vmfs_datastores.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/tests/test_vmfs_datastores.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/tests/test_vmfs_datastores.py 2019-06-01 02:18:13.000000000 +0000 @@ -13,6 +13,7 @@ FILESYSTEM_GROUP_TYPE, NODE_STATUS, ) +from maasserver.models.filesystemgroup import VMFS from maasserver.models.partition import MIN_PARTITION_SIZE from maasserver.models.partitiontable import PARTITION_TABLE_EXTRA_SPACE from maasserver.storage_layouts import VMFS6StorageLayout @@ -149,7 +150,9 @@ self.get_vmfs_uri(vmfs)) def test_GET(self): - vmfs = factory.make_VMFS() + part = factory.make_Partition() + name = factory.make_name('datastore') + vmfs = VMFS.objects.create_vmfs(name, [part]) response = self.client.get(self.get_vmfs_uri(vmfs)) self.assertThat(response, HasStatusCode(http.client.OK)) @@ -162,6 +165,10 @@ 'name': Equals(vmfs.name), 'size': Equals(vmfs.get_size()), 'human_size': Equals(human_readable_bytes(vmfs.get_size())), + 'filesystem': Equals({ + 'fstype': 'vmfs6', + 'mount_point': '/vmfs/volumes/%s' % name, + }), })) self.assertEquals( vmfs.filesystems.count(), len(parsed_result['devices'])) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/users.py maas-2.6.0-7802-g59416a869/src/maasserver/api/users.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/users.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/users.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2014-2018 Canonical Ltd. This software is licensed under the +# Copyright 2014-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """API handlers: `User`.""" @@ -165,6 +165,12 @@ @param (string) "{username}" [required=true] The username to delete. + @param (string) "transfer_resources_to" [required=false] An optional + username. If supplied, the allocated resources of the user being + deleted will be transferred to this user. A user can't be removed + unless its resources (machines, IP addresses, ...), are released or + transfered to another user. + @success (http-status-code) "204" 204 """ if request.user.username == username: diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/utils.py maas-2.6.0-7802-g59416a869/src/maasserver/api/utils.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/utils.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/utils.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2012-2016 Canonical Ltd. This software is licensed under the +# Copyright 2012-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Helpers for Piston-based MAAS APIs.""" @@ -193,12 +193,21 @@ :return: The oauth key from the header, or None. """ - for entry in auth_data.split(): - key_value = entry.split('=', 1) - if len(key_value) == 2: - key, value = key_value - if key == 'oauth_token': - return value.rstrip(',').strip('"') + # Values only separated by commas (no whitespace). + if len(auth_data.split()) == 2: + for entry in auth_data.split()[1].split(","): + key_value = entry.split('=', 1) + if len(key_value) == 2: + key, value = key_value + if key == 'oauth_token': + return value.strip('"') + else: + for entry in auth_data.split(): + key_value = entry.split('=', 1) + if len(key_value) == 2: + key, value = key_value + if key == 'oauth_token': + return value.rstrip(',').strip('"') return None diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/vmfs_datastores.py maas-2.6.0-7802-g59416a869/src/maasserver/api/vmfs_datastores.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/api/vmfs_datastores.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/api/vmfs_datastores.py 2019-06-01 02:18:13.000000000 +0000 @@ -34,6 +34,7 @@ 'devices', 'size', 'human_size', + 'filesystem', ) @@ -160,6 +161,19 @@ for filesystem in vmfs.filesystems.all() ] + @classmethod + def filesystem(cls, vmfs): + # XXX: This is almost the same as + # m.api.partitions.PartitionHandler.filesystem. + filesystem = vmfs.virtual_device.get_effective_filesystem() + if filesystem is not None: + return { + 'fstype': filesystem.fstype, + 'mount_point': filesystem.mount_point, + } + else: + return None + def read(self, request, system_id, id): """@description-title Read a VMFS datastore. @description Read a VMFS datastore with the given id on the machine diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/enum.py maas-2.6.0-7802-g59416a869/src/maasserver/enum.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/enum.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/enum.py 2019-06-01 02:18:13.000000000 +0000 @@ -129,6 +129,12 @@ (NODE_STATUS.FAILED_TESTING, "Failed testing"), ) +# A version of NODE_STATUS_CHOICES with one-word labels +NODE_STATUS_SHORT_LABEL_CHOICES = tuple( + sorted( + (attr.lower(), attr.lower()) + for attr in dir(NODE_STATUS) + if not attr.startswith('_') and attr != 'DEFAULT')) NODE_STATUS_CHOICES_DICT = OrderedDict(NODE_STATUS_CHOICES) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/eventloop.py maas-2.6.0-7802-g59416a869/src/maasserver/eventloop.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/eventloop.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/eventloop.py 2019-06-01 02:18:13.000000000 +0000 @@ -46,6 +46,8 @@ from socket import gethostname from maasserver.utils.orm import disable_all_database_connections +from maasserver.utils.threads import deferToDatabase +from provisioningserver.prometheus.metrics import set_global_labels from provisioningserver.utils.twisted import asynchronous from twisted.application.service import ( MultiService, @@ -214,11 +216,19 @@ def startService(self): yield maybeDeferred(self.eventloop.prepare) Service.startService(self) + yield self._set_globals() yield DeferredList([ maybeDeferred(service.startService) for service in self ]) + @inlineCallbacks + def _set_globals(self): + from maasserver.models.node import RegionControllerManager + maas_uuid = yield deferToDatabase( + RegionControllerManager().get_or_create_uuid) + set_global_labels(maas_uuid=maas_uuid, service_type='region') + class RegionEventLoop: """An event loop running in a region controller process. diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/filesystem.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/filesystem.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/filesystem.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/filesystem.py 2019-06-01 02:18:13.000000000 +0000 @@ -84,7 +84,8 @@ filesystem = Filesystem( node=self.node, fstype=self.cleaned_data["fstype"], mount_options=self.cleaned_data["mount_options"], - mount_point=self.cleaned_data["mount_point"]) + mount_point=self.cleaned_data["mount_point"], + acquired=self.node.owner is not None) filesystem.save() return filesystem diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/__init__.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/__init__.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/__init__.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/__init__.py 2019-06-01 02:18:13.000000000 +0000 @@ -874,11 +874,6 @@ self.is_bound = True self.data['install_rackd'] = install_rackd - def set_install_kvm(self, install_kvm=False): - """Sets whether to deploy the rack alongside this machine.""" - self.is_bound = True - self.data['install_kvm'] = install_kvm - def set_ephemeral_deploy(self, ephemeral_deploy=False): """Sets whether to deploy this machine ephemerally.""" self.is_bound = True @@ -916,7 +911,6 @@ 'min_hwe_kernel', 'hwe_kernel', 'install_rackd', - 'install_kvm', 'ephemeral_deploy', 'commission' ) @@ -947,17 +941,13 @@ self.request = request instance = kwargs.get('instance') - self.set_up_initial_device(instance) if instance is not None: self.initial['zone'] = instance.zone.name - def set_up_initial_device(self, instance): - """Initialize the 'parent' field if a device instance was given. - - This is a workaround for Django bug #17657. - """ - if instance is not None and instance.parent is not None: - self.initial['parent'] = instance.parent.system_id + def has_perm(self, user): + # see MAASAuthorizationBackend.has_perm for the logic behind the + # permission check + return user.has_perm(NodePermission.view) def save(self, commit=True): device = super(DeviceForm, self).save(commit=False) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/pods.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/pods.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/pods.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/pods.py 2019-06-01 02:18:13.000000000 +0000 @@ -57,6 +57,7 @@ storage_validator, ) from maasserver.rpc import getClientFromIdentifiers +from maasserver.utils.dns import validate_hostname from maasserver.utils.forms import set_form_error from maasserver.utils.orm import transactional from maasserver.utils.threads import deferToDatabase @@ -429,7 +430,8 @@ 'Node with hostname "%s" already exists' % hostname) self.fields['hostname'] = CharField( - required=False, validators=[duplicated_hostname]) + required=False, validators=[ + duplicated_hostname, validate_hostname]) self.initial['hostname'] = make_unique_hostname() self.fields['domain'] = ModelChoiceField( required=False, queryset=Domain.objects.all()) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/settings.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/settings.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/settings.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/settings.py 2019-06-01 02:18:13.000000000 +0000 @@ -793,7 +793,7 @@ 'default': '', 'form': forms.CharField, 'form_kwargs': { - 'label': 'VMware vCenter server FQDN or IP address.', + 'label': 'VMware vCenter server FQDN or IP address', 'required': False, 'help_text': ( 'VMware vCenter server FQDN or IP address which is passed ' @@ -804,7 +804,7 @@ 'default': '', 'form': forms.CharField, 'form_kwargs': { - 'label': 'VMware vCenter username.', + 'label': 'VMware vCenter username', 'required': False, 'help_text': ( 'VMware vCenter server username which is passed to a deployed ' @@ -815,7 +815,7 @@ 'default': '', 'form': forms.CharField, 'form_kwargs': { - 'label': 'VMware vCenter password.', + 'label': 'VMware vCenter password', 'required': False, 'help_text': ( 'VMware vCenter server password which is passed to a deployed ' @@ -826,7 +826,7 @@ 'default': '', 'form': forms.CharField, 'form_kwargs': { - 'label': 'VMware vCenter datacenter.', + 'label': 'VMware vCenter datacenter', 'required': False, 'help_text': ( 'VMware vCenter datacenter which is passed to a deployed ' diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_device.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_device.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_device.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_device.py 2019-06-01 02:18:13.000000000 +0000 @@ -16,6 +16,7 @@ Interface, ) from maasserver.testing.factory import factory +from maasserver.testing.fixtures import RBACEnabled from maasserver.testing.testcase import MAASServerTestCase from maasserver.utils.forms import get_QueryDict from maasserver.utils.orm import ( @@ -59,6 +60,37 @@ self.assertEqual(parent, device.parent) + def test_has_perm_no_rbac(self): + form = DeviceForm() + self.assertTrue(form.has_perm(factory.make_User())) + + def test_has_perm_rbac_no_permision(self): + self.useFixture(RBACEnabled()) + form = DeviceForm() + self.assertFalse(form.has_perm(factory.make_User())) + + def test_has_perm_rbac_global_admin(self): + self.useFixture(RBACEnabled()) + user = factory.make_admin() + form = DeviceForm() + self.assertTrue(form.has_perm(user)) + + def test_has_perm_rbac_permission_on_pool(self): + rbac = self.useFixture(RBACEnabled()) + user = factory.make_User() + rbac.store.allow( + user.username, factory.make_ResourcePool(), 'admin-machines') + form = DeviceForm() + self.assertTrue(form.has_perm(user)) + + def test_has_perm_rbac_read_permission_on_pool(self): + rbac = self.useFixture(RBACEnabled()) + user = factory.make_User() + rbac.store.allow( + user.username, factory.make_ResourcePool(), 'view') + form = DeviceForm() + self.assertFalse(form.has_perm(user)) + class TestDeviceWithMACsForm(MAASServerTestCase): diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_filesystem.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_filesystem.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_filesystem.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_filesystem.py 2019-06-01 02:18:13.000000000 +0000 @@ -183,13 +183,17 @@ class TestMountNonStorageFilesystemFormScenarios(MAASServerTestCase): scenarios = [ - (displayname, {"fstype": name}) + (displayname, {"fstype": name, "acquired": acquired}) for name, displayname in FILESYSTEM_FORMAT_TYPE_CHOICES + for acquired in [False, True] if name not in Filesystem.TYPES_REQUIRING_STORAGE ] def test_creates_filesystem_with_mount_point_and_options(self): - node = factory.make_Node() + owner = None + if self.acquired: + owner = factory.make_User() + node = factory.make_Node(owner=owner) mount_point = factory.make_absolute_path() mount_options = factory.make_name("options") form = MountNonStorageFilesystemForm(node, data={ @@ -201,7 +205,8 @@ filesystem = form.save() self.assertThat(filesystem, MatchesStructure.byEquality( node=node, fstype=self.fstype, mount_point=mount_point, - mount_options=mount_options, is_mounted=True)) + mount_options=mount_options, is_mounted=True, + acquired=self.acquired)) class TestUnmountNonStorageFilesystemForm(MAASServerTestCase): diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_machine.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_machine.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_machine.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_machine.py 2019-06-01 02:18:13.000000000 +0000 @@ -50,7 +50,6 @@ 'min_hwe_kernel', 'hwe_kernel', 'install_rackd', - 'install_kvm', 'ephemeral_deploy', 'commission', ], list(form.fields)) @@ -370,7 +369,6 @@ 'min_hwe_kernel', 'hwe_kernel', 'install_rackd', - 'install_kvm', 'ephemeral_deploy', 'cpu_count', 'memory', diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_pods.py maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_pods.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/forms/tests/test_pods.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/forms/tests/test_pods.py 2019-06-01 02:18:13.000000000 +0000 @@ -1457,6 +1457,21 @@ {'hostname': ['Node with hostname "test" already exists']}, form.errors) + def test__compose_hostname_with_underscore(self): + request = MagicMock() + pod = make_pod_with_hints() + + form = ComposeMachineForm( + data={'hostname': 'testing_hostname'}, request=request, pod=pod) + self.assertFalse(form.is_valid()) + self.assertEqual( + { + 'hostname': [ + "Host label cannot contain underscore: 'testing_hostname'." + ] + }, + form.errors) + def test__compose_without_commissioning(self): request = MagicMock() pod = make_pod_with_hints() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/macaroon_auth.py maas-2.6.0-7802-g59416a869/src/maasserver/macaroon_auth.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/macaroon_auth.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/macaroon_auth.py 2019-06-01 02:18:13.000000000 +0000 @@ -7,9 +7,11 @@ 'MacaroonAPIAuthentication', 'MacaroonAuthorizationBackend', 'MacaroonDischargeRequest', + 'UserDetails', 'validate_user_external_auth', ] +from collections import namedtuple from datetime import ( datetime, timedelta, @@ -257,6 +259,10 @@ self.status_code = status_code +# Details about a user from the extenral authentication source +UserDetails = namedtuple('UserDetails', ['username', 'fullname', 'email']) + + class MacaroonClient: """A base client for talking JSON with a macaroon based client.""" @@ -265,6 +271,10 @@ self._auth_info = auth_info self._client = _get_bakery_client(auth_info=auth_info) + def get_user_details(self, username: str) -> UserDetails: + """Return details about a user.""" + return UserDetails(username=username, fullname='', email='') + def _request(self, method, url, json=None, status_code=200): cookiejar = self._client.cookies resp = requests.request( @@ -289,6 +299,15 @@ url = auth_info.agents[0].url super(CandidClient, self).__init__(url, auth_info) + def get_user_details(self, username: str) -> UserDetails: + """Return details about a user.""" + url = self._url + quote('/v1/u/{}'.format(username)) + details = self._request('GET', url) + return UserDetails( + username=details['username'], + fullname=details.get('fullname', ''), + email=details.get('email', '')) + def get_groups(self, username): """Return a list of names fro groups a user belongs to.""" url = self._url + quote('/v1/u/{}/groups'.format(username)) @@ -327,10 +346,10 @@ active, superuser = False, False try: if auth_info.type == 'candid': - active, superuser = _validate_user_candid( + active, superuser, details = _validate_user_candid( auth_info, user.username, client=candid_client) elif auth_info.type == 'rbac': - active, superuser = _validate_user_rbac( + active, superuser, details = _validate_user_rbac( auth_info, user.username, client=rbac_client) except UserValidationFailed: return False @@ -338,6 +357,9 @@ if active ^ user.is_active: user.is_active = active user.is_superuser = superuser + # update user details + user.last_name = details.fullname + user.email = details.email user.save() return active @@ -357,7 +379,7 @@ else: # if no admin group is specified, all users are admins superuser = True - return True, superuser + return True, superuser, client.get_user_details(username) def _validate_user_rbac(auth_info, username, client=None): @@ -377,7 +399,10 @@ except APIError: raise UserValidationFailed() - return is_admin or access_to_pools, is_admin + return ( + is_admin or access_to_pools, + is_admin, + client.get_user_details(username)) class _IDClient(bakery.IdentityClient): diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/config.py maas-2.6.0-7802-g59416a869/src/maasserver/models/config.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/config.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/config.py 2019-06-01 02:18:13.000000000 +0000 @@ -105,7 +105,7 @@ 'disk_erase_with_secure_erase': True, 'disk_erase_with_quick_erase': False, # Curtin. - 'curtin_verbose': False, + 'curtin_verbose': True, # Netplan 'force_v1_network_yaml': False, # Analytics. diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/filesystemgroup.py maas-2.6.0-7802-g59416a869/src/maasserver/models/filesystemgroup.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/filesystemgroup.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/filesystemgroup.py 2019-06-01 02:18:13.000000000 +0000 @@ -292,6 +292,9 @@ fstype=FILESYSTEM_TYPE.VMFS6, partition=partition, filesystem_group=vmfs) vmfs.save(force_update=True) + vmfs.virtual_device.filesystem_set.create( + fstype=FILESYSTEM_TYPE.VMFS6, + mount_point='/vmfs/volumes/%s' % name) return vmfs diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/nodeprobeddetails.py maas-2.6.0-7802-g59416a869/src/maasserver/models/nodeprobeddetails.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/nodeprobeddetails.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/nodeprobeddetails.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2013-2018 Canonical Ltd. This software is licensed under the +# Copyright 2013-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Facilities to obtain probed details for nodes. @@ -49,7 +49,8 @@ for script_result in script_set.scriptresult_set.filter( status=SCRIPT_STATUS.PASSED, script_name__in=script_output_nsmap).only( - 'script_name', 'stdout', 'script_id', 'script_set_id'): + 'status', 'script_name', 'stdout', + 'script_id', 'script_set_id'): namespace = script_output_nsmap[script_result.name] details_template[namespace] = script_result.stdout return details_template diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/node.py maas-2.6.0-7802-g59416a869/src/maasserver/models/node.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/node.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/node.py 2019-06-01 02:18:13.000000000 +0000 @@ -1351,6 +1351,9 @@ @property def ephemeral_deployment(self): """Return if node is set to ephemeral deployment.""" + # Devices should always local boot. + if self.is_device: + return False return self.is_diskless or self.ephemeral_deploy def retrieve_storage_layout_issues( @@ -1505,6 +1508,8 @@ """Mark a node as being deployed.""" # Avoid circular dependencies from metadataserver.models import ScriptSet + from maasserver.models.event import Event + if not self.on_network(): raise ValidationError( {"network": @@ -1520,11 +1525,20 @@ self.current_installation_script_set = script_set self.save() + # Create a status message for DEPLOYING. + Event.objects.create_node_event(self, EVENT_TYPES.DEPLOYING) + def end_deployment(self): """Mark a node as successfully deployed.""" + # Avoid circular imports. + from maasserver.models.event import Event + self.status = NODE_STATUS.DEPLOYED self.save() + # Create a status message for DEPLOYED. + Event.objects.create_node_event(self, EVENT_TYPES.DEPLOYED) + def ip_addresses(self): """IP addresses allocated to this node. @@ -1979,6 +1993,7 @@ """ # Avoid circular imports. from metadataserver.models import ScriptSet + from maasserver.models.event import Event # Only commission if power type is configured. if self.power_type == '': @@ -1990,6 +2005,9 @@ user, EVENT_TYPES.REQUEST_NODE_START_COMMISSIONING, action='start commissioning') + # Create a status message for COMMISSIONING. + Event.objects.create_node_event(self, EVENT_TYPES.COMMISSIONING) + # Set the commissioning options on the node. self.enable_ssh = enable_ssh self.skip_bmc_config = skip_bmc_config @@ -2112,6 +2130,7 @@ NodeUserData, ScriptSet, ) + from maasserver.models.event import Event if not user.has_perm(NodePermission.edit, self): # You can't enter test mode on a node you don't own, @@ -2148,6 +2167,9 @@ user, EVENT_TYPES.REQUEST_NODE_START_TESTING, action='start testing') + # Create a status message for COMMISSIONING. + Event.objects.create_node_event(self, EVENT_TYPES.TESTING) + # Set the test options on the node. self.enable_ssh = enable_ssh @@ -2244,10 +2266,6 @@ "node %s is in state %s." % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) - self._register_request_event( - user, EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING, - action='abort commissioning', comment=comment) - try: # Node.stop() has synchronous and asynchronous parts, so catch # exceptions arising synchronously, and chain callbacks to the @@ -2262,6 +2280,17 @@ self.hostname, error) raise else: + # Avoid circular imports. + from maasserver.models.event import Event + + self._register_request_event( + user, EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING, + action='abort commissioning', comment=comment) + + # Create a status message for ABORTED_COMMISSIONING. + Event.objects.create_node_event( + self, EVENT_TYPES.ABORTED_COMMISSIONING) + # Don't permit naive mocking of stop(); it causes too much # confusion when testing. Return a Deferred from side_effect. assert isinstance(stopping, Deferred) or stopping is None @@ -2309,10 +2338,6 @@ "node %s is in state %s." % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) - self._register_request_event( - user, EVENT_TYPES.REQUEST_NODE_ABORT_TESTING, - action='abort testing', comment=comment) - try: # Node.stop() has synchronous and asynchronous parts, so catch # exceptions arising synchronously, and chain callbacks to the @@ -2324,6 +2349,17 @@ self.hostname, error) raise else: + # Avoid circular imports. + from maasserver.models.event import Event + + self._register_request_event( + user, EVENT_TYPES.REQUEST_NODE_ABORT_TESTING, + action='abort testing', comment=comment) + + # Create a status message for ABORTED_TESTING. + Event.objects.create_node_event( + self, EVENT_TYPES.ABORTED_TESTING) + # Don't permit naive mocking of stop(); it causes too much # confusion when testing. Return a Deferred from side_effect. assert isinstance(stopping, Deferred) or stopping is None @@ -2376,10 +2412,6 @@ "node %s is in state %s." % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) - self._register_request_event( - user, EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT, - action='abort deploying', comment=comment) - try: # Node.stop() has synchronous and asynchronous parts, so catch # exceptions arising synchronously, and chain callbacks to the @@ -2391,6 +2423,17 @@ self.hostname, error) raise else: + # Avoid circular imports. + from maasserver.models.event import Event + + self._register_request_event( + user, EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT, + action='abort deploying', comment=comment) + + # Create a status message for ABORTED_DEPLOYMENT. + Event.objects.create_node_event( + self, EVENT_TYPES.ABORTED_DEPLOYMENT) + # Don't permit naive mocking of stop(); it causes too much # confusion when testing. Return a Deferred from side_effect. assert isinstance(stopping, Deferred) or stopping is None @@ -2912,10 +2955,6 @@ "node %s is in state %s." % (self.system_id, NODE_STATUS_CHOICES_DICT[self.status])) - self._register_request_event( - user, EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK, - action='abort disk erasing', comment=comment) - try: # Node.stop() has synchronous and asynchronous parts, so catch # exceptions arising synchronously, and chain callbacks to the @@ -2927,6 +2966,17 @@ self.hostname, error) raise else: + # Avoid circular imports. + from maasserver.models.event import Event + + self._register_request_event( + user, EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK, + action='abort disk erasing', comment=comment) + + # Create a status message for ABORTED_DISK_ERASING. + Event.objects.create_node_event( + self, EVENT_TYPES.ABORTED_DISK_ERASING) + # Don't permit naive mocking of stop(); it causes too much # confusion when testing. Return a Deferred from side_effect. assert isinstance(stopping, Deferred) or stopping is None @@ -2997,6 +3047,9 @@ def _release(self, user=None): """Mark allocated or reserved node as available again and power off. """ + # Avoid circular imports. + from maasserver.models.event import Event + maaslog.info("%s: Releasing node", self.hostname) # Don't perform stop the node if its already off. Doing so will @@ -3055,6 +3108,9 @@ self.install_kvm = False self.save() + # Create a status message for RELEASING. + Event.objects.create_node_event(self, EVENT_TYPES.RELEASING) + # Clear the nodes acquired filesystems. self._clear_acquired_filesystems() @@ -3075,6 +3131,9 @@ final power-down. This method should be the absolute last method called. """ + # Avoid circular imports. + from maasserver.models.event import Event + if self.creation_type == NODE_CREATION_TYPE.DYNAMIC: self.delete() else: @@ -3083,6 +3142,8 @@ self.owner = None self.save() + # Create a status message for RELEASED. + Event.objects.create_node_event(self, EVENT_TYPES.RELEASED) # Remove all set owner data. OwnerData.objects.filter(node=self).delete() @@ -3263,6 +3324,9 @@ @transactional def update_power_state(self, power_state): """Update a node's power state """ + # Avoid circular imports. + from maasserver.models.event import Event + self.power_state = power_state self.power_state_updated = now() mark_ready = ( @@ -3275,14 +3339,26 @@ if self.status == NODE_STATUS.EXITING_RESCUE_MODE: if self.previous_status == NODE_STATUS.DEPLOYED: if power_state == POWER_STATE.ON: + # Create a status message for EXITED_RESCUE_MODE. + Event.objects.create_node_event( + self, EVENT_TYPES.EXITED_RESCUE_MODE) self.status = self.previous_status else: + # Create a status message for FAILED_EXITING_RESCUE_MODE. + Event.objects.create_node_event( + self, EVENT_TYPES.FAILED_EXITING_RESCUE_MODE) self.status = NODE_STATUS.FAILED_EXITING_RESCUE_MODE else: if power_state == POWER_STATE.OFF: self.status = self.previous_status self.owner = None + # Create a status message for EXITED_RESCUE_MODE. + Event.objects.create_node_event( + self, EVENT_TYPES.EXITED_RESCUE_MODE) else: + # Create a status message for FAILED_EXITING_RESCUE_MODE. + Event.objects.create_node_event( + self, EVENT_TYPES.FAILED_EXITING_RESCUE_MODE) self.status = NODE_STATUS.FAILED_EXITING_RESCUE_MODE self.save() @@ -3338,10 +3414,10 @@ # groups and cache sets once filesystems that make up have been cloned. source_groups = list( FilesystemGroup.objects.filter_by_node( - source_node).prefetch_related('filesystems')) + source_node).order_by('id').prefetch_related('filesystems')) source_groups += list( CacheSet.objects.get_cache_sets_for_node( - source_node).prefetch_related('filesystems')) + source_node).order_by('id').prefetch_related('filesystems')) # Clone the model at the physical level. filesystem_map = self._copy_between_block_device_mappings(mapping) @@ -3492,15 +3568,17 @@ self_ptable.pk = None self_ptable.block_device = self_disk self_ptable.save(force_insert=True) - for source_partition in source_ptable.partitions.all(): + source_partitions = source_ptable.partitions.order_by('id') + for source_partition in source_partitions.all(): self_partition = copy.deepcopy(source_partition) self_partition.id = None self_partition.pk = None self_partition.uuid = None self_partition.partition_table = self_ptable self_partition.save(force_insert=True) - for source_filesystem in ( - source_partition.filesystem_set.all()): + source_filesystems = ( + source_partition.filesystem_set.order_by('id')) + for source_filesystem in source_filesystems.all(): if not source_filesystem.acquired: self_filesystem = copy.deepcopy(source_filesystem) self_filesystem.id = None @@ -3511,7 +3589,9 @@ self_filesystem.save(force_insert=True) filesystem_map[source_filesystem.id] = ( self_filesystem) - for source_filesystem in source_disk.filesystem_set.all(): + source_filesystems = ( + source_disk.filesystem_set.order_by('id')) + for source_filesystem in source_filesystems.all(): if not source_filesystem.acquired: self_filesystem = copy.deepcopy(source_filesystem) self_filesystem.id = None @@ -4132,14 +4212,10 @@ """ Return a suitable "purpose" for this boot, e.g. "install". """ - # XXX: allenap bug=1031406 2012-07-31: The boot purpose is - # still in flux. It may be that there will just be an - # "ephemeral" environment and an "install" environment, and - # the differing behaviour between, say, enlistment and - # commissioning - both of which will use the "ephemeral" - # environment - will be governed by varying the preseed or PXE - # configuration. - if self.status in COMMISSIONING_LIKE_STATUSES: + if self.status == NODE_STATUS.DEFAULT and self.is_device: + # Always local boot a device. + return "local" + elif self.status in COMMISSIONING_LIKE_STATUSES: # It is commissioning or disk erasing. The environment (boot # images, kernel options, etc for erasing is the same as that # of commissioning. @@ -4315,10 +4391,16 @@ return NODE_STATUS_CHOICES_DICT[self.status] @transactional - def start(self, user, user_data=None, comment=None): + def start( + self, user, user_data=None, comment=None, install_kvm=None, + bridge_stp=None, bridge_fd=None): if not user.has_perm(NodePermission.edit, self): # You can't start a node you don't own unless you're an admin. raise PermissionDenied() + # Set install_kvm if not already set. + if not self.install_kvm and install_kvm: + self.install_kvm = True + self.save() event = EVENT_TYPES.REQUEST_NODE_START allow_power_cycle = False # If status is ALLOCATED, this start is actually for a deployment. @@ -4328,6 +4410,9 @@ if self.status == NODE_STATUS.ALLOCATED: event = EVENT_TYPES.REQUEST_NODE_START_DEPLOYMENT allow_power_cycle = True + if self.install_kvm: + self._create_acquired_bridges( + bridge_stp=bridge_stp, bridge_fd=bridge_fd) # Bug #1630361: Make sure that there is a maas_facing_server_address in # the same address family as our configured interfaces. # Every node in a real system has a rack controller, but many tests do @@ -4637,13 +4722,11 @@ if power_error is None: message = "Power state queried: %s" % power_state Event.objects.create_node_event( - system_id=self.system_id, - event_type=EVENT_TYPES.NODE_POWER_QUERIED, + self, EVENT_TYPES.NODE_POWER_QUERIED, event_description=message) else: Event.objects.create_node_event( - system_id=self.system_id, - event_type=EVENT_TYPES.NODE_POWER_QUERY_FAILED, + self, EVENT_TYPES.NODE_POWER_QUERY_FAILED, event_description=power_error) return result @@ -4773,6 +4856,7 @@ """Start rescue mode.""" # Avoid circular imports. from metadataserver.models import NodeUserData + from maasserver.models.event import Event if not user.has_perm(NodePermission.edit, self): # You can't enter rescue mode on a node you don't own, @@ -4804,6 +4888,9 @@ self.owner = user self.save() + # Create a status message for ENTERING_RESCUE_MODE. + Event.objects.create_node_event(self, EVENT_TYPES.ENTERING_RESCUE_MODE) + try: cycling = self._power_cycle() except Exception as error: diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/__init__.py maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/__init__.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/__init__.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/__init__.py 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -# Copyright 2015-2017 Canonical Ltd. This software is licensed under the +# Copyright 2015-2019 Canonical Ltd. This software is licensed under the # GNU Affero General Public License version 3 (see the file LICENSE). """Signals coming off models.""" @@ -19,8 +19,10 @@ "nodes", "partitions", "power", + "scriptresult", "services", "staticipaddress", + "subnet", ] from maasserver.models.signals import ( @@ -39,6 +41,8 @@ nodes, partitions, power, + scriptresult, services, staticipaddress, + subnet, ) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/scriptresult.py maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/scriptresult.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/scriptresult.py 1970-01-01 00:00:00.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/scriptresult.py 2019-06-01 02:18:13.000000000 +0000 @@ -0,0 +1,74 @@ +# Copyright 2019 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Emit ScriptResult status transition event.""" + +__all__ = [ + "signals", +] + +from maasserver.models import Event +from maasserver.preseed import CURTIN_INSTALL_LOG +from maasserver.utils.signals import SignalsManager +from metadataserver.enum import ( + RESULT_TYPE, + SCRIPT_STATUS, + SCRIPT_STATUS_CHOICES, +) +from metadataserver.models.scriptresult import ScriptResult +from provisioningserver.events import EVENT_TYPES + + +signals = SignalsManager() + + +def emit_script_result_status_transition_event(instance, old_values, **kwargs): + """Send a status transition event.""" + script_result = instance + [old_status] = old_values + + if (script_result.script_set.result_type == RESULT_TYPE.TESTING and + old_status == SCRIPT_STATUS.PENDING and script_result.status in ( + SCRIPT_STATUS.INSTALLING, SCRIPT_STATUS.RUNNING)): + storage_name = script_result.parameters.get( + 'storage', {}).get('value', {}).get('name') + Event.objects.create_node_event( + script_result.script_set.node, EVENT_TYPES.RUNNING_TEST, + event_description="%s on %s" % ( + script_result.name, storage_name) if storage_name else + script_result.name) + + elif script_result.status in ( + SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT, + SCRIPT_STATUS.ABORTED): + Event.objects.create_node_event( + script_result.script_set.node, + EVENT_TYPES.SCRIPT_DID_NOT_COMPLETE, + event_description="%s %s" % ( + script_result.name, SCRIPT_STATUS_CHOICES[ + script_result.status][1].lower())) + else: + old_status_name = None + new_status_name = None + for status, status_name in SCRIPT_STATUS_CHOICES: + if old_status == status: + old_status_name = status_name + elif script_result.status == status: + new_status_name = status_name + Event.objects.create_node_event( + script_result.script_set.node, + EVENT_TYPES.SCRIPT_RESULT_CHANGED_STATUS, + event_description="%s changed status from '%s' to '%s'" % ( + script_result.name, old_status_name, new_status_name)) + if (CURTIN_INSTALL_LOG == script_result.name and not + script_result.script_set.node.netboot): + Event.objects.create_node_event( + script_result.script_set.node, EVENT_TYPES.REBOOTING) + + +signals.watch_fields( + emit_script_result_status_transition_event, + ScriptResult, ['status'], delete=False) + +# Enable all signals by default. +signals.enable() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/subnet.py maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/subnet.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/subnet.py 1970-01-01 00:00:00.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/subnet.py 2019-06-01 02:18:13.000000000 +0000 @@ -0,0 +1,55 @@ +# Copyright 2019 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Respond to Subnet CIDR changes.""" + +__all__ = [ + "signals", +] + +from django.db.models.signals import post_save +from maasserver.enum import IPADDRESS_TYPE +from maasserver.models import ( + StaticIPAddress, + Subnet, +) +from maasserver.utils.signals import SignalsManager + + +signals = SignalsManager() + + +def update_referenced_ip_addresses(subnet): + """Updates the `StaticIPAddress`'s to ensure that they are linked to the + correct subnet.""" + + # Remove the IP addresses that no longer fall with in the CIDR. + remove_ips = StaticIPAddress.objects.filter( + alloc_type=IPADDRESS_TYPE.USER_RESERVED, subnet_id=subnet.id) + remove_ips = remove_ips.extra( + where=['NOT(ip << %s)'], params=[subnet.cidr]) + remove_ips.update(subnet=None) + + # Add the IP addresses that now fall into CIDR. + add_ips = StaticIPAddress.objects.filter(subnet__isnull=True) + add_ips = add_ips.extra( + where=['ip << %s'], params=[subnet.cidr]) + add_ips.update(subnet_id=subnet.id) + + +def post_created(sender, instance, created, **kwargs): + if created: + update_referenced_ip_addresses(instance) + + +def updated_cidr(instance, old_values, **kwargs): + update_referenced_ip_addresses(instance) + + +signals.watch( + post_save, post_created, sender=Subnet) +signals.watch_fields( + updated_cidr, Subnet, ['cidr'], delete=False) + +# Enable all signals by default. +signals.enable() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/tests/test_scriptresult.py maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/tests/test_scriptresult.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/tests/test_scriptresult.py 1970-01-01 00:00:00.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/tests/test_scriptresult.py 2019-06-01 02:18:13.000000000 +0000 @@ -0,0 +1,158 @@ +# Copyright 2019 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Tests for ScriptResult status transition event.""" + +__all__ = [] + + +import json +import random + +from maasserver.models import Event +from maasserver.preseed import CURTIN_INSTALL_LOG +from maasserver.testing.factory import factory +from maasserver.testing.testcase import MAASServerTestCase +from metadataserver.enum import ( + RESULT_TYPE, + SCRIPT_STATUS, + SCRIPT_STATUS_CHOICES, +) +from provisioningserver.events import ( + EVENT_DETAILS, + EVENT_TYPES, +) + + +class TestStatusTransitionEvent(MAASServerTestCase): + + def test__running_or_installing_emits_event_empty_storage_parameters(self): + + script_result = factory.make_ScriptResult( + status=SCRIPT_STATUS.PENDING, script_set=factory.make_ScriptSet( + result_type=RESULT_TYPE.TESTING), script=factory.make_Script()) + script_result.status = random.choice([ + SCRIPT_STATUS.INSTALLING, SCRIPT_STATUS.RUNNING]) + script_result.parameters = json.dumps({}) + script_result.save() + + latest_event = Event.objects.last() + self.assertEqual( + ( + EVENT_TYPES.RUNNING_TEST, + EVENT_DETAILS[ + EVENT_TYPES.RUNNING_TEST].description, + script_result.name, + ), + ( + latest_event.type.name, + latest_event.type.description, + latest_event.description, + )) + + def test__running_or_installing_emits_event_with_storage_parameters(self): + + script_result = factory.make_ScriptResult( + status=SCRIPT_STATUS.PENDING, script_set=factory.make_ScriptSet( + result_type=RESULT_TYPE.TESTING), script=factory.make_Script()) + script_result.status = random.choice([ + SCRIPT_STATUS.INSTALLING, SCRIPT_STATUS.RUNNING]) + script_result.parameters = json.dumps({ + 'storage': { + 'value': { + 'name': factory.make_name('name') + } + } + }) + script_result.save() + + latest_event = Event.objects.last() + self.assertEqual( + ( + EVENT_TYPES.RUNNING_TEST, + EVENT_DETAILS[ + EVENT_TYPES.RUNNING_TEST].description, + "%s on %s" % (script_result.name, script_result.parameters.get( + 'storage').get('value').get('name')), + ), + ( + latest_event.type.name, + latest_event.type.description, + latest_event.description, + )) + + def test__script_did_not_complete_emits_event(self): + + script_result = factory.make_ScriptResult( + status=SCRIPT_STATUS.RUNNING, script_set=factory.make_ScriptSet( + result_type=RESULT_TYPE.TESTING), script=factory.make_Script()) + script_result.status = random.choice([ + SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT, + SCRIPT_STATUS.ABORTED]) + script_result.save() + + latest_event = Event.objects.last() + self.assertEqual( + ( + EVENT_TYPES.SCRIPT_DID_NOT_COMPLETE, + EVENT_DETAILS[ + EVENT_TYPES.SCRIPT_DID_NOT_COMPLETE].description, + "%s %s" % (script_result.name, SCRIPT_STATUS_CHOICES[ + script_result.status][1].lower()), + ), + ( + latest_event.type.name, + latest_event.type.description, + latest_event.description, + )) + + def test__script_changed_status_emits_event(self): + + old_status = SCRIPT_STATUS.RUNNING + script_result = factory.make_ScriptResult( + status=old_status, script_set=factory.make_ScriptSet( + result_type=RESULT_TYPE.COMMISSIONING), + script=factory.make_Script()) + new_status = SCRIPT_STATUS.PASSED + script_result.status = new_status + script_result.save() + + latest_event = Event.objects.last() + self.assertEqual( + ( + EVENT_TYPES.SCRIPT_RESULT_CHANGED_STATUS, + EVENT_DETAILS[ + EVENT_TYPES.SCRIPT_RESULT_CHANGED_STATUS].description, + "%s changed status from '%s' to '%s'" % ( + script_result.name, SCRIPT_STATUS_CHOICES[old_status][1], + SCRIPT_STATUS_CHOICES[new_status][1]), + ), + ( + latest_event.type.name, + latest_event.type.description, + latest_event.description, + )) + + def test__install_log_emits_event(self): + + old_status = SCRIPT_STATUS.RUNNING + script_result = factory.make_ScriptResult( + status=old_status, script_set=factory.make_ScriptSet( + result_type=RESULT_TYPE.COMMISSIONING), + script=factory.make_Script(name=CURTIN_INSTALL_LOG)) + script_result.status = SCRIPT_STATUS.PASSED + script_result.script_set.node.netboot = False + script_result.save() + + latest_event = Event.objects.last() + self.assertEqual( + ( + EVENT_TYPES.REBOOTING, + EVENT_DETAILS[EVENT_TYPES.REBOOTING].description, + "", + ), + ( + latest_event.type.name, + latest_event.type.description, + latest_event.description, + )) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/tests/test_subnet.py maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/tests/test_subnet.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/signals/tests/test_subnet.py 1970-01-01 00:00:00.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/signals/tests/test_subnet.py 2019-06-01 02:18:13.000000000 +0000 @@ -0,0 +1,66 @@ +# Copyright 2019 Canonical Ltd. This software is licensed under the +# GNU Affero General Public License version 3 (see the file LICENSE). + +"""Test the behaviour of subnet signals.""" + +__all__ = [] + +from maasserver.enum import IPADDRESS_TYPE +from maasserver.testing.factory import factory +from maasserver.testing.testcase import MAASServerTestCase + + +class TestSubnetSignals(MAASServerTestCase): + + scenarios = ( + ('ipv4', { + 'network_maker': factory.make_ipv4_network, + }), + ('ipv6', { + 'network_maker': factory.make_ipv6_network, + }), + ) + + def test_creating_subnet_links_to_existing_ip_address(self): + network = self.network_maker() + ip = factory.pick_ip_in_network(network) + ip_address = factory.make_StaticIPAddress( + ip=ip, alloc_type=IPADDRESS_TYPE.USER_RESERVED) + + # Ensure that for this test to really be testing the logic the + # `StaticIPAddress` needs to not have a subnet assigned. + self.assertIsNone(ip_address.subnet) + + # Creating the subnet, must link the created `StaticIPAddress` to + # that subnet. + subnet = factory.make_Subnet(cidr=network.cidr) + ip_address.refresh_from_db() + self.assertEqual(subnet, ip_address.subnet) + + def test_updating_subnet_removes_existing_ip_address_adds_another(self): + network1 = self.network_maker() + network2 = self.network_maker(but_not=network1) + ip1 = factory.pick_ip_in_network(network1) + ip2 = factory.pick_ip_in_network(network2) + + # Create the second IP address not linked to network2. + ip_address2 = factory.make_StaticIPAddress( + ip=ip2, alloc_type=IPADDRESS_TYPE.USER_RESERVED) + self.assertIsNone(ip_address2.subnet) + + # Create the first IP address assigned to the network. + subnet = factory.make_Subnet(cidr=network1.cidr) + ip_address1 = factory.make_StaticIPAddress( + ip=ip1, alloc_type=IPADDRESS_TYPE.USER_RESERVED, subnet=subnet) + self.assertEqual(subnet, ip_address1.subnet) + + # Update the subnet to have the CIDR of network2. + subnet.cidr = network2.cidr + subnet.gateway_ip = None + subnet.save() + + # IP1 should not have a subnet, and IP2 should not have the subnet. + ip_address1.refresh_from_db() + ip_address2.refresh_from_db() + self.assertIsNone(ip_address1.subnet) + self.assertEqual(subnet, ip_address2.subnet) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/tests/test_filesystemgroup.py maas-2.6.0-7802-g59416a869/src/maasserver/models/tests/test_filesystemgroup.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/tests/test_filesystemgroup.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/tests/test_filesystemgroup.py 2019-06-01 02:18:13.000000000 +0000 @@ -32,6 +32,7 @@ RAID, RAID_SUPERBLOCK_OVERHEAD, RAIDManager, + VMFS, VolumeGroup, VolumeGroupManager, ) @@ -878,6 +879,14 @@ vmfs = factory.make_VMFS() self.assertTrue(vmfs.is_vmfs()) + def test_creating_vmfs_automatically_creates_mounted_fs(self): + part = factory.make_Partition() + name = factory.make_name('datastore') + vmfs = VMFS.objects.create_vmfs(name, [part]) + self.assertEquals( + '/vmfs/volumes/%s' % name, + vmfs.virtual_device.get_effective_filesystem().mount_point) + def test_can_save_new_filesystem_group_without_filesystems(self): fsgroup = FilesystemGroup( group_type=FILESYSTEM_GROUP_TYPE.LVM_VG, diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/tests/test_node.py maas-2.6.0-7802-g59416a869/src/maasserver/models/tests/test_node.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/models/tests/test_node.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/models/tests/test_node.py 2019-06-01 02:18:13.000000000 +0000 @@ -1051,6 +1051,10 @@ node = factory.make_Node(ephemeral_deploy=False) self.assertFalse(node.ephemeral_deployment) + def test_ephemeral_deployment_checks_is_device(self): + device = factory.make_Device() + self.assertFalse(device.ephemeral_deployment) + def test_system_id_is_a_valid_znum(self): node = factory.make_Node() self.assertThat( @@ -1961,19 +1965,20 @@ self.expectThat(node.owner, Equals(owner)) self.expectThat(node.agent_name, Equals(agent_name)) - def test_abort_disk_erasing_logs_user_request(self): + def test_abort_disk_erasing_logs_user_request_and_creates_sts_msg(self): owner = factory.make_User() node = factory.make_Node(status=NODE_STATUS.DISK_ERASING, owner=owner) node_stop = self.patch(node, '_stop') # Return a post-commit hook from Node.stop(). node_stop.side_effect = lambda user: post_commit() self.patch(Node, "_set_status") - register_event = self.patch(node, '_register_request_event') with post_commit_hooks: node.abort_disk_erasing(owner) - self.assertThat(register_event, MockCalledOnceWith( - owner, EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK, - action='abort disk erasing', comment=None)) + events = Event.objects.filter(node=node) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_ABORT_ERASE_DISK) + self.assertEqual( + events[1].type.name, EVENT_TYPES.ABORTED_DISK_ERASING) def test_start_disk_erasing_reverts_to_sane_state_on_error(self): # If start_disk_erasing encounters an error when calling start(), it @@ -2089,7 +2094,7 @@ node.abort_operation(user) self.assertThat(abort_testing, MockCalledOnceWith(user, None)) - def test_abort_deployment_logs_user_request(self): + def test_abort_deployment_logs_user_request_and_creates_sts_msg(self): agent_name = factory.make_name('agent-name') admin = factory.make_admin() node = factory.make_Node( @@ -2098,12 +2103,13 @@ self.patch(Node, "_clear_status_expires") self.patch(Node, "_set_status") self.patch(Node, "_stop").return_value = None - register_event = self.patch(node, '_register_request_event') with post_commit_hooks: node.abort_deploying(admin) - self.assertThat(register_event, MockCalledOnceWith( - admin, EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT, - action='abort deploying', comment=None)) + events = Event.objects.filter(node=node) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_ABORT_DEPLOYMENT) + self.assertEqual( + events[1].type.name, EVENT_TYPES.ABORTED_DEPLOYMENT) def test_abort_deployment_sets_script_result_to_aborted(self): node = factory.make_Node( @@ -2149,17 +2155,18 @@ self.assertThat(mock_set_status, MockCalledOnceWith( node.system_id, status=status)) - def test_abort_testing_logs_user_request(self): + def test_abort_testing_logs_user_request_and_creates_sts_msg(self): node = factory.make_Node(status=NODE_STATUS.TESTING) admin = factory.make_admin() self.patch(Node, "_set_status") self.patch(Node, "_stop").return_value = None - register_event = self.patch(node, '_register_request_event') with post_commit_hooks: node.abort_testing(admin) - self.assertThat(register_event, MockCalledOnceWith( - admin, EVENT_TYPES.REQUEST_NODE_ABORT_TESTING, - action='abort testing', comment=None)) + events = Event.objects.filter(node=node) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_ABORT_TESTING) + self.assertEqual( + events[1].type.name, EVENT_TYPES.ABORTED_TESTING) def test_abort_testing_logs_and_raises_errors_in_stopping(self): admin = factory.make_admin() @@ -2310,7 +2317,7 @@ self.expectThat(mock_stop, MockCalledOnceWith(node.owner)) self.expectThat(mock_finalize_release, MockCalledOnceWith()) - def test_release_node_that_has_power_off(self): + def test_release_node_that_has_power_off_and_creates_status_messages(self): agent_name = factory.make_name('agent-name') owner = factory.make_User() owner_data = { @@ -2324,6 +2331,9 @@ node.power_state = POWER_STATE.OFF with post_commit_hooks: node.release() + events = Event.objects.filter(node=node) + self.expectThat(events[1].type.name, Equals(EVENT_TYPES.RELEASING)) + self.expectThat(events[2].type.name, Equals(EVENT_TYPES.RELEASED)) self.expectThat(node._stop, MockNotCalled()) self.expectThat(Node._set_status_expires, MockNotCalled()) self.expectThat(node.status, Equals(NODE_STATUS.READY)) @@ -2517,17 +2527,16 @@ node.release() self.assertFalse(node.install_rackd) - def test_release_logs_user_request(self): + def test_release_logs_user_request_and_creates_sts_msg(self): owner = factory.make_User() node = factory.make_Node(status=NODE_STATUS.ALLOCATED, owner=owner) self.patch(node, '_stop') self.patch(node, '_set_status') - register_event = self.patch(node, '_register_request_event') with post_commit_hooks: node.release(owner) - self.assertThat(register_event, MockCalledOnceWith( - owner, EVENT_TYPES.REQUEST_NODE_RELEASE, action='release', - comment=None)) + events = Event.objects.filter(node=node) + self.assertEqual(events[0].type.name, EVENT_TYPES.REQUEST_NODE_RELEASE) + self.assertEqual(events[1].type.name, EVENT_TYPES.RELEASING) def test_release_clears_osystem_and_distro_series(self): node = factory.make_Node( @@ -3052,10 +3061,9 @@ "%s: Could not start node for commissioning: %s", node.hostname, exception)) - def test_start_commissioning_logs_user_request(self): + def test_start_commissioning_logs_user_request_creates_sts_msg(self): node = factory.make_Node( interface=True, status=NODE_STATUS.NEW, power_type='manual') - register_event = self.patch(node, '_register_request_event') node_start = self.patch(node, '_start') # Return a post-commit hook from Node.start(). node_start.side_effect = lambda *args, **kwargs: post_commit() @@ -3063,9 +3071,10 @@ node.start_commissioning(admin) post_commit_hooks.reset() # Ignore these for now. node = reload_object(node) - self.assertThat(register_event, MockCalledOnceWith( - admin, EVENT_TYPES.REQUEST_NODE_START_COMMISSIONING, - action='start commissioning')) + events = Event.objects.filter(node=node) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_START_COMMISSIONING) + self.assertEqual(events[1].type.name, EVENT_TYPES.COMMISSIONING) def test_abort_commissioning_reverts_to_sane_state_on_error(self): # If abort commissioning hits an error when trying to stop the @@ -3130,18 +3139,19 @@ call(node.current_commissioning_script_set_id), call(node.current_testing_script_set_id))) - def test_abort_commissioning_logs_user_request(self): + def test_abort_commissioning_logs_user_request_and_creates_sts_msg(self): node = factory.make_Node(status=NODE_STATUS.COMMISSIONING) admin = factory.make_admin() self.patch(Node, "_clear_status_expires") self.patch(Node, "_set_status") self.patch(Node, "_stop").return_value = None - register_event = self.patch(node, '_register_request_event') with post_commit_hooks: node.abort_commissioning(admin) - self.assertThat(register_event, MockCalledOnceWith( - admin, EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING, - action='abort commissioning', comment=None)) + events = Event.objects.filter(node=node) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_ABORT_COMMISSIONING) + self.assertEqual( + events[1].type.name, EVENT_TYPES.ABORTED_COMMISSIONING) def test_abort_commissioning_logs_and_raises_errors_in_stopping(self): admin = factory.make_admin() @@ -3264,19 +3274,19 @@ self.assertRaises( ValidationError, node.start_testing, admin) - def test_start_testing_logs_user_request(self): + def test_start_testing_logs_user_request_creates_sts_msg(self): script = factory.make_Script(script_type=SCRIPT_TYPE.TESTING) node = factory.make_Node( interface=True, status=NODE_STATUS.DEPLOYED, power_type='manual') - register_event = self.patch(node, '_register_request_event') self.patch(node, '_power_cycle').return_value = None admin = factory.make_admin() node.start_testing(admin, testing_scripts=[script.name]) + events = Event.objects.filter(node=node) post_commit_hooks.reset() # Ignore these for now. node = reload_object(node) - self.assertThat(register_event, MockCalledOnceWith( - admin, EVENT_TYPES.REQUEST_NODE_START_TESTING, - action='start testing')) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_START_TESTING) + self.assertEqual(events[1].type.name, EVENT_TYPES.TESTING) def test_start_testing_changes_status_and_starts_node(self): script = factory.make_Script(script_type=SCRIPT_TYPE.TESTING) @@ -3891,17 +3901,50 @@ self.assertThat( node.status, Equals(NODE_STATUS.FAILED_EXITING_RESCUE_MODE)) - def test_end_deployment_changes_state(self): + def test_update_power_state_fails_exiting_rescue_mode_status_msg(self): + node = factory.make_Node( + status=NODE_STATUS.EXITING_RESCUE_MODE, + previous_status=NODE_STATUS.DEPLOYED) + node.update_power_state(POWER_STATE.OFF) + event = Event.objects.last() + self.assertEqual( + event.type.name, EVENT_TYPES.FAILED_EXITING_RESCUE_MODE) + + def test_update_power_state_creates_status_message_for_deployed(self): + node = factory.make_Node( + status=NODE_STATUS.EXITING_RESCUE_MODE, + previous_status=NODE_STATUS.DEPLOYED) + node.update_power_state(POWER_STATE.ON) + event = Event.objects.get(node=node) + self.assertThat( + node.status, Equals(NODE_STATUS.DEPLOYED)) + self.assertEqual(event.type.name, EVENT_TYPES.EXITED_RESCUE_MODE) + + def test_update_power_state_creates_status_message_for_non_deployed(self): + node = factory.make_Node( + status=NODE_STATUS.EXITING_RESCUE_MODE, + previous_status=NODE_STATUS.READY) + node.update_power_state(POWER_STATE.OFF) + event = Event.objects.get(node=node) + self.assertThat( + node.status, Equals(NODE_STATUS.READY)) + self.assertEqual(event.type.name, EVENT_TYPES.EXITED_RESCUE_MODE) + + def test_end_deployment_changes_state_and_creates_sts_msg(self): self.disable_node_query() node = factory.make_Node(status=NODE_STATUS.DEPLOYING) node.end_deployment() + event = Event.objects.get(node=node) self.assertEqual(NODE_STATUS.DEPLOYED, reload_object(node).status) + self.assertEqual(event.type.name, EVENT_TYPES.DEPLOYED) - def test_start_deployment_changes_state(self): + def test_start_deployment_changes_state_and_creates_sts_msg(self): node = factory.make_Node_with_Interface_on_Subnet( status=NODE_STATUS.ALLOCATED) node._start_deployment() + event = Event.objects.get(node=node) self.assertEqual(NODE_STATUS.DEPLOYING, reload_object(node).status) + self.assertEqual(event.type.name, EVENT_TYPES.DEPLOYING) def test_start_deployment_creates_installation_script_set(self): node = factory.make_Node_with_Interface_on_Subnet( @@ -3951,6 +3994,9 @@ ("local", {"status": NODE_STATUS.DEPLOYING, "netboot": False}), ("local", {"status": NODE_STATUS.DEPLOYED}), ("poweroff", {"status": NODE_STATUS.RETIRED}), + ("local", { + "status": NODE_STATUS.DEFAULT, + "node_type": NODE_TYPE.DEVICE}), ] node = factory.make_Node() mock_get_boot_images_for = self.patch( @@ -4557,10 +4603,9 @@ self.assertRaises( UnknownPowerType, node.start_rescue_mode, factory.make_admin()) - def test_start_rescue_mode_logs_user_request(self): + def test_start_rescue_mode_logs_user_request_and_creates_sts_msg(self): node = factory.make_Node(status=random.choice([ NODE_STATUS.READY, NODE_STATUS.BROKEN, NODE_STATUS.DEPLOYED])) - mock_register_event = self.patch(node, '_register_request_event') mock_node_power_cycle = self.patch(node, '_power_cycle') # Return a post-commit hook from Node.power_cycle(). mock_node_power_cycle.side_effect = lambda: post_commit() @@ -4568,10 +4613,10 @@ node.start_rescue_mode(admin) post_commit_hooks.reset() # Ignore these for now. node = reload_object(node) - self.assertThat( - mock_register_event, MockCalledOnceWith( - admin, EVENT_TYPES.REQUEST_NODE_START_RESCUE_MODE, - action='start rescue mode')) + events = Event.objects.filter(node=node) + self.assertEqual( + events[0].type.name, EVENT_TYPES.REQUEST_NODE_START_RESCUE_MODE) + self.assertEqual(events[1].type.name, EVENT_TYPES.ENTERING_RESCUE_MODE) def test_start_rescue_mode_sets_status_owner_and_power_cycles_node(self): node = factory.make_Node(status=random.choice([ @@ -7070,6 +7115,23 @@ node.start(user) self.assertEquals(NODE_STATUS.DEPLOYING, node.status) + def test__creates_acquired_bridges_for_install_kvm(self): + user = factory.make_User() + node = self.make_acquired_node_with_interface( + user, power_type="manual") + bridge_stp = factory.pick_bool() + bridge_fd = random.randint(0, 500) + node.start( + user, install_kvm=True, bridge_stp=bridge_stp, bridge_fd=bridge_fd) + node = reload_object(node) + bridge = BridgeInterface.objects.get(node=node) + interface = node.interface_set.first() + self.assertEquals(NODE_STATUS.DEPLOYING, node.status) + self.assertEquals(bridge.mac_address, interface.mac_address) + self.assertEquals(bridge.params['bridge_stp'], bridge_stp) + self.assertEquals(bridge.params['bridge_fd'], bridge_fd) + self.assertTrue(node.install_kvm) + def test__doesnt_change_broken(self): user = factory.make_User() node = self.make_acquired_node_with_interface( @@ -7404,8 +7466,7 @@ MockCalledOnceWith(ANY, power_query, ANY)) self.assertThat( mock_create_node_event, MockCalledOnceWith( - system_id=node.system_id, - event_type=EVENT_TYPES.NODE_POWER_QUERIED, + node, EVENT_TYPES.NODE_POWER_QUERIED, event_description="Power state queried: %s" % POWER_STATE.ON)) @wait_for_reactor @@ -7428,8 +7489,7 @@ MockCalledOnceWith(ANY, power_query, ANY)) self.assertThat( mock_create_node_event, MockCalledOnceWith( - system_id=node.system_id, - event_type=EVENT_TYPES.NODE_POWER_QUERY_FAILED, + node, EVENT_TYPES.NODE_POWER_QUERY_FAILED, event_description=power_error)) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/node_action.py maas-2.6.0-7802-g59416a869/src/maasserver/node_action.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/node_action.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/node_action.py 2019-06-01 02:18:13.000000000 +0000 @@ -450,9 +450,7 @@ if self.node.owner is None: with locks.node_acquire: try: - bridge_all = True if install_kvm else False - self.node.acquire( - self.user, token=None, bridge_all=bridge_all) + self.node.acquire(self.user, token=None) except ValidationError as e: raise NodeActionError(e) if install_kvm: diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/node_constraint_filter_forms.py maas-2.6.0-7802-g59416a869/src/maasserver/node_constraint_filter_forms.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/node_constraint_filter_forms.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/node_constraint_filter_forms.py 2019-06-01 02:18:13.000000000 +0000 @@ -2,7 +2,7 @@ # GNU Affero General Public License version 3 (see the file LICENSE). __all__ = [ - 'AcquireNodeForm', + 'FilterNodeForm', ] @@ -19,6 +19,10 @@ Q, ) from django.forms.fields import Field +from maasserver.enum import ( + NODE_STATUS, + NODE_STATUS_SHORT_LABEL_CHOICES, +) from maasserver.fields import ( mac_validator, MODEL_NAME_VALIDATOR, @@ -206,7 +210,7 @@ return list(result) -# Mapping used to rename the fields from the AcquireNodeForm form. +# Mapping used to rename the fields from the FilterNodeForm form. # The new names correspond to the names used by Juju. This is used so # that the search form present on the node listing page can be used to # filter nodes using Juju's semantics. @@ -254,7 +258,7 @@ def detect_nonexistent_names(model_class, names): """Check for, and return, names of nonexistent objects. - Used for checking object names as passed to the `AcquireNodeForm`. + Used for checking object names as passed to the `FilterNodeForm`. :param model_class: A model class that has a name attribute. :param names: List, tuple, or set of purpoprted zone names. @@ -591,27 +595,13 @@ return LabeledConstraintMap(value) -class AcquireNodeForm(RenamableFieldsForm): - """A form handling the constraints used to acquire a node.""" - - name = forms.CharField( - label="The hostname of the desired node", required=False) - - system_id = forms.CharField( - label="The system_id of the desired node", required=False) +class FilterNodeForm(RenamableFieldsForm): + """A form for filtering nodes.""" # This becomes a multiple-choice field during cleaning, to accommodate # architecture wildcards. arch = forms.CharField(label="Architecture", required=False) - cpu_count = forms.FloatField( - label="CPU count", required=False, - error_messages={'invalid': "Invalid CPU count: number required."}) - - mem = forms.FloatField( - label="Memory", required=False, - error_messages={'invalid': "Invalid memory: number of MiB required."}) - tags = UnconstrainedMultipleChoiceField(label="Tags", required=False) not_tags = UnconstrainedMultipleChoiceField( @@ -713,7 +703,13 @@ interfaces = LabeledConstraintMapField( validators=[interfaces_validator], label="Interfaces", required=False) - ignore_unknown_constraints = False + cpu_count = forms.FloatField( + label="CPU count", required=False, + error_messages={'invalid': "Invalid CPU count: number required."}) + + mem = forms.FloatField( + label="Memory", required=False, + error_messages={'invalid': "Invalid memory: number of MiB required."}) pod = forms.CharField( label="The name of the desired pod", required=False) @@ -727,6 +723,8 @@ not_pod_type = forms.CharField( label="The power_type of the undesired pod", required=False) + ignore_unknown_constraints = False + @classmethod def Strict(cls, *args, **kwargs): """A stricter version of the form which rejects unknown parameters.""" @@ -846,18 +844,15 @@ value = self.cleaned_data[self.get_field_name('not_vlans')] return self._clean_specifiers(VLAN, value) - def __init__(self, *args, **kwargs): - super(AcquireNodeForm, self).__init__(*args, **kwargs) - def clean(self): if not self.ignore_unknown_constraints: unknown_constraints = set( self.data).difference(set(self.field_mapping.values())) for constraint in unknown_constraints: if constraint not in IGNORED_FIELDS: - msg = "Unable to allocate a machine. No such constraint." + msg = "No such constraint." self._errors[constraint] = self.error_class([msg]) - return super(AcquireNodeForm, self).clean() + return super().clean() def describe_constraint(self, field_name): """Return a human-readable representation of a constraint. @@ -909,37 +904,26 @@ :return: A QuerySet of the nodes that match the form's constraints. :rtype: `django.db.models.query.QuerySet` """ - filtered_nodes = nodes - filtered_nodes = self.filter_by_pod_or_pod_type(filtered_nodes) - filtered_nodes = self.filter_by_hostname(filtered_nodes) - filtered_nodes = self.filter_by_system_id(filtered_nodes) - filtered_nodes = self.filter_by_arch(filtered_nodes) - filtered_nodes = self.filter_by_cpu_count(filtered_nodes) - filtered_nodes = self.filter_by_mem(filtered_nodes) - filtered_nodes = self.filter_by_tags(filtered_nodes) - filtered_nodes = self.filter_by_zone(filtered_nodes) - filtered_nodes = self.filter_by_pool(filtered_nodes) - filtered_nodes = self.filter_by_subnets(filtered_nodes) - filtered_nodes = self.filter_by_vlans(filtered_nodes) - filtered_nodes = self.filter_by_fabrics(filtered_nodes) - filtered_nodes = self.filter_by_fabric_classes(filtered_nodes) + filtered_nodes = self._apply_filters(nodes) compatible_nodes, filtered_nodes = self.filter_by_storage( filtered_nodes) compatible_interfaces, filtered_nodes = self.filter_by_interfaces( filtered_nodes) - filtered_nodes = self.reorder_nodes_by_cost(filtered_nodes) return filtered_nodes, compatible_nodes, compatible_interfaces - def reorder_nodes_by_cost(self, filtered_nodes): - # This uses a very simple procedure to compute a machine's - # cost. This procedure is loosely based on how ec2 computes - # the costs of machines. This is here to give a hint to let - # the call to acquire() decide which machine to return based - # on the machine's cost when multiple machines match the - # constraints. - filtered_nodes = filtered_nodes.distinct().extra( - select={'cost': "cpu_count + memory / 1024."}) - return filtered_nodes.order_by("cost") + def _apply_filters(self, nodes): + nodes = self.filter_by_arch(nodes) + nodes = self.filter_by_tags(nodes) + nodes = self.filter_by_zone(nodes) + nodes = self.filter_by_pool(nodes) + nodes = self.filter_by_subnets(nodes) + nodes = self.filter_by_vlans(nodes) + nodes = self.filter_by_fabrics(nodes) + nodes = self.filter_by_fabric_classes(nodes) + nodes = self.filter_by_cpu_count(nodes) + nodes = self.filter_by_mem(nodes) + nodes = self.filter_by_pod_or_pod_type(nodes) + return nodes.distinct() def filter_by_interfaces(self, filtered_nodes): compatible_interfaces = {} @@ -1077,22 +1061,6 @@ filtered_nodes = filtered_nodes.filter(system_id=system_id) return filtered_nodes - def filter_by_hostname(self, filtered_nodes): - # Filter by hostname. - hostname = self.cleaned_data.get(self.get_field_name('name')) - if hostname: - # If the given hostname has a domain part, try matching - # against the nodes' FQDN. - if "." in hostname: - host, domain = hostname.split('.', 1) - hostname_clause = Q(hostname=host) - domain_clause = Q(domain__name=domain) - clause = (hostname_clause & domain_clause) - else: - clause = Q(hostname=hostname) - filtered_nodes = filtered_nodes.filter(clause) - return filtered_nodes - def filter_by_pod_or_pod_type(self, filtered_nodes): # Filter by pod, pod type, not_pod or not_pod_type. # We are filtering for all of these to keep the query count down. @@ -1113,4 +1081,130 @@ pods = pods.exclude(power_type=not_pod_type) filtered_nodes = filtered_nodes.filter( bmc_id__in=pods.values_list('id', flat=True)) + return filtered_nodes.distinct() + + +class AcquireNodeForm(FilterNodeForm): + """A form handling the constraints used to acquire a node.""" + + name = forms.CharField( + label="The hostname of the desired node", required=False) + + system_id = forms.CharField( + label="The system_id of the desired node", required=False) + + def _apply_filters(self, nodes): + nodes = super()._apply_filters(nodes) + nodes = self.filter_by_hostname(nodes) + nodes = self.filter_by_system_id(nodes) + return nodes + + def filter_by_hostname(self, filtered_nodes): + # Filter by hostname. + hostname = self.cleaned_data.get(self.get_field_name('name')) + if hostname: + # If the given hostname has a domain part, try matching + # against the nodes' FQDN. + if "." in hostname: + host, domain = hostname.split('.', 1) + hostname_clause = Q(hostname=host) + domain_clause = Q(domain__name=domain) + clause = (hostname_clause & domain_clause) + else: + clause = Q(hostname=hostname) + filtered_nodes = filtered_nodes.filter(clause) + return filtered_nodes + + def filter_nodes(self, nodes): + result = super().filter_nodes(nodes) + filtered_nodes, compatible_nodes, compatible_interfaces = result + filtered_nodes = self.reorder_nodes_by_cost(filtered_nodes) + return filtered_nodes, compatible_nodes, compatible_interfaces + + def reorder_nodes_by_cost(self, filtered_nodes): + # This uses a very simple procedure to compute a machine's + # cost. This procedure is loosely based on how ec2 computes + # the costs of machines. This is here to give a hint to let + # the call to acquire() decide which machine to return based + # on the machine's cost when multiple machines match the + # constraints. + filtered_nodes = filtered_nodes.extra( + select={'cost': "cpu_count + memory / 1024."}) + return filtered_nodes.order_by("cost") + + +class ReadNodesForm(FilterNodeForm): + + id = UnconstrainedMultipleChoiceField( + label="System IDs to filter on", required=False) + + hostname = UnconstrainedMultipleChoiceField( + label="Hostnames to filter on", required=False) + + mac_address = ValidatorMultipleChoiceField( + validator=mac_validator, label="MAC addresses to filter on", + required=False, + error_messages={ + 'invalid_list': "Invalid parameter: invalid MAC address format", + }) + + domain = UnconstrainedMultipleChoiceField( + label="Domain names to filter on", required=False) + + agent_name = forms.CharField( + label="Only include nodes with events matching the agent name", + required=False) + + status = forms.ChoiceField( + label="Only inclides nodes with the specified status", + choices=NODE_STATUS_SHORT_LABEL_CHOICES, required=False) + + def _apply_filters(self, nodes): + nodes = super()._apply_filters(nodes) + nodes = self.filter_by_ids(nodes) + nodes = self.filter_by_hostnames(nodes) + nodes = self.filter_by_mac_addresses(nodes) + nodes = self.filter_by_domain(nodes) + nodes = self.filter_by_agent_name(nodes) + nodes = self.filter_by_status(nodes) + return nodes + + def filter_by_ids(self, filtered_nodes): + ids = self.cleaned_data.get(self.get_field_name('id')) + if ids: + filtered_nodes = filtered_nodes.filter(system_id__in=ids) + return filtered_nodes + + def filter_by_hostnames(self, filtered_nodes): + hostnames = self.cleaned_data.get(self.get_field_name('hostname')) + if hostnames: + filtered_nodes = filtered_nodes.filter(hostname__in=hostnames) + return filtered_nodes + + def filter_by_mac_addresses(self, filtered_nodes): + mac_addresses = self.cleaned_data.get( + self.get_field_name('mac_address')) + if mac_addresses: + filtered_nodes = filtered_nodes.filter( + interface__mac_address__in=mac_addresses) + return filtered_nodes + + def filter_by_domain(self, filtered_nodes): + domains = self.cleaned_data.get(self.get_field_name('domain')) + if domains: + filtered_nodes = filtered_nodes.filter(domain__name__in=domains) + return filtered_nodes + + def filter_by_agent_name(self, filtered_nodes): + field_name = self.get_field_name('agent_name') + if field_name in self.data: + agent_name = self.cleaned_data.get(field_name) + filtered_nodes = filtered_nodes.filter(agent_name=agent_name) + return filtered_nodes + + def filter_by_status(self, filtered_nodes): + status = self.cleaned_data.get(self.get_field_name('status')) + if status: + status_id = getattr(NODE_STATUS, status.upper()) + filtered_nodes = filtered_nodes.filter(status=status_id) return filtered_nodes diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/prometheus/stats.py maas-2.6.0-7802-g59416a869/src/maasserver/prometheus/stats.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/prometheus/stats.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/prometheus/stats.py 2019-06-01 02:18:13.000000000 +0000 @@ -20,6 +20,7 @@ get_kvm_pods_stats, get_maas_stats, get_machines_by_architecture, + get_subnets_utilisation_stats, ) from maasserver.utils.orm import transactional from maasserver.utils.threads import deferToDatabase @@ -33,7 +34,6 @@ MetricDefinition, PrometheusMetrics, ) -from provisioningserver.utils.env import get_maas_id from twisted.application.internet import TimerService @@ -58,6 +58,18 @@ MetricDefinition( 'Gauge', 'maas_net_subnets_v6', 'Number of IPv6 subnets'), MetricDefinition( + 'Gauge', 'maas_net_subnet_ip_count', + 'Number of IPs in a subnet by status', ['cidr', 'status']), + MetricDefinition( + 'Gauge', 'maas_net_subnet_ip_dynamic', + 'Number of used IPs in a subnet', ['cidr', 'status']), + MetricDefinition( + 'Gauge', 'maas_net_subnet_ip_reserved', + 'Number of used IPs in a subnet', ['cidr', 'status']), + MetricDefinition( + 'Gauge', 'maas_net_subnet_ip_static', + 'Number of used IPs in a subnet', ['cidr']), + MetricDefinition( 'Gauge', 'maas_machines_total_mem', 'Amount of combined memory for all machines'), MetricDefinition( @@ -89,15 +101,14 @@ def prometheus_stats_handler(request): - have_prometheus = ( - PROMETHEUS_SUPPORTED and - Config.objects.get_config('prometheus_enabled')) + configs = Config.objects.get_configs(['prometheus_enabled', 'uuid']) + have_prometheus = PROMETHEUS_SUPPORTED and configs['prometheus_enabled'] if not have_prometheus: return HttpResponseNotFound() metrics = create_metrics( STATS_DEFINITIONS, - extra_labels={'maas_id': get_maas_id}, + extra_labels={'maas_id': configs['uuid']}, update_handlers=[update_prometheus_stats], registry=prom_cli.CollectorRegistry()) return HttpResponse( @@ -158,6 +169,24 @@ 'maas_machine_arches', 'set', value=machines, labels={'arches': arch}) + # Update metrics for subnets + for cidr, stats in get_subnets_utilisation_stats().items(): + for status in ('available', 'unavailable'): + metrics.update( + 'maas_net_subnet_ip_count', 'set', + value=stats[status], + labels={'cidr': cidr, 'status': status}) + metrics.update( + 'maas_net_subnet_ip_static', 'set', + value=stats['static'], labels={'cidr': cidr}) + for addr_type in ('dynamic', 'reserved'): + metric_name = 'maas_net_subnet_ip_{}'.format(addr_type) + for status in ('available', 'used'): + metrics.update( + metric_name, 'set', + value=stats['{}_{}'.format(addr_type, status)], + labels={'cidr': cidr, 'status': status}) + return metrics diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/prometheus/tests/test_stats.py maas-2.6.0-7802-g59416a869/src/maasserver/prometheus/tests/test_stats.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/prometheus/tests/test_stats.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/prometheus/tests/test_stats.py 2019-06-01 02:18:13.000000000 +0000 @@ -10,6 +10,10 @@ from unittest import mock from django.db import transaction +from maasserver.enum import ( + IPADDRESS_TYPE, + IPRANGE_TYPE, +) from maasserver.models import Config from maasserver.prometheus import stats from maasserver.prometheus.stats import ( @@ -83,7 +87,7 @@ "amd64": 2, "i386": 1, } - self.patch(stats, 'get_maas_id').return_value = 'abcde' + Config.objects.set_config('uuid', 'abcde') Config.objects.set_config('prometheus_enabled', True) response = self.client.get(reverse('metrics')) content = response.content.decode("utf-8") @@ -152,6 +156,26 @@ } mock_pods = self.patch(stats, "get_kvm_pods_stats") mock_pods.return_value = pods + subnet_stats = { + '1.2.0.0/16': { + 'available': 2 ** 16 - 3, + 'dynamic_available': 0, + 'dynamic_used': 0, + 'reserved_available': 0, + 'reserved_used': 0, + 'static': 0, + 'unavailable': 1}, + '::1/128': { + 'available': 1, + 'dynamic_available': 0, + 'dynamic_used': 0, + 'reserved_available': 0, + 'reserved_used': 0, + 'static': 0, + 'unavailable': 0}} + mock_subnet_stats = self.patch( + stats, "get_subnets_utilisation_stats") + mock_subnet_stats.return_value = subnet_stats metrics = create_metrics( STATS_DEFINITIONS, registry=prometheus_client.CollectorRegistry()) update_prometheus_stats(metrics) @@ -161,6 +185,8 @@ mock_arches, MockCalledOnce()) self.assertThat( mock_pods, MockCalledOnce()) + self.assertThat( + mock_subnet_stats, MockCalledOnce()) def test_push_stats_to_prometheus(self): factory.make_RegionRackController() @@ -173,6 +199,56 @@ push_gateway, job="stats_for_%s" % maas_name, registry=mock.ANY)) + def test_subnet_stats(self): + subnet = factory.make_Subnet( + cidr='1.2.0.0/16', gateway_ip='1.2.0.254') + factory.make_IPRange( + subnet=subnet, start_ip='1.2.0.11', end_ip='1.2.0.20', + alloc_type=IPRANGE_TYPE.DYNAMIC) + factory.make_IPRange( + subnet=subnet, start_ip='1.2.0.51', end_ip='1.2.0.70', + alloc_type=IPRANGE_TYPE.RESERVED) + factory.make_StaticIPAddress( + ip='1.2.0.12', + alloc_type=IPADDRESS_TYPE.DHCP, subnet=subnet) + for n in (60, 61): + factory.make_StaticIPAddress( + ip='1.2.0.{}'.format(n), + alloc_type=IPADDRESS_TYPE.USER_RESERVED, subnet=subnet) + for n in (80, 90, 100): + factory.make_StaticIPAddress( + ip='1.2.0.{}'.format(n), + alloc_type=IPADDRESS_TYPE.STICKY, subnet=subnet) + metrics = create_metrics( + STATS_DEFINITIONS, registry=prometheus_client.CollectorRegistry()) + update_prometheus_stats(metrics) + output = metrics.generate_latest().decode('ascii') + self.assertIn( + 'maas_net_subnet_ip_count' + '{cidr="1.2.0.0/16",status="available"} 65500.0', + output) + self.assertIn( + 'maas_net_subnet_ip_count' + '{cidr="1.2.0.0/16",status="unavailable"} 34.0', + output) + self.assertIn( + 'maas_net_subnet_ip_dynamic' + '{cidr="1.2.0.0/16",status="available"} 9.0', + output) + self.assertIn( + 'maas_net_subnet_ip_dynamic{cidr="1.2.0.0/16",status="used"} 1.0', + output) + self.assertIn( + 'maas_net_subnet_ip_reserved' + '{cidr="1.2.0.0/16",status="available"} 18.0', + output) + self.assertIn( + 'maas_net_subnet_ip_reserved{cidr="1.2.0.0/16",status="used"} 2.0', + output) + self.assertIn( + 'maas_net_subnet_ip_static{cidr="1.2.0.0/16"} 3.0', + output) + class TestPrometheusService(MAASTestCase): """Tests for `ImportPrometheusService`.""" diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rbac.py maas-2.6.0-7802-g59416a869/src/maasserver/rbac.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rbac.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/rbac.py 2019-06-01 02:18:13.000000000 +0000 @@ -19,6 +19,7 @@ AuthInfo, get_auth_info, MacaroonClient, + UserDetails, ) from maasserver.models import ( Config, @@ -53,7 +54,7 @@ class RBACClient(MacaroonClient): """A client for RBAC API.""" - API_BASE_URL = '/api/service/v1/resources' + API_BASE_URL = '/api/service/v1' def __init__(self, url: str=None, auth_info: AuthInfo=None): if url is None: @@ -65,7 +66,17 @@ def _get_resource_type_url(self, resource_type: str): """Return the URL for `resource_type`.""" return self._url + quote( - '{}/{}'.format(self.API_BASE_URL, resource_type)) + '{}/resources/{}'.format(self.API_BASE_URL, resource_type)) + + def get_user_details(self, username: str) -> UserDetails: + """Return details about a user.""" + url = self._url + quote( + '{}/user/{}'.format(self.API_BASE_URL, username)) + details = self._request('GET', url) + return UserDetails( + username=details['username'], + fullname=details.get('name', ''), + email=details.get('email', '')) def get_resources(self, resource_type: str) -> Sequence[Resource]: """Return list of resources with `resource_type`.""" @@ -201,6 +212,12 @@ [''] if '' in pool_identifiers else pool_identifiers) return result + def get_user_details(self, username): + return UserDetails( + username=username, + fullname='User username', + email=username + '@example.com') + # Set when their is no client for the current request. NO_CLIENT = object() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/routablepairs.py maas-2.6.0-7802-g59416a869/src/maasserver/routablepairs.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/routablepairs.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/routablepairs.py 2019-06-01 02:18:13.000000000 +0000 @@ -36,6 +36,7 @@ FROM maasserver_routable_pairs WHERE left_node_id IN (%s) AND right_node_id IN (%s) + AND metric < 4 ORDER BY metric ASC """) diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/boot.py maas-2.6.0-7802-g59416a869/src/maasserver/rpc/boot.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/boot.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/rpc/boot.py 2019-06-01 02:18:13.000000000 +0000 @@ -39,6 +39,7 @@ from maasserver.utils.orm import transactional from maasserver.utils.osystems import validate_hwe_kernel from provisioningserver.events import EVENT_TYPES +from provisioningserver.logger import get_maas_logger from provisioningserver.rpc.exceptions import BootConfigNoResponse from provisioningserver.utils.network import get_source_address from provisioningserver.utils.twisted import ( @@ -48,6 +49,9 @@ from provisioningserver.utils.url import splithost +maaslog = get_maas_logger("rpc.boot") + + DEFAULT_ARCH = 'i386' @@ -90,8 +94,7 @@ event_description=options[purpose]) # Create a status message for performing a PXE boot. Event.objects.create_node_event( - machine, event_type=EVENT_TYPES.PERFORMING_PXE_BOOT, - event_description='') + machine, event_type=EVENT_TYPES.PERFORMING_PXE_BOOT) def get_boot_filenames( @@ -294,7 +297,7 @@ def get_config( system_id, local_ip, remote_ip, arch=None, subarch=None, mac=None, hardware_uuid=None, bios_boot_method=None): - """Get the booting configration for the a machine. + """Get the booting configration for a machine. Returns a structure suitable for returning in the response for :py:class:`~provisioningserver.rpc.region.GetBootConfig`. @@ -409,6 +412,16 @@ # Early out if the machine is booting local. if purpose == 'local': + if machine.is_device: + # Log that we are setting to local boot for a device. + maaslog.warning( + "Device %s with MAC address %s is PXE booting; " + "instructing the device to boot locally." % ( + machine.hostname, mac)) + # Set the purpose to 'local-device' so we can log a message + # on the rack. + purpose = 'local-device' + return { "system_id": machine.system_id, "arch": arch, diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/regionservice.py maas-2.6.0-7802-g59416a869/src/maasserver/rpc/regionservice.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/regionservice.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/rpc/regionservice.py 2019-06-01 02:18:13.000000000 +0000 @@ -47,7 +47,10 @@ IPAddress, ) from provisioningserver.logger import LegacyLogger -from provisioningserver.prometheus.metrics import PROMETHEUS_METRICS +from provisioningserver.prometheus.metrics import ( + GLOBAL_LABELS, + PROMETHEUS_METRICS, +) from provisioningserver.rpc import ( cluster, common, @@ -686,7 +689,7 @@ nodegroup_uuid) yield self.initResponder(rack_controller) - except: + except Exception: # Ensure we're not hanging onto this connection. self.factory.service._removeConnectionFor(self.ident, self) # Tell the logs about it. @@ -698,7 +701,10 @@ raise exceptions.CannotRegisterRackController(msg) else: # Done registering the rack controller and connection. - return {'system_id': self.ident} + return { + 'system_id': self.ident, + 'uuid': GLOBAL_LABELS['maas_uuid'] + } @inlineCallbacks def performHandshake(self): @@ -953,7 +959,7 @@ for index, endpoint in enumerate(endpoints): try: port = yield endpoint.listen(factory) - except: + except Exception: if index == last: raise else: diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/tests/test_boot.py maas-2.6.0-7802-g59416a869/src/maasserver/rpc/tests/test_boot.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/tests/test_boot.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/rpc/tests/test_boot.py 2019-06-01 02:18:13.000000000 +0000 @@ -16,6 +16,7 @@ INTERFACE_TYPE, IPADDRESS_TYPE, NODE_STATUS, + NODE_TYPE, ) from maasserver.models import ( Config, @@ -261,6 +262,47 @@ "http_boot": True, }, config) + def test__changes_purpose_to_local_device_for_device(self): + rack_controller = factory.make_RackController() + local_ip = factory.make_ip_address() + remote_ip = factory.make_ip_address() + device = self.make_node_with_extra( + status=NODE_STATUS.DEPLOYED, netboot=False, + node_type=NODE_TYPE.DEVICE) + device.boot_cluster_ip = local_ip + device.save() + mac = device.get_boot_interface().mac_address + self.patch_autospec(boot_module, 'event_log_pxe_request') + maaslog = self.patch(boot_module, 'maaslog') + config = get_config( + rack_controller.system_id, local_ip, remote_ip, mac=mac, + query_count=8) + self.assertEquals({ + "system_id": device.system_id, + "arch": device.split_arch()[0], + "subarch": device.split_arch()[1], + "osystem": '', + "release": '', + "kernel": '', + "initrd": '', + "boot_dtb": '', + "purpose": 'local-device', + "hostname": device.hostname, + "domain": device.domain.name, + "preseed_url": ANY, + "fs_host": local_ip, + "log_host": local_ip, + "log_port": 5247, + "extra_opts": '', + "http_boot": True, + }, config) + self.assertThat( + maaslog.warning, + MockCalledOnceWith( + "Device %s with MAC address %s is PXE booting; " + "instructing the device to boot locally." % ( + device.hostname, mac))) + def test__purpose_local_to_xinstall_for_ephemeral_deployment(self): # A diskless node is one that it is ephemerally deployed. rack_controller = factory.make_RackController() diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/tests/test_regionservice.py maas-2.6.0-7802-g59416a869/src/maasserver/rpc/tests/test_regionservice.py --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/rpc/tests/test_regionservice.py 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/rpc/tests/test_regionservice.py 2019-06-01 02:18:13.000000000 +0000 @@ -305,7 +305,10 @@ @wait_for_reactor @inlineCallbacks - def test_register_returns_system_id(self): + def test_register_returns_system_id_and_uuid(self): + uuid = 'a-b-c' + self.patch(regionservice, 'GLOBAL_LABELS', {'maas_uuid': uuid}) + yield self.installFakeRegion() rack_controller = yield deferToDatabase(factory.make_RackController) protocol = self.make_Region() @@ -318,6 +321,7 @@ }) self.assertThat( response['system_id'], Equals(rack_controller.system_id)) + self.assertThat(response['uuid'], Equals(uuid)) @wait_for_reactor @inlineCallbacks diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/css/build.css maas-2.6.0-7802-g59416a869/src/maasserver/static/css/build.css --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/css/build.css 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/css/build.css 2019-06-01 02:18:13.000000000 +0000 @@ -1 +1 @@ -.p-icon--minus,.p-icon--plus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron,.p-icon--close,.p-icon--help,.p-icon--information,.p-icon--info,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--contextual-menu,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--pass,.p-icon--share,.p-icon--user,.p-icon--question,.p-icon--spinner,.p-icon--edit,.p-icon--status-failed,.p-icon--status-in-progress,.p-icon--status-queued,.p-icon--status-succeeded,.p-icon--status-waiting,.p-icon--timed-out,.p-icon--success-muted,.p-icon--locked,.p-icon--compose-machine,.p-icon--account,.p-icon--mount,.p-icon--unmount,.p-icon--partition,.p-icon--debug,.p-icon--remove,.p-icon--settings,.p-icon--sync,.p-icon--system-shutdown,.p-icon--tags,.p-icon--logical-volume,.p-icon--pending,.p-icon--running,.p-icon--power-error,.p-icon--power-on,.p-icon--power-off,.p-icon--power-unknown,.p-icon--lock,.p-icon--x,.p-icon--tick,.p-table--machines .p-icon--placeholder{height:1rem;width:1rem;background-position:center;background-repeat:no-repeat;background-size:contain;display:inline-block;margin:0;padding:0;position:relative;top:-2px;vertical-align:sub}.p-icon--facebook,.p-icon--google,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu{height:2.5rem;width:2.5rem;display:inline-block}.p-icon--minus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--minus,.p-icon--minus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23cdcdcd' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--plus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--plus,.p-icon--plus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23cdcdcd' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--minus,.p-icon--plus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron,.p-icon--close,.p-icon--help,.p-icon--information,.p-icon--info,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--contextual-menu,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--pass,.p-icon--share,.p-icon--user,.p-icon--question,.p-icon--spinner,.p-icon--edit,.p-icon--status-failed,.p-icon--status-in-progress,.p-icon--status-queued,.p-icon--status-succeeded,.p-icon--status-waiting,.p-icon--timed-out,.p-icon--success-muted,.p-icon--locked,.p-icon--compose-machine,.p-icon--account,.p-icon--mount,.p-icon--unmount,.p-icon--partition,.p-icon--debug,.p-icon--remove,.p-icon--settings,.p-icon--sync,.p-icon--system-shutdown,.p-icon--tags,.p-icon--logical-volume,.p-icon--pending,.p-icon--running,.p-icon--power-error,.p-icon--power-on,.p-icon--power-off,.p-icon--power-unknown,.p-icon--lock,.p-icon--x,.p-icon--tick,.p-table--machines .p-icon--placeholder{height:1rem;width:1rem;background-position:center;background-repeat:no-repeat;background-size:contain;display:inline-block;margin:0;padding:0;position:relative;top:-2px;vertical-align:sub}.p-icon--facebook,.p-icon--google,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu{height:2.5rem;width:2.5rem;display:inline-block}.p-media-object,.p-media-object--large{display:flex;flex-shrink:0;margin-bottom:1rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item{color:#111;padding-left:2rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue{background-position:0 75%;background-repeat:no-repeat;background-size:1rem}/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,.p-option-selector__input,textarea{font-family:inherit;font-size:100%}button,input{overflow:visible}button,select,.p-option-selector__input{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}blockquote{border-left:2px solid #666}blockquote>cite{display:block}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}button{background-color:#fff;border-color:#cdcdcd;color:#111}button:visited{color:#111}button:active,button:hover{background-color:#f7f7f7;border-color:#cdcdcd}button:disabled:active,button:disabled:hover,button.is--disabled:active,button.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}button .p-link--external{color:currentColor}button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{transition-duration:0.165s;transition-property:background-color,border-color;transition-timing-function:cubic-bezier(0.55, 0.055, 0.675, 0.19);border-radius:.125rem;border-style:solid;border-width:1px;cursor:pointer;display:inline-block;font-size:1rem;font-weight:300;line-height:1.5rem;margin-bottom:1.2rem;padding:.3375rem 1rem;text-align:center;text-decoration:none}button:focus,[type='submit']:focus,.p-button:focus,.p-button--neutral:focus,.p-button--brand:focus,.p-button--positive:focus,.p-button--negative:focus,.p-button--base:focus{outline:1px solid #19b6ee;outline-offset:2px}button:active,[type='submit']:active,.p-button:active,.p-button--neutral:active,.p-button--brand:active,.p-button--positive:active,.p-button--negative:active,.p-button--base:active,button:focus,[type='submit']:focus,.p-button:focus,.p-button--neutral:focus,.p-button--brand:focus,.p-button--positive:focus,.p-button--negative:focus,.p-button--base:focus,button:hover,[type='submit']:hover,.p-button:hover,.p-button--neutral:hover,.p-button--brand:hover,.p-button--positive:hover,.p-button--negative:hover,.p-button--base:hover{text-decoration:none}button:disabled,[type='submit']:disabled,.p-button:disabled,.p-button--neutral:disabled,.p-button--brand:disabled,.p-button--positive:disabled,.p-button--negative:disabled,.p-button--base:disabled,button.is--disabled,.is--disabled[type='submit'],.is--disabled.p-button,.is--disabled.p-button--neutral,.is--disabled.p-button--brand,.is--disabled.p-button--positive,.is--disabled.p-button--negative,.is--disabled.p-button--base{cursor:not-allowed;opacity:.5}@media only screen and (max-width: 460px){button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{width:100%}}@media only screen and (min-width: 461px){button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{width:auto}button:not(:last-of-type):not(:only-of-type),[type='submit']:not(:last-of-type):not(:only-of-type),.p-button:not(:last-of-type):not(:only-of-type),.p-button--neutral:not(:last-of-type):not(:only-of-type),.p-button--brand:not(:last-of-type):not(:only-of-type),.p-button--positive:not(:last-of-type):not(:only-of-type),.p-button--negative:not(:last-of-type):not(:only-of-type),.p-button--base:not(:last-of-type):not(:only-of-type){margin-right:1rem}}table button,table [type='submit'],table .p-button,table .p-button--neutral,table .p-button--brand,table .p-button--positive,table .p-button--negative,table .p-button--base{margin-bottom:.1rem;padding-bottom:.0875rem;padding-top:.0875rem}p button,p [type='submit'],p .p-button,p .p-button--neutral,p .p-button--brand,p .p-button--positive,p .p-button--negative,p .p-button--base{margin-bottom:.1rem;margin-top:-.4rem}p+p>button,p+p>[type='submit'],p+p>.p-button,p+p>.p-button--neutral,p+p>.p-button--brand,p+p>.p-button--positive,p+p>.p-button--negative,p+p>.p-button--base{margin-top:.1rem}@media only screen and (max-width: 460px){p button+button,p [type='submit']+button,p .p-button+button,p .p-button--neutral+button,p .p-button--brand+button,p .p-button--positive+button,p .p-button--negative+button,p .p-button--base+button,p button+[type='submit'],p [type='submit']+[type='submit'],p .p-button+[type='submit'],p .p-button--neutral+[type='submit'],p .p-button--brand+[type='submit'],p .p-button--positive+[type='submit'],p .p-button--negative+[type='submit'],p .p-button--base+[type='submit'],p button+.p-button,p [type='submit']+.p-button,p .p-button+.p-button,p .p-button--neutral+.p-button,p .p-button--brand+.p-button,p .p-button--positive+.p-button,p .p-button--negative+.p-button,p .p-button--base+.p-button,p button+.p-button--neutral,p [type='submit']+.p-button--neutral,p .p-button+.p-button--neutral,p .p-button--neutral+.p-button--neutral,p .p-button--brand+.p-button--neutral,p .p-button--positive+.p-button--neutral,p .p-button--negative+.p-button--neutral,p .p-button--base+.p-button--neutral,p button+.p-button--brand,p [type='submit']+.p-button--brand,p .p-button+.p-button--brand,p .p-button--neutral+.p-button--brand,p .p-button--brand+.p-button--brand,p .p-button--positive+.p-button--brand,p .p-button--negative+.p-button--brand,p .p-button--base+.p-button--brand,p button+.p-button--positive,p [type='submit']+.p-button--positive,p .p-button+.p-button--positive,p .p-button--neutral+.p-button--positive,p .p-button--brand+.p-button--positive,p .p-button--positive+.p-button--positive,p .p-button--negative+.p-button--positive,p .p-button--base+.p-button--positive,p button+.p-button--negative,p [type='submit']+.p-button--negative,p .p-button+.p-button--negative,p .p-button--neutral+.p-button--negative,p .p-button--brand+.p-button--negative,p .p-button--positive+.p-button--negative,p .p-button--negative+.p-button--negative,p .p-button--base+.p-button--negative,p button+.p-button--base,p [type='submit']+.p-button--base,p .p-button+.p-button--base,p .p-button--neutral+.p-button--base,p .p-button--brand+.p-button--base,p .p-button--positive+.p-button--base,p .p-button--negative+.p-button--base,p .p-button--base+.p-button--base{margin-top:.6rem}}code,samp,kbd{font-family:"Ubuntu Mono", Consolas, Monaco, Courier, monospace;font-weight:300;text-align:left}pre,code{direction:ltr;hyphens:none;tab-size:4;white-space:pre-wrap;word-spacing:normal;word-wrap:break-word}code{display:inline}pre{background-color:#f7f7f7;border:1px solid #cdcdcd;border-radius:.125rem;color:#111;display:block;margin-bottom:1rem;margin-top:0;overflow:auto;padding:.5rem 1rem;text-align:left;text-shadow:none}[type='text'],[type='date'],[type='datetime'],[type='datatime-local'],[type='month'],[type='time'],[type='week'],[type='color'],[type='number'],[type='search'],[type='password'],[type='email'],[type='url'],[type='tel'],select,.p-option-selector__input,textarea,[type='file'],.p-code-snippet{margin-bottom:.2rem;padding-bottom:.3375rem;padding-top:.3375rem}[type='text'],[type='date'],[type='datetime'],[type='datatime-local'],[type='month'],[type='time'],[type='week'],[type='color'],[type='number'],[type='search'],[type='password'],[type='email'],[type='url'],[type='tel'],select,.p-option-selector__input,textarea{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 1px rgba(0,0,0,0.12);color:#111;font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;font-size:1rem;font-weight:300;line-height:1.5rem;min-width:10em;padding-left:.5rem;padding-right:.5rem;vertical-align:baseline;width:100%}[type='text']:focus,[type='date']:focus,[type='datetime']:focus,[type='datatime-local']:focus,[type='month']:focus,[type='time']:focus,[type='week']:focus,[type='color']:focus,[type='number']:focus,[type='search']:focus,[type='password']:focus,[type='email']:focus,[type='url']:focus,[type='tel']:focus,select:focus,.p-option-selector__input:focus,textarea:focus{outline:1px solid #19b6ee;outline-offset:2px}table [type='text'],table [type='date'],table [type='datetime'],table [type='datatime-local'],table [type='month'],table [type='time'],table [type='week'],table [type='color'],table [type='number'],table [type='search'],table [type='password'],table [type='email'],table [type='url'],table [type='tel'],table select,table .p-option-selector__input,table textarea{margin:0 0 .1rem 0;padding-bottom:.0875rem;padding-top:.0875rem}[type='text']:active,[type='date']:active,[type='datetime']:active,[type='datatime-local']:active,[type='month']:active,[type='time']:active,[type='week']:active,[type='color']:active,[type='number']:active,[type='search']:active,[type='password']:active,[type='email']:active,[type='url']:active,[type='tel']:active,select:active,.p-option-selector__input:active,textarea:active{border-color:#666;color:#111;outline:none}[type='text']::placeholder,[type='date']::placeholder,[type='datetime']::placeholder,[type='datatime-local']::placeholder,[type='month']::placeholder,[type='time']::placeholder,[type='week']::placeholder,[type='color']::placeholder,[type='number']::placeholder,[type='search']::placeholder,[type='password']::placeholder,[type='email']::placeholder,[type='url']::placeholder,[type='tel']::placeholder,select::placeholder,.p-option-selector__input::placeholder,textarea::placeholder{color:#666;opacity:1}.has-error[type='text'],.has-error[type='date'],.has-error[type='datetime'],.has-error[type='datatime-local'],.has-error[type='month'],.has-error[type='time'],.has-error[type='week'],.has-error[type='color'],.has-error[type='number'],.has-error[type='search'],.has-error[type='password'],.has-error[type='email'],.has-error[type='url'],.has-error[type='tel'],select.has-error,.has-error.p-option-selector__input,textarea.has-error{border:1px solid #c7162b}.has-caution[type='text'],.has-caution[type='date'],.has-caution[type='datetime'],.has-caution[type='datatime-local'],.has-caution[type='month'],.has-caution[type='time'],.has-caution[type='week'],.has-caution[type='color'],.has-caution[type='number'],.has-caution[type='search'],.has-caution[type='password'],.has-caution[type='email'],.has-caution[type='url'],.has-caution[type='tel'],select.has-caution,.has-caution.p-option-selector__input,textarea.has-caution{border:1px solid #f99b11}.has-warning[type='text'],.has-warning[type='date'],.has-warning[type='datetime'],.has-warning[type='datatime-local'],.has-warning[type='month'],.has-warning[type='time'],.has-warning[type='week'],.has-warning[type='color'],.has-warning[type='number'],.has-warning[type='search'],.has-warning[type='password'],.has-warning[type='email'],.has-warning[type='url'],.has-warning[type='tel'],select.has-warning,.has-warning.p-option-selector__input,textarea.has-warning{border:1px solid #f99b11}.has-success[type='text'],.has-success[type='date'],.has-success[type='datetime'],.has-success[type='datatime-local'],.has-success[type='month'],.has-success[type='time'],.has-success[type='week'],.has-success[type='color'],.has-success[type='number'],.has-success[type='search'],.has-success[type='password'],.has-success[type='email'],.has-success[type='url'],.has-success[type='tel'],select.has-success,.has-success.p-option-selector__input,textarea.has-success{border:1px solid #0e8420}.has-information[type='text'],.has-information[type='date'],.has-information[type='datetime'],.has-information[type='datatime-local'],.has-information[type='month'],.has-information[type='time'],.has-information[type='week'],.has-information[type='color'],.has-information[type='number'],.has-information[type='search'],.has-information[type='password'],.has-information[type='email'],.has-information[type='url'],.has-information[type='tel'],select.has-information,.has-information.p-option-selector__input,textarea.has-information{border:1px solid #335280}[type='checkbox'],[type='radio']{float:left;height:1.5rem;margin-bottom:0;margin-right:1.5rem;margin-top:0;padding:0;vertical-align:middle;width:auto}[type='checkbox']:focus,[type='radio']:focus{outline:1px solid #19b6ee;outline-offset:0}[disabled][type='text'],[disabled][type='date'],[disabled][type='datetime'],[disabled][type='datatime-local'],[disabled][type='month'],[disabled][type='time'],[disabled][type='week'],[disabled][type='color'],[disabled][type='number'],[disabled][type='search'],[disabled][type='password'],[disabled][type='email'],[disabled][type='url'],[disabled][type='tel'],select[disabled],[disabled].p-option-selector__input,textarea[disabled],[disabled='disabled'][type='text'],[disabled='disabled'][type='date'],[disabled='disabled'][type='datetime'],[disabled='disabled'][type='datatime-local'],[disabled='disabled'][type='month'],[disabled='disabled'][type='time'],[disabled='disabled'][type='week'],[disabled='disabled'][type='color'],[disabled='disabled'][type='number'],[disabled='disabled'][type='search'],[disabled='disabled'][type='password'],[disabled='disabled'][type='email'],[disabled='disabled'][type='url'],[disabled='disabled'][type='tel'],select[disabled='disabled'],[disabled='disabled'].p-option-selector__input,textarea[disabled='disabled'],[disabled][type='checkbox']+label,[disabled][type='radio']+label,[disabled='disabled'][type='checkbox']+label,[disabled='disabled'][type='radio']+label,.p-switch:disabled+.p-switch__slider{cursor:not-allowed;opacity:.5}[readonly][type='text'],[readonly][type='date'],[readonly][type='datetime'],[readonly][type='datatime-local'],[readonly][type='month'],[readonly][type='time'],[readonly][type='week'],[readonly][type='color'],[readonly][type='number'],[readonly][type='search'],[readonly][type='password'],[readonly][type='email'],[readonly][type='url'],[readonly][type='tel'],select[readonly],[readonly].p-option-selector__input,textarea[readonly],[readonly='readonly'][type='text'],[readonly='readonly'][type='date'],[readonly='readonly'][type='datetime'],[readonly='readonly'][type='datatime-local'],[readonly='readonly'][type='month'],[readonly='readonly'][type='time'],[readonly='readonly'][type='week'],[readonly='readonly'][type='color'],[readonly='readonly'][type='number'],[readonly='readonly'][type='search'],[readonly='readonly'][type='password'],[readonly='readonly'][type='email'],[readonly='readonly'][type='url'],[readonly='readonly'][type='tel'],select[readonly='readonly'],[readonly='readonly'].p-option-selector__input,textarea[readonly='readonly']{color:#cdcdcd;cursor:default}[readonly][type='text']:hover,[readonly][type='date']:hover,[readonly][type='datetime']:hover,[readonly][type='datatime-local']:hover,[readonly][type='month']:hover,[readonly][type='time']:hover,[readonly][type='week']:hover,[readonly][type='color']:hover,[readonly][type='number']:hover,[readonly][type='search']:hover,[readonly][type='password']:hover,[readonly][type='email']:hover,[readonly][type='url']:hover,[readonly][type='tel']:hover,select[readonly]:hover,[readonly].p-option-selector__input:hover,textarea[readonly]:hover,[readonly='readonly'][type='text']:hover,[readonly='readonly'][type='date']:hover,[readonly='readonly'][type='datetime']:hover,[readonly='readonly'][type='datatime-local']:hover,[readonly='readonly'][type='month']:hover,[readonly='readonly'][type='time']:hover,[readonly='readonly'][type='week']:hover,[readonly='readonly'][type='color']:hover,[readonly='readonly'][type='number']:hover,[readonly='readonly'][type='search']:hover,[readonly='readonly'][type='password']:hover,[readonly='readonly'][type='email']:hover,[readonly='readonly'][type='url']:hover,[readonly='readonly'][type='tel']:hover,select[readonly='readonly']:hover,[readonly='readonly'].p-option-selector__input:hover,textarea[readonly='readonly']:hover,[readonly][type='text']:active,[readonly][type='date']:active,[readonly][type='datetime']:active,[readonly][type='datatime-local']:active,[readonly][type='month']:active,[readonly][type='time']:active,[readonly][type='week']:active,[readonly][type='color']:active,[readonly][type='number']:active,[readonly][type='search']:active,[readonly][type='password']:active,[readonly][type='email']:active,[readonly][type='url']:active,[readonly][type='tel']:active,select[readonly]:active,[readonly].p-option-selector__input:active,textarea[readonly]:active,[readonly='readonly'][type='text']:active,[readonly='readonly'][type='date']:active,[readonly='readonly'][type='datetime']:active,[readonly='readonly'][type='datatime-local']:active,[readonly='readonly'][type='month']:active,[readonly='readonly'][type='time']:active,[readonly='readonly'][type='week']:active,[readonly='readonly'][type='color']:active,[readonly='readonly'][type='number']:active,[readonly='readonly'][type='search']:active,[readonly='readonly'][type='password']:active,[readonly='readonly'][type='email']:active,[readonly='readonly'][type='url']:active,[readonly='readonly'][type='tel']:active,select[readonly='readonly']:active,[readonly='readonly'].p-option-selector__input:active,textarea[readonly='readonly']:active{border-color:#666;outline:none}label{cursor:pointer;display:block;margin-bottom:.6rem}label.has-error{color:#c7162b}label.has-caution{color:#f99b11}label.has-warning{color:#f99b11}label.has-success{color:#0e8420}label.has-information{color:#335280}[type='file']{width:100%}[type='file']:focus{outline:1px solid #19b6ee;outline-offset:2px}[type='reset']{display:none}[type='search']{-moz-appearance:none;-webkit-appearance:none;appearance:none;border-radius:0}[type='search']::-webkit-search-results-decoration{display:none}[type='search']::-webkit-search-cancel-button{-webkit-appearance:searchfield-cancel-button;cursor:pointer}[type='checkbox']+label,[type='radio']+label{vertical-align:middle;width:100%}[type='radio']{margin-top:.4rem}[type='submit']{background-color:#0e8420;border-color:#0e8420;color:#fff}[type='submit']:visited{color:#fff}[type='submit']:active,[type='submit']:hover{background-color:#095615;border-color:#095615}[type='submit']:disabled:active,[type='submit']:disabled:hover,[type='submit'].is--disabled:active,[type='submit'].is--disabled:hover{background-color:#0e8420;border-color:#0e8420}[type='submit'] .p-link--external{color:currentColor}select,.p-option-selector__input{-moz-appearance:none;-webkit-appearance:none;appearance:none;background:#fff url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjRweCIgd2lkdGg9IjEwcHgiIHZlcnNpb249IjEuMSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjAgMCAxMCA0Ij4gPHRpdGxlPmFjY29yZGlvbi1vcGVuPC90aXRsZT4gPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+IDxnIGlkPSJmaWx0ZXItcGFuZWwiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSIgZmlsbD0ibm9uZSI+ICA8ZyBpZD0iYWNjb3JkaW9uLW9wZW4iIGZpbGw9IiM4ODgiIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiPiAgIDxwYXRoIGlkPSJjaGV2cm9uIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIiBkPSJtNi4zNjEgMC44NjIzYzAuNTE4IDAuMzY1IDEuMDUyIDAuNzc4MSAxLjYwMSAxLjIzOCAwLjU0OSAwLjQ1ODUgMS4wODkgMC45NTE4IDEuNjIxIDEuNDc3MiAwLjE0MiAwLjE0MDQgMC4yODEgMC4yODIxIDAuNDE1IDAuNDIyNWgtMS41NDFjLTAuMzA0LTAuMjg4OC0wLjYyLTAuNTcwOS0wLjk0Ny0wLjg0NjMtMC4xMzc5LTAuMTE2MS0wLjI3NjgtMC4yMjk3LTAuNDE2OC0wLjM0MDgtMC4xNjM2LTAuMTI5Ny0wLjMyODYtMC4yNTU4LTAuNDk1NC0wLjM3ODMtMC4wODUyLTAuMDYyNS0wLjE3MDgtMC4xMjQxLTAuMjU2OC0wLjE4NDYtMC4zOTctMC4yODIxLTAuOTM1LTAuNjI1Ny0xLjMxNS0wLjg0NzZoLTAuMDU0Yy0wLjM4IDAuMjIxOS0wLjkxOCAwLjU2NTUtMS4zMTUgMC44NDc2LTAuMzk4IDAuMjgwNy0wLjc4OCAwLjU4MjktMS4xNjkgMC45MDM3LTAuMzI3IDAuMjc1NC0wLjY0MyAwLjU1NzUtMC45NDcgMC44NDYzaC0xLjU0MWMwLjEzNS0wLjE0MDQgMC4yNzMtMC4yODIxIDAuNDE1LTAuNDIyNSAwLjUzMi0wLjUyNTQgMS4wNzItMS4wMTg3IDEuNjIxLTEuNDc3MiAwLjU1LTAuNDU5OSAxLjA4My0wLjg3MyAxLjYwMS0xLjIzOCAwLjUxOS0wLjM2NDk3IDAuOTczLTAuNjUyNDEgMS4zNjItMC44NjIzIDAuMzkgMC4yMDk4OSAwLjg0NCAwLjQ5NzMzIDEuMzYyIDAuODYyM3oiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuOTk5IDIpIHJvdGF0ZSgxODApIHRyYW5zbGF0ZSgtNC45OTkgLTIpIi8+ICA8L2c+IDwvZz48L3N2Zz4=") no-repeat;background-position:right .5rem center;background-size:.75rem;color:#111;min-height:24px;padding-right:1.5rem;text-indent:.01px;text-overflow:''}select:hover,.p-option-selector__input:hover{cursor:pointer}select[multiple],[multiple].p-option-selector__input,select[size],[size].p-option-selector__input{background-image:none;height:auto}select[multiple] option,[multiple].p-option-selector__input option,select[size] option,[size].p-option-selector__input option{font-weight:300;line-height:.875rem;padding:.5rem .5rem}textarea{margin-bottom:.2rem;overflow:auto;vertical-align:top}fieldset{background-color:#f7f7f7;border:0;border-radius:.125rem;color:#111}label{width:fit-content}input[type="checkbox"]{opacity:0;position:absolute}input[type="checkbox"]+label{margin-bottom:.5rem;margin-top:-.4rem;padding-left:2rem;position:relative}input[type="checkbox"]+label:focus{outline:1px solid #19b6ee;outline-offset:2px}input[type="checkbox"]+label::before{border:1px solid #cdcdcd;border-radius:.125rem;content:"";display:inline-block;height:1rem;left:0;top:.65rem;width:1rem}input[type="checkbox"]+label::after{border-bottom:2px solid;border-left:2px solid;content:none;display:inline-block;height:7px;left:2px;top:12px;transform:rotate(-45deg);width:11px}input[type="checkbox"]+label::before,input[type="checkbox"]+label::after{display:inline-block;position:absolute}input[type="checkbox"]:checked+label::after{content:""}hr{background-color:#cdcdcd;border:0;height:1px;margin-bottom:.4375rem;margin-top:0;position:relative;width:100%}hr+p{margin-top:-.5rem}.row.is-bordered{position:relative}.row.is-bordered::before{background:#cdcdcd;content:'';height:1px;margin-bottom:.4375rem;width:100%}a{color:#007aa6;text-decoration:none}a:focus{outline:thin dotted #cdcdcd}a:hover{cursor:pointer;text-decoration:underline}a:visited{color:#005573}li>ul,li>ol{margin-bottom:0;padding-top:0}li>ul>li:last-of-type,li>ol>li:last-of-type{padding-bottom:0}ol,ul{margin-bottom:1rem;margin-left:1rem;margin-top:0;padding-left:1rem}nav ol,nav ul{list-style:none;list-style-image:none}li,dl{margin:0;padding:0}dd{margin-left:1rem}dt{border-top:1px dotted #666}dt:first-of-type{border-top:0}img{border:0;border-radius:.125rem;height:auto;max-width:100%}svg:not(:root){overflow:hidden}figure{margin-bottom:1rem;margin-left:0;width:100%}figure caption,figure figcaption{display:block;font-style:italic;margin-top:.25rem;width:100%}object,iframe,embed,canvas,video,audio{display:block;margin:0 auto 20px;max-width:100%}audio:not([controls]){display:none;height:0}[hidden]{display:none}.p-card,.p-card--highlighted,.p-card--muted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-switch__slider,.p-switch__slider::before,.p-filter .p-accordion{border-radius:.125rem}.p-card--highlighted,.p-card--muted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-switch__slider::before,.page-header,.p-filter .p-accordion{box-shadow:0 1px 5px 1px rgba(17,17,17,0.2)}.p-card,.p-filter .p-accordion{border:.0625rem solid #cdcdcd}.p-card--muted{background-color:#f7f7f7;color:#111}.p-card,.p-card--highlighted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-table-expanding .p-table-expanding__panel,.p-table-expanding .p-table-expanding__panel--bordered,.p-filter .p-filter__dropdown{background-color:#fff;color:#111}.p-card,.p-card--highlighted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-table-expanding .p-table-expanding__panel,.p-table-expanding .p-table-expanding__panel--bordered{margin-bottom:1rem;overflow:auto;padding:.5rem}.p-tabs__list::after,.p-table--machines::after,.u-hr--fixed-width::after,.p-option-selector__option+.p-option-selector__option::after{background-color:#cdcdcd;content:'';height:.0625rem;left:0;position:absolute;right:0}.p-tabs__list::after,.p-table--machines::after{bottom:0}.u-hr--fixed-width::after,.p-option-selector__option+.p-option-selector__option::after{top:0}.p-icon--minus,.p-icon--plus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron,.p-icon--close,.p-icon--help,.p-icon--information,.p-icon--info,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--contextual-menu,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--pass,.p-icon--share,.p-icon--user,.p-icon--question,.p-icon--spinner,.p-icon--edit,.p-icon--status-failed,.p-icon--status-in-progress,.p-icon--status-queued,.p-icon--status-succeeded,.p-icon--status-waiting,.p-icon--timed-out,.p-icon--success-muted,.p-icon--locked,.p-icon--compose-machine,.p-icon--account,.p-icon--mount,.p-icon--unmount,.p-icon--partition,.p-icon--debug,.p-icon--remove,.p-icon--settings,.p-icon--sync,.p-icon--system-shutdown,.p-icon--tags,.p-icon--logical-volume,.p-icon--pending,.p-icon--running,.p-icon--power-error,.p-icon--power-on,.p-icon--power-off,.p-icon--power-unknown,.p-icon--lock,.p-icon--x,.p-icon--tick,.p-table--machines .p-icon--placeholder,.p-icon--facebook,.p-icon--google,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu,.p-switch__slider span,button.p-switch span,.u-hide-text{overflow:hidden;text-indent:calc(100% + 10rem);white-space:nowrap}.p-inline-images::after,.p-list::after,.p-list-step::after,.p-stepped-list--detailed::after,.u-clearfix::after,.p-meter--cpu-cores__container::after,.p-legend::after,.p-legend__item::after,.p-form__group::after,.p-option-selector__header::after{clear:both;content:'';display:block}table{border:0;border-collapse:collapse;margin-bottom:1rem;overflow-x:auto;table-layout:fixed;width:100%}td,th{font-weight:300;padding-left:0;text-align:left;text-overflow:ellipsis}@media screen and (min-width: 768px){td:not(:last-child),th:not(:last-child){padding-right:1rem}}thead tr{border-bottom:1px solid #111;vertical-align:top}tbody tr:not(:first-child){border-top:1px solid #cdcdcd}td,th,.p-navigation--sidebar .sidebar__link,.p-accordion__tab{padding-bottom:.1875rem;padding-top:.25rem}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("/MAAS/static/assets/fonts/e8c07df6-Ubuntu-L_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/8619add2-Ubuntu-L_W.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("/MAAS/static/assets/fonts/fff37993-Ubuntu-R_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/7af50859-Ubuntu-R_W.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("/MAAS/static/assets/fonts/f8097dea-Ubuntu-LI_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/8be89d02-Ubuntu-LI_W.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("/MAAS/static/assets/fonts/fca66073-ubuntu-ri-webfont.woff2") format("woff2"),url("/MAAS/static/assets/fonts/f0898c72-ubuntu-ri-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:100;src:url("/MAAS/static/assets/fonts/7f100985-Ubuntu-Th_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/502cc3a1-Ubuntu-Th_W.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:300;src:url("/MAAS/static/assets/fonts/fdd692b9-UbuntuMono-R_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/85edb898-UbuntuMono-R_W.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("/MAAS/static/assets/fonts/fdd692b9-UbuntuMono-R_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/85edb898-UbuntuMono-R_W.woff") format("woff")}html{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#111;font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;font-size:16px;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1.5rem}h1,h2,h3,h4,h5,h6,[class^="p-heading--"]{font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif}p:empty{line-height:0;margin:0;padding:0}button,input,select,.p-option-selector__input,textarea{font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif}blockquote{margin-bottom:0;margin-left:0;margin-top:0;overflow:auto;padding-left:1.5rem}blockquote>p{font-style:italic}blockquote>cite{font-style:normal}small.dense{margin-bottom:1.2rem}sub,sup{line-height:0;position:relative;vertical-align:baseline}abbr[title]{border-bottom:.1em dotted;cursor:pointer;text-decoration:none}@media (max-width: 768px){h3+p,h4+p,.p-heading--three+p,.p-heading--four+p{margin-top:-.5rem}}h5+p,h5+h5,h6+p,h6+h5,.p-heading--five+p,.p-heading--five+h5,.p-heading--six+p,.p-heading--six+h5,h5+h6,h6+h6,.p-heading--five+h6,.p-heading--six+h6,h5+.p-heading--five,h6+.p-heading--five,.p-heading--five+.p-heading--five,.p-heading--six+.p-heading--five,h5+.p-heading--six,h6+.p-heading--six,.p-heading--five+.p-heading--six,.p-heading--six+.p-heading--six{margin-top:0rem}.p-muted-heading+p,.p-muted-heading+h5,.p-muted-heading+h6,.p-muted-heading+.p-heading--five,.p-muted-heading+.p-heading--six{margin-top:-.5rem}p+p:not(.p-muted-heading),h5+p:not(.p-muted-heading),h6+p:not(.p-muted-heading),.p-heading--five+p:not(.p-muted-heading),.p-heading--six+p:not(.p-muted-heading){margin-top:-1rem}ul+h1,ul+h2,ul+.p-heading--one,ul+.p-heading--two,p+h1,p+h2,p+.p-heading--one,p+.p-heading--two,h5+h1,h5+h2,h5+.p-heading--one,h5+.p-heading--two,h6+h1,h6+h2,h6+.p-heading--one,h6+.p-heading--two,.p-heading--five+h1,.p-heading--five+h2,.p-heading--five+.p-heading--one,.p-heading--five+.p-heading--two,.p-heading-6+h1,.p-heading-6+h2,.p-heading-6+.p-heading--one,.p-heading-6+.p-heading--two{padding-top:2.2rem}@media (max-width: 768px){ul+h1,ul+h2,ul+.p-heading--one,ul+.p-heading--two,p+h1,p+h2,p+.p-heading--one,p+.p-heading--two,h5+h1,h5+h2,h5+.p-heading--one,h5+.p-heading--two,h6+h1,h6+h2,h6+.p-heading--one,h6+.p-heading--two,.p-heading--five+h1,.p-heading--five+h2,.p-heading--five+.p-heading--one,.p-heading--five+.p-heading--two,.p-heading-6+h1,.p-heading-6+h2,.p-heading-6+.p-heading--one,.p-heading-6+.p-heading--two{padding-top:1.7rem}}p+h2,p+.p-heading--two{padding-top:2.2rem}@media (max-width: 768px){p+h2,p+.p-heading--two{padding-top:1.6rem}}p+h3,p+.p-heading--three{padding-top:2.1rem}@media (max-width: 768px){p+h3,p+.p-heading--three{padding-top:1.5rem}}p+h4,p+.p-heading--four{padding-top:1.55rem}p+h5,p+.p-heading--five,p+h6,p+.p-heading--six{padding-top:1.4rem}p+.p-muted-heading{padding-top:1rem}h1,.p-heading--one,.p-media-object--large .p-media-object__title{max-width:20em;font-size:2.91029rem;font-style:normal;font-weight:100;line-height:3.5rem;margin-bottom:2.3rem;margin-top:0;padding-top:0.2rem}@media (max-width: 768px){h1,.p-heading--one,.p-media-object--large .p-media-object__title{font-size:2.22819rem;line-height:3rem;margin-bottom:1.8rem;padding-top:0.2rem}}h2,.p-heading--two{max-width:20em;font-size:2.22819rem;font-style:normal;font-weight:300;line-height:3rem;margin-bottom:1.8rem;margin-top:0;padding-top:0.2rem}@media (max-width: 768px){h2,.p-heading--two{font-size:1.83274rem;line-height:2.5rem;margin-bottom:1.4rem;padding-top:0.1rem}}h3,.p-navigation--sidebar .p-navigation__logo,.p-heading--three,.p-list-step>li::before,.p-stepped-list--detailed>li::before{max-width:25em;font-size:1.70596rem;font-style:normal;font-weight:300;line-height:2.5rem;margin-bottom:1.4rem;margin-top:0;padding-top:0.1rem}@media (max-width: 768px){h3,.p-navigation--sidebar .p-navigation__logo,.p-heading--three,.p-list-step>li::before,.p-stepped-list--detailed>li::before{font-size:1.49271rem;line-height:2rem;margin-bottom:1rem;padding-top:0}}h4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote>p,.p-pull-quote__citation,.page-header__title-domain,.page-header__title{max-width:25em;font-size:1.30612rem;font-style:normal;font-weight:300;line-height:2rem;margin-bottom:.95rem;margin-top:0;padding-top:0.05rem}@media (max-width: 768px){h4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote>p,.p-pull-quote__citation,.page-header__title-domain,.page-header__title{font-size:1.22176rem;line-height:1.5rem;margin-bottom:.7rem;padding-top:0.3rem}}h5,.p-heading--five{font-size:1rem;font-style:normal;font-weight:500}h6,.p-heading--six{font-size:1rem;font-style:italic;font-weight:300}label,dt,cite,dd,p,h5,.p-heading--five,h6,.p-heading--six,.p-footer__link,.p-navigation--sidebar .p-navigation__tagline,.p-breadcrumbs__item,.p-notification__response,.default-text,.p-p-compact,.p-form--stacked .p-form__control>.p-control-text,.p-storage__type{line-height:1.5rem;margin-bottom:.1rem;margin-top:0;padding-top:.4rem}dd,p,h5,.p-heading--five,h6,.p-heading--six,.p-footer__link{margin-bottom:1.1rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item,small,thead th,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before,.p-form-help-text,.p-form-validation__message,.p-tooltip__message,.p-p-small,.p-p-small--align-with-p,.p-double-row .p-double-row__muted-row,.p-muted-text,.p-domain-name .p-domain-name__tld{font-size:.875rem;line-height:1.25rem;margin-bottom:.7rem;padding-top:0.05rem}thead th,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before{color:#666;margin-bottom:.5rem;margin-top:0;text-transform:uppercase}p,h5,.p-heading--five,h6,.p-heading--six,.measure--p{max-width:38em}dt,strong,.p-notification__status{font-weight:400}.p-navigation{background-color:#000;display:flex;flex-shrink:0;position:relative}@media (max-width: 870px){.p-navigation{flex-direction:column}}.p-navigation a,.p-navigation a:visited,.p-navigation a:hover,.p-navigation a:focus{color:#f7f7f7;text-decoration:none}.p-navigation::after{background:transparent;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}.p-navigation__banner{display:flex;flex:0 0 auto;justify-content:space-between}.p-navigation__image{align-self:center;max-height:2rem;min-height:1.5rem}.p-navigation__link>a{display:block;margin-bottom:0;position:relative}@media (max-width: 870px){.p-navigation__link>a{padding:.75rem 1.5rem}.p-navigation__link>a::before{background:transparent;content:'';height:.0625rem;left:0;position:absolute;right:0;top:0}}@media (min-width: 871px){.p-navigation__link>a{border-left:1px solid transparent;padding:.75rem 1rem}.p-navigation__link>a::before{background:transparent;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}}.p-navigation__link>a:hover{background-color:#000}@media (min-width: 871px){.p-navigation__link.is-selected>a{position:relative}.p-navigation__link.is-selected>a::before{bottom:0;background-color:#e95420;content:'';position:absolute}.p-navigation__link.is-selected>a::before{height:.1875rem;width:auto;left:-1px;right:-1px;z-index:1}}.p-navigation__links,.p-navigation .p-navigation__links--right{list-style:none;margin:0;padding:0}@media (max-width: 870px){.p-navigation__links,.p-navigation .p-navigation__links--right{margin-top:-1px}}@media (min-width: 871px){.p-navigation__links,.p-navigation .p-navigation__links--right{display:flex;flex-wrap:wrap}}.p-navigation__logo{display:flex;flex:0 0 auto;height:3rem;margin:0 1rem 0 1.5rem}.p-navigation__logo .p-navigation__link{display:flex}.p-navigation__nav{display:none}@media (max-width: 870px){.p-navigation__nav{flex-direction:column}}@media (min-width: 871px){.p-navigation__nav{display:flex;justify-content:space-between;width:100%}}.p-navigation .p-search-box{min-width:10em}@media (max-width: 870px){.p-navigation .p-search-box{flex:1 0 auto;margin:-1px 1.5rem .5rem 1.5rem;order:-1}}@media (min-width: 871px){.p-navigation .p-search-box{display:flex;flex:1 1 auto;margin:.35rem 1rem auto auto;max-width:20rem;order:1}}.p-navigation__row,.p-navigation .row{display:flex;padding-left:0;padding-right:0;width:100%}@media (max-width: 870px){.p-navigation__row,.p-navigation .row{flex-direction:column}}.p-navigation:target .p-navigation__nav{display:flex}.p-navigation:target .p-navigation__toggle--open{display:none}@media (max-width: 870px){.p-navigation:target .p-navigation__toggle--close{display:block}}.p-navigation__toggle--open,.p-navigation__toggle--close{display:none;margin:0 1.5rem auto 1rem;padding:.75rem 0}@media (max-width: 870px){.p-navigation__toggle--open{display:block}}.p-navigation .u-image-position .u-image-position--right{order:2;position:relative;right:unset}.p-navigation--sidebar{background-color:#fff;display:flex;flex-shrink:0;position:relative;flex-direction:column;height:auto}@media (max-width: 870px){.p-navigation--sidebar{flex-direction:column}}.p-navigation--sidebar a,.p-navigation--sidebar a:visited,.p-navigation--sidebar a:hover,.p-navigation--sidebar a:focus{color:#111;text-decoration:none}.p-navigation--sidebar::after{background:#cdcdcd;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}.p-navigation--sidebar__banner{display:flex;flex:0 0 auto;justify-content:space-between}.p-navigation--sidebar__image{align-self:center;max-height:2rem;min-height:1.5rem}.p-navigation--sidebar__link>a{display:block;margin-bottom:0;position:relative}@media (max-width: 870px){.p-navigation--sidebar__link>a{padding:.75rem 1.5rem}.p-navigation--sidebar__link>a::before{background:#cdcdcd;content:'';height:.0625rem;left:0;position:absolute;right:0;top:0}}@media (min-width: 871px){.p-navigation--sidebar__link>a{border-left:1px solid #cdcdcd;padding:.75rem 1rem}.p-navigation--sidebar__link>a::before{background:#cdcdcd;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}}.p-navigation--sidebar__link>a:hover{background-color:#000}@media (min-width: 871px){.p-navigation--sidebar__link.is-selected>a{position:relative}.p-navigation--sidebar__link.is-selected>a::before{bottom:0;background-color:#e95420;content:'';position:absolute}.p-navigation--sidebar__link.is-selected>a::before{height:.1875rem;width:auto;left:-1px;right:-1px;z-index:1}}.p-navigation--sidebar__links{list-style:none;margin:0;padding:0}@media (max-width: 870px){.p-navigation--sidebar__links{margin-top:-1px}}@media (min-width: 871px){.p-navigation--sidebar__links{display:flex;flex-wrap:wrap}}.p-navigation--sidebar__logo{display:flex;flex:0 0 auto;height:3rem;margin:0 1rem 0 1.5rem}.p-navigation--sidebar__logo .p-navigation__link{display:flex}.p-navigation--sidebar__nav{display:none}@media (max-width: 870px){.p-navigation--sidebar__nav{flex-direction:column}}@media (min-width: 871px){.p-navigation--sidebar__nav{display:flex;justify-content:space-between;width:100%}}.p-navigation--sidebar .p-search-box{min-width:10em}@media (max-width: 870px){.p-navigation--sidebar .p-search-box{flex:1 0 auto;margin:-1px 1.5rem .5rem 1.5rem;order:-1}}@media (min-width: 871px){.p-navigation--sidebar .p-search-box{display:flex;flex:1 1 auto;margin:.35rem 1rem auto auto;max-width:20rem;order:1}}.p-navigation--sidebar__row,.p-navigation--sidebar .row{display:flex;padding-left:0;padding-right:0;width:100%}@media (max-width: 870px){.p-navigation--sidebar__row,.p-navigation--sidebar .row{flex-direction:column}}.p-navigation--sidebar:target .p-navigation__nav{display:flex}.p-navigation--sidebar:target .p-navigation__toggle--open{display:none}@media (max-width: 870px){.p-navigation--sidebar:target .p-navigation__toggle--close{display:block}}.p-navigation--sidebar__toggle--open,.p-navigation--sidebar__toggle--close{display:none;margin:0 1.5rem auto 1rem;padding:.75rem 0}@media (max-width: 870px){.p-navigation--sidebar__toggle--open{display:block}}.p-navigation--sidebar .u-image-position .u-image-position--right{order:2;position:relative;right:unset}.p-navigation--sidebar .p-navigation__banner .row{flex-direction:row}.p-navigation--sidebar .sidebar__cta{margin-top:0}.p-navigation--sidebar .sidebar__cta .p-inline-list{display:inline-block}.p-navigation--sidebar .sidebar__cta [class^="p-icon"]{cursor:pointer}@media (min-width: 871px){.p-navigation--sidebar .sidebar__cta{display:none}}.p-navigation--sidebar .sidebar__content{background:#fff;width:100%}@media (min-width: 871px){.p-navigation--sidebar .sidebar__content{display:block !important}}.p-navigation--sidebar .sidebar__link{color:#111;display:block;position:relative}.p-navigation--sidebar .sidebar__link:hover{color:#007aa6}.p-navigation--sidebar .sidebar__link:focus{outline:0}.p-navigation--sidebar .p-navigation__logo{display:flex;flex:0 0 auto;margin-left:0}.p-navigation--sidebar .p-navigation__logo .p-navigation__image{height:24px;width:auto}.p-navigation--sidebar .p-navigation__tagline{display:block}.p-navigation--sidebar .is-selected{font-weight:bold}.p-navigation--sidebar .sidebar__first-level{padding-left:0}.p-navigation--sidebar .sidebar__third-level{background-color:#666;margin-right:-4rem;padding-left:4rem;position:relative;right:3rem}.p-navigation--sidebar .sidebar__second-level,.p-navigation--sidebar .sidebar__third-level{display:none;list-style:none;margin-left:0;padding-bottom:.25rem;padding-top:.25rem}.p-navigation--sidebar .sidebar__second-level .is-deepest-level,.p-navigation--sidebar .sidebar__third-level .is-deepest-level{background-color:#f7f7f7}.p-navigation--sidebar .p-icon--plus,.p-navigation--sidebar .p-icon--minus{perspective:800px;perspective-origin:50% 100px;position:absolute;right:1rem;top:.5rem;transition:all .5s ease-in-out}.p-navigation--sidebar .p-icon--minus{display:none}.p-navigation--sidebar .is-selected .p-icon--minus{display:block}.p-navigation--sidebar .is-selected .p-icon--plus{display:none}.p-navigation--sidebar .is-selected+.sidebar__second-level,.p-navigation--sidebar .is-selected+.sidebar__third-level{display:block}.p-accordion__list{list-style-type:none;margin:0 0 1rem 0;padding:0}.p-accordion__group:not(:last-child) .p-accordion__tab{border-bottom:1px solid #cdcdcd}.p-accordion__tab{background-position:top .59375rem right 1rem;background-repeat:no-repeat;background-color:inherit;border:0;border-radius:0;margin-bottom:0;padding-left:1rem;padding-right:1rem;text-align:left;transition-duration:0s;width:100%;z-index:2}.p-accordion__tab[aria-expanded='true']{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E");background-size:.75rem}.p-accordion__tab[aria-expanded='false']{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E");background-size:.75rem}.p-accordion__tab:focus{outline:1px solid #007aa6;outline-offset:2px}.p-accordion__panel{border-bottom:1px solid #cdcdcd;margin:0;overflow:auto;padding-left:2rem}.p-accordion__panel[aria-hidden='true']{display:none}.p-accordion p{margin-bottom:.6rem}.p-aside{border-top:1px solid #cdcdcd;font-size:.875rem;padding:0 1.5rem}@media (min-width: 768px){.p-aside{border-left:1px solid #cdcdcd;border-top:0;padding:0 1rem}}.p-aside__header{color:#666;font-size:1rem;line-height:1.5;margin-bottom:1rem;text-transform:uppercase}.p-aside__section{padding:1rem 0}.p-aside__section:not(:last-child){border-bottom:1px dotted #cdcdcd}.p-aside__nav{list-style:none;margin:0;padding:0}.p-aside__nav .p-aside__link{border-bottom:0;color:#111;margin-bottom:.25rem}.p-aside__nav .p-aside__link:visited{color:#111}.p-aside__nav .p-aside__link:hover{color:#007aa6}.p-aside__nav .p-aside__link.is-active{font-weight:400;padding-left:.25rem}.p-breadcrumbs{list-style:none;margin:0;padding:0;width:100%}.p-breadcrumbs__item{display:inline-block;margin-bottom:.1rem}.p-breadcrumbs__item:not(:first-of-type){text-indent:1rem}.p-breadcrumbs__item:not(:first-of-type)::before{content:'\203A';margin-left:-.75rem;margin-right:.5rem}.p-button{background-color:#fff;border-color:#cdcdcd;color:#111}.p-button:visited{color:#111}.p-button:active,.p-button:hover{background-color:#f7f7f7;border-color:#cdcdcd}.p-button:disabled:active,.p-button:disabled:hover,.is--disabled.p-button:active,.is--disabled.p-button:hover{background-color:#fff;border-color:#fff}.p-button .p-link--external{color:currentColor}.p-button--neutral{background-color:#fff;border-color:#cdcdcd;color:#111}.p-button--neutral:visited{color:#111}.p-button--neutral:active,.p-button--neutral:hover{background-color:#dedede;border-color:#cdcdcd}.p-button--neutral:disabled:active,.p-button--neutral:disabled:hover,.is--disabled.p-button--neutral:active,.is--disabled.p-button--neutral:hover{background-color:transparent;border-color:#cdcdcd}.p-button--neutral .p-link--external{color:currentColor}.p-button--brand{background-color:#e95420;border-color:#e95420;color:#fff}.p-button--brand:visited{color:#fff}.p-button--brand:active,.p-button--brand:hover{background-color:#c34113;border-color:#c34113}.p-button--brand:disabled:active,.p-button--brand:disabled:hover,.is--disabled.p-button--brand:active,.is--disabled.p-button--brand:hover{background-color:#e95420;border-color:#e95420}.p-button--brand .p-link--external{color:currentColor}.p-button--positive{background-color:#0e8420;border-color:#0e8420;color:#fff}.p-button--positive:visited{color:#fff}.p-button--positive:active,.p-button--positive:hover{background-color:#095615;border-color:#095615}.p-button--positive:disabled:active,.p-button--positive:disabled:hover,.is--disabled.p-button--positive:active,.is--disabled.p-button--positive:hover{background-color:#0e8420;border-color:#0e8420}.p-button--positive .p-link--external{color:currentColor}.p-button--negative{background-color:#c7162b;border-color:#c7162b;color:#fff}.p-button--negative:visited{color:#fff}.p-button--negative:active,.p-button--negative:hover{background-color:#991121;border-color:#991121}.p-button--negative:disabled:active,.p-button--negative:disabled:hover,.is--disabled.p-button--negative:active,.is--disabled.p-button--negative:hover{background-color:#c7162b;border-color:#c7162b}.p-button--negative .p-link--external{color:currentColor}.p-button--base{background-color:transparent;border-color:transparent;color:#111}.p-button--base:visited{color:#111}.p-button--base:active,.p-button--base:hover{background-color:#f7f7f7;border-color:transparent}.p-button--base:disabled:active,.p-button--base:disabled:hover,.is--disabled.p-button--base:active,.is--disabled.p-button--base:hover{background-color:transparent;border-color:#cdcdcd}.p-button--base .p-link--external{color:currentColor}@media (min-width: 768px){[class^="p-button"].is-inline{margin-left:1rem;width:auto}}.p-card{padding:.4375rem}.p-card--overlay{background:rgba(255,255,255,0.9);color:#111;margin-bottom:1rem;overflow:auto;padding:.5rem}.p-card--muted{margin-bottom:1rem;overflow:auto;padding:.5rem}.p-card__image{margin-bottom:.5rem;vertical-align:top;width:100%}.p-card__content{margin-top:-1rem}.p-card__header{border-bottom:1px solid #cdcdcd;margin-bottom:.5rem}.p-card__header>.p-link--soft{display:inline-block;overflow:auto}.p-card__thumbnail{max-height:2rem}.p-card__content{margin-top:-1rem}.p-card__header{border-bottom:1px solid #cdcdcd;margin-bottom:.5rem}.p-card__header>.p-link--soft{display:inline-block;overflow:auto}.p-code-numbered{counter-reset:line-numbering;padding:0}.p-code-numbered .code-line{display:inline-block;padding:.5rem 1rem 0 5.5rem;position:relative;width:100%}.p-code-numbered .code-line:empty{display:block;min-height:2.5rem}.p-code-numbered .code-line:last-of-type,.p-code-numbered .code-line:last-of-type::before{padding-bottom:.5rem}.p-code-numbered .code-line::before{background:#fff;border-right:1px solid #cdcdcd;color:#666;content:counter(line-numbering);counter-increment:line-numbering;display:inline-block;height:100%;left:0;padding:.5rem 1rem 0 1rem;pointer-events:none;position:absolute;text-align:right;top:0;user-select:none;width:4.5rem}.p-code-snippet{background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;color:#111;display:flex;overflow:hidden;padding-left:.5rem;padding-right:.5rem;position:relative;transition:border .2s, background-color .2s;width:100%}.p-code-snippet+.p-code-snippet{margin-top:0}.p-code-snippet__input{background-color:transparent;background-image:url('data:image/svg+xml;utf8, ');background-position:0 center;background-repeat:no-repeat;border:0;box-shadow:none;color:#666;font-family:"Ubuntu Mono", Consolas, Monaco, Courier, monospace;font-weight:300;line-height:1.5rem;margin-bottom:0;padding:0;padding-left:1.5rem;width:100%}.p-code-snippet__action{background-color:#f7f7f7;background-image:url('data:image/svg+xml;utf8, ');background-position:center;background-repeat:no-repeat;background-size:1rem;border-color:transparent;border-left:1px solid #cdcdcd;border-radius:0;display:block;height:100%;margin-bottom:0;margin-top:0;padding:0;position:absolute;right:0;text-indent:-9999px;top:0;width:40px}.p-code-snippet__action:hover{border-color:transparent;border-left:1px solid #cdcdcd}.p-contextual-menu,.p-contextual-menu--left,.p-contextual-menu--center,.p-cta,.p-table-menu{display:inline-block;margin:0;position:relative}.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown{display:none;margin:0;max-width:21rem;min-width:10rem;padding:0;position:absolute;right:0;top:calc(100% + .25rem);z-index:1}.p-contextual-menu__dropdown::before,.p-cta__dropdown::before,.p-table-menu .p-table-menu__dropdown::before,.p-contextual-menu__dropdown::after,.p-cta__dropdown::after,.p-table-menu .p-table-menu__dropdown::after{border-bottom:8px solid rgba(17,17,17,0.05);border-left:8px solid transparent;border-right:8px solid transparent;bottom:100%;content:'';height:0;pointer-events:none;position:absolute;right:.5rem;width:0}.p-contextual-menu__dropdown::after,.p-cta__dropdown::after,.p-table-menu .p-table-menu__dropdown::after{border-bottom:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;right:.6rem}.p-contextual-menu__dropdown[aria-hidden="false"],[aria-hidden="false"].p-cta__dropdown,.p-table-menu [aria-hidden="false"].p-table-menu__dropdown{display:block}.p-contextual-menu__group{display:block;padding:.125rem 0}.p-contextual-menu__group+.p-contextual-menu__group{border-top:1px solid #cdcdcd;margin:0}.p-contextual-menu__link,.p-cta__link,.p-table-menu .p-table-menu__link,.p-table-menu .p-table-menu__check-power,.p-table-menu .p-table-menu__power-on,.p-table-menu .p-table-menu__power-off{border:0;clear:both;color:#111;display:block;line-height:1.5rem;margin:0;overflow:hidden;padding:.125rem .5rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;width:100%}.p-contextual-menu__link:hover,.p-cta__link:hover,.p-table-menu .p-table-menu__link:hover,.p-table-menu .p-table-menu__check-power:hover,.p-table-menu .p-table-menu__power-on:hover,.p-table-menu .p-table-menu__power-off:hover{background-color:#f7f7f7;text-decoration:none}.p-contextual-menu--left .p-contextual-menu__dropdown,.p-contextual-menu--left .p-cta__dropdown,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown{left:0}.p-contextual-menu--left .p-contextual-menu__dropdown::before,.p-contextual-menu--left .p-cta__dropdown::before,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown::before,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown::before,.p-contextual-menu--left .p-contextual-menu__dropdown::after,.p-contextual-menu--left .p-cta__dropdown::after,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown::after,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown::after{left:.5rem;right:initial}.p-contextual-menu--left .p-contextual-menu__dropdown::after,.p-contextual-menu--left .p-cta__dropdown::after,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown::after,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown::after{left:.6rem}.p-contextual-menu--center .p-contextual-menu__dropdown,.p-contextual-menu--center .p-cta__dropdown,.p-contextual-menu--center .p-table-menu .p-table-menu__dropdown,.p-table-menu .p-contextual-menu--center .p-table-menu__dropdown{left:50%;transform:translateX(-50%)}.p-contextual-menu--center .p-contextual-menu__dropdown::before,.p-contextual-menu--center .p-cta__dropdown::before,.p-contextual-menu--center .p-table-menu .p-table-menu__dropdown::before,.p-table-menu .p-contextual-menu--center .p-table-menu__dropdown::before,.p-contextual-menu--center .p-contextual-menu__dropdown::after,.p-contextual-menu--center .p-cta__dropdown::after,.p-contextual-menu--center .p-table-menu .p-table-menu__dropdown::after,.p-table-menu .p-contextual-menu--center .p-table-menu__dropdown::after{left:50%;right:initial;transform:translateX(-50%)}@media (min-width: 768px){.p-divider{display:flex}}@media (max-width: 768px){.p-divider__block{padding-bottom:1rem}.p-divider__block:not(:first-child){border-top:1px solid #cdcdcd;padding-top:.4375rem}}@media (min-width: 768px){.p-divider__block{padding-right:1rem}.p-divider__block:not(:nth-child(1))::before{border-left:1px solid #cdcdcd;bottom:.5rem;content:'';left:-1.5rem;position:absolute;top:.5rem}.p-divider__block:last-child{padding-right:0}}.p-footer{border-top:1px solid #cdcdcd;position:relative}@media only screen and (max-width: 1030px){.p-footer{padding-bottom:1.5rem;padding-top:1.5rem}}@media only screen and (min-width: 1030px){.p-footer{padding-bottom:3rem;padding-top:3rem}}.p-footer__copy{margin-bottom:0}.p-footer__links{margin:0;padding:0}@media (min-width: 768px){.p-footer__links{margin-top:0}}.p-footer__nav{margin-top:0}p+.p-footer__nav{margin-top:-1rem}.p-footer__item{display:block}@media (min-width: 768px){.p-footer__item{display:inline-block}}.p-footer__item:last-child a::after{opacity:0}.p-footer__link{border-bottom:0;color:#111;display:inline-block}.p-footer__link:visited{color:#000}.p-footer__link:hover{color:#007aa6}@media (min-width: 768px){.p-footer__link{margin-right:1rem;position:relative}.p-footer__link::after{content:'\00b7';display:inline-block;font-size:1.5rem;position:absolute;right:-.75rem;top:.4rem}}.p-footer__link:hover::after{color:#111}.p-form-help-text{color:#666}input+.p-form-help-text,.p-form-validation .p-form-help-text{margin-top:.05rem}.p-form-validation{color:#111;position:relative}.p-form-validation .p-form-validation__input{background-position:calc(100% - .5rem) 50%;background-repeat:no-repeat}.p-form-validation .p-form-validation__icon{position:relative}.p-form-validation .p-form-validation__icon::after{position:absolute;right:.5rem;top:calc(50% - .25rem)}input+.p-form-validation__message,.p-form-help-text .p-form-validation__message{margin-top:.05rem}.is-error .p-form-validation__input{background-image:url("/MAAS/static/assets/fonts/4b0cd7fc-icon-error.svg");border-color:#c7162b}.is-success .p-form-validation__input{background-image:url("/MAAS/static/assets/fonts/94949185-icon-success.svg");border-color:#0e8420}.is-caution .p-form-validation__input{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath stroke-linejoin='round' fill='%23f99b11' transform='matrix%282.28 0 0 2.437 -2180.8 -490.52%29' stroke='%23f99b11' stroke-width='.848' d='M963.07 207.03h-6.15l3.08-5.33z'/%3E%3Cpath d='M7 5v5h2V5H7zm0 6v2h2v-2H7z' fill='%23111'/%3E%3C/g%3E%3C/svg%3E");border-color:#f99b11}.p-form--stacked{width:100%}@media screen and (min-width: 768px){.p-form--stacked .p-form__group{align-items:baseline;display:flex;flex-flow:wrap}.p-form--stacked .p-form__group+.p-form__group{margin-top:.5rem}}@media screen and (min-width: 768px){.p-form--stacked .p-form__label{flex-basis:25%;flex-grow:1;margin:0;max-width:25%;padding-right:.5rem}}@media screen and (min-width: 768px){.p-form--stacked .p-form__control{flex-basis:75%;flex-grow:1;margin:0;max-width:75%}}@media screen and (min-width: 768px){.p-form--inline{align-items:baseline;display:inline-flex;flex-direction:row}.p-form--inline>*{margin:0}}@media screen and (min-width: 768px){.p-form--inline .p-form__group{display:inline-flex;width:auto}.p-form--inline .p-form__group+.p-form__group,.p-form--inline .p-form__group+[class*="p-button"]{margin-left:1.5rem}.p-form--inline .p-form__group .p-form__label,.p-form--inline .p-form__group .p-form__control,.p-form--inline .p-form__group .p-form-validation__message{align-self:baseline;box-sizing:border-box}.p-form--inline .p-form__group .p-form__label{flex-shrink:0;padding-right:1rem}.p-form--inline .p-form__group .p-form__control{display:inline-block}.p-form--inline .p-form__group .p-form-validation__message,.p-form--inline .p-form__group .p-form-help-text{clear:both;min-width:100%;width:0}}.p-form--inline [class*="p-button"]{flex:initial;flex-shrink:0;margin-top:0}form+[class*="p-button"]{margin-top:1.5rem}.row{width:100%}[grid-demo] [class*="col-"]{background:#cdcdcd;margin-bottom:.5rem}[grid-outline] [class*="col-"]{outline:1px solid #fff;padding:.5rem .5rem}@media screen and (max-width: 400px){@-ms-viewport{width:320px}}img{max-width:100%;height:auto}@media \0screen{img{width:auto}}.row{*zoom:1;margin-right:auto;margin-left:auto;max-width:90rem;padding-left:1.5rem;padding-right:1.5rem}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row .row{margin-right:0;margin-left:0;max-width:none;padding-right:0;padding-left:0}.mobile-col-1,.mobile-col-2,.mobile-col-3{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:7.80829%}.row .mobile-col-1:first-child,.row .mobile-col-2:first-child,.row .mobile-col-3:first-child,.first-mobile-col{margin-left:0}.mobile-col-1{width:19.14379%}.mobile-col-2{width:46.09586%}.mobile-col-3{width:73.04793%}.mobile-prefix-1{padding-left:26.95207%}.mobile-prefix-2{padding-left:53.90414%}.mobile-prefix-3{padding-left:80.85621%}.mobile-suffix-1{padding-right:26.95207%}.mobile-suffix-2{padding-right:53.90414%}.mobile-suffix-3{padding-right:80.85621%}.mobile-push-1{left:26.95207%}.mobile-push-2{left:53.90414%}.mobile-push-3{left:80.85621%}.mobile-pull-1{right:26.95207%}.mobile-pull-2{right:53.90414%}.mobile-pull-3{right:80.85621%}@media screen and (min-width: 620px){.tablet-col-1,.tablet-col-2,.p-form--stacked .p-form__label,.tablet-col-3,.p-pod-summary__cpu,.p-pod-summary__ram,.tablet-col-4,.p-form--stacked .p-form__control,.tablet-col-5{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:4.93155%}.row .tablet-col-1:first-child,.row .tablet-col-2:first-child,.row .p-form--stacked .p-form__label:first-child,.p-form--stacked .row .p-form__label:first-child,.row .tablet-col-3:first-child,.row .p-pod-summary__cpu:first-child,.row .p-pod-summary__ram:first-child,.row .tablet-col-4:first-child,.row .p-form--stacked .p-form__control:first-child,.p-form--stacked .row .p-form__control:first-child,.row .tablet-col-5:first-child,.first-tablet-col{margin-left:0}.tablet-col-1{width:12.55704%}.tablet-col-2,.p-form--stacked .p-form__label{width:30.04563%}.tablet-col-3,.p-pod-summary__cpu,.p-pod-summary__ram{width:47.53423%}.tablet-col-4,.p-form--stacked .p-form__control{width:65.02282%}.tablet-col-5{width:82.51141%}.tablet-prefix-1{padding-left:17.48859%}.tablet-prefix-2{padding-left:34.97718%}.tablet-prefix-3{padding-left:52.46577%}.tablet-prefix-4{padding-left:69.95437%}.tablet-prefix-5{padding-left:87.44296%}.tablet-suffix-1{padding-right:17.48859%}.tablet-suffix-2{padding-right:34.97718%}.tablet-suffix-3{padding-right:52.46577%}.tablet-suffix-4{padding-right:69.95437%}.tablet-suffix-5{padding-right:87.44296%}.tablet-push-1{left:17.48859%}.tablet-push-2{left:34.97718%}.tablet-push-3{left:52.46577%}.tablet-push-4{left:69.95437%}.tablet-push-5{left:87.44296%}.tablet-pull-1{right:17.48859%}.tablet-pull-2{right:34.97718%}.tablet-pull-3{right:52.46577%}.tablet-pull-4{right:69.95437%}.tablet-pull-5{right:87.44296%}}@media screen and (min-width: 768px){.col-1,.col-2,.p-form--stacked .p-form__label,.col-3,.col-4,.p-form--stacked .p-form__control,.p-pod-summary__aside,.p-storage__name,.col-5,.col-6,.col-7,.col-8,.p-pod-summary__storage,.col-9,.col-10,.col-11{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:3.2877%}.row .col-1:first-child,.row .col-2:first-child,.row .p-form--stacked .p-form__label:first-child,.p-form--stacked .row .p-form__label:first-child,.row .col-3:first-child,.row .col-4:first-child,.row .p-form--stacked .p-form__control:first-child,.p-form--stacked .row .p-form__control:first-child,.row .p-pod-summary__aside:first-child,.row .p-storage__name:first-child,.row .col-5:first-child,.row .col-6:first-child,.row .col-7:first-child,.row .col-8:first-child,.row .p-pod-summary__storage:first-child,.row .col-9:first-child,.row .col-10:first-child,.row .col-11:first-child,.first-col{margin-left:0}.col-1{width:5.31961%}.col-2,.p-form--stacked .p-form__label{width:13.92692%}.col-3{width:22.53423%}.col-4,.p-form--stacked .p-form__control,.p-pod-summary__aside,.p-storage__name{width:31.14153%}.col-5{width:39.74884%}.col-6{width:48.35615%}.col-7{width:56.96346%}.col-8,.p-pod-summary__storage{width:65.57077%}.col-9{width:74.17808%}.col-10{width:82.78538%}.col-11{width:91.39269%}.prefix-1{padding-left:8.60731%}.prefix-2{padding-left:17.21462%}.prefix-3{padding-left:25.82192%}.prefix-4{padding-left:34.42923%}.prefix-5{padding-left:43.03654%}.prefix-6{padding-left:51.64385%}.prefix-7{padding-left:60.25116%}.prefix-8{padding-left:68.85847%}.prefix-9{padding-left:77.46577%}.prefix-10{padding-left:86.07308%}.prefix-11{padding-left:94.68039%}.suffix-1{padding-right:8.60731%}.suffix-2{padding-right:17.21462%}.suffix-3{padding-right:25.82192%}.suffix-4{padding-right:34.42923%}.suffix-5{padding-right:43.03654%}.suffix-6{padding-right:51.64385%}.suffix-7{padding-right:60.25116%}.suffix-8{padding-right:68.85847%}.suffix-9{padding-right:77.46577%}.suffix-10{padding-right:86.07308%}.suffix-11{padding-right:94.68039%}.push-1{left:8.60731%}.push-2{left:17.21462%}.push-3{left:25.82192%}.push-4{left:34.42923%}.push-5{left:43.03654%}.push-6{left:51.64385%}.push-7{left:60.25116%}.push-8{left:68.85847%}.push-9{left:77.46577%}.push-10{left:86.07308%}.push-11{left:94.68039%}.pull-1{right:8.60731%}.pull-2{right:17.21462%}.pull-3{right:25.82192%}.pull-4{right:34.42923%}.pull-5{right:43.03654%}.pull-6{right:51.64385%}.pull-7{right:60.25116%}.pull-8{right:68.85847%}.pull-9{right:77.46577%}.pull-10{right:86.07308%}.pull-11{right:94.68039%}.col-11 .col-1,.col-11 .col-2,.col-11 .p-form--stacked .p-form__label,.p-form--stacked .col-11 .p-form__label,.col-11 .col-3,.col-11 .col-4,.col-11 .p-form--stacked .p-form__control,.p-form--stacked .col-11 .p-form__control,.col-11 .p-pod-summary__aside,.col-11 .p-storage__name,.col-11 .col-5,.col-11 .col-6,.col-11 .col-7,.col-11 .col-8,.col-11 .p-pod-summary__storage,.col-11 .col-9,.col-11 .col-10{margin-left:3.59733%}.col-11 .col-1{width:5.82061%}.col-11 .col-2,.col-11 .p-form--stacked .p-form__label,.p-form--stacked .col-11 .p-form__label{width:15.23855%}.col-11 .col-3{width:24.65649%}.col-11 .col-4,.col-11 .p-form--stacked .p-form__control,.p-form--stacked .col-11 .p-form__control,.col-11 .p-pod-summary__aside,.col-11 .p-storage__name{width:34.07442%}.col-11 .col-5{width:43.49236%}.col-11 .col-6{width:52.9103%}.col-11 .col-7{width:62.32824%}.col-11 .col-8,.col-11 .p-pod-summary__storage{width:71.74618%}.col-11 .col-9{width:81.16412%}.col-11 .col-10{width:90.58206%}.col-11 .prefix-1{padding-left:9.41794%}.col-11 .prefix-2{padding-left:18.83588%}.col-11 .prefix-3{padding-left:28.25382%}.col-11 .prefix-4{padding-left:37.67176%}.col-11 .prefix-5{padding-left:47.0897%}.col-11 .prefix-6{padding-left:56.50764%}.col-11 .prefix-7{padding-left:65.92558%}.col-11 .prefix-8{padding-left:75.34351%}.col-11 .prefix-9{padding-left:84.76145%}.col-11 .prefix-10{padding-left:94.17939%}.col-11 .suffix-1{padding-right:9.41794%}.col-11 .suffix-2{padding-right:18.83588%}.col-11 .suffix-3{padding-right:28.25382%}.col-11 .suffix-4{padding-right:37.67176%}.col-11 .suffix-5{padding-right:47.0897%}.col-11 .suffix-6{padding-right:56.50764%}.col-11 .suffix-7{padding-right:65.92558%}.col-11 .suffix-8{padding-right:75.34351%}.col-11 .suffix-9{padding-right:84.76145%}.col-11 .suffix-10{padding-right:94.17939%}.col-11 .push-1{left:9.41794%}.col-11 .push-2{left:18.83588%}.col-11 .push-3{left:28.25382%}.col-11 .push-4{left:37.67176%}.col-11 .push-5{left:47.0897%}.col-11 .push-6{left:56.50764%}.col-11 .push-7{left:65.92558%}.col-11 .push-8{left:75.34351%}.col-11 .push-9{left:84.76145%}.col-11 .push-10{left:94.17939%}.col-11 .pull-1{right:9.41794%}.col-11 .pull-2{right:18.83588%}.col-11 .pull-3{right:28.25382%}.col-11 .pull-4{right:37.67176%}.col-11 .pull-5{right:47.0897%}.col-11 .pull-6{right:56.50764%}.col-11 .pull-7{right:65.92558%}.col-11 .pull-8{right:75.34351%}.col-11 .pull-9{right:84.76145%}.col-11 .pull-10{right:94.17939%}.col-10 .col-1,.col-10 .col-2,.col-10 .p-form--stacked .p-form__label,.p-form--stacked .col-10 .p-form__label,.col-10 .col-3,.col-10 .col-4,.col-10 .p-form--stacked .p-form__control,.p-form--stacked .col-10 .p-form__control,.col-10 .p-pod-summary__aside,.col-10 .p-storage__name,.col-10 .col-5,.col-10 .col-6,.col-10 .col-7,.col-10 .col-8,.col-10 .p-pod-summary__storage,.col-10 .col-9{margin-left:3.97135%}.col-10 .col-1{width:6.42578%}.col-10 .col-2,.col-10 .p-form--stacked .p-form__label,.p-form--stacked .col-10 .p-form__label{width:16.82292%}.col-10 .col-3{width:27.22005%}.col-10 .col-4,.col-10 .p-form--stacked .p-form__control,.p-form--stacked .col-10 .p-form__control,.col-10 .p-pod-summary__aside,.col-10 .p-storage__name{width:37.61719%}.col-10 .col-5{width:48.01432%}.col-10 .col-6{width:58.41146%}.col-10 .col-7{width:68.80859%}.col-10 .col-8,.col-10 .p-pod-summary__storage{width:79.20573%}.col-10 .col-9{width:89.60286%}.col-10 .prefix-1{padding-left:10.39714%}.col-10 .prefix-2{padding-left:20.79427%}.col-10 .prefix-3{padding-left:31.19141%}.col-10 .prefix-4{padding-left:41.58854%}.col-10 .prefix-5{padding-left:51.98568%}.col-10 .prefix-6{padding-left:62.38281%}.col-10 .prefix-7{padding-left:72.77995%}.col-10 .prefix-8{padding-left:83.17708%}.col-10 .prefix-9{padding-left:93.57422%}.col-10 .suffix-1{padding-right:10.39714%}.col-10 .suffix-2{padding-right:20.79427%}.col-10 .suffix-3{padding-right:31.19141%}.col-10 .suffix-4{padding-right:41.58854%}.col-10 .suffix-5{padding-right:51.98568%}.col-10 .suffix-6{padding-right:62.38281%}.col-10 .suffix-7{padding-right:72.77995%}.col-10 .suffix-8{padding-right:83.17708%}.col-10 .suffix-9{padding-right:93.57422%}.col-10 .push-1{left:10.39714%}.col-10 .push-2{left:20.79427%}.col-10 .push-3{left:31.19141%}.col-10 .push-4{left:41.58854%}.col-10 .push-5{left:51.98568%}.col-10 .push-6{left:62.38281%}.col-10 .push-7{left:72.77995%}.col-10 .push-8{left:83.17708%}.col-10 .push-9{left:93.57422%}.col-10 .pull-1{right:10.39714%}.col-10 .pull-2{right:20.79427%}.col-10 .pull-3{right:31.19141%}.col-10 .pull-4{right:41.58854%}.col-10 .pull-5{right:51.98568%}.col-10 .pull-6{right:62.38281%}.col-10 .pull-7{right:72.77995%}.col-10 .pull-8{right:83.17708%}.col-10 .pull-9{right:93.57422%}.col-9 .col-1,.col-9 .col-2,.col-9 .p-form--stacked .p-form__label,.p-form--stacked .col-9 .p-form__label,.col-9 .col-3,.col-9 .col-4,.col-9 .p-form--stacked .p-form__control,.p-form--stacked .col-9 .p-form__control,.col-9 .p-pod-summary__aside,.col-9 .p-storage__name,.col-9 .col-5,.col-9 .col-6,.col-9 .col-7,.col-9 .col-8,.col-9 .p-pod-summary__storage{margin-left:4.43217%}.col-9 .col-1{width:7.1714%}.col-9 .col-2,.col-9 .p-form--stacked .p-form__label,.p-form--stacked .col-9 .p-form__label{width:18.77498%}.col-9 .col-3{width:30.37855%}.col-9 .col-4,.col-9 .p-form--stacked .p-form__control,.p-form--stacked .col-9 .p-form__control,.col-9 .p-pod-summary__aside,.col-9 .p-storage__name{width:41.98213%}.col-9 .col-5{width:53.5857%}.col-9 .col-6{width:65.18928%}.col-9 .col-7{width:76.79285%}.col-9 .col-8,.col-9 .p-pod-summary__storage{width:88.39643%}.col-9 .prefix-1{padding-left:11.60357%}.col-9 .prefix-2{padding-left:23.20715%}.col-9 .prefix-3{padding-left:34.81072%}.col-9 .prefix-4{padding-left:46.4143%}.col-9 .prefix-5{padding-left:58.01787%}.col-9 .prefix-6{padding-left:69.62145%}.col-9 .prefix-7{padding-left:81.22502%}.col-9 .prefix-8{padding-left:92.8286%}.col-9 .suffix-1{padding-right:11.60357%}.col-9 .suffix-2{padding-right:23.20715%}.col-9 .suffix-3{padding-right:34.81072%}.col-9 .suffix-4{padding-right:46.4143%}.col-9 .suffix-5{padding-right:58.01787%}.col-9 .suffix-6{padding-right:69.62145%}.col-9 .suffix-7{padding-right:81.22502%}.col-9 .suffix-8{padding-right:92.8286%}.col-9 .push-1{left:11.60357%}.col-9 .push-2{left:23.20715%}.col-9 .push-3{left:34.81072%}.col-9 .push-4{left:46.4143%}.col-9 .push-5{left:58.01787%}.col-9 .push-6{left:69.62145%}.col-9 .push-7{left:81.22502%}.col-9 .push-8{left:92.8286%}.col-9 .pull-1{right:11.60357%}.col-9 .pull-2{right:23.20715%}.col-9 .pull-3{right:34.81072%}.col-9 .pull-4{right:46.4143%}.col-9 .pull-5{right:58.01787%}.col-9 .pull-6{right:69.62145%}.col-9 .pull-7{right:81.22502%}.col-9 .pull-8{right:92.8286%}.col-8 .col-1,.p-pod-summary__storage .col-1,.col-8 .col-2,.p-pod-summary__storage .col-2,.col-8 .p-form--stacked .p-form__label,.p-form--stacked .col-8 .p-form__label,.p-pod-summary__storage .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__storage .p-form__label,.col-8 .col-3,.p-pod-summary__storage .col-3,.col-8 .col-4,.p-pod-summary__storage .col-4,.col-8 .p-form--stacked .p-form__control,.p-form--stacked .col-8 .p-form__control,.p-pod-summary__storage .p-form--stacked .p-form__control,.p-form--stacked .p-pod-summary__storage .p-form__control,.col-8 .p-pod-summary__aside,.p-pod-summary__storage .p-pod-summary__aside,.col-8 .p-storage__name,.p-pod-summary__storage .p-storage__name,.col-8 .col-5,.p-pod-summary__storage .col-5,.col-8 .col-6,.p-pod-summary__storage .col-6,.col-8 .col-7,.p-pod-summary__storage .col-7{margin-left:5.01397%}.col-8 .col-1,.p-pod-summary__storage .col-1{width:8.11278%}.col-8 .col-2,.p-pod-summary__storage .col-2,.col-8 .p-form--stacked .p-form__label,.p-form--stacked .col-8 .p-form__label,.p-pod-summary__storage .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__storage .p-form__label{width:21.23952%}.col-8 .col-3,.p-pod-summary__storage .col-3{width:34.36627%}.col-8 .col-4,.p-pod-summary__storage .col-4,.col-8 .p-form--stacked .p-form__control,.p-form--stacked .col-8 .p-form__control,.p-pod-summary__storage .p-form--stacked .p-form__control,.p-form--stacked .p-pod-summary__storage .p-form__control,.col-8 .p-pod-summary__aside,.p-pod-summary__storage .p-pod-summary__aside,.col-8 .p-storage__name,.p-pod-summary__storage .p-storage__name{width:47.49301%}.col-8 .col-5,.p-pod-summary__storage .col-5{width:60.61976%}.col-8 .col-6,.p-pod-summary__storage .col-6{width:73.74651%}.col-8 .col-7,.p-pod-summary__storage .col-7{width:86.87325%}.col-8 .prefix-1,.p-pod-summary__storage .prefix-1{padding-left:13.12675%}.col-8 .prefix-2,.p-pod-summary__storage .prefix-2{padding-left:26.25349%}.col-8 .prefix-3,.p-pod-summary__storage .prefix-3{padding-left:39.38024%}.col-8 .prefix-4,.p-pod-summary__storage .prefix-4{padding-left:52.50699%}.col-8 .prefix-5,.p-pod-summary__storage .prefix-5{padding-left:65.63373%}.col-8 .prefix-6,.p-pod-summary__storage .prefix-6{padding-left:78.76048%}.col-8 .prefix-7,.p-pod-summary__storage .prefix-7{padding-left:91.88722%}.col-8 .suffix-1,.p-pod-summary__storage .suffix-1{padding-right:13.12675%}.col-8 .suffix-2,.p-pod-summary__storage .suffix-2{padding-right:26.25349%}.col-8 .suffix-3,.p-pod-summary__storage .suffix-3{padding-right:39.38024%}.col-8 .suffix-4,.p-pod-summary__storage .suffix-4{padding-right:52.50699%}.col-8 .suffix-5,.p-pod-summary__storage .suffix-5{padding-right:65.63373%}.col-8 .suffix-6,.p-pod-summary__storage .suffix-6{padding-right:78.76048%}.col-8 .suffix-7,.p-pod-summary__storage .suffix-7{padding-right:91.88722%}.col-8 .push-1,.p-pod-summary__storage .push-1{left:13.12675%}.col-8 .push-2,.p-pod-summary__storage .push-2{left:26.25349%}.col-8 .push-3,.p-pod-summary__storage .push-3{left:39.38024%}.col-8 .push-4,.p-pod-summary__storage .push-4{left:52.50699%}.col-8 .push-5,.p-pod-summary__storage .push-5{left:65.63373%}.col-8 .push-6,.p-pod-summary__storage .push-6{left:78.76048%}.col-8 .push-7,.p-pod-summary__storage .push-7{left:91.88722%}.col-8 .pull-1,.p-pod-summary__storage .pull-1{right:13.12675%}.col-8 .pull-2,.p-pod-summary__storage .pull-2{right:26.25349%}.col-8 .pull-3,.p-pod-summary__storage .pull-3{right:39.38024%}.col-8 .pull-4,.p-pod-summary__storage .pull-4{right:52.50699%}.col-8 .pull-5,.p-pod-summary__storage .pull-5{right:65.63373%}.col-8 .pull-6,.p-pod-summary__storage .pull-6{right:78.76048%}.col-8 .pull-7,.p-pod-summary__storage .pull-7{right:91.88722%}.col-7 .col-1,.col-7 .col-2,.col-7 .p-form--stacked .p-form__label,.p-form--stacked .col-7 .p-form__label,.col-7 .col-3,.col-7 .col-4,.col-7 .p-form--stacked .p-form__control,.p-form--stacked .col-7 .p-form__control,.col-7 .p-pod-summary__aside,.col-7 .p-storage__name,.col-7 .col-5,.col-7 .col-6{margin-left:5.77159%}.col-7 .col-1{width:9.33863%}.col-7 .col-2,.col-7 .p-form--stacked .p-form__label,.p-form--stacked .col-7 .p-form__label{width:24.44886%}.col-7 .col-3{width:39.55909%}.col-7 .col-4,.col-7 .p-form--stacked .p-form__control,.p-form--stacked .col-7 .p-form__control,.col-7 .p-pod-summary__aside,.col-7 .p-storage__name{width:54.66932%}.col-7 .col-5{width:69.77954%}.col-7 .col-6{width:84.88977%}.col-7 .prefix-1{padding-left:15.11023%}.col-7 .prefix-2{padding-left:30.22046%}.col-7 .prefix-3{padding-left:45.33068%}.col-7 .prefix-4{padding-left:60.44091%}.col-7 .prefix-5{padding-left:75.55114%}.col-7 .prefix-6{padding-left:90.66137%}.col-7 .suffix-1{padding-right:15.11023%}.col-7 .suffix-2{padding-right:30.22046%}.col-7 .suffix-3{padding-right:45.33068%}.col-7 .suffix-4{padding-right:60.44091%}.col-7 .suffix-5{padding-right:75.55114%}.col-7 .suffix-6{padding-right:90.66137%}.col-7 .push-1{left:15.11023%}.col-7 .push-2{left:30.22046%}.col-7 .push-3{left:45.33068%}.col-7 .push-4{left:60.44091%}.col-7 .push-5{left:75.55114%}.col-7 .push-6{left:90.66137%}.col-7 .pull-1{right:15.11023%}.col-7 .pull-2{right:30.22046%}.col-7 .pull-3{right:45.33068%}.col-7 .pull-4{right:60.44091%}.col-7 .pull-5{right:75.55114%}.col-7 .pull-6{right:90.66137%}.col-6 .col-1,.col-6 .col-2,.col-6 .p-form--stacked .p-form__label,.p-form--stacked .col-6 .p-form__label,.col-6 .col-3,.col-6 .col-4,.col-6 .p-form--stacked .p-form__control,.p-form--stacked .col-6 .p-form__control,.col-6 .p-pod-summary__aside,.col-6 .p-storage__name,.col-6 .col-5{margin-left:6.79893%}.col-6 .col-1{width:11.00089%}.col-6 .col-2,.col-6 .p-form--stacked .p-form__label,.p-form--stacked .col-6 .p-form__label{width:28.80072%}.col-6 .col-3{width:46.60054%}.col-6 .col-4,.col-6 .p-form--stacked .p-form__control,.p-form--stacked .col-6 .p-form__control,.col-6 .p-pod-summary__aside,.col-6 .p-storage__name{width:64.40036%}.col-6 .col-5{width:82.20018%}.col-6 .prefix-1{padding-left:17.79982%}.col-6 .prefix-2{padding-left:35.59964%}.col-6 .prefix-3{padding-left:53.39946%}.col-6 .prefix-4{padding-left:71.19928%}.col-6 .prefix-5{padding-left:88.99911%}.col-6 .suffix-1{padding-right:17.79982%}.col-6 .suffix-2{padding-right:35.59964%}.col-6 .suffix-3{padding-right:53.39946%}.col-6 .suffix-4{padding-right:71.19928%}.col-6 .suffix-5{padding-right:88.99911%}.col-6 .push-1{left:17.79982%}.col-6 .push-2{left:35.59964%}.col-6 .push-3{left:53.39946%}.col-6 .push-4{left:71.19928%}.col-6 .push-5{left:88.99911%}.col-6 .pull-1{right:17.79982%}.col-6 .pull-2{right:35.59964%}.col-6 .pull-3{right:53.39946%}.col-6 .pull-4{right:71.19928%}.col-6 .pull-5{right:88.99911%}.col-5 .col-1,.col-5 .col-2,.col-5 .p-form--stacked .p-form__label,.p-form--stacked .col-5 .p-form__label,.col-5 .col-3,.col-5 .col-4,.col-5 .p-form--stacked .p-form__control,.p-form--stacked .col-5 .p-form__control,.col-5 .p-pod-summary__aside,.col-5 .p-storage__name{margin-left:8.27118%}.col-5 .col-1{width:13.38305%}.col-5 .col-2,.col-5 .p-form--stacked .p-form__label,.p-form--stacked .col-5 .p-form__label{width:35.03729%}.col-5 .col-3{width:56.69153%}.col-5 .col-4,.col-5 .p-form--stacked .p-form__control,.p-form--stacked .col-5 .p-form__control,.col-5 .p-pod-summary__aside,.col-5 .p-storage__name{width:78.34576%}.col-5 .prefix-1{padding-left:21.65424%}.col-5 .prefix-2{padding-left:43.30847%}.col-5 .prefix-3{padding-left:64.96271%}.col-5 .prefix-4{padding-left:86.61695%}.col-5 .suffix-1{padding-right:21.65424%}.col-5 .suffix-2{padding-right:43.30847%}.col-5 .suffix-3{padding-right:64.96271%}.col-5 .suffix-4{padding-right:86.61695%}.col-5 .push-1{left:21.65424%}.col-5 .push-2{left:43.30847%}.col-5 .push-3{left:64.96271%}.col-5 .push-4{left:86.61695%}.col-5 .pull-1{right:21.65424%}.col-5 .pull-2{right:43.30847%}.col-5 .pull-3{right:64.96271%}.col-5 .pull-4{right:86.61695%}.col-4 .col-1,.p-form--stacked .p-form__control .col-1,.p-pod-summary__aside .col-1,.p-storage__name .col-1,.col-4 .col-2,.p-form--stacked .p-form__control .col-2,.p-pod-summary__aside .col-2,.p-storage__name .col-2,.col-4 .p-form--stacked .p-form__label,.p-form--stacked .col-4 .p-form__label,.p-form--stacked .p-form__control .p-form__label,.p-pod-summary__aside .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__aside .p-form__label,.p-storage__name .p-form--stacked .p-form__label,.p-form--stacked .p-storage__name .p-form__label,.col-4 .col-3,.p-form--stacked .p-form__control .col-3,.p-pod-summary__aside .col-3,.p-storage__name .col-3{margin-left:10.55728%}.col-4 .col-1,.p-form--stacked .p-form__control .col-1,.p-pod-summary__aside .col-1,.p-storage__name .col-1{width:17.08204%}.col-4 .col-2,.p-form--stacked .p-form__control .col-2,.p-pod-summary__aside .col-2,.p-storage__name .col-2,.col-4 .p-form--stacked .p-form__label,.p-form--stacked .col-4 .p-form__label,.p-form--stacked .p-form__control .p-form__label,.p-pod-summary__aside .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__aside .p-form__label,.p-storage__name .p-form--stacked .p-form__label,.p-form--stacked .p-storage__name .p-form__label{width:44.72136%}.col-4 .col-3,.p-form--stacked .p-form__control .col-3,.p-pod-summary__aside .col-3,.p-storage__name .col-3{width:72.36068%}.col-4 .prefix-1,.p-form--stacked .p-form__control .prefix-1,.p-pod-summary__aside .prefix-1,.p-storage__name .prefix-1{padding-left:27.63932%}.col-4 .prefix-2,.p-form--stacked .p-form__control .prefix-2,.p-pod-summary__aside .prefix-2,.p-storage__name .prefix-2{padding-left:55.27864%}.col-4 .prefix-3,.p-form--stacked .p-form__control .prefix-3,.p-pod-summary__aside .prefix-3,.p-storage__name .prefix-3{padding-left:82.91796%}.col-4 .suffix-1,.p-form--stacked .p-form__control .suffix-1,.p-pod-summary__aside .suffix-1,.p-storage__name .suffix-1{padding-right:27.63932%}.col-4 .suffix-2,.p-form--stacked .p-form__control .suffix-2,.p-pod-summary__aside .suffix-2,.p-storage__name .suffix-2{padding-right:55.27864%}.col-4 .suffix-3,.p-form--stacked .p-form__control .suffix-3,.p-pod-summary__aside .suffix-3,.p-storage__name .suffix-3{padding-right:82.91796%}.col-4 .push-1,.p-form--stacked .p-form__control .push-1,.p-pod-summary__aside .push-1,.p-storage__name .push-1{left:27.63932%}.col-4 .push-2,.p-form--stacked .p-form__control .push-2,.p-pod-summary__aside .push-2,.p-storage__name .push-2{left:55.27864%}.col-4 .push-3,.p-form--stacked .p-form__control .push-3,.p-pod-summary__aside .push-3,.p-storage__name .push-3{left:82.91796%}.col-4 .pull-1,.p-form--stacked .p-form__control .pull-1,.p-pod-summary__aside .pull-1,.p-storage__name .pull-1{right:27.63932%}.col-4 .pull-2,.p-form--stacked .p-form__control .pull-2,.p-pod-summary__aside .pull-2,.p-storage__name .pull-2{right:55.27864%}.col-4 .pull-3,.p-form--stacked .p-form__control .pull-3,.p-pod-summary__aside .pull-3,.p-storage__name .pull-3{right:82.91796%}.col-3 .col-1,.col-3 .col-2,.col-3 .p-form--stacked .p-form__label,.p-form--stacked .col-3 .p-form__label{margin-left:14.5898%}.col-3 .col-1{width:23.6068%}.col-3 .col-2,.col-3 .p-form--stacked .p-form__label,.p-form--stacked .col-3 .p-form__label{width:61.8034%}.col-3 .prefix-1{padding-left:38.1966%}.col-3 .prefix-2{padding-left:76.3932%}.col-3 .suffix-1{padding-right:38.1966%}.col-3 .suffix-2{padding-right:76.3932%}.col-3 .push-1{left:38.1966%}.col-3 .push-2{left:76.3932%}.col-3 .pull-1{right:38.1966%}.col-3 .pull-2{right:76.3932%}.col-2 .col-1,.p-form--stacked .p-form__label .col-1{margin-left:23.6068%}.col-2 .col-1,.p-form--stacked .p-form__label .col-1{width:38.1966%}.col-2 .prefix-1,.p-form--stacked .p-form__label .prefix-1{padding-left:61.8034%}.col-2 .suffix-1,.p-form--stacked .p-form__label .suffix-1{padding-right:61.8034%}.col-2 .push-1,.p-form--stacked .p-form__label .push-1{left:61.8034%}.col-2 .pull-1,.p-form--stacked .p-form__label .pull-1{right:61.8034%}}.row .center-col{float:none;margin-left:auto !important;margin-right:auto}@media screen and (max-width: 619px){.hidden-mobile,.visible-tablet,.visible-desktop{display:none !important}}@media screen and (min-width: 620px) and (max-width: 767px){.visible-mobile,.hidden-tablet,.visible-desktop{display:none !important}}@media screen and (min-width: 768px){.visible-mobile,.visible-tablet,.hidden-desktop{display:none !important}}.p-heading-icon{margin-bottom:1rem}@media (min-width: 768px){.p-heading-icon{margin-bottom:0}}.p-heading-icon__header{display:flex;margin-bottom:.75rem}.p-heading-icon__title{margin-bottom:0;padding-top:0}.p-heading-icon__img{flex-shrink:0;height:2.5rem;margin-right:1rem;width:2.5rem}@media (min-width: 768px){.p-heading-icon__img{height:3.75rem;width:3.75rem}}.p-icon--plus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--plus,.p-icon--plus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23cdcdcd' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--minus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--minus,.p-icon--minus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23cdcdcd' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--expand{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23666' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23666' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23666' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--expand,.p-icon--expand.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23cdcdcd' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23cdcdcd' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23cdcdcd' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--collapse{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23666' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23666' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--collapse,.p-icon--collapse.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23cdcdcd' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23cdcdcd' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--chevron{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--chevron,.p-icon--chevron.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23cdcdcd' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--close{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23666'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--close,.p-icon--close.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23cdcdcd'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--help{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' color='%23000' d='M-.003.002h16v16h-16z'/%3E%3Cpath d='M8.004 5.23q-.431 0-.825.11-.394.098-.825.332l-.419-1.145q.456-.258 1.035-.406.59-.16 1.206-.16.739 0 1.219.21.48.196.763.504.283.308.394.677.111.37.111.714 0 .419-.16.751-.148.333-.382.616t-.504.542q-.271.246-.505.517-.234.258-.394.554-.148.295-.148.664v.148q0 .074.012.148h-1.28q-.025-.123-.037-.259-.012-.147-.012-.27 0-.407.135-.727.136-.32.345-.59t.443-.506q.246-.234.456-.467.209-.234.344-.48.136-.247.136-.542 0-.407-.283-.665-.271-.271-.825-.271zM8.984 12.01q0 .43-.283.7-.284.272-.702.272-.406 0-.702-.271-.283-.271-.283-.702 0-.43.283-.702.296-.283.702-.283.418 0 .702.283.283.271.283.702z' fill='%23666'/%3E%3Cpath d='M2.064 1.002c-.591 0-1.067.476-1.067 1.067v11.867c0 .591.476 1.067 1.067 1.067H13.93c.591 0 1.067-.476 1.067-1.067V2.07c0-.591-.476-1.067-1.067-1.067zm-.067 1h12v12h-12z' fill='%23666' color='%23000'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--help,.p-icon--help.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' color='%23000' d='M-.003.002h16v16h-16z'/%3E%3Cpath d='M8.004 5.23q-.431 0-.825.11-.394.098-.825.332l-.419-1.145q.456-.258 1.035-.406.59-.16 1.206-.16.739 0 1.219.21.48.196.763.504.283.308.394.677.111.37.111.714 0 .419-.16.751-.148.333-.382.616t-.504.542q-.271.246-.505.517-.234.258-.394.554-.148.295-.148.664v.148q0 .074.012.148h-1.28q-.025-.123-.037-.259-.012-.147-.012-.27 0-.407.135-.727.136-.32.345-.59t.443-.506q.246-.234.456-.467.209-.234.344-.48.136-.247.136-.542 0-.407-.283-.665-.271-.271-.825-.271zM8.984 12.01q0 .43-.283.7-.284.272-.702.272-.406 0-.702-.271-.283-.271-.283-.702 0-.43.283-.702.296-.283.702-.283.418 0 .702.283.283.271.283.702z' fill='%23cdcdcd'/%3E%3Cpath d='M2.064 1.002c-.591 0-1.067.476-1.067 1.067v11.867c0 .591.476 1.067 1.067 1.067H13.93c.591 0 1.067-.476 1.067-1.067V2.07c0-.591-.476-1.067-1.067-1.067zm-.067 1h12v12h-12z' fill='%23cdcdcd' color='%23000'/%3E%3C/svg%3E")}.p-icon--information,.p-icon--info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath d='M2.07 1c-.59 0-1.066.475-1.066 1.066v11.867c0 .591.475 1.067 1.066 1.067h11.867c.591 0 1.066-.476 1.066-1.067V2.066c0-.59-.475-1.066-1.066-1.066zm-.066 1h12v12h-12z' fill='%23666'/%3E%3Cpath d='M7 4v2h2V4zm0 3v5h2V7z' fill='%23666'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--information,[class*="--dark"] .p-icon--info,.p-icon--information.is-light,.is-light.p-icon--info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath d='M2.07 1c-.59 0-1.066.475-1.066 1.066v11.867c0 .591.475 1.067 1.066 1.067h11.867c.591 0 1.066-.476 1.066-1.067V2.066c0-.59-.475-1.066-1.066-1.066zm-.066 1h12v12h-12z' fill='%23cdcdcd'/%3E%3Cpath d='M7 4v2h2V4zm0 3v5h2V7z' fill='%23cdcdcd'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--delete{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M2 4v1h2V4H2zm11 0v1h2V4h-2zM2 6v8.506c0 .822.678 1.5 1.5 1.5h10c.822 0 1.5-.678 1.5-1.5V6h-2v7.506c0 .286-.214.5-.5.5h-8a.488.488 0 0 1-.5-.5V6H2z' fill='%23666'/%3E%3Cpath d='M6 0v3h1V1h3v2h1V0H6zM5 6h1v6H5zM8 6h1v6H8zM11 6h1v6h-1z' fill='%23666'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M3.5 2C2.678 2 2 2.678 2 3.5V5h13V3.5c0-.822-.678-1.5-1.5-1.5h-10zM2 6v8.006h2V6H2zm11 0v8.006h2V6h-2z' fill='%23666'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--delete,.p-icon--delete.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M2 4v1h2V4H2zm11 0v1h2V4h-2zM2 6v8.506c0 .822.678 1.5 1.5 1.5h10c.822 0 1.5-.678 1.5-1.5V6h-2v7.506c0 .286-.214.5-.5.5h-8a.488.488 0 0 1-.5-.5V6H2z' fill='%23cdcdcd'/%3E%3Cpath d='M6 0v3h1V1h3v2h1V0H6zM5 6h1v6H5zM8 6h1v6H8zM11 6h1v6h-1z' fill='%23cdcdcd'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M3.5 2C2.678 2 2 2.678 2 3.5V5h13V3.5c0-.822-.678-1.5-1.5-1.5h-10zM2 6v8.006h2V6H2zm11 0v8.006h2V6h-2z' fill='%23cdcdcd'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--error{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' viewBox='0 0 16.000017 16.000017' width='16'%3E%3Cg stroke-width='1.5' color='%23000'%3E%3Cpath d='M8 0C3.5906 0 0 3.5906 0 8s3.5906 8 8 8 8-3.5906 8-8-3.5906-8-8-8z' fill='%23c7162b'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath d='M5 5l6 6m0-6l-6 6' stroke-dashoffset='.8' stroke='%23fff' fill='none'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--warning{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath stroke-linejoin='round' fill='%23f99b11' transform='matrix%282.28 0 0 2.437 -2180.8 -490.52%29' stroke='%23f99b11' stroke-width='.848' d='M963.07 207.03h-6.15l3.08-5.33z'/%3E%3Cpath d='M7 5v5h2V5H7zm0 6v2h2v-2H7z' fill='%23111'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--external-link{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' d='M.003.001h16v16h-16z'/%3E%3Cpath d='M8.581 2.068S12.208.631 16-.005c.002 0 .002 0 .002.002v.006h.002c-.708 3.964-2.08 7.406-2.08 7.406L8.58 2.069z' fill='%23666'/%3E%3Cpath stroke-linejoin='round' d='M7.87 8.128l4.446-4.445' stroke='%23666' stroke-width='2.00001' fill='none'/%3E%3Cpath d='M1.503 2.001c-.822 0-1.5.678-1.5 1.5v11c0 .823.678 1.5 1.5 1.5h11c.823 0 1.5-.677 1.5-1.5v-5.5h-2v4.5c0 .287-.214.5-.5.5h-9a.488.488 0 0 1-.5-.5v-9c0-.285.215-.5.5-.5h4.5v-2h-5.5z' fill='%23666'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--external-link,.p-icon--external-link.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' d='M.003.001h16v16h-16z'/%3E%3Cpath d='M8.581 2.068S12.208.631 16-.005c.002 0 .002 0 .002.002v.006h.002c-.708 3.964-2.08 7.406-2.08 7.406L8.58 2.069z' fill='%23cdcdcd'/%3E%3Cpath stroke-linejoin='round' d='M7.87 8.128l4.446-4.445' stroke='%23cdcdcd' stroke-width='2.00001' fill='none'/%3E%3Cpath d='M1.503 2.001c-.822 0-1.5.678-1.5 1.5v11c0 .823.678 1.5 1.5 1.5h11c.823 0 1.5-.677 1.5-1.5v-5.5h-2v4.5c0 .287-.214.5-.5.5h-9a.488.488 0 0 1-.5-.5v-9c0-.285.215-.5.5-.5h4.5v-2h-5.5z' fill='%23cdcdcd'/%3E%3C/svg%3E")}.p-icon--contextual-menu{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='14' width='6' viewBox='0 0 6 14'%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cpath d='M-10-6h26v26h-26z'/%3E%3Cpath fill-rule='nonzero' fill='%23666' d='M0 0v2h6V0M0 6v2h6V6m-6 6v2h6v-2'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--contextual-menu,.p-icon--contextual-menu.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='14' width='6' viewBox='0 0 6 14'%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cpath d='M-10-6h26v26h-26z'/%3E%3Cpath fill-rule='nonzero' fill='%23cdcdcd' d='M0 0v2h6V0M0 6v2h6V6m-6 6v2h6v-2'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--code{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.212' fill='none' d='M.005.002h16v16h-16z'/%3E%3Cpath d='M2.671 2.002c-1.778 0-2.666 0-2.666 2.068v8.866c0 2.067.888 2.066 2.666 2.066H13.34c1.778 0 2.666 0 2.666-2.066v-8.8c0-2.133-.888-2.134-2.666-2.134H2.671zm1.28 1.89h1.101v1.143c.339.028.642.078.91.148.268.064.48.128.635.192L6.333 6.42a6.601 6.601 0 0 0-.73-.222 3.858 3.858 0 0 0-.953-.106c-.382 0-.67.072-.86.213a.646.646 0 0 0-.285.56c0 .142.028.261.084.36a.875.875 0 0 0 .256.254c.113.07.25.142.412.213.162.063.346.13.55.201.29.113.561.233.815.36.261.12.487.266.678.435.19.163.34.356.445.582.113.226.17.494.17.805 0 .466-.144.868-.433 1.207-.29.339-.766.558-1.43.657v1.324H3.95V11.97c-.508-.036-.922-.103-1.24-.201a4.692 4.692 0 0 1-.697-.286l.36-1.005c.225.113.496.214.814.306.324.092.692.139 1.101.139.487 0 .823-.072 1.006-.213a.703.703 0 0 0 .287-.582.764.764 0 0 0-.117-.424 1.09 1.09 0 0 0-.328-.319 2.828 2.828 0 0 0-.508-.253c-.19-.078-.404-.158-.637-.243a8.505 8.505 0 0 1-.656-.265 2.866 2.866 0 0 1-.582-.36c-.17-.148-.306-.324-.412-.529s-.16-.456-.16-.752c0-.487.146-.901.435-1.24.29-.346.734-.567 1.334-.666V3.892zm4.054 8.096h3.99v.996h-3.99v-.996z' fill='%23666'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--code,.p-icon--code.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.212' fill='none' d='M.005.002h16v16h-16z'/%3E%3Cpath d='M2.671 2.002c-1.778 0-2.666 0-2.666 2.068v8.866c0 2.067.888 2.066 2.666 2.066H13.34c1.778 0 2.666 0 2.666-2.066v-8.8c0-2.133-.888-2.134-2.666-2.134H2.671zm1.28 1.89h1.101v1.143c.339.028.642.078.91.148.268.064.48.128.635.192L6.333 6.42a6.601 6.601 0 0 0-.73-.222 3.858 3.858 0 0 0-.953-.106c-.382 0-.67.072-.86.213a.646.646 0 0 0-.285.56c0 .142.028.261.084.36a.875.875 0 0 0 .256.254c.113.07.25.142.412.213.162.063.346.13.55.201.29.113.561.233.815.36.261.12.487.266.678.435.19.163.34.356.445.582.113.226.17.494.17.805 0 .466-.144.868-.433 1.207-.29.339-.766.558-1.43.657v1.324H3.95V11.97c-.508-.036-.922-.103-1.24-.201a4.692 4.692 0 0 1-.697-.286l.36-1.005c.225.113.496.214.814.306.324.092.692.139 1.101.139.487 0 .823-.072 1.006-.213a.703.703 0 0 0 .287-.582.764.764 0 0 0-.117-.424 1.09 1.09 0 0 0-.328-.319 2.828 2.828 0 0 0-.508-.253c-.19-.078-.404-.158-.637-.243a8.505 8.505 0 0 1-.656-.265 2.866 2.866 0 0 1-.582-.36c-.17-.148-.306-.324-.412-.529s-.16-.456-.16-.752c0-.487.146-.901.435-1.24.29-.346.734-.567 1.334-.666V3.892zm4.054 8.096h3.99v.996h-3.99v-.996z' fill='%23cdcdcd'/%3E%3C/svg%3E")}.p-icon--menu{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='19' width='25' viewBox='0 0 79 60'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23666' d='M.995 0h78v12h-78zm0 24h78v12h-78zm0 24h78v12h-78z'/%3E%3Cpath d='M-5.005-15h90v90h-90z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--menu,.p-icon--menu.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='19' width='25' viewBox='0 0 79 60'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23cdcdcd' d='M.995 0h78v12h-78zm0 24h78v12h-78zm0 24h78v12h-78z'/%3E%3Cpath d='M-5.005-15h90v90h-90z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--copy{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='17' width='16'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M10.587 1.8h3.259c.472 0 .846.053 1.161.2s.567.412.716.748c.298.67.266 1.491.277 2.613V13.84c-.011 1.121.021 1.942-.277 2.613-.149.335-.401.6-.716.747s-.689.2-1.161.2H4.154c-.472 0-.846-.053-1.16-.2s-.568-.412-.717-.747c-.246-.554-.268-1.21-.273-2.053h.803c.016.854.058 1.428.178 1.707.072.166.151.26.336.348s.477.145.896.145h9.566c.42 0 .712-.057.897-.145a.602.602 0 0 0 .335-.348c.143-.331.175-1.081.185-2.222V5.309c-.01-1.137-.042-1.885-.185-2.216a.603.603 0 0 0-.335-.348c-.185-.088-.477-.145-.897-.145h-3.538c.182-.225.304-.5.342-.8zm-3.174 0c.038.3.16.575.341.8H4.217c-.42 0-.712.057-.896.145a.603.603 0 0 0-.336.348c-.143.33-.175 1.079-.185 2.216V10.8H2V5.361c.01-1.122-.021-1.942.277-2.613.149-.336.401-.601.716-.748s.689-.2 1.16-.2h3.26z'/%3E%3Cpath fill-rule='nonzero' d='M11.398 1.8v2.4H6.6V1.8h1.6c0 .447.353.8.8.8.445 0 .799-.353.799-.8h1.6z'/%3E%3Cpath fill-rule='nonzero' d='M10.6 1.6c0 .879-.722 1.6-1.6 1.6-.879 0-1.6-.721-1.6-1.6C7.4.72 8.121 0 9 0c.879 0 1.6.72 1.6 1.6zm-.8 0c0-.447-.354-.8-.8-.8-.447 0-.8.353-.8.8 0 .446.353.8.8.8.446 0 .8-.354.8-.8z'/%3E%3Cpath d='M8.4 7.2H14v1H8.4zM8.4 9.6H14v1H8.4zM10 12h4v1h-4z'/%3E%3Cpath fill-rule='nonzero' d='M4.4 10s2.134 1.026 4 2.505h-.002C6.427 14.03 4.4 15 4.4 15v-5z'/%3E%3Cpath d='M0 11.6h4.4v2H0z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--copy,.p-icon--copy.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='17' width='16'%3E%3Cg fill='%23cdcdcd' fill-rule='evenodd'%3E%3Cpath d='M10.587 1.8h3.259c.472 0 .846.053 1.161.2s.567.412.716.748c.298.67.266 1.491.277 2.613V13.84c-.011 1.121.021 1.942-.277 2.613-.149.335-.401.6-.716.747s-.689.2-1.161.2H4.154c-.472 0-.846-.053-1.16-.2s-.568-.412-.717-.747c-.246-.554-.268-1.21-.273-2.053h.803c.016.854.058 1.428.178 1.707.072.166.151.26.336.348s.477.145.896.145h9.566c.42 0 .712-.057.897-.145a.602.602 0 0 0 .335-.348c.143-.331.175-1.081.185-2.222V5.309c-.01-1.137-.042-1.885-.185-2.216a.603.603 0 0 0-.335-.348c-.185-.088-.477-.145-.897-.145h-3.538c.182-.225.304-.5.342-.8zm-3.174 0c.038.3.16.575.341.8H4.217c-.42 0-.712.057-.896.145a.603.603 0 0 0-.336.348c-.143.33-.175 1.079-.185 2.216V10.8H2V5.361c.01-1.122-.021-1.942.277-2.613.149-.336.401-.601.716-.748s.689-.2 1.16-.2h3.26z'/%3E%3Cpath fill-rule='nonzero' d='M11.398 1.8v2.4H6.6V1.8h1.6c0 .447.353.8.8.8.445 0 .799-.353.799-.8h1.6z'/%3E%3Cpath fill-rule='nonzero' d='M10.6 1.6c0 .879-.722 1.6-1.6 1.6-.879 0-1.6-.721-1.6-1.6C7.4.72 8.121 0 9 0c.879 0 1.6.72 1.6 1.6zm-.8 0c0-.447-.354-.8-.8-.8-.447 0-.8.353-.8.8 0 .446.353.8.8.8.446 0 .8-.354.8-.8z'/%3E%3Cpath d='M8.4 7.2H14v1H8.4zM8.4 9.6H14v1H8.4zM10 12h4v1h-4z'/%3E%3Cpath fill-rule='nonzero' d='M4.4 10s2.134 1.026 4 2.505h-.002C6.427 14.03 4.4 15 4.4 15v-5z'/%3E%3Cpath d='M0 11.6h4.4v2H0z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--search{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg transform='translate%28-74.67 -285.57%29 scale%28.66667%29' color='%23000'%3E%3Cpath opacity='.05' fill='none' d='M112 452.36h24v-24h-24z'/%3E%3Cpath style='isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M129.93 444.03l-2.27 2.273 6.07 6.07 2.27-2.27z' fill='%23666'/%3E%3Cellipse stroke-linejoin='round' stroke='%23666' rx='9.479' ry='9.479' cy='438.86' cx='122.5' stroke-linecap='round' stroke-width='2.041' fill='none'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--search,.p-icon--search.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg transform='translate%28-74.67 -285.57%29 scale%28.66667%29' color='%23000'%3E%3Cpath opacity='.05' fill='none' d='M112 452.36h24v-24h-24z'/%3E%3Cpath style='isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M129.93 444.03l-2.27 2.273 6.07 6.07 2.27-2.27z' fill='%23cdcdcd'/%3E%3Cellipse stroke-linejoin='round' stroke='%23cdcdcd' rx='9.479' ry='9.479' cy='438.86' cx='122.5' stroke-linecap='round' stroke-width='2.041' fill='none'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--success,.p-icon--pass{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23fff' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--share{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath style='block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M11.43.012a2.48 2.48 0 0 0-1.5.597l-.952.797v.574a6.7 6.7 0 0 1-.154 1.489c-.102.452-.286.84-.543 1.158a2.333 2.333 0 0 1-.999.756c-.421.185-.953.278-1.59.278-.622 0-1.072-.04-1.568-.112-.929.544-1.363 1.382-1.363 2.493s.53 1.732 1.363 2.53a14.294 14.294 0 0 1 1.569-.077c.636 0 1.168.093 1.59.278.42.174.751.427.998.756.257.318.44.7.543 1.152.103.452.154.95.154 1.495v.414l.922.78a2.49 2.49 0 0 0 1.813.63 2.49 2.49 0 0 0 1.713-.866 2.49 2.49 0 0 0 .57-1.833 2.49 2.49 0 0 0-.923-1.684l-.65-.55h-1.696c-.44 0-.848-.06-1.229-.182a2.59 2.59 0 0 1-.993-.55 2.54 2.54 0 0 1-.65-.934c-.16-.372-.242-.818-.242-1.335s.083-.967.242-1.347c.16-.38.377-.69.65-.934.282-.243.613-.428.993-.55a4.25 4.25 0 0 1 1.23-.17h1.536l.821-.686c.798-.646 1.116-1.822.752-2.782-.363-.96-1.381-1.63-2.406-1.585z' fill='%23666'/%3E%3Cpath opacity='.1' fill='none' d='M-.003.005h16v16h-16z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--share,.p-icon--share.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath style='block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M11.43.012a2.48 2.48 0 0 0-1.5.597l-.952.797v.574a6.7 6.7 0 0 1-.154 1.489c-.102.452-.286.84-.543 1.158a2.333 2.333 0 0 1-.999.756c-.421.185-.953.278-1.59.278-.622 0-1.072-.04-1.568-.112-.929.544-1.363 1.382-1.363 2.493s.53 1.732 1.363 2.53a14.294 14.294 0 0 1 1.569-.077c.636 0 1.168.093 1.59.278.42.174.751.427.998.756.257.318.44.7.543 1.152.103.452.154.95.154 1.495v.414l.922.78a2.49 2.49 0 0 0 1.813.63 2.49 2.49 0 0 0 1.713-.866 2.49 2.49 0 0 0 .57-1.833 2.49 2.49 0 0 0-.923-1.684l-.65-.55h-1.696c-.44 0-.848-.06-1.229-.182a2.59 2.59 0 0 1-.993-.55 2.54 2.54 0 0 1-.65-.934c-.16-.372-.242-.818-.242-1.335s.083-.967.242-1.347c.16-.38.377-.69.65-.934.282-.243.613-.428.993-.55a4.25 4.25 0 0 1 1.23-.17h1.536l.821-.686c.798-.646 1.116-1.822.752-2.782-.363-.96-1.381-1.63-2.406-1.585z' fill='%23cdcdcd'/%3E%3Cpath opacity='.1' fill='none' d='M-.003.005h16v16h-16z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--user{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.12' fill='none' color='%23000' d='M15.997 15.998v-16h-16v16z'/%3E%3Cpath style='text-decoration-color:%23000;font-variant-numeric:normal;text-decoration-line:none;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;text-decoration-style:solid;font-variant-ligatures:normal;isolation:auto;text-transform:none' d='M8 0c-.587 0-1.142.109-1.651.329-.508.209-.955.515-1.329.912h-.004a4.235 4.235 0 0 0-.844 1.426 5.128 5.128 0 0 0-.299 1.787c0 .653.098 1.256.3 1.802.199.539.48 1.012.843 1.41h.004c.25.264.531.49.841.676-.258.066-.701.144-.956.237-.878.322-1.617.766-2.196 1.334h-.004a5.586 5.586 0 0 0-1.286 2.03h-.002a7.541 7.541 0 0 0-.394 2.464v1.572L14.98 16v-1.572c0-.891-.139-1.7-.42-2.467a5.19 5.19 0 0 0-1.291-2.039c-.58-.567-1.316-1.011-2.194-1.333-.25-.093-.687-.17-.94-.236.31-.187.59-.414.834-.681.373-.397.661-.872.86-1.411a5.17 5.17 0 0 0 .3-1.803c0-.645-.098-1.243-.3-1.788a4.108 4.108 0 0 0-.86-1.427A3.652 3.652 0 0 0 9.652.33 4.14 4.14 0 0 0 8.001 0z' fill='%23666' color='%23000' white-space='normal'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--user,.p-icon--user.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.12' fill='none' color='%23000' d='M15.997 15.998v-16h-16v16z'/%3E%3Cpath style='text-decoration-color:%23000;font-variant-numeric:normal;text-decoration-line:none;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;text-decoration-style:solid;font-variant-ligatures:normal;isolation:auto;text-transform:none' d='M8 0c-.587 0-1.142.109-1.651.329-.508.209-.955.515-1.329.912h-.004a4.235 4.235 0 0 0-.844 1.426 5.128 5.128 0 0 0-.299 1.787c0 .653.098 1.256.3 1.802.199.539.48 1.012.843 1.41h.004c.25.264.531.49.841.676-.258.066-.701.144-.956.237-.878.322-1.617.766-2.196 1.334h-.004a5.586 5.586 0 0 0-1.286 2.03h-.002a7.541 7.541 0 0 0-.394 2.464v1.572L14.98 16v-1.572c0-.891-.139-1.7-.42-2.467a5.19 5.19 0 0 0-1.291-2.039c-.58-.567-1.316-1.011-2.194-1.333-.25-.093-.687-.17-.94-.236.31-.187.59-.414.834-.681.373-.397.661-.872.86-1.411a5.17 5.17 0 0 0 .3-1.803c0-.645-.098-1.243-.3-1.788a4.108 4.108 0 0 0-.86-1.427A3.652 3.652 0 0 0 9.652.33 4.14 4.14 0 0 0 8.001 0z' fill='%23cdcdcd' color='%23000' white-space='normal'/%3E%3C/svg%3E")}.p-icon--question{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' color='%23000' d='M-.003.002h16v16h-16z'/%3E%3Cpath d='M7.997.002c-4.41 0-8 3.59-8 8s3.59 8 8 8 8-3.589 8-8-3.589-8-8-8z' fill='%23335280' color='%23000'/%3E%3Cpath d='M8.004 5.23q-.431 0-.825.11-.394.098-.825.332l-.419-1.145q.456-.258 1.035-.406.59-.16 1.206-.16.739 0 1.219.21.48.196.763.504.283.308.394.677.111.37.111.714 0 .419-.16.751-.148.333-.382.616t-.504.542q-.271.246-.505.517-.234.258-.394.554-.148.295-.148.664v.148q0 .074.012.148h-1.28q-.025-.123-.037-.259-.012-.147-.012-.27 0-.407.135-.727.136-.32.345-.59t.443-.506q.246-.234.456-.467.209-.234.344-.48.136-.247.136-.542 0-.407-.283-.665-.271-.271-.825-.271zM8.984 12.01q0 .43-.283.7-.284.272-.702.272-.406 0-.702-.271-.283-.271-.283-.702 0-.43.283-.702.296-.283.702-.283.418 0 .702.283.283.271.283.702z' fill='%23fff'/%3E%3C/svg%3E")}.p-icon--spinner{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--spinner,.p-icon--spinner.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23cdcdcd' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--facebook{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' width='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Ccircle id='a' cx='20' cy='20' r='20'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cuse fill='%23666' fill-rule='nonzero' xlink:href='%23a'/%3E%3Cpath d='M30.037 10.001c-3.92 0-6.603 2.449-6.603 6.945v3.526H19v5.255h4.434V40c1.82-.246 3.6-.728 5.3-1.438V25.727h4.423l.66-5.255h-5.084V17.47c0-1.522.48-3.085 2.55-2.563H34v-4.7c-.47-.064-2.085-.207-3.963-.207v.001z' fill='%23FFF' fill-rule='nonzero' mask='url%28%23b%29'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--facebook:hover{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' width='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Ccircle id='a' cx='20' cy='20' r='20'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cuse fill='%233b5998' fill-rule='nonzero' xlink:href='%23a'/%3E%3Cpath d='M30.037 10.001c-3.92 0-6.603 2.449-6.603 6.945v3.526H19v5.255h4.434V40c1.82-.246 3.6-.728 5.3-1.438V25.727h4.423l.66-5.255h-5.084V17.47c0-1.522.48-3.085 2.55-2.563H34v-4.7c-.47-.064-2.085-.207-3.963-.207v.001z' fill='%23FFF' fill-rule='nonzero' mask='url%28%23b%29'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--google{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 0C8.955 0 0 8.955 0 20s8.955 20 20 20 20-8.955 20-20S31.045 0 20 0zm-4.862 26.805A6.799 6.799 0 0 1 8.333 20a6.799 6.799 0 0 1 6.805-6.805c1.839 0 3.374.67 4.559 1.778l-1.845 1.78c-.507-.486-1.39-1.05-2.714-1.05-2.323 0-4.218 1.925-4.218 4.299 0 2.373 1.897 4.298 4.218 4.298 2.694 0 3.707-1.937 3.86-2.937h-3.86V19.03h6.425c.06.34.107.68.107 1.128.002 3.887-2.605 6.647-6.532 6.647zm16.529-5.833H28.75v2.916h-1.945v-2.916h-2.917v-1.944h2.917v-2.916h1.945v2.916h2.917v1.944z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--google:hover{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 0C8.955 0 0 8.955 0 20s8.955 20 20 20 20-8.955 20-20S31.045 0 20 0zm-4.862 26.805A6.799 6.799 0 0 1 8.333 20a6.799 6.799 0 0 1 6.805-6.805c1.839 0 3.374.67 4.559 1.778l-1.845 1.78c-.507-.486-1.39-1.05-2.714-1.05-2.323 0-4.218 1.925-4.218 4.299 0 2.373 1.897 4.298 4.218 4.298 2.694 0 3.707-1.937 3.86-2.937h-3.86V19.03h6.425c.06.34.107.68.107 1.128.002 3.887-2.605 6.647-6.532 6.647zm16.529-5.833H28.75v2.916h-1.945v-2.916h-2.917v-1.944h2.917v-2.916h1.945v2.916h2.917v1.944z' fill='%23dd4b39' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--twitter{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23666'/%3E%3Cpath d='M16.34 30.55c8.87 0 13.72-7.35 13.72-13.72 0-.21 0-.42-.01-.62.94-.68 1.76-1.53 2.41-2.5-.86.38-1.79.64-2.77.76 1-.6 1.76-1.54 2.12-2.67-.93.55-1.96.95-3.06 1.17a4.799 4.799 0 0 0-3.52-1.52c-2.66 0-4.82 2.16-4.82 4.82 0 .38.04.75.13 1.1a13.68 13.68 0 0 1-9.94-5.04c-.41.71-.65 1.54-.65 2.42a4.8 4.8 0 0 0 2.15 4.01c-.79-.02-1.53-.24-2.18-.6v.06c0 2.34 1.66 4.28 3.87 4.73a4.807 4.807 0 0 1-2.18.08 4.815 4.815 0 0 0 4.5 3.35 9.693 9.693 0 0 1-7.14 1.99c2.11 1.38 4.65 2.18 7.37 2.18' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--twitter:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Ccircle cx='20' cy='20' r='20' fill='%231da1f2'/%3E%3Cpath d='M16.34 30.55c8.87 0 13.72-7.35 13.72-13.72 0-.21 0-.42-.01-.62.94-.68 1.76-1.53 2.41-2.5-.86.38-1.79.64-2.77.76 1-.6 1.76-1.54 2.12-2.67-.93.55-1.96.95-3.06 1.17a4.799 4.799 0 0 0-3.52-1.52c-2.66 0-4.82 2.16-4.82 4.82 0 .38.04.75.13 1.1a13.68 13.68 0 0 1-9.94-5.04c-.41.71-.65 1.54-.65 2.42a4.8 4.8 0 0 0 2.15 4.01c-.79-.02-1.53-.24-2.18-.6v.06c0 2.34 1.66 4.28 3.87 4.73a4.807 4.807 0 0 1-2.18.08 4.815 4.815 0 0 0 4.5 3.35 9.693 9.693 0 0 1-7.14 1.99c2.11 1.38 4.65 2.18 7.37 2.18' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--instagram{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 28.479h28.473V.009H0z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23666' fill-rule='nonzero'/%3E%3Cg transform='translate%286 6%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M14.237.009c-3.867 0-4.352.016-5.87.086-1.515.069-2.55.31-3.456.661-.936.364-1.73.851-2.522 1.642A6.978 6.978 0 0 0 .747 4.92C.395 5.826.155 6.86.086 8.376.016 9.894 0 10.379 0 14.246c0 3.866.016 4.35.086 5.87.069 1.515.31 2.55.661 3.455.364.936.851 1.73 1.642 2.522a6.98 6.98 0 0 0 2.522 1.642c.906.352 1.94.592 3.456.661 1.518.07 2.003.086 5.87.086 3.866 0 4.35-.016 5.87-.086 1.515-.069 2.55-.31 3.455-.661a6.98 6.98 0 0 0 2.522-1.642 6.98 6.98 0 0 0 1.642-2.522c.352-.905.592-1.94.661-3.456.07-1.518.086-2.003.086-5.87 0-3.866-.016-4.35-.086-5.87-.069-1.514-.31-2.55-.661-3.455a6.98 6.98 0 0 0-1.642-2.522A6.978 6.978 0 0 0 23.562.756c-.905-.352-1.94-.592-3.456-.661-1.518-.07-2.003-.086-5.87-.086zm0 2.565c3.8 0 4.251.015 5.752.083 1.388.063 2.142.295 2.644.49a4.41 4.41 0 0 1 1.637 1.065 4.41 4.41 0 0 1 1.065 1.637c.195.502.427 1.256.49 2.644.068 1.501.083 1.951.083 5.753 0 3.8-.015 4.251-.083 5.752-.063 1.388-.295 2.142-.49 2.644a4.41 4.41 0 0 1-1.065 1.637 4.41 4.41 0 0 1-1.637 1.065c-.502.195-1.256.427-2.644.49-1.5.068-1.95.083-5.752.083-3.802 0-4.252-.015-5.753-.083-1.388-.063-2.142-.295-2.644-.49a4.41 4.41 0 0 1-1.637-1.065 4.411 4.411 0 0 1-1.065-1.637c-.195-.502-.427-1.256-.49-2.644-.068-1.5-.083-1.951-.083-5.752 0-3.802.015-4.252.083-5.753.063-1.388.295-2.142.49-2.644a4.41 4.41 0 0 1 1.065-1.637A4.41 4.41 0 0 1 5.84 3.147c.502-.195 1.256-.427 2.644-.49 1.501-.068 1.951-.083 5.753-.083z' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath d='M20.24 24.991a4.746 4.746 0 1 1 0-9.49 4.746 4.746 0 0 1 0 9.49zm0-12.056a7.31 7.31 0 1 0 0 14.621 7.31 7.31 0 0 0 0-14.621zM29.54 12.646a1.708 1.708 0 1 1-3.416 0 1.708 1.708 0 0 1 3.417 0' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--instagram:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 28.479h28.473V.009H0z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23fb3958' fill-rule='nonzero'/%3E%3Cg transform='translate%286 6%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M14.237.009c-3.867 0-4.352.016-5.87.086-1.515.069-2.55.31-3.456.661-.936.364-1.73.851-2.522 1.642A6.978 6.978 0 0 0 .747 4.92C.395 5.826.155 6.86.086 8.376.016 9.894 0 10.379 0 14.246c0 3.866.016 4.35.086 5.87.069 1.515.31 2.55.661 3.455.364.936.851 1.73 1.642 2.522a6.98 6.98 0 0 0 2.522 1.642c.906.352 1.94.592 3.456.661 1.518.07 2.003.086 5.87.086 3.866 0 4.35-.016 5.87-.086 1.515-.069 2.55-.31 3.455-.661a6.98 6.98 0 0 0 2.522-1.642 6.98 6.98 0 0 0 1.642-2.522c.352-.905.592-1.94.661-3.456.07-1.518.086-2.003.086-5.87 0-3.866-.016-4.35-.086-5.87-.069-1.514-.31-2.55-.661-3.455a6.98 6.98 0 0 0-1.642-2.522A6.978 6.978 0 0 0 23.562.756c-.905-.352-1.94-.592-3.456-.661-1.518-.07-2.003-.086-5.87-.086zm0 2.565c3.8 0 4.251.015 5.752.083 1.388.063 2.142.295 2.644.49a4.41 4.41 0 0 1 1.637 1.065 4.41 4.41 0 0 1 1.065 1.637c.195.502.427 1.256.49 2.644.068 1.501.083 1.951.083 5.753 0 3.8-.015 4.251-.083 5.752-.063 1.388-.295 2.142-.49 2.644a4.41 4.41 0 0 1-1.065 1.637 4.41 4.41 0 0 1-1.637 1.065c-.502.195-1.256.427-2.644.49-1.5.068-1.95.083-5.752.083-3.802 0-4.252-.015-5.753-.083-1.388-.063-2.142-.295-2.644-.49a4.41 4.41 0 0 1-1.637-1.065 4.411 4.411 0 0 1-1.065-1.637c-.195-.502-.427-1.256-.49-2.644-.068-1.5-.083-1.951-.083-5.752 0-3.802.015-4.252.083-5.753.063-1.388.295-2.142.49-2.644a4.41 4.41 0 0 1 1.065-1.637A4.41 4.41 0 0 1 5.84 3.147c.502-.195 1.256-.427 2.644-.49 1.501-.068 1.951-.083 5.753-.083z' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath d='M20.24 24.991a4.746 4.746 0 1 1 0-9.49 4.746 4.746 0 0 1 0 9.49zm0-12.056a7.31 7.31 0 1 0 0 14.621 7.31 7.31 0 0 0 0-14.621zM29.54 12.646a1.708 1.708 0 1 1-3.416 0 1.708 1.708 0 0 1 3.417 0' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--linkedin{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%23666' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg fill='%23FFFFFE'%3E%3Cpath d='M11.07 8.406a2.743 2.743 0 0 1 2.731 2.75c0 1.52-1.225 2.753-2.731 2.753a2.743 2.743 0 0 1-2.734-2.752 2.742 2.742 0 0 1 2.734-2.751zM8.712 31.268h4.713V15.997H8.712v15.271zM16.382 15.997h4.52v2.087h.064c.63-1.201 2.167-2.467 4.46-2.467 4.773 0 5.654 3.163 5.654 7.274v8.377h-4.71v-7.426c0-1.771-.032-4.05-2.45-4.05-2.452 0-2.828 1.93-2.828 3.921v7.555h-4.71V15.997'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}.p-icon--linkedin:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%230071a1' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg fill='%23FFFFFE'%3E%3Cpath d='M11.07 8.406a2.743 2.743 0 0 1 2.731 2.75c0 1.52-1.225 2.753-2.731 2.753a2.743 2.743 0 0 1-2.734-2.752 2.742 2.742 0 0 1 2.734-2.751zM8.712 31.268h4.713V15.997H8.712v15.271zM16.382 15.997h4.52v2.087h.064c.63-1.201 2.167-2.467 4.46-2.467 4.773 0 5.654 3.163 5.654 7.274v8.377h-4.71v-7.426c0-1.771-.032-4.05-2.45-4.05-2.452 0-2.828 1.93-2.828 3.921v7.555h-4.71V15.997'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}.p-icon--youtube{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M.009 18.367V.006h26.06v18.36z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%23666' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg transform='translate%287 11%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M25.524 2.868A3.275 3.275 0 0 0 23.22.548C21.187 0 13.034 0 13.034 0S4.882 0 2.85.548a3.275 3.275 0 0 0-2.305 2.32C0 4.914 0 9.183 0 9.183s0 4.27.545 6.316a3.276 3.276 0 0 0 2.305 2.32c2.032.548 10.184.548 10.184.548s8.153 0 10.185-.548a3.276 3.276 0 0 0 2.305-2.32c.545-2.047.545-6.316.545-6.316s0-4.269-.545-6.315' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath fill='%23666' d='M17.368 24.06l6.814-3.876-6.814-3.877v7.753'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--youtube:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M.009 18.367V.006h26.06v18.36z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%23d9252a' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg transform='translate%287 11%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M25.524 2.868A3.275 3.275 0 0 0 23.22.548C21.187 0 13.034 0 13.034 0S4.882 0 2.85.548a3.275 3.275 0 0 0-2.305 2.32C0 4.914 0 9.183 0 9.183s0 4.27.545 6.316a3.276 3.276 0 0 0 2.305 2.32c2.032.548 10.184.548 10.184.548s8.153 0 10.185-.548a3.276 3.276 0 0 0 2.305-2.32c.545-2.047.545-6.316.545-6.316s0-4.269-.545-6.315' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath fill='%23d9252a' d='M17.368 24.06l6.814-3.876-6.814-3.877v7.753'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--canonical{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 32.735c-7.036 0-12.736-5.7-12.736-12.736 0-7.034 5.7-12.734 12.736-12.734 7.036 0 12.736 5.7 12.736 12.734 0 7.036-5.7 12.736-12.736 12.736zM40 20c0 11.045-8.955 20-20 20S0 31.045 0 20C0 8.954 8.955 0 20 0s20 8.954 20 20zM20 4.865C11.636 4.865 4.864 11.642 4.864 20c0 8.36 6.772 15.135 15.136 15.135 8.364 0 15.136-6.775 15.136-15.135 0-8.358-6.772-15.135-15.136-15.135z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--canonical:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 32.735c-7.036 0-12.736-5.7-12.736-12.736 0-7.034 5.7-12.734 12.736-12.734 7.036 0 12.736 5.7 12.736 12.734 0 7.036-5.7 12.736-12.736 12.736zM40 20c0 11.045-8.955 20-20 20S0 31.045 0 20C0 8.954 8.955 0 20 0s20 8.954 20 20zM20 4.865C11.636 4.865 4.864 11.642 4.864 20c0 8.36 6.772 15.135 15.136 15.135 8.364 0 15.136-6.775 15.136-15.135 0-8.358-6.772-15.135-15.136-15.135z' fill='%23772953' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--ubuntu{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Cpath d='M39.906 20.013c0 10.987-8.905 19.893-19.892 19.893C9.028 39.906.122 31 .122 20.013.122 9.028 9.028.122 20.014.122c10.987 0 19.892 8.905 19.892 19.891z' fill='%23666'/%3E%3Cpath d='M9.69 20.013a2.558 2.558 0 1 1-5.116 0 2.558 2.558 0 0 1 5.116 0zM24.241 32.45a2.559 2.559 0 0 0 4.43-2.558 2.557 2.557 0 1 0-4.43 2.558zm4.429-22.313a2.557 2.557 0 1 0-4.43-2.556 2.557 2.557 0 0 0 4.43 2.556zm-8.656 2.584a7.292 7.292 0 0 1 7.265 6.648l3.701-.059a10.954 10.954 0 0 0-3.227-7.094 3.591 3.591 0 0 1-3.097-.24A3.592 3.592 0 0 1 22.9 9.41c-.92-.25-1.888-.384-2.886-.384-1.75 0-3.404.41-4.874 1.137l1.801 3.234a7.278 7.278 0 0 1 3.073-.677zm-7.294 7.293a7.283 7.283 0 0 1 3.102-5.967l-1.9-3.177a11.005 11.005 0 0 0-4.533 6.341 3.59 3.59 0 0 1 1.343 2.803 3.592 3.592 0 0 1-1.343 2.804 11.01 11.01 0 0 0 4.532 6.343l1.9-3.177a7.286 7.286 0 0 1-3.1-5.97zm7.294 7.295a7.267 7.267 0 0 1-3.073-.678l-1.8 3.234a10.938 10.938 0 0 0 4.873 1.137c.998 0 1.966-.132 2.886-.383a3.587 3.587 0 0 1 1.756-2.564 3.591 3.591 0 0 1 3.097-.24 10.958 10.958 0 0 0 3.227-7.096l-3.701-.058a7.293 7.293 0 0 1-7.265 6.648z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--ubuntu:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Cpath d='M39.906 20.013c0 10.987-8.905 19.893-19.892 19.893C9.028 39.906.122 31 .122 20.013.122 9.028 9.028.122 20.014.122c10.987 0 19.892 8.905 19.892 19.891z' fill='%23e95420'/%3E%3Cpath d='M9.69 20.013a2.558 2.558 0 1 1-5.116 0 2.558 2.558 0 0 1 5.116 0zM24.241 32.45a2.559 2.559 0 0 0 4.43-2.558 2.557 2.557 0 1 0-4.43 2.558zm4.429-22.313a2.557 2.557 0 1 0-4.43-2.556 2.557 2.557 0 0 0 4.43 2.556zm-8.656 2.584a7.292 7.292 0 0 1 7.265 6.648l3.701-.059a10.954 10.954 0 0 0-3.227-7.094 3.591 3.591 0 0 1-3.097-.24A3.592 3.592 0 0 1 22.9 9.41c-.92-.25-1.888-.384-2.886-.384-1.75 0-3.404.41-4.874 1.137l1.801 3.234a7.278 7.278 0 0 1 3.073-.677zm-7.294 7.293a7.283 7.283 0 0 1 3.102-5.967l-1.9-3.177a11.005 11.005 0 0 0-4.533 6.341 3.59 3.59 0 0 1 1.343 2.803 3.592 3.592 0 0 1-1.343 2.804 11.01 11.01 0 0 0 4.532 6.343l1.9-3.177a7.286 7.286 0 0 1-3.1-5.97zm7.294 7.295a7.267 7.267 0 0 1-3.073-.678l-1.8 3.234a10.938 10.938 0 0 0 4.873 1.137c.998 0 1.966-.132 2.886-.383a3.587 3.587 0 0 1 1.756-2.564 3.591 3.591 0 0 1 3.097-.24 10.958 10.958 0 0 0 3.227-7.096l-3.701-.058a7.293 7.293 0 0 1-7.265 6.648z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--medium{height:1.25rem;width:1.25rem}.p-icon--large{height:1.5rem;width:1.5rem}.p-icon--x-large{height:1.75rem;width:1.75rem}.p-icon--x-large{height:2.25rem;width:2.25rem}.p-icon--xx-large{height:3rem;width:3rem}[class*="p-button-"] [class*="p-icon-"]{top:-1px;vertical-align:middle}.p-image--bordered{border-color:#cdcdcd;border-style:solid;border-width:1px}.p-image--shadowed{box-shadow:0 1px 5px 1px rgba(205,205,205,0.2)}.p-inline-images{display:block;list-style:none;margin-left:0;padding-left:0;text-align:center}.p-inline-images__item{display:inline-block;margin:1rem;overflow:hidden;text-align:center;vertical-align:middle}@media only screen and (min-width: 768px){.p-inline-images__item{margin:1.875rem}}.p-inline-images__logo,.p-inline-images__item img{max-height:3rem;max-width:7rem;width:auto}@media screen and (min-width: 768px){.p-inline-images__logo,.p-inline-images__item img{max-height:5.5rem;max-width:9rem}}.p-inline-images__img{display:inline-block;margin:2rem;max-width:6rem;text-align:center;vertical-align:middle;width:100%}@media (min-width: 768px){.p-inline-images__img{margin:3rem;max-width:11.25rem}}.p-link--soft{color:#111}.p-link--soft:visited{color:#111;text-decoration:none}.p-link--soft:hover{color:#007aa6}.p-link--soft.is-selected{font-weight:400}.p-link--strong{color:currentColor;font-weight:400}.p-link--strong:visited{color:currentColor}.p-link--strong:hover{color:#007aa6;text-decoration:underline}.p-link--inverted{color:#f7f7f7;font-weight:400}.p-link--inverted:hover{color:#f7f7f7}.p-link--inverted:visited{color:#dedede}@supports (mask-size: 1em) or (-webkit-mask-size: 1em){.p-link--external::after{-webkit-mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E") no-repeat 0 0/cover;background-color:currentColor;content:'';margin:0 0 0 .25em;mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E") no-repeat 0 0/cover;padding-right:.75em}.p-link--no-underline{border:0}}@supports not ((mask-size: 1em) or (-webkit-mask-size: 1em)){.p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");background-position:top right;background-repeat:no-repeat;background-size:.75em;margin-top:-.25em;padding:.25em 1em 0 0}.p-link--external.p-link--strong,.p-link--external.p-link--soft,.p-link--external.sidebar__link{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.p-link--soft:hover,.p-link--external.sidebar__link:hover{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.p-link--inverted{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23f7f7f7' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23f7f7f7' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23f7f7f7' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.p-link--inverted:visited{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23dedede' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23dedede' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23dedede' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.sidebar__link{display:inline-block;padding:0 1em 1em 0}.p-link--no-underline{border:0}.p-button .p-link--external,.p-button--neutral .p-link--external,.p-button--base .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-button--positive .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-button--negative .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-button--brand .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-strip--dark * .p-link--external.p-link--soft,.p-strip--accent * .p-link--external.p-link--soft,.p-strip--image.is-dark * .p-link--external.p-link--soft{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-strip--dark * .p-link--external.p-link--soft:hover,.p-strip--accent * .p-link--external.p-link--soft:hover,.p-strip--image.is-dark * .p-link--external.p-link--soft:hover{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-strip--dark * .p-link--external.p-link--strong,.p-strip--accent * .p-link--external.p-link--strong,.p-strip--image.is-dark * .p-link--external.p-link--strong{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}}.p-top{border-bottom:1px dotted #cdcdcd;clear:both;margin:20px 0}.p-top__link{background:#fff;color:#111;float:right;margin-right:5px;padding:0 5px;position:relative;text-decoration:none;top:-.725rem}.p-list-tree__item--group::after,.p-list-tree .p-list-tree[aria-hidden="false"]::after{background-position:center;background-repeat:no-repeat;content:' ';display:block;height:.9375rem;left:-.75rem;pointer-events:none;position:absolute;top:.4rem;width:.9375rem}.p-list-tree{border-left:1px solid #cdcdcd;list-style-type:none;margin-left:1rem;padding:0 0 0 .25rem}.p-list-tree__item{margin-top:.125rem;padding-left:.8rem;position:relative}.p-list-tree__item::before{background:#cdcdcd;content:' ';display:block;height:1px;left:-.25rem;pointer-events:none;position:absolute;top:.8rem;width:.625rem}.p-list-tree__item--group::after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='15' width='15' viewBox='0 0 15 15'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='%23FFF'/%3E%3Cpath stroke='%23888' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23888' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23888' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-list-tree__toggle{background:transparent;border:0;font-weight:normal;margin:0 0 0 -1.75rem;padding:0 0 0 1.75rem;transition-duration:0s;width:auto}.p-list-tree__toggle:hover{background:transparent;color:#007aa6;text-decoration:underline}.p-list-tree__toggle:focus{background:transparent;outline:1px dotted #cdcdcd}.p-list-tree .p-list-tree{display:none;margin-left:0}.p-list-tree .p-list-tree[aria-hidden="false"]{display:block}.p-list-tree .p-list-tree[aria-hidden="false"]::after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='15' width='15' viewBox='0 0 15 15'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='%23FFF'/%3E%3Cpath stroke='%23888' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23888' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E");z-index:1}.p-list-step,.p-stepped-list--detailed{counter-reset:li;list-style:none;margin-left:4rem;padding-left:0}.p-list-step>li::before,.p-stepped-list--detailed>li::before{background-color:#666;border-radius:100%;color:#fff;content:counter(li);counter-increment:li;direction:rtl;display:inline-block;margin-left:-4rem;margin-top:0.1rem;padding:0;position:absolute;text-align:center;width:2.5rem}@media (max-width: 768px){.p-list-step>li::before,.p-stepped-list--detailed>li::before{margin-top:0;width:2rem}}.p-list{list-style:none;margin-left:0;padding-left:0}.p-list .p-list__item{padding-bottom:.125rem;padding-top:.125rem}form .p-list .p-list__item{padding-bottom:0;padding-top:0}form .p-list .p-list__item label{margin-bottom:.1rem}.p-list--divided{list-style:none;margin-left:0;padding-left:0}.p-list--divided .p-list__item{padding-bottom:.125rem;padding-top:.125rem}form .p-list--divided .p-list__item{padding-bottom:0;padding-top:0}form .p-list--divided .p-list__item label{margin-bottom:.1rem}.p-list--divided .p-list__item{position:relative}.p-list--divided .p-list__item::after{border-bottom:1px dotted #cdcdcd;bottom:0;content:'';height:1px;left:0;position:absolute;right:0}.p-list--divided .p-list__item:last-of-type::after,.p-list--divided .p-list__item .last-item::after{border-bottom:0}.p-list--divided.is-split .p-list__item:last-of-type{border-bottom:1px dotted #cdcdcd}.is-ticked{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'%3E%3Ccircle fill='%23e95420' cx='7' cy='7' r='7'/%3E%3Cpath fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /%3E%3C/svg%3E");background-position-y:.4375rem;background-repeat:no-repeat;padding-left:2rem}.p-inline-list{margin-left:0;padding-left:0}.p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem}.p-inline-list__item:last-of-type,.p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot{margin-left:0;padding-left:0}.p-inline-list--middot .p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem;margin-right:1.25em;position:relative}.p-inline-list--middot .p-inline-list__item:last-of-type,.p-inline-list--middot .p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot .p-inline-list__item::after{color:#666;content:'\00b7';font-size:1.4em;line-height:0;position:absolute;right:-.5em;top:.4em}.p-inline-list--middot .p-inline-list__item:hover::after{color:#666}.p-inline-list--middot .p-inline-list__item:last-of-type::after,.p-inline-list--middot .p-inline-list__item .last-item::after{content:''}.p-list-step__item{float:none;margin-left:0;width:100%}.p-list-step__content{margin-top:-1rem}.p-list-step__bullet{display:none}@media (min-width: 768px){.p-stepped-list--detailed .p-list-step__content{margin-top:0}.p-stepped-list--detailed .p-list-step__item{display:flex;margin:0}.p-stepped-list--detailed .p-list-step__item>:nth-child(2n){display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;width:48.35615%;margin-left:3.2877%}.p-stepped-list--detailed .p-list-step__item>:nth-child(2n+1){display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;width:48.35615%}}@media (min-width: 768px){@supports (columns: 1){[class*='p-list'].is-split{column-gap:2rem;columns:2}[class*='p-list'].is-split .p-list__item{display:inline-block;width:100%}}@supports not (columns: 1){[class*='p-list'].is-split{display:flex;flex-wrap:wrap}[class*='p-list'].is-split .p-list__item{width:calc(50% - .5rem)}}[class*='p-list'].is-split:nth-child(2n-1){margin-right:1rem}}.p-matrix{display:flex;flex-wrap:wrap;list-style:none;margin-bottom:1rem;margin-left:0;padding-left:0}.p-matrix__item{border-top:1px solid #cdcdcd;display:flex;flex:1 1 auto;padding-bottom:.5rem;padding-top:.4375rem}@media (min-width: 620px){.p-matrix__item{display:flex;flex-wrap:wrap;width:33.333%}}@media (min-width: 620px) and (max-width: 1030px){.p-matrix__item{flex-direction:column}}@media (min-width: 768px){.p-matrix__item{border-right:1px solid #cdcdcd;padding-left:.5rem;padding-right:.5rem;width:33.333%}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(3n+1){padding-left:0}.p-matrix__item:nth-child(3n+3){border-right:0}.p-matrix__item:nth-child(1),.p-matrix__item:nth-child(2),.p-matrix__item:nth-child(3){border-top:0}}@media (min-width: 1030px){.p-matrix__item{border-right:1px solid #cdcdcd;padding:.5rem;width:33.333%}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(3n+1){padding-left:0}.p-matrix__item:nth-child(3n+3){border-right:0;padding-right:0}.p-matrix__item:nth-last-child(1),.p-matrix__item:nth-last-child(2),.p-matrix__item:nth-last-child(3){border-bottom:0}}.p-matrix__img{align-self:flex-start;margin-bottom:.5rem;margin-right:1rem;max-height:3rem;max-width:3rem;width:auto}.p-matrix__content{display:flex;flex:1 1 auto;flex-direction:column;padding-right:1rem}@media (min-width: 1030px){.p-matrix__content{width:calc(100% - 4rem)}}.p-matrix__title{margin-top:-.5rem}.p-matrix__desc{margin-bottom:.1rem;margin-top:-1rem}.p-matrix__desc>p:last-child{margin-bottom:0}.p-matrix__desc+.p-matrix__desc{margin-top:0}@media (max-width: 768px){.p-matrix__desc{margin-top:-.5rem}}.p-media-object__image{align-self:flex-start;border-radius:.125rem;flex-basis:inherit;flex-shrink:0;margin-right:1rem;max-height:5rem;max-width:5rem;vertical-align:middle;width:auto}.p-media-object__content{margin-bottom:.6rem;margin-top:0}.p-media-object__image.is-round{border-radius:50%}.p-media-object__title{margin-bottom:.2rem;margin-top:-.5rem}@media only screen and (min-width: 768px){.p-media-object__title{margin-bottom:-0.05rem}}.p-media-object__meta-list{list-style:none;margin:0;padding-left:0;padding-top:.5rem}.p-media-object__meta-list-item--date{background-image:url('data:image/svg+xml;utf8,')}.p-media-object__meta-list-item--location{background-image:url('data:image/svg+xml;utf8,')}.p-media-object__meta-list-item--venue{background-image:url('data:image/svg+xml;utf8,')}.p-media-object--large .p-media-object__image{max-height:6rem;max-width:6rem}.p-media-object--large .p-media-object__title{margin-bottom:.3rem;margin-top:-.5rem}@media only screen and (min-width: 768px){.p-media-object--large .p-media-object__title{margin-bottom:-0.2rem}}.p-modal{align-items:center;background:rgba(17,17,17,0.85);content:'';display:flex;height:100vh;justify-content:center;left:0;margin:0;overflow:scroll;padding:1.5rem;position:absolute;top:0;width:100%}.p-modal__dialog{bottom:1.5rem;left:1.5rem;max-width:90rem;overflow:scroll;position:absolute;right:1.5rem;top:1.5rem;width:auto}@media screen and (min-width: 768px){.p-modal__dialog{bottom:initial;left:initial;overflow:visible;position:relative;right:initial;top:initial}}.p-modal__header{display:flex;justify-content:space-between}.p-modal__title{align-self:flex-end}.p-modal__close{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23888'/%3E%3C/g%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;background-size:1rem;border:0;box-sizing:content-box;height:1rem;margin:-1rem -1rem 0 0;padding:1rem;text-indent:-999em;width:1rem}.p-modal__close:focus{outline:1px solid #007aa6;outline-offset:2px}.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information{display:flex;padding:0}.p-notification{position:relative}.p-notification::before{top:0;background-color:#666;content:'';position:absolute}.p-notification::before{height:.1875rem;width:auto;left:0;right:0}.p-notification+.p-notification{margin-top:1rem}.p-notification__response{background-position:1rem .9rem;background-repeat:no-repeat;background-size:1rem;padding:.65rem 1rem .25rem}.p-notification__status::after,.p-notification__action::before{content:' '}.p-notification .p-icon--close{background-color:transparent;background-size:1rem;border:0;margin:1.1875rem 1rem auto auto;padding:.5rem}.p-notification__response,.p-notification--floating{max-width:60em}.p-notification--positive{position:relative}.p-notification--positive::before{top:0;background-color:#0e8420;content:'';position:absolute}.p-notification--positive::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--positive .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='17px' height='17px' viewBox='0 0 17 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='notification-success' transform='translate(1.000000, 1.000000)'%3E%3Cg id='Page-3---colours'%3E%3Cg id='Notifications---single'%3E%3Cg id='Group'%3E%3Cg id='ICON'%3E%3Ccircle id='circle6710' stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.2500086' cy='7.2500086' r='7.2500086'%3E%3C/circle%3E%3Cpolygon id='path6712' fill='%23fff' points='11.0502986 4.1734486 10.9843986 4.2311486 6.2496486 8.3783686 3.4740786 5.9974286 2.6350186 6.9463086 6.2503386 10.7500186 11.7500086 4.9627786 11.0502986 4.1734886'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--caution{position:relative}.p-notification--caution::before{top:0;background-color:#f99b11;content:'';position:absolute}.p-notification--caution::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--caution .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath stroke-linejoin='round' fill='%23f99b11' transform='matrix%282.28 0 0 2.437 -2180.8 -490.52%29' stroke='%23f99b11' stroke-width='.848' d='M963.07 207.03h-6.15l3.08-5.33z'/%3E%3Cpath d='M7 5v5h2V5H7zm0 6v2h2v-2H7z' fill='%23111'/%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--negative{position:relative}.p-notification--negative::before{top:0;background-color:#c7162b;content:'';position:absolute}.p-notification--negative::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--negative .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='17px' viewBox='0 0 16 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-3---colours' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='Notifications---single' transform='translate(-215.000000, -271.000000)'%3E%3Cg id='Group' transform='translate(205.000000, 254.000000)'%3E%3Cg id='ICON' transform='translate(10.000000, 17.000000)'%3E%3Crect id='rect6415' x='0' y='0.36218' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle6417' stroke='%23c7162b' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8.36218' r='7.2500086'%3E%3C/circle%3E%3Cpath d='M5.00001,5.36218 L11.00001,11.36218' id='path6479-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3Cpath d='M11.00001,5.36218 L5.00001,11.36218' id='path6481-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--information{position:relative}.p-notification--information::before{top:0;background-color:#335280;content:'';position:absolute}.p-notification--information::before{height:.1875rem;width:auto;left:0;right:0}.p-pagination__link--previous::before,.p-pagination__link--next::after{color:#666;content:'\203A';font-size:2rem;position:absolute;top:1rem}.p-pagination{display:flex;width:100%}.p-pagination__link,.p-pagination__link--previous,.p-pagination__link--next{margin-top:0;padding:1rem;position:relative;width:50%}.p-pagination__link:hover,.p-pagination__link--previous:hover,.p-pagination__link--next:hover{background:#f7f7f7;text-decoration:none}.p-pagination__link--previous{padding-left:2.5rem;text-align:left}@media (max-width: 460px){.p-pagination__link--previous{width:auto}.p-pagination__link--previous:only-child{width:100%}.p-pagination__link--previous:not(:only-child) *{display:none;max-width:.25rem;padding-left:1.5rem}}.p-pagination__link--previous::before{left:.5rem;transform:scaleX(-1)}.p-pagination__link--next{padding-right:2.5rem;text-align:right}@media (max-width: 460px){.p-pagination__link--next{width:100%}}.p-pagination__link--next:only-child{margin-left:auto}.p-pagination__link--next::after{right:.5rem}.p-pagination__label,.p-pagination__title{color:#111;display:block;margin-top:0;width:100%}.p-pagination__label{margin-bottom:.25rem}.p-pagination__title{font-size:1.125rem}@media (min-width: 620px){.p-pagination__title{font-size:1.25rem}}.p-pull-quote{border:0;margin:1rem 0 1.5rem;overflow:visible;padding:0 2rem;position:relative}.p-pull-quote>p:first-of-type::before{color:#cdcdcd;display:inline-block;font-size:3rem;font-weight:bold;max-width:1.25rem;position:absolute;content:'\201C\2002';left:.25rem;top:.05rem}@media (max-width: 768px){.p-pull-quote>p:first-of-type::before{font-size:2.75rem}}@media (max-width: 768px){.p-pull-quote>p:first-of-type::before{top:.3rem}}.p-pull-quote>p:last-of-type{margin-bottom:0}.p-pull-quote>p:last-of-type::after{color:#cdcdcd;display:inline-block;font-size:3rem;font-weight:bold;max-width:1.25rem;position:absolute;content:'\2002\201E';margin-left:.25rem;margin-top:-2.2rem}@media (max-width: 768px){.p-pull-quote>p:last-of-type::after{font-size:2.75rem}}@media (max-width: 768px){.p-pull-quote>p:last-of-type::after{margin-top:-1.7rem}}.p-pull-quote__citation{font-style:italic;margin-top:.25rem}.p-search-box__button,.p-search-box__reset{background:#fff;border:1px solid #cdcdcd;display:block;height:100%;margin:0;padding:0 .5rem;position:absolute;top:0;width:2.5rem}.p-search-box__button:hover,.p-search-box__reset:hover{background:inherit}.p-search-box__button:hover:disabled,.p-search-box__reset:hover:disabled{cursor:not-allowed}.p-search-box{box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);display:flex;margin-bottom:1.2rem;position:relative}.p-search-box__input{box-shadow:none;flex-grow:2;margin-bottom:0}.p-search-box__input::-webkit-search-cancel-button{-webkit-appearance:none}.p-search-box__input:not(:valid) ~ .p-search-box__reset{display:none}.p-search-box__button{right:0}.p-search-box__reset{border-left:0;border-right:0;right:2.5rem}.p-slider{appearance:none;border-radius:3px;margin:.5rem 0;padding:0;width:100%}.p-slider::-webkit-slider-runnable-track{border:1px solid #cdcdcd;border-radius:3px;height:6px}.p-slider::-webkit-slider-thumb{appearance:none;background:#fff;border:0;border-radius:2px;box-shadow:0 0 2px 1px rgba(0,0,0,0.2);height:24px;margin-top:-10.5px;width:24px}.p-slider::-webkit-slider-thumb:hover{cursor:pointer}.p-slider::-moz-range-track{background:#fff;border:1px solid #cdcdcd;border-radius:2px;height:4px}.p-slider::-moz-range-progress{background-color:#335280;border-radius:2px;height:4px}.p-slider::-moz-range-thumb{background:#fff;border:0;border-radius:2px;box-shadow:0 0 2px 1px rgba(0,0,0,0.2);height:24px;width:24px}.p-slider::-moz-range-thumb:hover{cursor:pointer}.p-slider::-moz-focus-outer{border:0}.p-slider::-ms-track{background:transparent;border-color:transparent;border-width:12px;color:transparent;height:6px;width:calc(100% - ($thumb-size / 2))}.p-slider::-ms-fill-lower{background:#335280;border:1px solid #cdcdcd;border-radius:2px}.p-slider::-ms-fill-upper{background:#fff;border:1px solid #cdcdcd;border-radius:2px}.p-slider::-ms-thumb{background:#fff;border:0;border-radius:2px;box-shadow:0 0 2px 1px rgba(0,0,0,0.2);height:24px;margin:0 2px;width:24px}.p-slider::-ms-thumb:hover{cursor:pointer}.p-slider::-ms-tooltip{display:none}.p-slider:focus{outline:none}.p-slider:focus::-webkit-slider-thumb{outline:1px solid #19b6ee;outline-offset:2px}.p-slider:focus::-moz-range-thumb{outline:1px solid #19b6ee;outline-offset:2px}.p-slider:focus::-ms-thumb{outline:1px solid #19b6ee;outline-offset:2px}.p-slider:disabled{opacity:.5}.p-slider__wrapper{align-items:center;display:inline-flex;width:100%}.p-slider__input{height:2.625em;margin:0 0 0 1rem;min-width:3.5em;text-align:center;width:5%}@media only screen and (max-width: 1030px){[class^='p-strip'].is-shallow{padding-bottom:.5rem;padding-top:.5rem}}@media only screen and (min-width: 1030px){[class^='p-strip'].is-shallow{padding-bottom:1rem;padding-top:1rem}}@media only screen and (max-width: 1030px){.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image{padding-bottom:1.5rem;padding-top:1.5rem}}@media only screen and (min-width: 1030px){.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image{padding-bottom:3rem;padding-top:3rem}}@media only screen and (max-width: 1030px){[class^='p-strip'].is-deep{padding:2.5rem 0 2.5rem}}@media only screen and (min-width: 1030px){[class^='p-strip'].is-deep{padding:5rem 0}}.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image{clear:both;width:100%}.p-strip{background-color:transparent}.p-strip--light{background-color:#f7f7f7}.p-strip--dark{background-color:#111;color:#f7f7f7}.p-strip--accent{background-color:#e95420;color:#111}.p-strip--image{background-repeat:no-repeat;background-size:cover}.p-strip--image.is-light{color:#000}.p-strip--image.is-dark{color:#fff}[class^='p-strip'].is-bordered{border-bottom:1px solid #cdcdcd;margin-bottom:-.0625rem}.p-switch{height:1.5rem;margin:0;position:relative;width:3rem}.p-switch:checked+.p-switch__slider::before{left:50%}.p-switch:focus{outline:0}.p-switch:focus+.p-switch__slider{outline:1px solid #19b6ee;outline-offset:2px}.p-switch__slider{background:linear-gradient(to right, #335280 50%, #cdcdcd 50%);box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);height:1.5rem;margin:.1rem 0 .5rem 0;position:relative;width:3rem}.p-switch__slider::before{transition-duration:0.5s;transition-property:all;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:#fff;content:"";height:1.5rem;left:0;position:absolute;width:1.5rem}button.p-switch{align-items:stretch;border:0;display:inline-flex;height:1.5rem;padding:initial;width:3rem}button.p-switch :first-child,button.p-switch :last-child{box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);line-height:1.5rem;margin:0;text-align:center;width:50%}button.p-switch :first-child{background-color:#335280;border-radius:2px 0 0 2px;color:#fff}button.p-switch :last-child{background-color:#cdcdcd;border-radius:0 2px 2px 0}button.p-switch::before{transition-duration:0.5s;transition-property:all;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:inherit;background-color:#fff;border-radius:.125rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);content:'';display:block;height:100%;left:0;max-height:2rem;padding:0;position:absolute;top:0;width:50%}button.p-switch::after{display:none}button.p-switch[aria-checked='true']::before{left:50%}.p-table-expanding{display:flex;flex-flow:column nowrap;justify-content:space-between}.p-table-expanding tbody{margin:0}.p-table-expanding tr{display:flex;flex-flow:row;flex-wrap:wrap;margin:0;width:100%}.p-table-expanding tr+tr{margin:0}.p-table-expanding th,.p-table-expanding td{display:flex;flex-basis:0;flex-flow:row nowrap;flex-grow:1;margin:0;word-break:break-word}.p-table-expanding th.p-table-expanding__panel,.p-table-expanding th.p-table-expanding__panel--bordered,.p-table-expanding td.p-table-expanding__panel,.p-table-expanding td.p-table-expanding__panel--bordered{flex-basis:100%;max-width:100%}.p-table-expanding th.p-table-expanding__panel[aria-hidden="true"],.p-table-expanding th[aria-hidden="true"].p-table-expanding__panel--bordered,.p-table-expanding td.p-table-expanding__panel[aria-hidden="true"],.p-table-expanding td[aria-hidden="true"].p-table-expanding__panel--bordered{display:none}.p-table-expanding th.p-table-expanding__panel .row,.p-table-expanding th.p-table-expanding__panel--bordered .row,.p-table-expanding td.p-table-expanding__panel .row,.p-table-expanding td.p-table-expanding__panel--bordered .row{max-width:100%;padding:0;width:100%}@media screen and (max-width: 1030px){.p-table--mobile-card thead{display:none}.p-table--mobile-card tbody{display:flex;flex-wrap:wrap;justify-content:space-between}.p-table--mobile-card tr{border-top:1px solid #cdcdcd;display:flex;flex-wrap:wrap;margin:-1px 0 .5rem;width:100%}.p-table--mobile-card td,.p-table--mobile-card tbody th{align-items:flex-start;display:flex;flex:0 1 auto;flex-direction:column;justify-content:left !important;margin:0;overflow:visible;padding-bottom:0;padding-top:0;text-align:left !important;width:25%}.p-table--mobile-card td[aria-label],.p-table--mobile-card tbody th[aria-label]{text-align:right}.p-table--mobile-card td[aria-label]::before,.p-table--mobile-card tbody th[aria-label]::before{content:attr(aria-label);display:block;flex:0 1 auto;margin-bottom:0;width:100%}.p-table--mobile-card td.u-align--right,.p-table--mobile-card tbody th.u-align--right{justify-content:unset !important}.p-table--mobile-card .p-contextual-menu,.p-table--mobile-card .p-contextual-menu--left,.p-table--mobile-card .p-contextual-menu--center,.p-table--mobile-card .p-cta,.p-table--mobile-card .p-table-menu{width:100%}.p-table--mobile-card .p-contextual-menu [role="menuitem"],.p-table--mobile-card .p-contextual-menu--left [role="menuitem"],.p-table--mobile-card .p-contextual-menu--center [role="menuitem"],.p-table--mobile-card .p-cta [role="menuitem"],.p-table--mobile-card .p-table-menu [role="menuitem"]{display:none}.p-table--mobile-card .p-contextual-menu__dropdown,.p-table--mobile-card .p-cta__dropdown,.p-table--mobile-card .p-table-menu .p-table-menu__dropdown,.p-table-menu .p-table--mobile-card .p-table-menu__dropdown{box-shadow:none;display:block;max-width:100%;position:relative}.p-table--mobile-card .p-contextual-menu__dropdown::before,.p-table--mobile-card .p-cta__dropdown::before,.p-table--mobile-card .p-table-menu .p-table-menu__dropdown::before,.p-table-menu .p-table--mobile-card .p-table-menu__dropdown::before{display:none}.p-table--mobile-card .p-contextual-menu__group{padding:0}.p-table--mobile-card .p-contextual-menu__group+.p-contextual-menu__group{margin-top:.5rem;padding-top:.5rem}.p-table--mobile-card .p-contextual-menu__link,.p-table--mobile-card .p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__power-off{border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#000;cursor:pointer;display:block;line-height:1rem;outline:none;padding:.5rem 1.5rem;text-align:center;text-decoration:none;width:100%}.p-table--mobile-card .p-contextual-menu__link+.p-contextual-menu__link,.p-table--mobile-card .p-cta__link+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-contextual-menu__link,.p-table--mobile-card .p-contextual-menu__link+.p-cta__link,.p-table--mobile-card .p-cta__link+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-cta__link,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__power-off{margin-top:.25rem}}.p-table--sortable th[role="columnheader"][aria-sort="ascending"]::after,.p-table--sortable [role="columnheader"].is-sorted.sort-asc::after,.p-table--sortable th[role="columnheader"][aria-sort="descending"]::after,.p-table--sortable [role="columnheader"].is-sorted.sort-desc::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10' viewBox='0 0 10 4'%3E%3Cpath d='M3.637 3.138c-.518-.365-1.052-.778-1.6-1.238C1.486 1.44.946.948.414.423.273.283.135.14 0 0h1.54c.305.29.62.57.948.846.138.116.277.23.417.34.163.13.328.257.495.38.085.062.17.123.257.184.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.4-.28.79-.583 1.17-.904C7.837.57 8.153.29 8.457 0h1.54c-.134.14-.272.282-.414.422C9.05.948 8.51 1.442 7.963 1.9c-.55.46-1.084.873-1.602 1.238S5.39 3.79 5 4c-.39-.21-.845-.497-1.363-.862z' fill='%23888' fill-rule='evenodd'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;background-size:100%;content:'';display:inline-block;height:.4rem;margin-left:.25rem;vertical-align:middle;width:1rem}.p-table--sortable{table-layout:fixed}.p-table--sortable th[role="columnheader"][aria-sort],.p-table--sortable [role="columnheader"]{align-items:center;cursor:pointer;white-space:nowrap}.p-table--sortable th[role="columnheader"][aria-sort="descending"]::after,.p-table--sortable [role="columnheader"].is-sorted.sort-desc::after{transform:rotate(180deg)}.p-table--sortable th[role="columnheader"][aria-sort]:hover,.p-table--sortable [role="columnheader"]:hover{color:#007aa6;text-decoration:underline}.p-tabs{border-radius:0;overflow:hidden;padding:0;position:relative}.p-tabs::before{bottom:0;color:#666;content:'\203A';display:block;font-size:2rem;line-height:1.5rem;padding-right:1.5rem;pointer-events:none;position:absolute;right:.5rem;text-align:right;top:15%;width:1rem;z-index:10}@media screen and (min-width: 768px){.p-tabs::before{display:none}}.p-tabs__list{margin:0 auto .5rem;overflow-x:scroll;padding:0;position:relative;white-space:nowrap;width:100%}@media screen and (min-width: 768px){.p-tabs__list{max-width:90rem;overflow:hidden}}.p-tabs__item{display:inline-block;float:none;margin:0;padding:0;width:auto}@media screen and (min-width: 768px){.p-tabs__item{float:left}}.p-tabs__item:last-child{margin-right:3rem}@media screen and (min-width: 768px){.p-tabs__item:last-child{margin-right:0}}.p-tabs__link{color:#000;display:inline-block;padding:.75rem 1rem}.p-tabs__link:visited,.p-tabs__link:active,.p-tabs__link:hover{color:#000;text-decoration:none}.p-tabs__link:hover,.p-tabs__link[aria-selected="true"],.p-tabs__link.is-active{position:relative}.p-tabs__link:hover::before,.p-tabs__link[aria-selected="true"]::before,.p-tabs__link.is-active::before{bottom:0;background-color:#666;content:'';position:absolute}.p-tabs__link:hover::before,.p-tabs__link[aria-selected="true"]::before,.p-tabs__link.is-active::before{height:.1875rem;width:auto;left:-1px;right:-1px;z-index:1}.p-tooltip{position:relative}.p-tooltip__message{background-color:#111;border:0;border-radius:.125rem;color:#fff;display:none;left:0;margin-bottom:0;min-width:155px;padding:.5rem 1rem;position:absolute;text-align:left;text-decoration:initial;top:100%;transform:translateX(0%) translateY(13px);white-space:pre;z-index:1}.p-tooltip__message::before{border-bottom:8px solid #111;border-left:8px solid transparent;border-right:8px solid transparent;bottom:100%;content:'';height:0;left:1rem;pointer-events:none;position:absolute;width:0}.p-tooltip:focus .p-tooltip__message,.p-tooltip:hover .p-tooltip__message{display:inline;text-decoration:initial}.p-tooltip--btm-center .p-tooltip__message{bottom:inherit;left:50%;top:100%;transform:translateX(-50%) translateY(13px)}.p-tooltip--btm-center .p-tooltip__message::before{left:50%;transform:translateX(-50%)}.p-tooltip--btm-right .p-tooltip__message{bottom:inherit;left:initial;right:0;top:100%;transform:translateY(13px)}.p-tooltip--btm-right .p-tooltip__message::before{left:initial;right:.5rem}.p-tooltip--top-left .p-tooltip__message{bottom:100%;left:0;top:initial;transform:translateX(0%) translateY(-13px)}.p-tooltip--top-left .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #111;bottom:-1rem;left:.5rem}.p-tooltip--top-center .p-tooltip__message{bottom:100%;left:50%;top:initial;transform:translateX(-50%) translateY(-13px)}.p-tooltip--top-center .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #111;bottom:-1rem;left:50%;transform:translateX(-50%)}.p-tooltip--top-right .p-tooltip__message{bottom:100%;left:initial;right:0;top:initial;transform:translateX(0%) translateY(-13px)}.p-tooltip--top-right .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #111;bottom:-1rem;left:initial;right:.5rem}.p-tooltip--right .p-tooltip__message{bottom:inherit;left:100%;top:50%;transform:translateX(14px) translateY(-50%)}.p-tooltip--right .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid #111;border-top:8px solid transparent;bottom:inherit;left:0;top:50%;transform:translateX(-16px) translateY(-50%)}.p-tooltip--left .p-tooltip__message{bottom:inherit;left:-16px;top:50%;transform:translateX(-100%) translateY(-50%)}.p-tooltip--left .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid #111;border-right:8px solid transparent;border-top:8px solid transparent;bottom:inherit;left:100%;top:50%;transform:translateX(0) translateY(-50%)}.u-align--center{justify-content:center !important;text-align:center !important}.u-align--left{justify-content:flex-start !important;text-align:left !important}.u-align--right{justify-content:flex-end !important;text-align:right !important}.u-align--bottom{margin-top:auto !important}.u-align-text--center{margin-left:auto !important;margin-right:auto !important;text-align:center !important}.u-align-text--left{margin-right:auto !important;text-align:left !important}.u-align-text--right{margin-left:auto !important;text-align:right !important}.u-animation--spin{animation:spin 1s infinite linear}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.u-baseline-grid{position:relative}.u-baseline-grid::after{background:linear-gradient(to top, rgba(255,0,0,0.3), rgba(255,0,0,0.3) 1px, transparent 1px, transparent);background-size:100% .5rem;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:100}.u-baseline-grid__toggle{bottom:1rem;position:fixed;right:1.5rem;z-index:101}.u-embedded-media{height:0;margin-top:.5rem;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative}.u-embedded-media__element{height:100%;left:0;position:absolute;top:0;width:100%}@media only screen and (min-width: 768px){.u-equal-height{display:flex}}.u-float--right{float:right !important}.u-float--left{float:left !important}.u-float-right{float:right !important}@media (max-width: 620px){.u-float-right--small{float:right !important}}@media (min-width: 768px) and (max-width: 1030px){.u-float-right--medium{float:right !important}}@media (min-width: 1030px){.u-float-right--large{float:right !important}}.u-float-left{float:left !important}@media (max-width: 620px){.u-float-left--small{float:left !important}}@media (min-width: 768px) and (max-width: 1030px){.u-float-left--medium{float:left !important}}@media (min-width: 1030px){.u-float-left--large{float:left !important}}.u-hide{display:none !important}@media screen and (max-width: 768px){.u-hide--small{display:none !important}}@media (min-width: 768px) and (max-width: 1030px){.u-hide--medium{display:none !important}}@media screen and (min-width: 1030px){.u-hide--large{display:none !important}}@media (min-width: 768px){.u-image-position{overflow:hidden;position:relative}.u-image-position .u-image-position--top,.u-image-position .u-image-position--bottom,.u-image-position .u-image-position--left,.u-image-position .u-image-position--right{margin:0;position:absolute}.u-image-position [class*='col-']{position:static}.u-image-position--top{top:0}.u-image-position--bottom{bottom:0}.u-image-position--left{left:0}.u-image-position--right{right:0}}.u-no-margin{margin:0 !important}.u-no-margin--top{margin-top:0 !important}.u-no-margin--right{margin-right:0 !important}.u-no-margin--bottom{margin-bottom:0 !important}.u-no-margin--left{margin-left:0 !important}.u-off-screen{height:1px !important;left:-10000px !important;overflow:hidden !important;position:absolute !important;top:auto !important;width:1px !important}.u-no-padding{padding:0 !important}.u-no-padding--top{padding-top:0 !important}.u-no-padding--right{padding-right:0 !important}.u-no-padding--bottom{padding-bottom:0 !important}.u-no-padding--left{padding-left:0 !important}.u-show{display:inherit !important}@media screen and (max-width: 768px){.u-show--small{display:inherit !important}}@media (min-width: 768px) and (max-width: 1030px){.u-show--medium{display:inherit !important}}@media screen and (min-width: 1030px){.u-show--large{display:inherit !important}}.u-sv-3::after,.u-sv-2::after,.u-sv-1::after,.u-sv0::after,.u-sv1::after,.u-sv2::after,.u-sv3::after{content:'';display:block;height:.0625rem;position:relative}.u-sv-3::after{margin-top:-1.5625rem}.u-sv-2::after{margin-top:-1.0625rem}.u-sv-1::after{margin-top:-.5625rem}.u-sv0::after{margin-top:-.0625rem}.u-sv1::after{margin-top:.4375rem}.u-sv2::after{margin-top:.9375rem}.u-sv3::after{margin-top:1.4375rem}@media (min-width: 768px){.u-vertically-center{align-items:center !important;display:flex !important}.u-vertically-center>img{align-self:center !important}}.u-hidden{display:none !important}@media screen and (max-width: 768px){.u-hidden--small{display:none !important}}@media (min-width: 768px) and (max-width: 1030px){.u-hidden--medium{display:none !important}}@media screen and (min-width: 1030px){.u-hidden--large{display:none !important}}.u-visible{display:inherit !important}@media screen and (max-width: 768px){.u-visible--small{display:inherit !important}}@media (min-width: 768px) and (max-width: 1030px){.u-visible--medium{display:inherit !important}}@media screen and (min-width: 1030px){.u-visible--large{display:inherit !important}}.u-position-relative{position:relative}.u-width--auto{width:auto !important}.u-flex--no-wrap{display:flex;flex-wrap:wrap}.u-text--light{color:#666}.u-td-outdent--left{margin-left:-.5rem}.u-td-outdent--right{margin-right:-.5rem}.u-td-outdent-focusable--left{margin-left:-.3125rem}.u-td-outdent-focusable--right{margin-right:-.3125rem}.u-valign--middle{vertical-align:middle}.u-space-between{display:flex;justify-content:space-between}.u-disable{opacity:.5;pointer-events:none !important;user-select:none}.hide-create-tag-label .create-tag-label{display:none}table thead th{font-size:.76562rem;font-weight:400;margin-bottom:.1rem;padding-top:.15rem}h4+h4,.p-heading--four+h4,h4+.p-heading--four,.p-heading--four+.p-heading--four{margin-top:-1rem !important}.default-text{display:block}.p-p-small,.p-p-small--align-with-p{display:block}.p-p-small--align-with-p{padding-top:0.55rem}.p-p-compact{display:block;margin-bottom:.6rem}.u-no-max-width{max-width:unset}@media only screen and (max-width: 460px){button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{width:auto}}.p-key-icon--free::before,.p-key-icon--used::before,.p-key-icon--requests::before,.p-key-icon--other::before{content:"•";float:left;font-size:2rem;line-height:1.5rem;margin-right:.5rem;padding-top:.4rem;width:.5rem}.p-key-icon--free{background-color:rgba(0,122,166,0.2)}.p-key-icon--used{background-color:#007aa6}.p-key-icon--requests{background-color:#0e8420}.p-key-icon--other{background-color:rgba(14,132,32,0.2)}.p-slider{-webkit-appearance:none;-moz-appearance:none}.p-slider::-webkit-slider-thumb{-webkit-appearance:none;-webkit-box-shadow:0 0 2px 1px rgba(0,0,0,0.2)}.p-slider__wrapper{-webkit-box-align:center;-ms-flex-align:center}.p-slider{margin:0 0 1rem 0}.p-slider__wrapper{margin-top:-1rem}.p-slider__input{height:inherit !important}.p-slider+button{margin-left:1rem}.u-baseline-grid{position:relative}.u-baseline-grid::after{background:linear-gradient(to top, rgba(255,0,0,0.3), rgba(255,0,0,0.3) 1px, transparent 1px, transparent);background-size:100% .5rem;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:100}.u-baseline-grid__toggle{bottom:1rem;position:fixed;right:1.5rem;z-index:101}.p-footer__link{color:#007aa6}.p-footer__link::after{color:#111}body{background-color:#f7f7f7}maas-obj-field,maas-obj-errors,maas-obj-form,maas-machines-table,storage-disks-partitions,storage-filesystems{display:block;position:relative}maas-obj-field[type="text"],maas-obj-field[type="password"]{background-color:transparent;border:0;border-radius:0;box-shadow:none;padding:0}textarea{min-height:175px}.field-description textarea{min-height:initial}.p-table--mobile-card td[aria-label]{min-height:2rem}@media (max-width: 768px){.p-table--mobile-card td,.p-table--mobile-card tr{overflow-x:visible}}.u-float-none{float:none !important}p:empty,ul:empty,label:empty{margin:0;padding:0}.tags .input{width:100% !important}dl dt:first-of-type{margin-top:0;padding-top:0}.p-navigation--sidebar{background:#fff}.p-navigation--sidebar .sidebar__content{top:0;padding:1rem}.sidebar__content .sidebar__second-level .is-active{background-position-y:0.9rem}.u-float--none{float:none !important}.u-remove-max-width{max-width:none}.p-navigation--sidebar{margin-bottom:1rem}.p-navigation--sidebar::after{background:transparent}.p-form__control{margin-top:0;white-space:normal}.p-form-help-text{display:inline-block}select,.p-option-selector__input{-moz-appearance:none;-webkit-appearance:none;appearance:none;padding-right:2.3rem}maas-obj-field[type="password"]{background:transparent;border:0}.p-list-tree .p-list-tree::after{display:none !important}.editable{padding:.5rem 1rem;border:1px solid transparent}.editable:hover,.editable.editmode{border:1px solid #cdcdcd}.page-header__title-domain{display:inline-block;width:auto}.col-12{width:100%}.errorlist{list-style:none;display:inline-block;margin-left:0}.sidebar__second-level .is-active{background:transparent url("../assets/images/89c10794-remove.svg") top .5rem right 1.5rem no-repeat;background-size:12px}.p-form__control [type='checkbox']{height:auto;min-height:auto}.p-script-expander{border-color:#cdcdcd;border-bottom-style:solid;border-bottom-width:1px}.p-script-expander__content{overflow:hidden;width:100%;margin-top:0}.p-script-expander__controls{vertical-align:top}.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown{display:block;min-width:13rem;width:100%}.p-tooltip__message{margin:0;z-index:100}.u-upper-case--first{text-transform:capitalize}.u-no-wrap{white-space:nowrap}.u-wrap{white-space:normal}.p-form__group .col-1:first-child,.p-form__group .col-2:first-child,.p-form__group .p-form--stacked .p-form__label:first-child,.p-form--stacked .p-form__group .p-form__label:first-child,.p-form__group .col-3:first-child,.p-form__group .col-4:first-child,.p-form__group .p-form--stacked .p-form__control:first-child,.p-form--stacked .p-form__group .p-form__control:first-child,.p-form__group .p-pod-summary__aside:first-child,.p-form__group .p-storage__name:first-child,.p-form__group .col-5:first-child,.p-form__group .col-6:first-child,.p-form__group .col-7:first-child,.p-form__group .col-8:first-child,.p-form__group .p-pod-summary__storage:first-child,.p-form__group .col-9:first-child,.p-form__group .col-10:first-child,.p-form__group .col-11:first-child,.p-form__group .col-12:first-child{margin-left:0}.flex-row{display:flex;justify-content:space-between}@media (max-width: 768px){.flex-row{flex-direction:column}}.flex-item{flex:1}.p-inline-list--settings .p-inline-list__item{display:inline-block;margin-right:1.5rem;vertical-align:top}.p-inline-list--settings [type='checkbox']{float:none}.p-inline-list--settings label{display:inline}.p-inline-list__item div{display:inline-block}.p-list--divided .p-list__item::after{border-bottom-style:solid;border-bottom-color:#e5e5e5}@media (min-width: 620px){table th,table td,table p{text-overflow:ellipsis;overflow-x:hidden;overflow-y:visible;white-space:nowrap}}table{overflow-x:visible}table input[type="radio"],table input[type="checkbox"]{float:none}table form input[type="radio"],table form input[type="checkbox"]{float:left}table thead th{margin-bottom:0;padding-bottom:.35rem;text-transform:uppercase}table th,table td{display:table-cell !important;flex-basis:auto !important;flex-grow:0;vertical-align:top;padding-bottom:0.05rem}table th:first-of-type,table td:first-of-type{padding-left:.5rem}table th:not(:last-child),table td:not(:last-child){padding-right:.5rem}tr.is-active{background-color:#fff}thead tr{border-bottom-color:#cdcdcd}tbody tr:not(:first-child){border-top-color:#e5e5e5}tr.ng-hide+tr{border:0}.p-table--action-cell{overflow:visible}maas-obj-field{margin-bottom:0 !important;padding:0 !important}[type='text'].u-min-margin--bottom,[type='date'].u-min-margin--bottom,[type='datetime'].u-min-margin--bottom,[type='datatime-local'].u-min-margin--bottom,[type='month'].u-min-margin--bottom,[type='time'].u-min-margin--bottom,[type='week'].u-min-margin--bottom,[type='color'].u-min-margin--bottom,[type='number'].u-min-margin--bottom,[type='search'].u-min-margin--bottom,[type='password'].u-min-margin--bottom,[type='email'].u-min-margin--bottom,[type='url'].u-min-margin--bottom,[type='tel'].u-min-margin--bottom,textarea.u-min-margin--bottom,select.u-min-margin--bottom,.u-min-margin--bottom.p-option-selector__input,.p-table--pod-networking-config input,.p-table--pod-storage-config input,.p-table--pod-networking-config select,.p-table--pod-networking-config .p-option-selector__input{margin-bottom:.2rem !important}table [type='text'],table [type='date'],table [type='datetime'],table [type='datatime-local'],table [type='month'],table [type='time'],table [type='week'],table [type='color'],table [type='number'],table [type='search'],table [type='password'],table [type='email'],table [type='url'],table [type='tel'],table textarea,table select,table .p-option-selector__input{margin-bottom:.7rem;padding-bottom:.3375rem;padding-top:.3375rem;min-height:2.3rem;min-width:auto}.is-small [type='text'],.is-small [type='date'],.is-small [type='datetime'],.is-small [type='datatime-local'],.is-small [type='month'],.is-small [type='time'],.is-small [type='week'],.is-small [type='color'],.is-small [type='number'],.is-small [type='search'],.is-small [type='password'],.is-small [type='email'],.is-small [type='url'],.is-small [type='tel'],.is-small textarea,.is-small select,.is-small .p-option-selector__input{margin-bottom:.1rem;padding-bottom:.0875rem;padding-top:.0875rem}p.u-min-margin--bottom{margin-bottom:.1rem}input[type="checkbox"]+label::after{top:13px}th:not(.p-table__group-label) input[type="checkbox"]+label::before{top:.65em}th:not(.p-table__group-label) input[type="checkbox"]+label::after{top:11px}.p-inline-list__item input[type="checkbox"]+label::before{top:.5rem}.p-inline-list__item input[type="checkbox"]+label::after{top:11px}.u-full-width-input .p-form__control{width:100%}.obj-saving{margin-right:.5rem}a:visited{color:#007aa6}.p-action-button{position:relative}.p-action-button::before{background-size:1rem;content:'';height:1rem;left:1rem;position:absolute;top:.5875rem;width:1rem}.p-action-button.is-indeterminate,.p-action-button.is-done{padding-left:2.5rem}.p-action-button.is-indeterminate::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E");animation:spin 1s infinite linear}.p-action-button.is-indeterminate.p-button--positive::before,.p-action-button.is-indeterminate.p-button--negative::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23fff' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-action-button.is-done::before{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23fff' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E");animation:none}.p-action-button.is-done.p-button--positive::before{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23fff' stroke-width='1.5' fill='%23fff' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%230e8420' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E")}.p-action-button.is-done.p-button--negative::before{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23fff' stroke-width='1.5' fill='%23fff' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23c7162b' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon{padding-right:.5rem}.p-icon--edit{background-image:url("data:image/svg+xml;charset=UTF-8,%3csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3e%3ctitle%3eedit%3c/title%3e%3cg fill='%23666666' fill-rule='evenodd'%3e%3cpath d='M17 15h5v1h-5zm-3 3h8v1h-8zm-3 3h11v1H11zm5.75-21L3.47 13.517S.956 17.465 0 21.987v.004l.002.004V22c4.532-.955 8.48-3.472 8.48-3.472L22 5.25 16.75 0zM4.51 14.555L7.454 17.5c-.2.114-2.99 2.064-5.544 2.602V20.093l-.002-.003c.537-2.546 2.485-5.334 2.602-5.537v.002z'/%3e%3cpath d='M2.234 18l1.85 1.85L1 21'/%3e%3c/g%3e%3c/svg%3e")}[class$="--dark"] .p-icon--edit{background-image:url("data:image/svg+xml;charset=UTF-8,%3csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3e%3ctitle%3eedit%3c/title%3e%3cg fill='%23CDCDCD' fill-rule='evenodd'%3e%3cpath d='M17 15h5v1h-5zm-3 3h8v1h-8zm-3 3h11v1H11zm5.75-21L3.47 13.517S.956 17.465 0 21.987v.004l.002.004V22c4.532-.955 8.48-3.472 8.48-3.472L22 5.25 16.75 0zM4.51 14.555L7.454 17.5c-.2.114-2.99 2.064-5.544 2.602V20.093l-.002-.003c.537-2.546 2.485-5.334 2.602-5.537v.002z'/%3e%3cpath d='M2.234 18l1.85 1.85L1 21'/%3e%3c/g%3e%3c/svg%3e")}.p-icon--status-failed{background:url('data:image/svg+xml;utf8,%3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23C7162B" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23C7162B" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-in-progress{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23335280" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23335280" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-queued{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23666666" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23666666" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-succeeded{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%230E8420" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%230E8420" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-waiting{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23F99B11" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23F99B11" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--timed-out{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='16px' width='16px' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 16 16'%3E%3Ctitle%3Etimed out%3C/title%3E%3Cg id='Page-1' fill-rule='evenodd' fill='none'%3E%3Cg id='smoke-testing-status' transform='translate%28-62 -206%29'%3E%3Cg id='timed-out' transform='translate%2850 191%29'%3E%3Cg transform='translate%2812 15%29'%3E%3Crect id='rect4970' y='0.00002' x='0' height='16' width='16'/%3E%3Ccircle id='circle4972' stroke-width='1.5' cy='8' stroke='%23E95420' cx='8' r='7.25'/%3E%3Cpolyline id='path839' stroke='%23E95420' stroke-width='2' points='11.8 11.8 7.9999 8 7.9999 3'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A")}.p-icon--success-muted{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='17' viewBox='0 0 17 17'%3E%3Cg transform='translate(1 1)' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23CDCDCD' stroke-width='1.5' fill='%23CDCDCD' cx='7.25001' cy='7.25001' r='7.25001'/%3E%3Cpath fill='%23fff' d='M11.0503 4.17345l-.0659.0577-4.73475 4.14722-2.77557-2.38094-.83906.94888 3.61532 3.80373L11.75 4.96278l-.6997-.7893'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--locked{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath d='M8,3e-05 C5.7926,3e-05 4,1.79263 4,4.00003 L4,7.00003 L2,7.00003 L2,15.99953 L14,15.99953 L14,7.00003 L12,7.00003 L12,4.00003 C12,1.79263 10.207,3e-05 8,3e-05 L8,0 L8,3e-05 Z M8,1.00003 C9.6706,1.00003 11,2.32933 11,4.00003 L11,7.00003 L5,7.00003 L5,4.00003 C5,2.32933 6.3293,1.00003 8,1.00003 Z M9,9.50003 L9,13.99953 L7,13.99953 L7,9.99953 L9,9.50003 L9,9.50003 Z' id='padlock-icon'%3E%3C/path%3E%3C/defs%3E%3Cg id='padlock-16'%3E%3Cuse id='lock-icon' fill='%23666666' fill-rule='nonzero' xlink:href='%23padlock-icon'%3E%3C/use%3E%3C/g%3E%3C/svg%3E%0A")}.p-icon--status-waiting{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23666666" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23666666" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-succeeded{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%230E8420" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%230E8420" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-queued{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23CDCDCD" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23CDCDCD" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-in-progress{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23335280" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23335280" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-failed{background:url('data:image/svg+xml;utf8,%3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23C7162B" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23C7162B" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--compose-machine{background:url('data:image/svg+xml;utf8,%3Csvg width="149" height="105" viewBox="0 0 149 105" xmlns="http://www.w3.org/2000/svg"%3E%3Ctitle%3Ecompose-machine%3C/title%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cpath d="M9.746 1.845H37.3c5.98 0 7.723 1.735 7.723 7.68v85.64c0 5.946-1.744 7.68-7.723 7.68H9.746c-5.98 0-7.723-1.734-7.723-7.68V9.525c0-5.945 1.744-7.68 7.723-7.68z" stroke="%23CDCDCD" stroke-width="3"/%3E%3Cpath d="M13.49 86.845c-2.48 0-4.502 2.018-4.502 4.5 0 2.48 2.02 4.5 4.502 4.5 2.48 0 4.498-2.02 4.498-4.5 0-2.482-2.017-4.5-4.498-4.5z" stroke="%23CDCDCD" stroke-width="2"/%3E%3Cpath fill="%23CDCDCD" d="M8.476 8.346v1H38.54v-1M8.436 13.372v1H38.5v-1M8.476 18.346v1H38.54v-1M8.436 23.372v1H38.5v-1"/%3E%3Cg%3E%3Cpath d="M111.746 1.845H139.3c5.98 0 7.723 1.735 7.723 7.68v85.64c0 5.946-1.744 7.68-7.723 7.68h-27.554c-5.98 0-7.723-1.734-7.723-7.68V9.525c0-5.945 1.744-7.68 7.723-7.68z" stroke="%23CDCDCD" stroke-width="3"/%3E%3Cpath d="M115.49 86.845c-2.48 0-4.502 2.018-4.502 4.5 0 2.48 2.02 4.5 4.502 4.5 2.48 0 4.498-2.02 4.498-4.5 0-2.482-2.017-4.5-4.498-4.5z" stroke="%23CDCDCD" stroke-width="2"/%3E%3Cpath fill="%23CDCDCD" d="M110.476 8.346v1h30.065v-1M110.436 13.372v1H140.5v-1M110.476 18.346v1h30.065v-1M110.436 23.372v1H140.5v-1"/%3E%3C/g%3E%3Cg%3E%3Cpath d="M60.746 1.845H88.3c5.98 0 7.723 1.735 7.723 7.68v85.64c0 5.946-1.744 7.68-7.723 7.68H60.746c-5.98 0-7.723-1.734-7.723-7.68V9.525c0-5.945 1.744-7.68 7.723-7.68z" stroke="%23CDCDCD" stroke-width="3"/%3E%3Cpath d="M64.49 86.845c-2.48 0-4.502 2.018-4.502 4.5 0 2.48 2.02 4.5 4.502 4.5 2.48 0 4.498-2.02 4.498-4.5 0-2.482-2.017-4.5-4.498-4.5z" stroke="%23CDCDCD" stroke-width="2"/%3E%3Cpath fill="%23CDCDCD" d="M59.476 8.346v1H89.54v-1M59.436 13.372v1H89.5v-1M59.476 18.346v1H89.54v-1M59.436 23.372v1H89.5v-1"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');background-size:100% 100%}.p-icon--account{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal' d='M48 6C24.828 6 6 24.83 6 48s18.828 41.998 42 41.998S90 71.17 90 48C90 24.83 71.172 6 48 6zm0 4c21.01 0 37.998 16.99 37.998 38 0 9.324-3.35 17.852-8.908 24.46a25.598 25.598 0 0 0-4.94-7.495c-1.955-2.062-4.308-3.79-7.017-5.192a24.975 24.975 0 0 1-3.697 2.682c3.188 1.365 5.775 3.117 7.81 5.264h.002c2.162 2.274 3.75 4.902 4.81 7.94C67.26 82.07 58.1 86 48 86c-10.135 0-19.325-3.96-26.133-10.412 1.123-3.013 2.706-5.623 4.78-7.875 2.208-2.328 5.055-4.2 8.626-5.606 3.507-1.38 7.732-2.106 12.696-2.11L48 60c2.886 0 5.613-.508 8.12-1.54l.017-.01.017-.01c2.49-1.078 4.66-2.615 6.45-4.575 1.84-1.96 3.258-4.302 4.242-6.967.994-2.693 1.476-5.672 1.476-8.898v-.002c0-3.18-.484-6.132-1.476-8.822-.98-2.706-2.397-5.073-4.24-7.037-1.79-1.966-3.973-3.483-6.47-4.515C53.623 16.54 50.89 16 48 16c-2.892 0-5.624.54-8.135 1.625a18.703 18.703 0 0 0-6.54 4.5l-.01.012-.01.01c-1.792 1.968-3.176 4.333-4.155 7.037l-.002.004c-.99 2.686-1.47 5.634-1.47 8.81 0 3.226.48 6.207 1.474 8.9.98 2.657 2.366 4.993 4.153 6.954l.01.01.01.01a19.447 19.447 0 0 0 4.152 3.345c-1.274.325-2.5.71-3.668 1.17-4.03 1.587-7.416 3.776-10.074 6.58l-.01.012-.01.01c-2.007 2.177-3.62 4.66-4.855 7.41a37.86 37.86 0 0 1-8.858-24.4c0-21.01 16.99-38 37.998-38zm0 9.998c2.408 0 4.573.438 6.562 1.3l.018.01.016.006c1.995.823 3.66 1.983 5.066 3.526l.01.012.01.01c1.453 1.548 2.587 3.42 3.406 5.685l.002.008.004.006c.81 2.195 1.226 4.662 1.226 7.44 0 2.83-.418 5.323-1.226 7.514-.818 2.216-1.953 4.07-3.412 5.623l-.01.01-.01.013c-1.408 1.546-3.083 2.735-5.086 3.606C52.584 55.583 50.412 56 48 56c-2.413 0-4.586-.416-6.578-1.234a15.41 15.41 0 0 1-5.168-3.62c-1.412-1.553-2.528-3.41-3.348-5.632-.808-2.19-1.226-4.684-1.226-7.514 0-2.778.417-5.245 1.226-7.44l.002-.005.004-.008c.82-2.27 1.936-4.147 3.342-5.693a14.599 14.599 0 0 1 5.15-3.54l.016-.007.017-.008c1.99-.864 4.156-1.302 6.563-1.302z' font-family='sans-serif' white-space='normal' overflow='visible' solid-color='%23FFFFFF' fill='%23666666'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--mount{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333493 4.2333317'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath d='M0 .265v.793h4.233V.265zm3.44.264h.529v.265h-.53zM3.175 2.117v.793h-.794v.265h.794v.794h.265v-.794h.793V2.91H3.44v-.793z' style='marker:none' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--unmount{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333493 4.2333317'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath d='M0 .265v.793h4.233V.265zm3.44.264h.529v.265h-.53zM2.381 2.91v.265h1.852V2.91z' style='marker:none' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--partition{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333398 4.2333315'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath d='M0 .794V3.44h1.852v-.265H.265V1.058h1.587V.794zM3.175 1.323v.794h-.794v.264h.794v.794h.265v-.794h.793v-.264H3.44v-.794zM1.852.264h.265v.53h-.265z' style='marker:none' color='%23000' overflow='visible' fill='gray'/%3E%3Cpath style='marker:none' color='%23000' overflow='visible' fill='gray' d='M1.852 1.058h.265v.53h-.265zM1.852 1.852h.265v.53h-.265zM1.852 2.645h.265v.53h-.265zM1.852 3.44h.265v.528h-.265z'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--debug{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3Edebug icon 8@2x%3C/title%3E%3Cpath d='M11.673 12.994L14.68 16l1.313-1.316-3.004-3.005A5.501 5.501 0 0 0 8.5 3C5.46 3 3 5.463 3 8.5 3 11.54 5.46 14 8.5 14c1.18 0 2.276-.37 3.173-1.006zM4.25 8.5a4.25 4.25 0 1 1 8.5 0 4.25 4.25 0 0 1-8.5 0zM6 9h5v1H6V9zm0-2h5v1H6V7zM4.71 0C2.845 0 1.646.095.87.87.093 1.648 0 2.847 0 4.716v4.568c0 1.87.094 3.068.87 3.844.667.668 1.68.824 3.13.857v-1.002c-1.327-.044-2.075-.21-2.424-.56-.41-.41-.576-1.318-.576-3.14V4.716c0-1.82.167-2.727.576-3.137C1.986 1.168 2.892 1 4.71 1h4.577c1.82 0 2.726.17 3.135.578.348.35.514 1.097.558 2.422h1.006c-.033-1.45-.19-2.46-.857-3.13C12.352.096 11.153 0 9.286 0H4.71z' fill='gray' fill-rule='evenodd'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--remove{background-image:url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3Eremove%3C/title%3E%3Cg fill='gray' fill-rule='evenodd'%3E%3Cpath d='M16.36 0L18 1.64 1.64 18 0 16.36z'/%3E%3Cpath d='M18 16.36L16.36 18 0 1.64 1.64 0z'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--settings{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath style='marker:none' overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M79.197 66.033c9.916-17.191 3.984-39.246-13.223-49.19-17.207-9.944-39.253-4.057-49.17 13.134C6.89 47.168 12.82 69.222 30.027 79.166c17.207 9.944 39.255 4.058 49.17-13.133zm-3.465-2.002c-8.834 15.316-28.37 20.535-43.708 11.67-15.338-8.863-20.59-28.407-11.756-43.723 8.834-15.316 28.37-20.535 43.708-11.671 15.338 8.864 20.59 28.408 11.756 43.724z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3Cpath style='marker:none' d='M54.902 5.582L41.098 7.645v7.097a34.033 33.971 43.146 0 1 13.804.028V5.582zm-28.246 6.244L16.14 20.768l5.181 6.175a34.033 33.971 43.146 0 1 10.58-8.867l-5.244-6.25zm42.631.067L64.08 18.1a34.033 33.971 43.146 0 1 .895.474 34.033 33.971 43.146 0 1 9.673 8.406l5.272-6.28-10.633-8.807zM8.398 34.008l-2.31 13.611 7.947 1.402a34.033 33.971 43.146 0 1 2.387-13.597l-8.024-1.416zm79.118.015l-7.987 1.409a34.033 33.971 43.146 0 1 2.432 13.588L90 47.604l-2.484-13.58zM15.758 58.645L8.67 62.738h-.002l6.98 11.91 7.018-4.05a34.033 33.971 43.146 0 1-6.908-11.953zm64.512.015a34.033 33.971 43.146 0 1-2.805 6.371 34.033 33.971 43.146 0 1-4.059 5.608l7.022 4.054 6.826-12-6.984-4.033zM30.148 76.867l-2.804 7.703 13.004 4.639 2.757-7.58a34.033 33.971 43.146 0 1-12.08-4.195 34.033 33.971 43.146 0 1-.877-.567zm35.733.08a34.033 33.971 43.146 0 1-12.979 4.707l2.782 7.639 12.941-4.807-2.744-7.539z' overflow='visible' fill='gray'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M68.805 60.027c6.61-11.46 2.652-26.178-8.816-32.806-11.47-6.628-26.185-2.7-32.795 8.76-6.61 11.459-2.651 26.18 8.817 32.808 11.47 6.628 26.184 2.697 32.794-8.762zm-3.463-2.001C59.814 67.61 47.61 70.87 38.01 65.324c-9.6-5.548-12.88-17.757-7.351-27.341 5.528-9.585 17.732-12.846 27.332-7.298 9.6 5.547 12.88 17.757 7.351 27.34z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--sync{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath style='marker:none' overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath d='M84.93 14.958L67.965 31.934a177.473 177.473 0 0 0 11.842 3.925A211.649 211.649 0 0 0 92 39c-.936-3.985-1.999-8.035-3.187-12.147a206.82 206.82 0 0 0-3.882-11.894zM11.035 81L28 64.024A177.472 177.472 0 0 0 16.158 60.1a211.647 211.647 0 0 0-12.193-3.141c.936 3.985 1.999 8.035 3.187 12.147 1.217 4.136 2.511 8.1 3.882 11.894z' style='marker:none' overflow='visible' fill='gray'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M48 8C25.932 8 8 25.932 8 48c0 .652.02 1.3.05 1.945 1.356.336 2.717.686 4.083 1.053A36.54 36.54 0 0 1 12 48c0-19.906 16.094-36 36-36 13.005 0 24.38 6.872 30.705 17.186l2.86-2.862C74.444 15.31 62.076 8 48 8zm35.863 36.969c.084 1 .137 2.009.137 3.031 0 19.906-16.094 36-36 36-13.025 0-24.416-6.892-30.736-17.232l-2.88 2.88C21.512 80.682 33.908 88 48 88c22.068 0 40-17.932 40-40a2 2 0 0 0-.041-.406 40.381 40.381 0 0 0-.057-1.584 211.753 211.753 0 0 1-4.039-1.041z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--system-shutdown{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath style='marker:none' overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath d='M46 6l4-1v35h-4z' style='marker:none' overflow='visible' fill='gray'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M30.006 16.798c-14.099 8.144-20.983 24.771-16.77 40.504C17.45 73.034 31.72 83.989 48 83.989s30.55-10.955 34.763-26.687c4.214-15.733-2.669-32.36-16.767-40.504l-2.002 3.463C76.54 27.509 82.65 42.263 78.9 56.266 75.15 70.27 62.488 79.99 48 79.99S20.85 70.27 17.1 56.266c-3.75-14.003 2.357-28.757 14.905-36.005l-2-3.463z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--tags{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 15.999999'%3E%3Cpath opacity='.212' fill='none' d='M0 0h16v16H0z'/%3E%3Cpath style='marker:none' d='M8.691 0L.775 7.918c-1.218 1.218-.913 1.522.305 2.74l2.13 2.131 2.132 2.13c1.218 1.219 1.522 1.524 2.74.306L16 7.309V.914c0-.304-.076-.533-.228-.686C15.619.076 15.39 0 15.086 0zm.418 1.008h5.883V6.89l-7.525 7.523c-.795.839-1.472-.388-2.074-.87a46.125 46.125 0 0 0-1.47-1.468 45.917 45.917 0 0 0-1.468-1.469c-.481-.602-1.708-1.28-.87-2.074L9.11 1.008z' color='%23000' overflow='visible' fill='gray'/%3E%3Cpath style='marker:none' d='M12.518 2.25a1.25 1.25 0 0 1 .867.365 1.25 1.25 0 0 1 0 1.77 1.25 1.25 0 0 1-1.77 0 1.25 1.25 0 0 1 0-1.77 1.25 1.25 0 0 1 .903-.365z' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--logical-volume{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333398 4.2333403'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath style='marker:none' d='M3.175 2.381v.794h-.794v.265h.794v.793h.265V3.44h.793v-.265H3.44v-.794zM2.381 0v1.852h1.852V0zm.265.265h1.323v1.323H2.646zM0 0v1.852h1.852V0zm.265.265h1.323v1.323H.264zM0 2.381v1.852h1.852V2.381zm.265.265h1.323v1.323H.264z' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--pending{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3Epending%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='smoke-testing-status' transform='translate%28-62.000000, -112.000000%29'%3E%3Cg id='pending' transform='translate%2850.000000, 97.000000%29'%3E%3Cg transform='translate%2812.000000, 15.000000%29'%3E%3Crect id='rect4970' x='0' y='1.99999999e-05' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle4972' stroke='%23F99B11' stroke-width='1.5' cx='8' cy='8.00002' r='7.2500086'%3E%3C/circle%3E%3Crect id='rect4980' fill='%23F99B11' fill-rule='nonzero' x='7' y='7.00002' width='2' height='2'%3E%3C/rect%3E%3Crect id='rect4982' fill='%23F99B11' fill-rule='nonzero' x='10' y='7.00002' width='2' height='2'%3E%3C/rect%3E%3Crect id='rect4984' fill='%23F99B11' fill-rule='nonzero' x='4' y='7.00002' width='2' height='2'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--running{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3Erunning%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='smoke-testing-status' transform='translate%28-62.000000, -159.000000%29'%3E%3Cg id='running' transform='translate%2850.000000, 144.000000%29'%3E%3Cg transform='translate%2812.000000, 15.000000%29'%3E%3Crect id='rect6425' x='9.99999997e-06' y='9.99999989e-06' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle6427' stroke='%230E8420' stroke-width='1.5' fill='%230E8420' fill-rule='nonzero' cx='8.00001' cy='8.00001' r='7.2500086'%3E%3C/circle%3E%3Cpolygon id='path6429' fill='%23FFFFFF' fill-rule='nonzero' points='6.00002 12.00001 6.00002 4.00001 12.00002 8.00001'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--timed-out{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='16px' width='16px' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 16 16'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3Etimed out%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cg id='Page-1' fill-rule='evenodd' fill='none'%3E%3Cg id='smoke-testing-status' transform='translate%28-62 -206%29'%3E%3Cg id='timed-out' transform='translate%2850 191%29'%3E%3Cg transform='translate%2812 15%29'%3E%3Crect id='rect4970' y='0.00002' x='0' height='16' width='16'/%3E%3Ccircle id='circle4972' stroke-width='1.5' cy='8' stroke='%23E95420' cx='8' r='7.25'/%3E%3Cpolyline id='path839' stroke='%23E95420' stroke-width='2' points='11.8 11.8 7.9999 8 7.9999 3'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");background-size:100% 100%}.p-icon--power-error{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%23C7162B%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-icon--power-on{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%230E8420%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-icon--power-off{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-icon--power-unknown{background-image:url("data:image/svg+xml,%0A%3Csvg width='7px' height='12px' viewBox='0 0 7 12' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 50.2 %2855047%29 - http://www.bohemiancoding.com/sketch --%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='prototype--tables' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='207-group-selection' transform='translate%28-333.000000, -143.000000%29'%3E%3Cg id='Group' transform='translate%28333.000000, 141.000000%29'%3E%3Crect id='Rectangle' x='0' y='0' width='16' height='16'%3E%3C/rect%3E%3Cpath d='M2.79224377,3.71191136 C2.40443019,3.71191136 2.03324277,3.7590023 1.67867036,3.8531856 C1.32409795,3.94736889 0.952910526,4.09972194 0.565096953,4.31024931 L0,2.76454294 C0.409974349,2.53185479 0.878113712,2.34626108 1.40443213,2.20775623 C1.93075055,2.06925139 2.47091136,2 3.02493075,2 C3.68975402,2 4.23822499,2.09141183 4.67036011,2.27423823 C5.10249524,2.45706463 5.44598211,2.68697922 5.70083102,2.96398892 C5.95567994,3.24099861 6.13296349,3.54570471 6.23268698,3.87811634 C6.33241047,4.21052798 6.38227147,4.5318544 6.38227147,4.84210526 C6.38227147,5.21883845 6.31302008,5.55678521 6.17451524,5.85595568 C6.03601039,6.15512615 5.8614969,6.43213169 5.65096953,6.68698061 C5.44044216,6.94182953 5.21329762,7.18282435 4.96952909,7.4099723 C4.72576055,7.63712025 4.49861601,7.8698049 4.28808864,8.10803324 C4.07756127,8.34626158 3.90304778,8.59833662 3.76454294,8.86426593 C3.62603809,9.13019524 3.5567867,9.42936122 3.5567867,9.76177285 L3.5567867,9.95290859 C3.5567867,10.0249311 3.56232681,10.0941825 3.5734072,10.1606648 L1.84487535,10.1606648 C1.82271457,10.0498609 1.80609424,9.93074856 1.79501385,9.8033241 C1.78393346,9.67589964 1.77839335,9.55678726 1.77839335,9.44598338 C1.77839335,9.08033058 1.83933457,8.75346404 1.96121884,8.46537396 C2.0831031,8.17728388 2.2382262,7.91135856 2.4265928,7.66759003 C2.61495939,7.4238215 2.81717343,7.19667695 3.033241,6.98614958 C3.24930856,6.77562222 3.4515226,6.56509801 3.6398892,6.35457064 C3.82825579,6.14404327 3.98337889,5.92797895 4.10526316,5.70637119 C4.22714742,5.48476343 4.28808864,5.24099856 4.28808864,4.97506925 C4.28808864,4.60941645 4.16343615,4.30748042 3.91412742,4.06925208 C3.6648187,3.83102374 3.29086122,3.71191136 2.79224377,3.71191136 Z M4.08864266,12.6869806 C4.08864266,13.0747942 3.96122011,13.3905805 3.70637119,13.634349 C3.45152227,13.8781176 3.13573596,14 2.75900277,14 C2.39334997,14 2.08033371,13.8781176 1.8199446,13.634349 C1.55955548,13.3905805 1.42936288,13.0747942 1.42936288,12.6869806 C1.42936288,12.299167 1.55955548,11.9806107 1.8199446,11.7313019 C2.08033371,11.4819932 2.39334997,11.3573407 2.75900277,11.3573407 C3.13573596,11.3573407 3.45152227,11.4819932 3.70637119,11.7313019 C3.96122011,11.9806107 4.08864266,12.299167 4.08864266,12.6869806 Z' id='%3F' fill='%23CDCDCD'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}[class^="p-icon"]{margin-right:.25em}[class^="p-icon"].on-right{margin-left:.25em;margin-right:0}.p-icon--lock{background-image:url("https://assets.ubuntu.com/v1/5d2e0e21-padlock.svg");background-size:1rem 1rem;display:block;position:relative;margin-right:1rem;top:0 !important}.p-icon--lock.is-open{background-image:url("https://assets.ubuntu.com/v1/a6c61cd6-padlock_open.svg")}.p-icon--x{background-size:1rem 1rem;display:block;position:relative}.p-icon--tick{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='22px' height='16px' viewBox='0 0 22 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='confirm-tick' transform='translate(-1.000000, -1.000000)'%3E%3Cpolygon id='Shape' points='0 0 24 0 24 24 0 24'%3E%3C/polygon%3E%3Cpolygon id='Shape' fill-opacity='0.999998987' fill='%23666666' fill-rule='nonzero' points='3.872 6.93333333 1.6 9.20533333 9.33333333 16.9386667 22.4 3.872 20.128 1.6 9.33333333 12.3973333'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:1rem 1rem;display:block;position:relative}[class^="p-icon"]{height:1em;width:1em}.p-dropdown.active .p-navigation__toggle--open{display:none}@media (max-width: 870px){.p-dropdown.active .p-navigation__toggle--close{display:inline-block}}.p-dropdown.active .p-navigation__nav{display:block}.p-navigation{border-bottom:1px solid #333}@media (min-width: 870px){.p-navigation{border-bottom:0}}@media (max-width: 870px){.p-navigation__banner{overflow:hidden;position:relative}}.p-navigation .p-navigation__links,.p-navigation .p-navigation__links--right{z-index:6}@media (max-width: 870px){.p-navigation .p-navigation__links,.p-navigation .p-navigation__links--right{border-bottom:1px solid #333}}.p-navigation .p-navigation__links:last-of-type,.p-navigation .p-navigation__links--right:last-of-type{border-right-color:#333}@media (min-width: 870px){.p-navigation .p-navigation__links--right{position:absolute;right:0}}.p-navigation .p-navigation__links .p-navigation__link,.p-navigation .p-navigation__links--right .p-navigation__link{border-color:#333}@media (min-width: 870px){.p-navigation .p-navigation__links .p-navigation__link:hover,.p-navigation .p-navigation__links--right .p-navigation__link:hover{background-color:#000}}@media (min-width: 870px){.p-navigation .p-navigation__links .p-navigation__link.is-selected>a,.p-navigation .p-navigation__links--right .p-navigation__link.is-selected>a{border-bottom-color:#e95420}}@media (max-width: 870px){.p-navigation .p-navigation__links .p-navigation__link.is-selected>a,.p-navigation .p-navigation__links--right .p-navigation__link.is-selected>a{border-bottom:0}}.p-navigation .p-navigation__links .p-navigation__link:first-child,.p-navigation .p-navigation__links--right .p-navigation__link:first-child{border-top:0}@media (min-width: 870px){.p-navigation .p-navigation__links .p-navigation__link a:hover,.p-navigation .p-navigation__links--right .p-navigation__link a:hover{background-color:#333}.p-navigation .p-navigation__links .p-navigation__link a.active,.p-navigation .p-navigation__links--right .p-navigation__link a.active{box-shadow:inset 0 -3px #e95420}}.p-dropdown{height:3rem}.p-dropdown__toggle{background-color:#000}.p-dropdown .p-icon--chevron{margin-bottom:-2px;margin-left:10px}.p-dropdown .active{background-color:#000}.p-dropdown .active .p-icon--chevron{transform:rotate(180deg)}.p-navigation .p-navigation__links .p-dropdown__menu,.p-navigation .p-navigation__links--right .p-dropdown__menu{background-color:#000;margin:0;padding:0}.p-navigation .p-navigation__links .p-dropdown__menu .p-navigation__link,.p-navigation .p-navigation__links--right .p-dropdown__menu .p-navigation__link{border-left:0;width:100%;float:none}@media (min-width: 1031px){.u-hide-nav-viewport--large{display:none !important}}@media (max-width: 1030px) and (min-width: 871px){.u-hide-nav-viewport--medium{display:none !important}}@media (max-width: 870px){.u-hide-nav-viewport--small{display:none !important}}.page-header{padding:1.5rem 0;position:sticky;top:0;z-index:5}.page-header__title{display:inline-block}.page-header__status{color:#666;display:inline;margin-left:1rem;position:relative;width:auto}.page-header__status [class^="p-icon"]{margin-bottom:0.1rem}.page-header__controls{float:right}@media (max-width: 620px){.page-header__controls{float:none}}.page-header.is-not-sticky{position:absolute}.page-header__controls--discoveries button:last-child{margin-left:1.5rem}.page-header__controls--discoveries .maas-p-switch{position:relative;top:.4rem}@media screen and (max-width: 986px){.p-tabs.p-tabs--machine-details::before{display:block}}.p-tabs::before{display:none;background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, #fff 100%);background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, #fff 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, #fff 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#ffffff',GradientType=1 );padding-left:1.5rem;padding-right:0;right:1.5rem;width:2.05rem;z-index:1}.p-tabs__list{margin-bottom:0;font-size:0;overflow-x:auto}.p-tabs__list::after{content:none}.p-tabs__item{float:none;font-size:16px}.maas-p-switch{height:1.5rem;margin:0 auto;position:relative;width:3rem;display:inline-block}.maas-p-switch--input{cursor:pointer;height:100% !important;left:0;position:absolute;top:0;width:100%}.maas-p-switch--mask{background:#f7f7f7;height:100%;line-height:1.5rem;margin-top:0;pointer-events:none;position:relative}.maas-p-switch--mask::before{transition-duration:0.333s;transition-property:background-color;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:#cdcdcd;content:'';display:block;height:100%;text-align:right;width:100%;padding:0 .3rem 0 .45rem;box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);border-radius:3px;font-size:.875rem}.maas-p-switch--mask::after{transition-duration:0.333s;transition-property:left;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);width:50%;display:block;position:absolute;top:0;left:0;height:100%;background:#fff;content:' ';box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);border-radius:3px}.maas-p-switch--input:checked+.maas-p-switch--mask::before{text-align:left;content:'';color:#fff;background:#335280}.maas-p-switch--input:checked+.maas-p-switch--mask::after{left:50%}.maas-p-switch--input:disabled{cursor:not-allowed}.maas-p-switch--input:disabled+.maas-p-switch--mask::before{background:#f3f3f3;color:#999}.maas-p-switch--input:disabled:checked+.maas-p-switch--mask::before{background:#809fcc;color:#fff}.p-table-expanding .p-table-expanding__panel,.p-table-expanding .p-table-expanding__panel--bordered{background-color:#fff;margin-bottom:0;margin-left:0 !important;padding:.5rem;width:100%;white-space:wrap}.p-table-expanding .p-table-expanding__panel.is-active,.p-table-expanding .is-active.p-table-expanding__panel--bordered{flex-grow:1}.p-table-expanding .p-table-expanding__panel--bordered{border-top:1px solid #cdcdcd}.p-table-expanding td{display:table-cell}.p-table-expanding td[colspan='2']{flex:2}.p-table-expanding tr{display:table-row}.p-table-expanding tr .is-active{background-color:#fff}.p-table-expanding>thead>tr,.p-table-expanding>tbody>tr{display:flex}.p-table-expanding .tags-input .tags .input{margin-bottom:0}.p-card--highlighted.is-error{border-top:3px solid #c7162b}div[class*='p-card--'],.p-card{padding:1rem}.p-card__content{margin-top:0}h2+.p-card__content,.p-heading--2+.p-card__content{margin-top:-1rem}.p-card__content .muted-label{color:#666}.p-card__header{border-bottom:0}.p-meter__container,.p-meter--cpu-cores{padding-top:.2rem;margin-bottom:-.2rem}.p-meter{border-radius:.5rem;display:block;height:1rem;position:relative;overflow:hidden;width:100%;-moz-appearance:none;-webkit-appearance:none;background:none;background-color:rgba(0,122,166,0.2)}.p-meter__container{position:relative}.p-meter.is-over{background-color:#f99b11}.p-meter.is-over::-webkit-meter-bar{background-color:#f99b11}.p-meter.is-over::-webkit-meter-optimum-value{background-color:#f99b11}.p-meter.is-over::-moz-meter-bar{background-color:#f99b11}.p-meter::-webkit-meter-inner-element{display:block}.p-meter::-webkit-meter-bar{background:rgba(0,122,166,0.2)}.p-meter::-webkit-meter-optimum-value{background:#007aa6}.p-meter::-moz-meter-bar{background:#007aa6}.p-meter--kvm::-webkit-meter-bar{background-color:transparent}.p-meter--kvm::-webkit-meter-optimum-value{background-color:transparent}.p-meter--kvm::-moz-meter-bar{background:#007aa6}.p-meter--cpu-cores__container{border-radius:.5rem;display:flex;position:relative;overflow:hidden;width:100%}.p-meter--cpu-cores__container.is-over span{background-color:#f99b11}.p-meter--cpu-cores .p-meter--cpu-cores__core--used,.p-meter--cpu-cores .p-meter--cpu-cores__core--available{border-right:1px solid #fff;display:block;float:left;height:1rem;position:relative;flex:1}.p-meter--cpu-cores__core--used{background:#007aa6}.p-meter--cpu-cores__core--available{background:rgba(0,122,166,0.2)}.p-meter__graph{position:absolute;top:0;width:100%}.p-meter__graph-content{background-image:linear-gradient(90deg, #007aa6 0%, #007aa6 100%, transparent 100%, transparent 100%);background-size:100% 100%;display:block;text-indent:-9999px;height:inherit}.p-legend{list-style:none;margin-bottom:0;padding:0}.p-legend--numbers{margin-left:0;padding-left:0}.p-legend::after{content:unset}.p-legend__item{display:block;float:left}.p-legend__item:not(:first-child){margin-left:1rem;float:right}.p-legend__item::before{content:"•";padding-top:.4rem;float:left;font-size:2rem;line-height:1.5rem;display:inline-block;margin-right:.5rem;width:.5rem}.p-legend__item--requests::before{color:#007aa6}.p-legend__item--used::before{color:#007aa6}.p-legend__item--free::before{color:rgba(0,122,166,0.2)}.p-legend__text{float:left;margin-bottom:.6rem}.p-strip{padding:1rem 0}@media only screen and (min-width: 1030px){.p-strip{padding:1.5rem 0}}.p-strip--light{padding:1rem 0;background-color:#fff}@media only screen and (min-width: 1030px){.p-strip--light{padding:1.5rem 0}}.p-strip--dark{padding:1rem 0}@media only screen and (min-width: 1030px){.p-strip--dark{padding:1.5rem 0}}.p-strip.is-shallow,.p-strip--light.is-shallow,.p-strip--dark.is-shallow{padding:.5rem 0}@media only screen and (min-width: 1030px){.p-strip.is-shallow,.p-strip--light.is-shallow,.p-strip--dark.is-shallow{padding:1rem 0}}tags-input{display:block}tags-input .autocomplete{margin-top:-1px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border-radius:0 0 3px 3px;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);max-height:300px;transition:max-height .3s ease-in;overflow-y:scroll;overflow-x:visible}tags-input .autocomplete.no-suggestion{overflow-y:visible}tags-input .autocomplete .suggestion-list{margin:0;clear:both;min-width:160px;width:100%;box-sizing:border-box}tags-input .autocomplete .suggestion-list .suggestion-item{float:left;margin:0;padding:5px 10px;border-top:1px solid #d2d2d2;width:100%}tags-input .autocomplete .suggestion-list .suggestion-item:first-child{border-top:0}tags-input .autocomplete .suggestion-list .suggestion-item:hover{background-color:#f7f7f7}.autocomplete.no-suggestion .suggestion-list .suggestion-item{color:#666}.autocomplete.no-suggestion .suggestion-list .suggestion-item:hover{background-color:#fff}#tags{display:block}.tags--inline .host{position:relative;margin-bottom:5px;height:100%}.tags--inline .tags{border:1px solid #d2d2d2;border-radius:2px;-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;height:100%}.tags--inline .tags:focus,.tags--inline .tags.focused{border-color:#888;outline:none}.tags--inline .tags .tag-list{margin:0;padding:0;list-style-type:none}.tags--inline .tags .tag-list .tag-item{line-height:24px;margin:4px}.tags--inline .tags .input{border:0;outline:0;margin:2px;padding:0 0 0 5px;height:30px;box-shadow:none}.tags--inline .tags .input:placeholder{color:transparent}.tags{width:100%}.tags .tag-list{width:100%;margin:0;overflow:hidden}.tags .tag-list .tag-item{float:left;line-height:36px;margin-bottom:0;margin-top:0;margin-right:10px;word-wrap:break-word}.tags .tag-list .tag-item .remove-button{border-bottom:0}.tags .input{width:100% !important}.tag-item{display:inline-block;background-color:#f7f7f7;padding:0 5px}table button,table [class^="p-button"]{padding-bottom:.3375rem;padding-top:.3375rem}.p-button--narrow{padding-left:.75rem;padding-right:.75rem}.p-button--min-margin-bottom,.p-button--icon{margin-bottom:.1rem}.p-button--base.is-small{padding:.5rem;margin:0}.p-button--close{padding:.3375rem .5rem}.p-button--lock{margin-left:-.5rem}.p-button--icon{padding:.5rem .5rem}input+.p-button--icon{margin-left:1rem}.p-button--close{align-self:flex-start;border:0;float:right;margin:0 0 auto auto;width:auto}.p-button--close [class^="p-icon"]{margin-right:0}[class*='p-button'] [class^="p-icon"],button [class^="p-icon"]{margin-right:0}.p-table-expanding__panel *[class*='p-button'],.p-table-expanding .p-table-expanding__panel--bordered *[class*='p-button']{margin-bottom:.2rem}.p-table-expanding__panel *[class*='p-button']:not(.p-button--close),.p-table-expanding .p-table-expanding__panel--bordered *[class*='p-button']:not(.p-button--close){padding:.3375rem 1rem}*[class*='p-button'].is-small{padding:.3375rem 1rem}.p-cta__toggle::after{background-position-y:center;background-repeat:no-repeat;background-size:1rem;content:'';height:1rem;position:absolute;width:1rem}.p-cta__toggle{margin-bottom:.25rem;padding-right:3rem}.p-cta__toggle::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E");right:1rem;top:.675rem}.p-cta__toggle.p-button--positive::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23fff' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-cta__toggle.is-selected::after{transform:rotate(180deg)}.p-cta__dropdown{min-width:100%;top:100%;width:auto;z-index:2}.p-cta__group+.p-cta__group .p-cta__link:first-child{border-top:1px solid #cdcdcd}.p-cta__link{display:flex;justify-content:space-between;padding:.25rem 1rem;transition-duration:0s}.p-cta__link.is-unavailable{opacity:.5;cursor:not-allowed}.p-cta__count{padding-left:1rem}.p-form--stacked .p-form__group{align-items:flex-start}.p-form--stacked .p-form__label,.p-form--stacked .p-form__control{flex:0 0 auto;max-width:none}.p-form--stacked .p-form__control>.p-control-text{display:block}.p-form--stacked .p-form__control--placeholder{display:block;margin-bottom:.7rem;min-height:2.3rem;padding-bottom:.3375rem;padding-top:.3375rem}.p-form--inline .p-form__group .p-form__label{flex-shrink:1}.p-form--inline,.p-form--inline .p-form__group{width:100%}.p-form--stacked .p-form__group+.p-form__group{margin-top:0}.form__group-input input.in-warning{border-color:#f99b11 !important;padding-right:2rem}.p-form__label{color:#111}.p-form__label.is-disabled{color:#666}maas-obj-form[disabled="disabled"] .p-form__label{color:#666}.u-hr--fixed-width{position:relative}.u-hr--fixed-width::after{left:1.5rem;margin:0 auto;max-width:87rem;right:1.5rem}.p-notification__response{max-width:none}.p-notification--group>[class^="p-notification"]{flex-direction:column}.p-notification--group .p-list--divided{margin-top:.5rem;border-top:1px dotted #cdcdcd}.p-input--overcommit{float:left;width:3rem;max-width:3rem !important;min-width:3rem}.p-pod-summary{display:flex}@media (max-width: 1030px){.p-pod-summary{flex-direction:column}}@media (min-width: 1030px){.p-pod-summary{flex-direction:row;flex-wrap:wrap}}.p-pod-summary__cpu,.p-pod-summary__ram,.p-pod-summary__aside,.p-pod-summary__storage{flex:0 0 auto}@media (min-width: 1030px){.p-pod-summary__cpu,.p-pod-summary__ram{margin-bottom:1rem}}.p-pod-summary__ram{position:relative}@media (min-width: 620px) and (max-width: 1030px){.p-pod-summary__ram::after{content:unset}}.p-pod-summary__storage{position:relative}@media (max-width: 1030px){.p-pod-summary__storage{width:100%;margin-left:0}}@media (min-width: 1030px){.p-pod-summary__storage::after{content:unset}.p-pod-summary__storage::before{background-color:#cdcdcd;content:'';height:100%;left:-2.73045%;position:absolute;width:1px}}@media (max-width: 1029px){.p-pod-summary__aside{width:100%}}@media (min-width: 1030px){.p-pod-summary__cpu,.p-pod-summary__ram{width:100%;margin-left:0}}.p-storage{margin-bottom:.0625rem;padding-top:.5rem}@media (max-width: 1029px){.p-storage::after{background:transparent}}.p-storage__name{margin-top:-.5rem}@media (max-width: 1030px){.p-storage__meter{float:left}}@media (max-width: 620px){.p-storage__meter{width:100%}}.p-storage__disk-name{float:left;margin-bottom:.1rem;margin-right:-5rem;width:calc(100% - 5rem)}.p-storage__info{float:right;text-align:right;width:5rem}.p-storage__path{display:block;color:#666;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.faded{opacity:.5}.p-overcommit-switch{float:left;width:100%}.p-pod-edit{align-items:flex-start;display:flex;justify-content:space-between}@media (max-width: 620px){.p-pod-edit__label{display:none}}.p-pod-edit__label,.p-pod-edit .p-button{flex:0 0 auto}.p-pod-edit *+*{margin-left:1rem}.p-pod-edit .p-code-snippet-wrapper{flex:1 0 auto}@media (max-width: 620px){.p-pod-edit .p-code-snippet-wrapper{margin-left:0}}.p-search-box{width:85%;box-shadow:none}.p-search-box.u-min-margin--bottom{margin-bottom:.2rem}.p-footer{margin-top:auto}.p-footer__logo{margin:0.5rem 0;height:1rem}html,body{height:100%}.has-sticky-footer{display:flex;flex-direction:column}.p-filter .p-filter__dropdown-button::after,.p-filter .p-button--base.is-active::after{background-position-y:center;background-repeat:no-repeat;background-size:1rem;content:'';height:1rem;position:absolute;width:1rem}.p-filter .p-filter__dropdown-button,.p-filter .p-button--base{margin:0;text-align:left;width:100%}.p-filter{position:relative}.p-filter .p-filter__dropdown{position:absolute;top:0;z-index:2}.p-filter .p-filter__dropdown-button::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E");right:1rem;top:.675rem}.p-filter .p-filter__dropdown-button.is-selected+.p-accordion{display:block}.p-filter .p-filter__dropdown-button.is-selected::after{transform:rotate(180deg)}.p-filter .p-accordion{border-top:0;display:none;max-height:66vh;overflow-y:auto}.p-filter .p-accordion__list{margin-bottom:0}.p-filter .p-accordion__tab{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E");background-position:right 1rem center;background-size:.75rem;border-bottom:0 !important;padding:.5rem 1rem}.p-filter .p-accordion__tab:focus{outline:1px solid #19b6ee;outline-offset:2px}.p-filter .p-accordion__tab.is-selected{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-filter .p-accordion__tab.is-selected+.p-accordion__panel{display:block}.p-filter .p-accordion__panel{border:0;display:none;padding:0}.p-filter .p-list{margin:0}.p-filter .p-button--base{padding:0 2.5rem;position:relative;transition-duration:0s}.p-filter .p-button--base.is-active{font-weight:400}.p-filter .p-button--base.is-active::after{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='22px' height='16px' viewBox='0 0 22 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='confirm-tick' transform='translate(-1.000000, -1.000000)'%3E%3Cpolygon id='Shape' points='0 0 24 0 24 24 0 24'%3E%3C/polygon%3E%3Cpolygon id='Shape' fill-opacity='0.999998987' fill='%23666666' fill-rule='nonzero' points='3.872 6.93333333 1.6 9.20533333 9.33333333 16.9386667 22.4 3.872 20.128 1.6 9.33333333 12.3973333'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E");left:1rem;top:.25rem}.p-table--machines tbody .p-table__row:hover{background-color:#fff;box-shadow:0 1px 3px 0 rgba(17,17,17,0.2)}.p-table--machines tbody .p-table__row:hover .p-table-menu__toggle{display:inline-block}.p-table__row--muted{background:#f7f7f7}.p-table--network-discovery tr{justify-content:space-between}.p-table--network-discovery th,.p-table--network-discovery td{flex:0 0 auto}.p-table--network-discovery__name{width:15%}.p-table--network-discovery__mac{width:20%}.p-table--network-discovery__ip{width:25%}.p-table--network-discovery__rack{width:15%}.p-table--network-discovery__last-seen{width:calc(25% - 50px)}.p-table--network-discovery__chevron{flex:0 0 auto;width:50px}.p-table--pods .p-table__row .p-table__cell:nth-child(1){width:18%}.p-table--pods .p-table__row .p-table__cell:nth-child(2){width:9%}.p-table--pods .p-table__row .p-table__cell:nth-child(3){width:14%}.p-table--pods .p-table__row .p-table__cell:nth-child(4){width:14%}.p-table--pods .p-table__row .p-table__cell:nth-child(5){width:10%}.p-table--pods .p-table__row .p-table__cell:nth-child(6){width:14%}.p-table--pods .p-table__row .p-table__cell:nth-child(7){width:15%}.p-table--pod-networking-config input,.p-table--pod-storage-config input{min-width:auto}@media (min-width: 620px){.p-table--pod-networking-config{margin-bottom:0}.p-table--pod-networking-config .p-table__row th:nth-child(1),.p-table--pod-networking-config .p-table__row td:nth-child(1){width:3.125rem}.p-table--pod-networking-config .p-table__row th:nth-child(2),.p-table--pod-networking-config .p-table__row td:nth-child(2){width:10%}.p-table--pod-networking-config .p-table__row th:nth-child(3),.p-table--pod-networking-config .p-table__row td:nth-child(3){width:25%}.p-table--pod-networking-config .p-table__row th:nth-child(4),.p-table--pod-networking-config .p-table__row td:nth-child(4){width:15%}.p-table--pod-networking-config .p-table__row th:nth-child(5),.p-table--pod-networking-config .p-table__row td:nth-child(5){width:23%}.p-table--pod-networking-config .p-table__row th:nth-child(6),.p-table--pod-networking-config .p-table__row td:nth-child(6){width:10%}.p-table--pod-networking-config .p-table__row th:nth-child(7),.p-table--pod-networking-config .p-table__row td:nth-child(7){width:12%}.p-table--pod-networking-config .p-table__row th:nth-child(8),.p-table--pod-networking-config .p-table__row td:nth-child(8){width:5%}}@media (min-width: 620px){.p-table--pod-storage-config{margin-bottom:0}.p-table--pod-storage-config .p-table__row th:nth-child(1),.p-table--pod-storage-config .p-table__row td:nth-child(1){width:3.125rem}.p-table--pod-storage-config .p-table__row th:nth-child(2),.p-table--pod-storage-config .p-table__row td:nth-child(2){width:10%}.p-table--pod-storage-config .p-table__row th:nth-child(3),.p-table--pod-storage-config .p-table__row td:nth-child(3){width:40%}.p-table--pod-storage-config .p-table__row th:nth-child(4),.p-table--pod-storage-config .p-table__row td:nth-child(4){width:40%}.p-table--pod-storage-config .p-table__row th:nth-child(5),.p-table--pod-storage-config .p-table__row td:nth-child(5){width:10%}}.p-table--pod-networking-config--message{margin-left:35%}.p-table--devices .p-table__row .p-table__cell:nth-child(1){width:33%}.p-table--devices .p-table__row .p-table__cell:nth-child(2){width:17%}.p-table--devices .p-table__row .p-table__cell:nth-child(3){width:15%}@media (max-width: 1000px){.p-table--devices .p-table__row .p-table__cell:nth-child(3){display:none !important}}.p-table--devices .p-table__row .p-table__cell:nth-child(4){width:20%}.p-table--devices .p-table__row .p-table__cell:nth-child(5){width:15%}.p-table--controllers .p-table__row .p-table__cell:nth-child(1){width:30%}.p-table--controllers .p-table__row .p-table__cell:nth-child(2){width:10%}.p-table--controllers .p-table__row .p-table__cell:nth-child(3){width:20%}.p-table--controllers .p-table__row .p-table__cell:nth-child(4){width:15%}.p-table--controllers .p-table__row .p-table__cell:nth-child(5){width:20%}.p-table--controllers .p-table__row .p-table__cell:nth-child(6){width:15%}.p-table--images .p-table__row .p-table__cell:nth-child(1){width:20%}.p-table--images .p-table__row .p-table__cell:nth-child(2){width:15%}.p-table--images .p-table__row .p-table__cell:nth-child(3){width:15%}.p-table--images .p-table__row .p-table__cell:nth-child(4){width:35%}.p-table--images .p-table__row .p-table__cell:nth-child(5){width:15%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(1){width:15%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(2){width:15%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(3){width:7%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(4){width:9%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(5){width:12%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(6){width:10%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(7){width:12%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(8){width:10%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(9){width:10%}.p-table--machines{margin:0;position:relative}.p-table--machines .p-table__header{border-bottom:1px solid #cdcdcd}.p-table--machines .p-table__group{border:0;position:relative}.p-table--machines .p-table__group .p-table__group-label{color:#111;font-size:1rem;padding:.5rem 0 .5rem .5rem;text-transform:none}.p-table--machines .p-table__group .p-table__group-toggle{padding:0 .25rem;position:absolute;right:.75rem;top:1rem}.p-table--machines .p-table__group.is-open{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-table--machines .p-table__row{position:relative}.p-table--machines .p-table__row::after{content:''}.p-table--machines .p-table__row.is-grouped{border:0}.p-table--machines .p-table__row.is-grouped::after{position:absolute;left:2.5rem;right:0;height:.0625rem;background-color:#e5e5e5}.p-table--machines .p-table__row.is-grouped td:first-child{padding-left:2.5rem}.p-table--machines .p-table__row td{vertical-align:top}.p-table--machines .p-table__row .p-table__col--name{position:relative}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--name{width:46%}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--name{width:30%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--name{width:22%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--name{width:20%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--name{width:17%}}.p-table--machines .p-table__row .p-table__col--name .p-tooltip{position:static}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--power{width:8%}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--power{width:8%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--power{width:4%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--power{width:10%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--power{width:9%}}.p-table--machines .p-table__row .p-table__col--status{position:relative}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--status{width:46%}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--status{width:44%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--status{width:22%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--status{width:22%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--status{width:18%}}.p-table--machines .p-table__row .p-table__col--status .p-tooltip{position:static}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--owner{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--owner{width:18%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--owner{width:8%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--owner{width:10%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--owner{width:9%}}.p-table--machines .p-table__row .p-table__col--pool{overflow:visible}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--pool{width:7%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--zone{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--zone{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--zone{display:none !important}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--zone{width:10%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--zone{width:9%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--fabric{width:8%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--cores{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--cores{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--cores{width:10%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--cores{width:6%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--cores{width:5%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--ram{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--ram{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--ram{width:12%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--ram{width:8%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--ram{width:7%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--disks{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--disks{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--disks{width:10%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--disks{width:6%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--disks{width:5%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--storage{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--storage{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--storage{width:10%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--storage{width:8%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--storage{width:6%}}.p-table--machines .p-table__placeholder *{height:0 !important;padding:0 !important;visibility:hidden}.p-table--machines .p-icon--placeholder{height:1rem;margin-right:.5rem;width:1rem}.p-table--machines .p-tooltip__message--latest-event{max-width:500px;white-space:inherit}.p-table--machines:last-of-type::after{display:none}.p-table--controller-interfaces .p-table--is-device th:nth-child(1),.p-table--controller-interfaces .p-table--is-device td:nth-child(1){width:30%}.p-table--controller-interfaces .p-table--is-device th:nth-child(2),.p-table--controller-interfaces .p-table--is-device td:nth-child(2){width:25%}.p-table--controller-interfaces .p-table--is-device th:nth-child(3),.p-table--controller-interfaces .p-table--is-device td:nth-child(3){width:25%}.p-table--controller-interfaces .p-table--is-device th:nth-child(4),.p-table--controller-interfaces .p-table--is-device td:nth-child(4){width:15%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(1),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(1){width:20%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(2),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(2){width:6%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(3),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(3){width:10%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(4),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(4){width:14%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(5),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(5){width:16%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(6),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(6){width:28%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(7),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(7){width:6%}.p-table--controllers-commissioning .p-table__row th:nth-child(1),.p-table--controllers-commissioning .p-table__row td:nth-child(1){width:15%}.p-table--controllers-commissioning .p-table__row th:nth-child(2),.p-table--controllers-commissioning .p-table__row td:nth-child(2){width:15%}.p-table--controllers-commissioning .p-table__row th:nth-child(3),.p-table--controllers-commissioning .p-table__row td:nth-child(3){width:20%}.p-table--controllers-commissioning .p-table__row th:nth-child(4),.p-table--controllers-commissioning .p-table__row td:nth-child(4){width:20%}.p-table--controllers-commissioning .p-table__row th:nth-child(5),.p-table--controllers-commissioning .p-table__row td:nth-child(5){width:25%}.p-table--controllers-commissioning .p-table__row th:nth-child(6),.p-table--controllers-commissioning .p-table__row td:nth-child(6){width:5%}@media (min-width: 620px){.p-table--controller-vlans .p-table__row th:nth-child(1),.p-table--controller-vlans .p-table__row td:nth-child(1){width:15%}.p-table--controller-vlans .p-table__row th:nth-child(2),.p-table--controller-vlans .p-table__row td:nth-child(2){width:15%}.p-table--controller-vlans .p-table__row th:nth-child(3),.p-table--controller-vlans .p-table__row td:nth-child(3){width:10%}.p-table--controller-vlans .p-table__row th:nth-child(4),.p-table--controller-vlans .p-table__row td:nth-child(4){width:20%}.p-table--controller-vlans .p-table__row th:nth-child(5),.p-table--controller-vlans .p-table__row td:nth-child(5){width:20%}.p-table--controller-vlans .p-table__row th:nth-child(6),.p-table--controller-vlans .p-table__row td:nth-child(6){width:20%}}@media (max-width: 768px){.p-table--create-raid__name{width:50%}}@media (min-width: 768px){.p-table--create-raid__name{width:30%}}.p-table--create-raid__size{width:10%}.p-table--create-raid__type{width:20%}.p-table--create-raid__active{width:10%}.p-table--create-raid__spare{width:10%}@media (max-width: 768px){.p-table--create-volume-group__name{width:50%}}@media (min-width: 768px){.p-table--create-volume-group__name{width:30%}}.p-table--create-volume-group__size{width:30%}.p-table--create-volume-group__type{width:20%}.p-table--create-volume-group__empty{width:10%}@media (max-width: 768px){.p-table--bcache__name{width:50%}}@media (min-width: 768px){.p-table--bcache__name{width:30%}}.p-table--bcache__size{width:30%}.p-table--bcache__type{width:20%}.p-table--bcache__empty{width:10%}.p-double-row{overflow:visible}.p-double-row .p-double-row__checkbox,.p-double-row .p-double-row__icon-container{display:block;float:left;width:1rem}.p-double-row .p-double-row__main-row,.p-double-row .p-double-row__muted-row{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}.p-double-row .p-double-row__checkbox{margin-right:1rem}.p-double-row .p-double-row__icon-container{margin-right:.5rem}.p-double-row .p-double-row__rows-container--icon{float:left;width:calc(100% - 1.5rem)}.p-double-row .p-double-row__rows-container--checkbox{float:left;width:calc(100% - 2rem)}.p-double-row .p-double-row__muted-row{color:#666;margin-bottom:.2rem}.p-checkbox--action.actionable::before{background-color:#0e8420}.p-checkbox--action.not-actionable::before{background-color:#f99b11}.p-checkbox--action.actionable::after,.p-checkbox--action.not-actionable::after{color:#fff}.p-muted-text{color:#666;margin:0;padding:0}.p-table__group-label .p-muted-text{font-weight:300;padding-left:2rem}.p-link--muted:visited{color:#666}.p-link--muted:hover{color:#007aa6}.p-domain-name{display:inline-block}.p-domain-name .p-domain-name__host{font-weight:400}.p-domain-name .p-domain-name__tld{margin-bottom:.2rem}.p-table-menu{margin-bottom:-.5rem;width:100%}.p-table-menu .p-table-menu__link,.p-table-menu .p-table-menu__check-power,.p-table-menu .p-table-menu__power-on,.p-table-menu .p-table-menu__power-off{padding:.5rem 1rem;position:relative;transition:0s}.p-table-menu .p-table-menu__link::before,.p-table-menu .p-table-menu__check-power::before,.p-table-menu .p-table-menu__power-on::before,.p-table-menu .p-table-menu__power-off::before{background-position:center;background-repeat:no-repeat;background-size:1rem;content:'';height:17px;left:1rem;position:absolute;top:.75rem;width:1rem}.p-table-menu .p-table-menu__title,.p-table-menu .p-table-menu__title--icon{border-bottom:1px solid #e5e5e5;color:#666;font-size:.75rem;font-weight:400;padding:.25rem 1rem;text-transform:uppercase}.p-table-menu .p-table-menu__title--icon{padding-left:2.5rem}.p-table-menu .p-table-menu__footer{border-top:1px solid #e5e5e5;color:#666;padding:.25rem 2.5rem}.p-table-menu .p-table-menu__toggle{background-color:rgba(255,255,255,0.75);border:0;cursor:pointer;display:none;opacity:.25;position:absolute;right:0;top:2px}td:hover .p-table-menu .p-table-menu__toggle{opacity:1}.p-table-menu .p-table-menu__dropdown{left:-1rem;max-width:none;min-width:100%;top:2rem;width:-moz-max-content;width:max-content}.p-table-menu .p-table-menu__dropdown .p-contextual-menu__group{border-color:#e5e5e5}.p-table-menu .p-table-menu__check-power{padding-left:2.5rem;padding-right:2.5rem}.p-table-menu .p-table-menu__power-on{padding-left:2.5rem;padding-right:2.5rem}.p-table-menu .p-table-menu__power-on::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%230E8420%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-table-menu .p-table-menu__power-off{padding-left:2.5rem;padding-right:2.5rem}.p-table-menu .p-table-menu__power-off::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-table-menu .p-double-row__icon-container{cursor:pointer}@media (max-width: 599px){.u-hide--br1{display:none !important}}@media (max-width: 899px){.u-hide--br2{display:none !important}}@media (max-width: 1029px){.u-hide--br3{display:none !important}}@media (max-width: 1359px){.u-hide--br4{display:none !important}}.p-space-between{display:flex;flex-wrap:wrap;justify-content:space-between}@media (max-width: 620px){.p-space-between{flex-direction:column}}@media (max-width: 620px){.p-space-between .p-space-between__align-right{align-self:flex-end}}.p-chart{background-color:rgba(0,122,166,0.2);position:relative;overflow:hidden;width:100%;height:1rem;border-radius:1rem;background-color:rgba(0,122,166,0.2)}.p-chart__container{padding-top:.6rem;margin-bottom:-.1rem}.p-chart__bar,.p-chart__bar--used,.p-chart__bar--other,.p-chart__bar--requests{bottom:0;left:0;position:absolute;top:0}.p-chart__bar--used{background-color:#007aa6;border-right:1px solid #fff}.p-chart__bar--other{background-color:#1baf66;border-right:1px solid #fff}.p-chart__bar--requests{background-color:#0e8420;opacity:.15}.p-chart__bar--requests.is-selected{opacity:1}.p-chart__bar--requests.is-over{background-color:#f99b11}.is-over .p-chart__bar--requests{background-color:#f99b11}.p-key-list{display:flex;list-style:none;margin-left:0;padding-left:0;position:relative}.p-key-list__item--requests,.p-key-list__item--other-requests,.p-key-list__item--used,.p-key-list__item--free{display:flex;flex:1;padding-right:1rem}.p-key-list__item--requests::before,.p-key-list__item--other-requests::before,.p-key-list__item--used::before,.p-key-list__item--free::before{content:"•";float:left;font-size:2rem;line-height:1.5rem;margin-right:.5rem;padding-top:.4rem;width:.5rem}.p-key-list__item--requests:last-of-type,.p-key-list__item--other-requests:last-of-type,.p-key-list__item--used:last-of-type,.p-key-list__item--free:last-of-type{text-align:right;justify-content:flex-end}.p-key-list__item--requests::before{color:#0e8420}.p-key-list__item--other-requests{padding-right:0.25rem}.p-key-list__item--other-requests::before{color:#1aaf65}.p-key-list__item--used::before{color:#007aa6}.p-key-list__item--free::before{color:rgba(0,122,166,0.2)}.p-option-selector{position:relative}.p-option-selector__header{padding-top:.25rem}.p-option-selector__header .p-button--close{margin-left:.5rem;float:none}.p-option-selector__title{padding:.75rem 1rem 0}.p-option-selector__input{color:#111 !important;cursor:pointer}.p-option-selector__input.in-warning{border-color:#f99b11 !important}.p-option-selector [readonly][type="text"].p-option-selector__input{color:#111;border-color:#cdcdcd}@media (min-width: 768px){.p-option-selector .p-option-selector-subnets__options{width:70vw}}@media (min-width: 1030px){.p-option-selector .p-option-selector-subnets__options{width:650px}}.p-option-selector .p-option-selector__options-key{padding:0 1rem}.p-option-selector__option{cursor:pointer;display:flex;width:100%;position:relative}.p-option-selector__option:focus{outline:1px solid #19b6ee;outline-offset:2px}.p-option-selector__option:hover{background-color:#f7f7f7}.p-option-selector__option.is-over{opacity:.5;pointer-events:none;cursor:not-allowed}.p-option-selector__option.is-selected{background-color:#f7f7f7;background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='22px' height='16px' viewBox='0 0 22 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='confirm-tick' transform='translate(-1.000000, -1.000000)'%3E%3Cpolygon id='Shape' points='0 0 24 0 24 24 0 24'%3E%3C/polygon%3E%3Cpolygon id='Shape' fill-opacity='0.999998987' fill='%23666666' fill-rule='nonzero' points='3.872 6.93333333 1.6 9.20533333 9.33333333 16.9386667 22.4 3.872 20.128 1.6 9.33333333 12.3973333'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:1rem 1rem;background-repeat:no-repeat;background-position:1rem center}.p-option-selector__option+.p-option-selector__option::after{background-color:#e5e5e5}.p-option-selector__option+.p-option-selector__option::after{background-color:#e5e5e5}.p-table--pod-networking-config .p-option-selector__option+.p-option-selector__option::after{left:1rem;right:1rem}.p-table--pod-storage-config .p-option-selector__option+.p-option-selector__option::after{left:3rem;right:1rem}.p-table--pod-networking-config .p-option-selector__options{position:absolute;top:2.25rem;padding:1rem 0 0;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);background-color:#fff;z-index:10;width:100%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__options{width:70vw}}@media (min-width: 1030px){.p-table--pod-networking-config .p-option-selector__options{left:0;right:-126%;width:auto}}.p-table--pod-networking-config .p-option-selector__options-key{padding:0 1rem}.p-table--pod-networking-config .p-option-selector__option-cell{padding:.5rem 1rem}.p-table--pod-networking-config .p-option-selector__option-cell:first-child{width:70%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__option-cell:first-child{width:35%}}@media (min-width: 1030px){.p-table--pod-networking-config .p-option-selector__option-cell:first-child{width:40%}}.p-table--pod-networking-config .p-option-selector__option-cell>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(2){width:30%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(2){width:20%}}@media (min-width: 1030px){.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(2){width:15%}}.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(3){width:100%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(3){width:45%}}.p-option-selector-subnets__option-cell{padding:.5rem 1rem}.p-option-selector-subnets__option-cell:first-child{width:70%}@media (min-width: 768px){.p-option-selector-subnets__option-cell:first-child{width:35%}}@media (min-width: 1030px){.p-option-selector-subnets__option-cell:first-child{width:44%}}.p-option-selector-subnets__option-cell>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-option-selector-subnets__option-cell:nth-child(2){width:55%}@media (min-width: 768px){.p-option-selector-subnets__option-cell:nth-child(2){width:50%}}@media (min-width: 1030px){.p-option-selector-subnets__option-cell:nth-child(2){width:25%}}.p-option-selector-subnets__option-cell:nth-child(3){width:100%}@media (min-width: 768px){.p-option-selector-subnets__option-cell:nth-child(3){width:35%}}@media (min-width: 1030px){.p-option-selector-subnets__option-cell:nth-child(3){width:31%}}.p-table--pod-storage-config .p-option-selector__options{background-color:#fff;border-radius:.125rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);position:absolute;top:2.25rem;width:100vw;z-index:10}@media (min-width: 620px){.p-table--pod-storage-config .p-option-selector__options{width:70vw}}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__options{width:70vw}}@media (min-width: 1030px){.p-table--pod-storage-config .p-option-selector__options{width:750px}}.p-table--pod-storage-config .p-option-selector__options-key{padding:0 1rem}.p-table--pod-storage-config .p-option-selector__option-cell{padding:0 1rem 0 0}.p-table--pod-storage-config .p-option-selector__option-cell:first-child{padding-left:3rem;width:70%}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__option-cell:first-child{width:35%}}@media (min-width: 1030px){.p-table--pod-storage-config .p-option-selector__option-cell:first-child{width:40%}}.p-table--pod-storage-config .p-option-selector__option-cell>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(2){width:30%}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(2){width:20%}}@media (min-width: 1030px){.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(2){width:15%}}.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(3){width:100%}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(3){width:45%}} +.p-icon--minus,.p-icon--plus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron,.p-icon--close,.p-icon--help,.p-icon--information,.p-icon--info,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--contextual-menu,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--pass,.p-icon--share,.p-icon--user,.p-icon--question,.p-icon--spinner,.p-icon--edit,.p-icon--status-failed,.p-icon--status-in-progress,.p-icon--status-queued,.p-icon--status-succeeded,.p-icon--status-waiting,.p-icon--timed-out,.p-icon--success-muted,.p-icon--locked,.p-icon--compose-machine,.p-icon--account,.p-icon--mount,.p-icon--unmount,.p-icon--partition,.p-icon--debug,.p-icon--remove,.p-icon--settings,.p-icon--sync,.p-icon--system-shutdown,.p-icon--tags,.p-icon--logical-volume,.p-icon--pending,.p-icon--running,.p-icon--power-error,.p-icon--power-on,.p-icon--power-off,.p-icon--power-unknown,.p-icon--lock,.p-icon--x,.p-icon--tick,.p-table--machines .p-icon--placeholder{height:1rem;width:1rem;background-position:center;background-repeat:no-repeat;background-size:contain;display:inline-block;margin:0;padding:0;position:relative;top:-2px;vertical-align:sub}.p-icon--facebook,.p-icon--google,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu{height:2.5rem;width:2.5rem;display:inline-block}.p-icon--minus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--minus,.p-icon--minus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23cdcdcd' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--plus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--plus,.p-icon--plus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23cdcdcd' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--minus,.p-icon--plus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron,.p-icon--close,.p-icon--help,.p-icon--information,.p-icon--info,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--contextual-menu,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--pass,.p-icon--share,.p-icon--user,.p-icon--question,.p-icon--spinner,.p-icon--edit,.p-icon--status-failed,.p-icon--status-in-progress,.p-icon--status-queued,.p-icon--status-succeeded,.p-icon--status-waiting,.p-icon--timed-out,.p-icon--success-muted,.p-icon--locked,.p-icon--compose-machine,.p-icon--account,.p-icon--mount,.p-icon--unmount,.p-icon--partition,.p-icon--debug,.p-icon--remove,.p-icon--settings,.p-icon--sync,.p-icon--system-shutdown,.p-icon--tags,.p-icon--logical-volume,.p-icon--pending,.p-icon--running,.p-icon--power-error,.p-icon--power-on,.p-icon--power-off,.p-icon--power-unknown,.p-icon--lock,.p-icon--x,.p-icon--tick,.p-table--machines .p-icon--placeholder{height:1rem;width:1rem;background-position:center;background-repeat:no-repeat;background-size:contain;display:inline-block;margin:0;padding:0;position:relative;top:-2px;vertical-align:sub}.p-icon--facebook,.p-icon--google,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu{height:2.5rem;width:2.5rem;display:inline-block}.p-media-object,.p-media-object--large{display:flex;flex-shrink:0;margin-bottom:1rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item{color:#111;padding-left:2rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue{background-position:0 75%;background-repeat:no-repeat;background-size:1rem}/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}h1{font-size:2em;margin:0.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace, monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace, monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}button,input,optgroup,select,.p-option-selector__input,textarea{font-family:inherit;font-size:100%}button,input{overflow:visible}button,select,.p-option-selector__input{text-transform:none}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:0.35em 0.75em 0.625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}blockquote{border-left:2px solid #666}blockquote>cite{display:block}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}button{background-color:#fff;border-color:#cdcdcd;color:#111}button:visited{color:#111}button:active,button:hover{background-color:#f7f7f7;border-color:#cdcdcd}button:disabled:active,button:disabled:hover,button.is--disabled:active,button.is--disabled:hover{background-color:transparent;border-color:#cdcdcd}button .p-link--external{color:currentColor}button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{transition-duration:0.165s;transition-property:background-color,border-color;transition-timing-function:cubic-bezier(0.55, 0.055, 0.675, 0.19);border-radius:.125rem;border-style:solid;border-width:1px;cursor:pointer;display:inline-block;font-size:1rem;font-weight:300;line-height:1.5rem;margin-bottom:1.2rem;padding:.3375rem 1rem;text-align:center;text-decoration:none}button:focus,[type='submit']:focus,.p-button:focus,.p-button--neutral:focus,.p-button--brand:focus,.p-button--positive:focus,.p-button--negative:focus,.p-button--base:focus{outline:1px solid #19b6ee;outline-offset:2px}button:active,[type='submit']:active,.p-button:active,.p-button--neutral:active,.p-button--brand:active,.p-button--positive:active,.p-button--negative:active,.p-button--base:active,button:focus,[type='submit']:focus,.p-button:focus,.p-button--neutral:focus,.p-button--brand:focus,.p-button--positive:focus,.p-button--negative:focus,.p-button--base:focus,button:hover,[type='submit']:hover,.p-button:hover,.p-button--neutral:hover,.p-button--brand:hover,.p-button--positive:hover,.p-button--negative:hover,.p-button--base:hover{text-decoration:none}button:disabled,[type='submit']:disabled,.p-button:disabled,.p-button--neutral:disabled,.p-button--brand:disabled,.p-button--positive:disabled,.p-button--negative:disabled,.p-button--base:disabled,button.is--disabled,.is--disabled[type='submit'],.is--disabled.p-button,.is--disabled.p-button--neutral,.is--disabled.p-button--brand,.is--disabled.p-button--positive,.is--disabled.p-button--negative,.is--disabled.p-button--base{cursor:not-allowed;opacity:.5}@media only screen and (max-width: 460px){button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{width:100%}}@media only screen and (min-width: 461px){button,[type='submit'],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{width:auto}button:not(:last-of-type):not(:only-of-type),[type='submit']:not(:last-of-type):not(:only-of-type),.p-button:not(:last-of-type):not(:only-of-type),.p-button--neutral:not(:last-of-type):not(:only-of-type),.p-button--brand:not(:last-of-type):not(:only-of-type),.p-button--positive:not(:last-of-type):not(:only-of-type),.p-button--negative:not(:last-of-type):not(:only-of-type),.p-button--base:not(:last-of-type):not(:only-of-type){margin-right:1rem}}table button,table [type='submit'],table .p-button,table .p-button--neutral,table .p-button--brand,table .p-button--positive,table .p-button--negative,table .p-button--base{margin-bottom:.1rem;padding-bottom:.0875rem;padding-top:.0875rem}p button,p [type='submit'],p .p-button,p .p-button--neutral,p .p-button--brand,p .p-button--positive,p .p-button--negative,p .p-button--base{margin-bottom:.1rem;margin-top:-.4rem}p+p>button,p+p>[type='submit'],p+p>.p-button,p+p>.p-button--neutral,p+p>.p-button--brand,p+p>.p-button--positive,p+p>.p-button--negative,p+p>.p-button--base{margin-top:.1rem}@media only screen and (max-width: 460px){p button+button,p [type='submit']+button,p .p-button+button,p .p-button--neutral+button,p .p-button--brand+button,p .p-button--positive+button,p .p-button--negative+button,p .p-button--base+button,p button+[type='submit'],p [type='submit']+[type='submit'],p .p-button+[type='submit'],p .p-button--neutral+[type='submit'],p .p-button--brand+[type='submit'],p .p-button--positive+[type='submit'],p .p-button--negative+[type='submit'],p .p-button--base+[type='submit'],p button+.p-button,p [type='submit']+.p-button,p .p-button+.p-button,p .p-button--neutral+.p-button,p .p-button--brand+.p-button,p .p-button--positive+.p-button,p .p-button--negative+.p-button,p .p-button--base+.p-button,p button+.p-button--neutral,p [type='submit']+.p-button--neutral,p .p-button+.p-button--neutral,p .p-button--neutral+.p-button--neutral,p .p-button--brand+.p-button--neutral,p .p-button--positive+.p-button--neutral,p .p-button--negative+.p-button--neutral,p .p-button--base+.p-button--neutral,p button+.p-button--brand,p [type='submit']+.p-button--brand,p .p-button+.p-button--brand,p .p-button--neutral+.p-button--brand,p .p-button--brand+.p-button--brand,p .p-button--positive+.p-button--brand,p .p-button--negative+.p-button--brand,p .p-button--base+.p-button--brand,p button+.p-button--positive,p [type='submit']+.p-button--positive,p .p-button+.p-button--positive,p .p-button--neutral+.p-button--positive,p .p-button--brand+.p-button--positive,p .p-button--positive+.p-button--positive,p .p-button--negative+.p-button--positive,p .p-button--base+.p-button--positive,p button+.p-button--negative,p [type='submit']+.p-button--negative,p .p-button+.p-button--negative,p .p-button--neutral+.p-button--negative,p .p-button--brand+.p-button--negative,p .p-button--positive+.p-button--negative,p .p-button--negative+.p-button--negative,p .p-button--base+.p-button--negative,p button+.p-button--base,p [type='submit']+.p-button--base,p .p-button+.p-button--base,p .p-button--neutral+.p-button--base,p .p-button--brand+.p-button--base,p .p-button--positive+.p-button--base,p .p-button--negative+.p-button--base,p .p-button--base+.p-button--base{margin-top:.6rem}}code,samp,kbd{font-family:"Ubuntu Mono", Consolas, Monaco, Courier, monospace;font-weight:300;text-align:left}pre,code{direction:ltr;hyphens:none;tab-size:4;white-space:pre-wrap;word-spacing:normal;word-wrap:break-word}code{display:inline}pre{background-color:#f7f7f7;border:1px solid #cdcdcd;border-radius:.125rem;color:#111;display:block;margin-bottom:1rem;margin-top:0;overflow:auto;padding:.5rem 1rem;text-align:left;text-shadow:none}[type='text'],[type='date'],[type='datetime'],[type='datatime-local'],[type='month'],[type='time'],[type='week'],[type='color'],[type='number'],[type='search'],[type='password'],[type='email'],[type='url'],[type='tel'],select,.p-option-selector__input,textarea,[type='file'],.p-code-snippet{margin-bottom:.2rem;padding-bottom:.3375rem;padding-top:.3375rem}[type='text'],[type='date'],[type='datetime'],[type='datatime-local'],[type='month'],[type='time'],[type='week'],[type='color'],[type='number'],[type='search'],[type='password'],[type='email'],[type='url'],[type='tel'],select,.p-option-selector__input,textarea{appearance:textfield;background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:inset 0 1px 1px rgba(0,0,0,0.12);color:#111;font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;font-size:1rem;font-weight:300;line-height:1.5rem;min-width:10em;padding-left:.5rem;padding-right:.5rem;vertical-align:baseline;width:100%}[type='text']:focus,[type='date']:focus,[type='datetime']:focus,[type='datatime-local']:focus,[type='month']:focus,[type='time']:focus,[type='week']:focus,[type='color']:focus,[type='number']:focus,[type='search']:focus,[type='password']:focus,[type='email']:focus,[type='url']:focus,[type='tel']:focus,select:focus,.p-option-selector__input:focus,textarea:focus{outline:1px solid #19b6ee;outline-offset:2px}table [type='text'],table [type='date'],table [type='datetime'],table [type='datatime-local'],table [type='month'],table [type='time'],table [type='week'],table [type='color'],table [type='number'],table [type='search'],table [type='password'],table [type='email'],table [type='url'],table [type='tel'],table select,table .p-option-selector__input,table textarea{margin:0 0 .1rem 0;padding-bottom:.0875rem;padding-top:.0875rem}[type='text']:active,[type='date']:active,[type='datetime']:active,[type='datatime-local']:active,[type='month']:active,[type='time']:active,[type='week']:active,[type='color']:active,[type='number']:active,[type='search']:active,[type='password']:active,[type='email']:active,[type='url']:active,[type='tel']:active,select:active,.p-option-selector__input:active,textarea:active{border-color:#666;color:#111;outline:none}[type='text']::placeholder,[type='date']::placeholder,[type='datetime']::placeholder,[type='datatime-local']::placeholder,[type='month']::placeholder,[type='time']::placeholder,[type='week']::placeholder,[type='color']::placeholder,[type='number']::placeholder,[type='search']::placeholder,[type='password']::placeholder,[type='email']::placeholder,[type='url']::placeholder,[type='tel']::placeholder,select::placeholder,.p-option-selector__input::placeholder,textarea::placeholder{color:#666;opacity:1}.has-error[type='text'],.has-error[type='date'],.has-error[type='datetime'],.has-error[type='datatime-local'],.has-error[type='month'],.has-error[type='time'],.has-error[type='week'],.has-error[type='color'],.has-error[type='number'],.has-error[type='search'],.has-error[type='password'],.has-error[type='email'],.has-error[type='url'],.has-error[type='tel'],select.has-error,.has-error.p-option-selector__input,textarea.has-error{border:1px solid #c7162b}.has-caution[type='text'],.has-caution[type='date'],.has-caution[type='datetime'],.has-caution[type='datatime-local'],.has-caution[type='month'],.has-caution[type='time'],.has-caution[type='week'],.has-caution[type='color'],.has-caution[type='number'],.has-caution[type='search'],.has-caution[type='password'],.has-caution[type='email'],.has-caution[type='url'],.has-caution[type='tel'],select.has-caution,.has-caution.p-option-selector__input,textarea.has-caution{border:1px solid #f99b11}.has-warning[type='text'],.has-warning[type='date'],.has-warning[type='datetime'],.has-warning[type='datatime-local'],.has-warning[type='month'],.has-warning[type='time'],.has-warning[type='week'],.has-warning[type='color'],.has-warning[type='number'],.has-warning[type='search'],.has-warning[type='password'],.has-warning[type='email'],.has-warning[type='url'],.has-warning[type='tel'],select.has-warning,.has-warning.p-option-selector__input,textarea.has-warning{border:1px solid #f99b11}.has-success[type='text'],.has-success[type='date'],.has-success[type='datetime'],.has-success[type='datatime-local'],.has-success[type='month'],.has-success[type='time'],.has-success[type='week'],.has-success[type='color'],.has-success[type='number'],.has-success[type='search'],.has-success[type='password'],.has-success[type='email'],.has-success[type='url'],.has-success[type='tel'],select.has-success,.has-success.p-option-selector__input,textarea.has-success{border:1px solid #0e8420}.has-information[type='text'],.has-information[type='date'],.has-information[type='datetime'],.has-information[type='datatime-local'],.has-information[type='month'],.has-information[type='time'],.has-information[type='week'],.has-information[type='color'],.has-information[type='number'],.has-information[type='search'],.has-information[type='password'],.has-information[type='email'],.has-information[type='url'],.has-information[type='tel'],select.has-information,.has-information.p-option-selector__input,textarea.has-information{border:1px solid #335280}[type='checkbox'],[type='radio']{float:left;height:1.5rem;margin-bottom:0;margin-right:1.5rem;margin-top:0;padding:0;vertical-align:middle;width:auto}[type='checkbox']:focus,[type='radio']:focus{outline:1px solid #19b6ee;outline-offset:0}[disabled][type='text'],[disabled][type='date'],[disabled][type='datetime'],[disabled][type='datatime-local'],[disabled][type='month'],[disabled][type='time'],[disabled][type='week'],[disabled][type='color'],[disabled][type='number'],[disabled][type='search'],[disabled][type='password'],[disabled][type='email'],[disabled][type='url'],[disabled][type='tel'],select[disabled],[disabled].p-option-selector__input,textarea[disabled],[disabled='disabled'][type='text'],[disabled='disabled'][type='date'],[disabled='disabled'][type='datetime'],[disabled='disabled'][type='datatime-local'],[disabled='disabled'][type='month'],[disabled='disabled'][type='time'],[disabled='disabled'][type='week'],[disabled='disabled'][type='color'],[disabled='disabled'][type='number'],[disabled='disabled'][type='search'],[disabled='disabled'][type='password'],[disabled='disabled'][type='email'],[disabled='disabled'][type='url'],[disabled='disabled'][type='tel'],select[disabled='disabled'],[disabled='disabled'].p-option-selector__input,textarea[disabled='disabled'],[disabled][type='checkbox']+label,[disabled][type='radio']+label,[disabled='disabled'][type='checkbox']+label,[disabled='disabled'][type='radio']+label,.p-switch:disabled+.p-switch__slider{cursor:not-allowed;opacity:.5}[readonly][type='text'],[readonly][type='date'],[readonly][type='datetime'],[readonly][type='datatime-local'],[readonly][type='month'],[readonly][type='time'],[readonly][type='week'],[readonly][type='color'],[readonly][type='number'],[readonly][type='search'],[readonly][type='password'],[readonly][type='email'],[readonly][type='url'],[readonly][type='tel'],select[readonly],[readonly].p-option-selector__input,textarea[readonly],[readonly='readonly'][type='text'],[readonly='readonly'][type='date'],[readonly='readonly'][type='datetime'],[readonly='readonly'][type='datatime-local'],[readonly='readonly'][type='month'],[readonly='readonly'][type='time'],[readonly='readonly'][type='week'],[readonly='readonly'][type='color'],[readonly='readonly'][type='number'],[readonly='readonly'][type='search'],[readonly='readonly'][type='password'],[readonly='readonly'][type='email'],[readonly='readonly'][type='url'],[readonly='readonly'][type='tel'],select[readonly='readonly'],[readonly='readonly'].p-option-selector__input,textarea[readonly='readonly']{color:#cdcdcd;cursor:default}[readonly][type='text']:hover,[readonly][type='date']:hover,[readonly][type='datetime']:hover,[readonly][type='datatime-local']:hover,[readonly][type='month']:hover,[readonly][type='time']:hover,[readonly][type='week']:hover,[readonly][type='color']:hover,[readonly][type='number']:hover,[readonly][type='search']:hover,[readonly][type='password']:hover,[readonly][type='email']:hover,[readonly][type='url']:hover,[readonly][type='tel']:hover,select[readonly]:hover,[readonly].p-option-selector__input:hover,textarea[readonly]:hover,[readonly='readonly'][type='text']:hover,[readonly='readonly'][type='date']:hover,[readonly='readonly'][type='datetime']:hover,[readonly='readonly'][type='datatime-local']:hover,[readonly='readonly'][type='month']:hover,[readonly='readonly'][type='time']:hover,[readonly='readonly'][type='week']:hover,[readonly='readonly'][type='color']:hover,[readonly='readonly'][type='number']:hover,[readonly='readonly'][type='search']:hover,[readonly='readonly'][type='password']:hover,[readonly='readonly'][type='email']:hover,[readonly='readonly'][type='url']:hover,[readonly='readonly'][type='tel']:hover,select[readonly='readonly']:hover,[readonly='readonly'].p-option-selector__input:hover,textarea[readonly='readonly']:hover,[readonly][type='text']:active,[readonly][type='date']:active,[readonly][type='datetime']:active,[readonly][type='datatime-local']:active,[readonly][type='month']:active,[readonly][type='time']:active,[readonly][type='week']:active,[readonly][type='color']:active,[readonly][type='number']:active,[readonly][type='search']:active,[readonly][type='password']:active,[readonly][type='email']:active,[readonly][type='url']:active,[readonly][type='tel']:active,select[readonly]:active,[readonly].p-option-selector__input:active,textarea[readonly]:active,[readonly='readonly'][type='text']:active,[readonly='readonly'][type='date']:active,[readonly='readonly'][type='datetime']:active,[readonly='readonly'][type='datatime-local']:active,[readonly='readonly'][type='month']:active,[readonly='readonly'][type='time']:active,[readonly='readonly'][type='week']:active,[readonly='readonly'][type='color']:active,[readonly='readonly'][type='number']:active,[readonly='readonly'][type='search']:active,[readonly='readonly'][type='password']:active,[readonly='readonly'][type='email']:active,[readonly='readonly'][type='url']:active,[readonly='readonly'][type='tel']:active,select[readonly='readonly']:active,[readonly='readonly'].p-option-selector__input:active,textarea[readonly='readonly']:active{border-color:#666;outline:none}label{cursor:pointer;display:block;margin-bottom:.6rem}label.has-error{color:#c7162b}label.has-caution{color:#f99b11}label.has-warning{color:#f99b11}label.has-success{color:#0e8420}label.has-information{color:#335280}[type='file']{width:100%}[type='file']:focus{outline:1px solid #19b6ee;outline-offset:2px}[type='reset']{display:none}[type='search']{-moz-appearance:none;-webkit-appearance:none;appearance:none;border-radius:0}[type='search']::-webkit-search-results-decoration{display:none}[type='search']::-webkit-search-cancel-button{-webkit-appearance:searchfield-cancel-button;cursor:pointer}[type='checkbox']+label,[type='radio']+label{vertical-align:middle;width:100%}[type='radio']{margin-top:.4rem}[type='submit']{background-color:#0e8420;border-color:#0e8420;color:#fff}[type='submit']:visited{color:#fff}[type='submit']:active,[type='submit']:hover{background-color:#095615;border-color:#095615}[type='submit']:disabled:active,[type='submit']:disabled:hover,[type='submit'].is--disabled:active,[type='submit'].is--disabled:hover{background-color:#0e8420;border-color:#0e8420}[type='submit'] .p-link--external{color:currentColor}select,.p-option-selector__input{-moz-appearance:none;-webkit-appearance:none;appearance:none;background:#fff url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpza2V0Y2g9Imh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaC9ucyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBoZWlnaHQ9IjRweCIgd2lkdGg9IjEwcHgiIHZlcnNpb249IjEuMSIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZpZXdCb3g9IjAgMCAxMCA0Ij4gPHRpdGxlPmFjY29yZGlvbi1vcGVuPC90aXRsZT4gPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+IDxnIGlkPSJmaWx0ZXItcGFuZWwiIGZpbGwtcnVsZT0iZXZlbm9kZCIgc2tldGNoOnR5cGU9Ik1TUGFnZSIgZmlsbD0ibm9uZSI+ICA8ZyBpZD0iYWNjb3JkaW9uLW9wZW4iIGZpbGw9IiM4ODgiIHNrZXRjaDp0eXBlPSJNU0FydGJvYXJkR3JvdXAiPiAgIDxwYXRoIGlkPSJjaGV2cm9uIiBza2V0Y2g6dHlwZT0iTVNTaGFwZUdyb3VwIiBkPSJtNi4zNjEgMC44NjIzYzAuNTE4IDAuMzY1IDEuMDUyIDAuNzc4MSAxLjYwMSAxLjIzOCAwLjU0OSAwLjQ1ODUgMS4wODkgMC45NTE4IDEuNjIxIDEuNDc3MiAwLjE0MiAwLjE0MDQgMC4yODEgMC4yODIxIDAuNDE1IDAuNDIyNWgtMS41NDFjLTAuMzA0LTAuMjg4OC0wLjYyLTAuNTcwOS0wLjk0Ny0wLjg0NjMtMC4xMzc5LTAuMTE2MS0wLjI3NjgtMC4yMjk3LTAuNDE2OC0wLjM0MDgtMC4xNjM2LTAuMTI5Ny0wLjMyODYtMC4yNTU4LTAuNDk1NC0wLjM3ODMtMC4wODUyLTAuMDYyNS0wLjE3MDgtMC4xMjQxLTAuMjU2OC0wLjE4NDYtMC4zOTctMC4yODIxLTAuOTM1LTAuNjI1Ny0xLjMxNS0wLjg0NzZoLTAuMDU0Yy0wLjM4IDAuMjIxOS0wLjkxOCAwLjU2NTUtMS4zMTUgMC44NDc2LTAuMzk4IDAuMjgwNy0wLjc4OCAwLjU4MjktMS4xNjkgMC45MDM3LTAuMzI3IDAuMjc1NC0wLjY0MyAwLjU1NzUtMC45NDcgMC44NDYzaC0xLjU0MWMwLjEzNS0wLjE0MDQgMC4yNzMtMC4yODIxIDAuNDE1LTAuNDIyNSAwLjUzMi0wLjUyNTQgMS4wNzItMS4wMTg3IDEuNjIxLTEuNDc3MiAwLjU1LTAuNDU5OSAxLjA4My0wLjg3MyAxLjYwMS0xLjIzOCAwLjUxOS0wLjM2NDk3IDAuOTczLTAuNjUyNDEgMS4zNjItMC44NjIzIDAuMzkgMC4yMDk4OSAwLjg0NCAwLjQ5NzMzIDEuMzYyIDAuODYyM3oiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQuOTk5IDIpIHJvdGF0ZSgxODApIHRyYW5zbGF0ZSgtNC45OTkgLTIpIi8+ICA8L2c+IDwvZz48L3N2Zz4=") no-repeat;background-position:right .5rem center;background-size:.75rem;color:#111;min-height:24px;padding-right:1.5rem;text-indent:.01px;text-overflow:''}select:hover,.p-option-selector__input:hover{cursor:pointer}select[multiple],[multiple].p-option-selector__input,select[size],[size].p-option-selector__input{background-image:none;height:auto}select[multiple] option,[multiple].p-option-selector__input option,select[size] option,[size].p-option-selector__input option{font-weight:300;line-height:.875rem;padding:.5rem .5rem}textarea{margin-bottom:.2rem;overflow:auto;vertical-align:top}fieldset{background-color:#f7f7f7;border:0;border-radius:.125rem;color:#111}label{width:fit-content}input[type="checkbox"]{opacity:0;position:absolute}input[type="checkbox"]+label{margin-bottom:.5rem;margin-top:-.4rem;padding-left:2rem;position:relative}input[type="checkbox"]+label:focus{outline:1px solid #19b6ee;outline-offset:2px}input[type="checkbox"]+label::before{border:1px solid #cdcdcd;border-radius:.125rem;content:"";display:inline-block;height:1rem;left:0;top:.65rem;width:1rem}input[type="checkbox"]+label::after{border-bottom:2px solid;border-left:2px solid;content:none;display:inline-block;height:7px;left:2px;top:12px;transform:rotate(-45deg);width:11px}input[type="checkbox"]+label::before,input[type="checkbox"]+label::after{display:inline-block;position:absolute}input[type="checkbox"]:checked+label::after{content:""}hr{background-color:#cdcdcd;border:0;height:1px;margin-bottom:.4375rem;margin-top:0;position:relative;width:100%}hr+p{margin-top:-.5rem}.row.is-bordered{position:relative}.row.is-bordered::before{background:#cdcdcd;content:'';height:1px;margin-bottom:.4375rem;width:100%}a{color:#007aa6;text-decoration:none}a:focus{outline:thin dotted #cdcdcd}a:hover{cursor:pointer;text-decoration:underline}a:visited{color:#005573}li>ul,li>ol{margin-bottom:0;padding-top:0}li>ul>li:last-of-type,li>ol>li:last-of-type{padding-bottom:0}ol,ul{margin-bottom:1rem;margin-left:1rem;margin-top:0;padding-left:1rem}nav ol,nav ul{list-style:none;list-style-image:none}li,dl{margin:0;padding:0}dd{margin-left:1rem}dt{border-top:1px dotted #666}dt:first-of-type{border-top:0}img{border:0;border-radius:.125rem;height:auto;max-width:100%}svg:not(:root){overflow:hidden}figure{margin-bottom:1rem;margin-left:0;width:100%}figure caption,figure figcaption{display:block;font-style:italic;margin-top:.25rem;width:100%}object,iframe,embed,canvas,video,audio{display:block;margin:0 auto 20px;max-width:100%}audio:not([controls]){display:none;height:0}[hidden]{display:none}.p-card,.p-card--highlighted,.p-card--muted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-switch__slider,.p-switch__slider::before,.p-filter .p-accordion{border-radius:.125rem}.p-card--highlighted,.p-card--muted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-switch__slider::before,.page-header,.p-filter .p-accordion{box-shadow:0 1px 5px 1px rgba(17,17,17,0.2)}.p-card,.p-filter .p-accordion{border:.0625rem solid #cdcdcd}.p-card--muted{background-color:#f7f7f7;color:#111}.p-card,.p-card--highlighted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-table-expanding .p-table-expanding__panel,.p-table-expanding .p-table-expanding__panel--bordered,.p-filter .p-filter__dropdown{background-color:#fff;color:#111}.p-card,.p-card--highlighted,.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown,.p-modal__dialog,.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information,.p-table-expanding .p-table-expanding__panel,.p-table-expanding .p-table-expanding__panel--bordered{margin-bottom:1rem;overflow:auto;padding:.5rem}.p-tabs__list::after,.p-table--machines::after,.u-hr--fixed-width::after,.p-option-selector__option+.p-option-selector__option::after{background-color:#cdcdcd;content:'';height:.0625rem;left:0;position:absolute;right:0}.p-tabs__list::after,.p-table--machines::after{bottom:0}.u-hr--fixed-width::after,.p-option-selector__option+.p-option-selector__option::after{top:0}.p-icon--minus,.p-icon--plus,.p-icon--expand,.p-icon--collapse,.p-icon--chevron,.p-icon--close,.p-icon--help,.p-icon--information,.p-icon--info,.p-icon--delete,.p-icon--error,.p-icon--warning,.p-icon--external-link,.p-icon--contextual-menu,.p-icon--code,.p-icon--menu,.p-icon--copy,.p-icon--search,.p-icon--success,.p-icon--pass,.p-icon--share,.p-icon--user,.p-icon--question,.p-icon--spinner,.p-icon--edit,.p-icon--status-failed,.p-icon--status-in-progress,.p-icon--status-queued,.p-icon--status-succeeded,.p-icon--status-waiting,.p-icon--timed-out,.p-icon--success-muted,.p-icon--locked,.p-icon--compose-machine,.p-icon--account,.p-icon--mount,.p-icon--unmount,.p-icon--partition,.p-icon--debug,.p-icon--remove,.p-icon--settings,.p-icon--sync,.p-icon--system-shutdown,.p-icon--tags,.p-icon--logical-volume,.p-icon--pending,.p-icon--running,.p-icon--power-error,.p-icon--power-on,.p-icon--power-off,.p-icon--power-unknown,.p-icon--lock,.p-icon--x,.p-icon--tick,.p-table--machines .p-icon--placeholder,.p-icon--facebook,.p-icon--google,.p-icon--twitter,.p-icon--instagram,.p-icon--linkedin,.p-icon--youtube,.p-icon--canonical,.p-icon--ubuntu,.p-switch__slider span,button.p-switch span,.u-hide-text{overflow:hidden;text-indent:calc(100% + 10rem);white-space:nowrap}.p-inline-images::after,.p-list::after,.p-list-step::after,.p-stepped-list--detailed::after,.u-clearfix::after,.p-meter--cpu-cores__container::after,.p-legend::after,.p-legend__item::after,.p-form__group::after,.p-option-selector__header::after{clear:both;content:'';display:block}table{border:0;border-collapse:collapse;margin-bottom:1rem;overflow-x:auto;table-layout:fixed;width:100%}td,th{font-weight:300;padding-left:0;text-align:left;text-overflow:ellipsis}@media screen and (min-width: 768px){td:not(:last-child),th:not(:last-child){padding-right:1rem}}thead tr{border-bottom:1px solid #111;vertical-align:top}tbody tr:not(:first-child){border-top:1px solid #cdcdcd}td,th,.p-navigation--sidebar .sidebar__link,.p-accordion__tab{padding-bottom:.1875rem;padding-top:.25rem}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:300;src:url("/MAAS/static/assets/fonts/e8c07df6-Ubuntu-L_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/8619add2-Ubuntu-L_W.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:400;src:url("/MAAS/static/assets/fonts/fff37993-Ubuntu-R_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/7af50859-Ubuntu-R_W.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:300;src:url("/MAAS/static/assets/fonts/f8097dea-Ubuntu-LI_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/8be89d02-Ubuntu-LI_W.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:italic;font-weight:400;src:url("/MAAS/static/assets/fonts/fca66073-ubuntu-ri-webfont.woff2") format("woff2"),url("/MAAS/static/assets/fonts/f0898c72-ubuntu-ri-webfont.woff") format("woff")}@font-face{font-family:'Ubuntu';font-style:normal;font-weight:100;src:url("/MAAS/static/assets/fonts/7f100985-Ubuntu-Th_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/502cc3a1-Ubuntu-Th_W.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:300;src:url("/MAAS/static/assets/fonts/fdd692b9-UbuntuMono-R_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/85edb898-UbuntuMono-R_W.woff") format("woff")}@font-face{font-family:'Ubuntu Mono';font-style:normal;font-weight:400;src:url("/MAAS/static/assets/fonts/fdd692b9-UbuntuMono-R_W.woff2") format("woff2"),url("/MAAS/static/assets/fonts/85edb898-UbuntuMono-R_W.woff") format("woff")}html{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;color:#111;font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;font-size:16px;font-smoothing:subpixel-antialiased;font-weight:300;line-height:1.5rem}h1,h2,h3,h4,h5,h6,[class^="p-heading--"]{font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif}p:empty{line-height:0;margin:0;padding:0}button,input,select,.p-option-selector__input,textarea{font-family:"Ubuntu", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif}blockquote{margin-bottom:0;margin-left:0;margin-top:0;overflow:auto;padding-left:1.5rem}blockquote>p{font-style:italic}blockquote>cite{font-style:normal}small.dense{margin-bottom:1.2rem}sub,sup{line-height:0;position:relative;vertical-align:baseline}abbr[title]{border-bottom:.1em dotted;cursor:pointer;text-decoration:none}@media (max-width: 768px){h3+p,h4+p,.p-heading--three+p,.p-heading--four+p{margin-top:-.5rem}}h5+p,h5+h5,h6+p,h6+h5,.p-heading--five+p,.p-heading--five+h5,.p-heading--six+p,.p-heading--six+h5,h5+h6,h6+h6,.p-heading--five+h6,.p-heading--six+h6,h5+.p-heading--five,h6+.p-heading--five,.p-heading--five+.p-heading--five,.p-heading--six+.p-heading--five,h5+.p-heading--six,h6+.p-heading--six,.p-heading--five+.p-heading--six,.p-heading--six+.p-heading--six{margin-top:0rem}.p-muted-heading+p,.p-muted-heading+h5,.p-muted-heading+h6,.p-muted-heading+.p-heading--five,.p-muted-heading+.p-heading--six{margin-top:-.5rem}p+p:not(.p-muted-heading),h5+p:not(.p-muted-heading),h6+p:not(.p-muted-heading),.p-heading--five+p:not(.p-muted-heading),.p-heading--six+p:not(.p-muted-heading){margin-top:-1rem}ul+h1,ul+h2,ul+.p-heading--one,ul+.p-heading--two,p+h1,p+h2,p+.p-heading--one,p+.p-heading--two,h5+h1,h5+h2,h5+.p-heading--one,h5+.p-heading--two,h6+h1,h6+h2,h6+.p-heading--one,h6+.p-heading--two,.p-heading--five+h1,.p-heading--five+h2,.p-heading--five+.p-heading--one,.p-heading--five+.p-heading--two,.p-heading-6+h1,.p-heading-6+h2,.p-heading-6+.p-heading--one,.p-heading-6+.p-heading--two{padding-top:2.2rem}@media (max-width: 768px){ul+h1,ul+h2,ul+.p-heading--one,ul+.p-heading--two,p+h1,p+h2,p+.p-heading--one,p+.p-heading--two,h5+h1,h5+h2,h5+.p-heading--one,h5+.p-heading--two,h6+h1,h6+h2,h6+.p-heading--one,h6+.p-heading--two,.p-heading--five+h1,.p-heading--five+h2,.p-heading--five+.p-heading--one,.p-heading--five+.p-heading--two,.p-heading-6+h1,.p-heading-6+h2,.p-heading-6+.p-heading--one,.p-heading-6+.p-heading--two{padding-top:1.7rem}}p+h2,p+.p-heading--two{padding-top:2.2rem}@media (max-width: 768px){p+h2,p+.p-heading--two{padding-top:1.6rem}}p+h3,p+.p-heading--three{padding-top:2.1rem}@media (max-width: 768px){p+h3,p+.p-heading--three{padding-top:1.5rem}}p+h4,p+.p-heading--four{padding-top:1.55rem}p+h5,p+.p-heading--five,p+h6,p+.p-heading--six{padding-top:1.4rem}p+.p-muted-heading{padding-top:1rem}h1,.p-heading--one,.p-media-object--large .p-media-object__title{max-width:20em;font-size:2.91029rem;font-style:normal;font-weight:100;line-height:3.5rem;margin-bottom:2.3rem;margin-top:0;padding-top:0.2rem}@media (max-width: 768px){h1,.p-heading--one,.p-media-object--large .p-media-object__title{font-size:2.22819rem;line-height:3rem;margin-bottom:1.8rem;padding-top:0.2rem}}h2,.p-heading--two{max-width:20em;font-size:2.22819rem;font-style:normal;font-weight:300;line-height:3rem;margin-bottom:1.8rem;margin-top:0;padding-top:0.2rem}@media (max-width: 768px){h2,.p-heading--two{font-size:1.83274rem;line-height:2.5rem;margin-bottom:1.4rem;padding-top:0.1rem}}h3,.p-navigation--sidebar .p-navigation__logo,.p-heading--three,.p-list-step>li::before,.p-stepped-list--detailed>li::before{max-width:25em;font-size:1.70596rem;font-style:normal;font-weight:300;line-height:2.5rem;margin-bottom:1.4rem;margin-top:0;padding-top:0.1rem}@media (max-width: 768px){h3,.p-navigation--sidebar .p-navigation__logo,.p-heading--three,.p-list-step>li::before,.p-stepped-list--detailed>li::before{font-size:1.49271rem;line-height:2rem;margin-bottom:1rem;padding-top:0}}h4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote>p,.p-pull-quote__citation,.page-header__title-domain,.page-header__title{max-width:25em;font-size:1.30612rem;font-style:normal;font-weight:300;line-height:2rem;margin-bottom:.95rem;margin-top:0;padding-top:0.05rem}@media (max-width: 768px){h4,.p-heading--four,.p-matrix__title,.p-media-object__title,.p-modal__title,.p-pull-quote>p,.p-pull-quote__citation,.page-header__title-domain,.page-header__title{font-size:1.22176rem;line-height:1.5rem;margin-bottom:.7rem;padding-top:0.3rem}}h5,.p-heading--five{font-size:1rem;font-style:normal;font-weight:500}h6,.p-heading--six{font-size:1rem;font-style:italic;font-weight:300}label,dt,cite,dd,p,h5,.p-heading--five,h6,.p-heading--six,.p-footer__link,.p-navigation--sidebar .p-navigation__tagline,.p-breadcrumbs__item,.p-notification__response,.default-text,.p-p-compact,.p-form--stacked .p-form__control>.p-control-text,.p-storage__type{line-height:1.5rem;margin-bottom:.1rem;margin-top:0;padding-top:.4rem}dd,p,h5,.p-heading--five,h6,.p-heading--six,.p-footer__link{margin-bottom:1.1rem}.p-media-object__meta-list-item--date,.p-media-object__meta-list-item--location,.p-media-object__meta-list-item--venue,.p-media-object__meta-list-item,small,thead th,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before,.p-form-help-text,.p-form-validation__message,.p-tooltip__message,.p-p-small,.p-p-small--align-with-p,.p-double-row .p-double-row__muted-row,.p-muted-text,.p-domain-name .p-domain-name__tld{font-size:.875rem;line-height:1.25rem;margin-bottom:.7rem;padding-top:0.05rem}thead th,.p-muted-heading,.p-table--mobile-card td::before,.p-table--mobile-card tbody th::before{color:#666;margin-bottom:.5rem;margin-top:0;text-transform:uppercase}p,h5,.p-heading--five,h6,.p-heading--six,.measure--p{max-width:38em}dt,strong,.p-notification__status{font-weight:400}.p-navigation{background-color:#333;display:flex;flex-shrink:0;position:relative}@media (max-width: 870px){.p-navigation{flex-direction:column}}.p-navigation a,.p-navigation a:visited,.p-navigation a:hover,.p-navigation a:focus{color:#f7f7f7;text-decoration:none}.p-navigation::after{background:transparent;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}.p-navigation__banner{display:flex;flex:0 0 auto;justify-content:space-between}.p-navigation__image{align-self:center;max-height:2rem;min-height:1.5rem}.p-navigation__link>a{display:block;margin-bottom:0;position:relative}@media (max-width: 870px){.p-navigation__link>a{padding:.75rem 1.5rem}.p-navigation__link>a::before{background:transparent;content:'';height:.0625rem;left:0;position:absolute;right:0;top:0}}@media (min-width: 871px){.p-navigation__link>a{border-left:1px solid transparent;padding:.75rem 1rem}.p-navigation__link>a::before{background:transparent;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}}.p-navigation__link>a:hover{background-color:#2b2b2b}@media (min-width: 871px){.p-navigation__link.is-selected>a{position:relative}.p-navigation__link.is-selected>a::before{bottom:0;background-color:#e95420;content:'';position:absolute}.p-navigation__link.is-selected>a::before{height:.1875rem;width:auto;left:-1px;right:-1px;z-index:1}}.p-navigation__links,.p-navigation .p-navigation__links--right{list-style:none;margin:0;padding:0}@media (max-width: 870px){.p-navigation__links,.p-navigation .p-navigation__links--right{margin-top:-1px}}@media (min-width: 871px){.p-navigation__links,.p-navigation .p-navigation__links--right{display:flex;flex-wrap:wrap}}.p-navigation__logo{display:flex;flex:0 0 auto;height:3rem;margin:0 1rem 0 1.5rem}.p-navigation__logo .p-navigation__link{display:flex}.p-navigation__nav{display:none}@media (max-width: 870px){.p-navigation__nav{flex-direction:column}}@media (min-width: 871px){.p-navigation__nav{display:flex;justify-content:space-between;width:100%}}.p-navigation .p-search-box{min-width:10em}@media (max-width: 870px){.p-navigation .p-search-box{flex:1 0 auto;margin:-1px 1.5rem .5rem 1.5rem;order:-1}}@media (min-width: 871px){.p-navigation .p-search-box{display:flex;flex:1 1 auto;margin:.35rem 1rem auto auto;max-width:20rem;order:1}}.p-navigation__row,.p-navigation .row{display:flex;padding-left:0;padding-right:0;width:100%}@media (max-width: 870px){.p-navigation__row,.p-navigation .row{flex-direction:column}}.p-navigation:target .p-navigation__nav{display:flex}.p-navigation:target .p-navigation__toggle--open{display:none}@media (max-width: 870px){.p-navigation:target .p-navigation__toggle--close{display:block}}.p-navigation__toggle--open,.p-navigation__toggle--close{display:none;margin:0 1.5rem auto 1rem;padding:.75rem 0}@media (max-width: 870px){.p-navigation__toggle--open{display:block}}.p-navigation .u-image-position .u-image-position--right{order:2;position:relative;right:unset}.p-navigation--sidebar{background-color:#fff;display:flex;flex-shrink:0;position:relative;flex-direction:column;height:auto}@media (max-width: 870px){.p-navigation--sidebar{flex-direction:column}}.p-navigation--sidebar a,.p-navigation--sidebar a:visited,.p-navigation--sidebar a:hover,.p-navigation--sidebar a:focus{color:#111;text-decoration:none}.p-navigation--sidebar::after{background:#cdcdcd;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}.p-navigation--sidebar__banner{display:flex;flex:0 0 auto;justify-content:space-between}.p-navigation--sidebar__image{align-self:center;max-height:2rem;min-height:1.5rem}.p-navigation--sidebar__link>a{display:block;margin-bottom:0;position:relative}@media (max-width: 870px){.p-navigation--sidebar__link>a{padding:.75rem 1.5rem}.p-navigation--sidebar__link>a::before{background:#cdcdcd;content:'';height:.0625rem;left:0;position:absolute;right:0;top:0}}@media (min-width: 871px){.p-navigation--sidebar__link>a{border-left:1px solid #cdcdcd;padding:.75rem 1rem}.p-navigation--sidebar__link>a::before{background:#cdcdcd;bottom:0;content:'';height:.0625rem;left:0;position:absolute;right:0}}.p-navigation--sidebar__link>a:hover{background-color:#2b2b2b}@media (min-width: 871px){.p-navigation--sidebar__link.is-selected>a{position:relative}.p-navigation--sidebar__link.is-selected>a::before{bottom:0;background-color:#e95420;content:'';position:absolute}.p-navigation--sidebar__link.is-selected>a::before{height:.1875rem;width:auto;left:-1px;right:-1px;z-index:1}}.p-navigation--sidebar__links{list-style:none;margin:0;padding:0}@media (max-width: 870px){.p-navigation--sidebar__links{margin-top:-1px}}@media (min-width: 871px){.p-navigation--sidebar__links{display:flex;flex-wrap:wrap}}.p-navigation--sidebar__logo{display:flex;flex:0 0 auto;height:3rem;margin:0 1rem 0 1.5rem}.p-navigation--sidebar__logo .p-navigation__link{display:flex}.p-navigation--sidebar__nav{display:none}@media (max-width: 870px){.p-navigation--sidebar__nav{flex-direction:column}}@media (min-width: 871px){.p-navigation--sidebar__nav{display:flex;justify-content:space-between;width:100%}}.p-navigation--sidebar .p-search-box{min-width:10em}@media (max-width: 870px){.p-navigation--sidebar .p-search-box{flex:1 0 auto;margin:-1px 1.5rem .5rem 1.5rem;order:-1}}@media (min-width: 871px){.p-navigation--sidebar .p-search-box{display:flex;flex:1 1 auto;margin:.35rem 1rem auto auto;max-width:20rem;order:1}}.p-navigation--sidebar__row,.p-navigation--sidebar .row{display:flex;padding-left:0;padding-right:0;width:100%}@media (max-width: 870px){.p-navigation--sidebar__row,.p-navigation--sidebar .row{flex-direction:column}}.p-navigation--sidebar:target .p-navigation__nav{display:flex}.p-navigation--sidebar:target .p-navigation__toggle--open{display:none}@media (max-width: 870px){.p-navigation--sidebar:target .p-navigation__toggle--close{display:block}}.p-navigation--sidebar__toggle--open,.p-navigation--sidebar__toggle--close{display:none;margin:0 1.5rem auto 1rem;padding:.75rem 0}@media (max-width: 870px){.p-navigation--sidebar__toggle--open{display:block}}.p-navigation--sidebar .u-image-position .u-image-position--right{order:2;position:relative;right:unset}.p-navigation--sidebar .p-navigation__banner .row{flex-direction:row}.p-navigation--sidebar .sidebar__cta{margin-top:0}.p-navigation--sidebar .sidebar__cta .p-inline-list{display:inline-block}.p-navigation--sidebar .sidebar__cta [class^="p-icon"]{cursor:pointer}@media (min-width: 871px){.p-navigation--sidebar .sidebar__cta{display:none}}.p-navigation--sidebar .sidebar__content{background:#fff;width:100%}@media (min-width: 871px){.p-navigation--sidebar .sidebar__content{display:block !important}}.p-navigation--sidebar .sidebar__link{color:#111;display:block;position:relative}.p-navigation--sidebar .sidebar__link:hover{color:#007aa6}.p-navigation--sidebar .sidebar__link:focus{outline:0}.p-navigation--sidebar .p-navigation__logo{display:flex;flex:0 0 auto;margin-left:0}.p-navigation--sidebar .p-navigation__logo .p-navigation__image{height:24px;width:auto}.p-navigation--sidebar .p-navigation__tagline{display:block}.p-navigation--sidebar .is-selected{font-weight:bold}.p-navigation--sidebar .sidebar__first-level{padding-left:0}.p-navigation--sidebar .sidebar__third-level{background-color:#666;margin-right:-4rem;padding-left:4rem;position:relative;right:3rem}.p-navigation--sidebar .sidebar__second-level,.p-navigation--sidebar .sidebar__third-level{display:none;list-style:none;margin-left:0;padding-bottom:.25rem;padding-top:.25rem}.p-navigation--sidebar .sidebar__second-level .is-deepest-level,.p-navigation--sidebar .sidebar__third-level .is-deepest-level{background-color:#f7f7f7}.p-navigation--sidebar .p-icon--plus,.p-navigation--sidebar .p-icon--minus{perspective:800px;perspective-origin:50% 100px;position:absolute;right:1rem;top:.5rem;transition:all .5s ease-in-out}.p-navigation--sidebar .p-icon--minus{display:none}.p-navigation--sidebar .is-selected .p-icon--minus{display:block}.p-navigation--sidebar .is-selected .p-icon--plus{display:none}.p-navigation--sidebar .is-selected+.sidebar__second-level,.p-navigation--sidebar .is-selected+.sidebar__third-level{display:block}.p-accordion__list{list-style-type:none;margin:0 0 1rem 0;padding:0}.p-accordion__group:not(:last-child) .p-accordion__tab{border-bottom:1px solid #cdcdcd}.p-accordion__tab{background-position:top .59375rem right 1rem;background-repeat:no-repeat;background-color:inherit;border:0;border-radius:0;margin-bottom:0;padding-left:1rem;padding-right:1rem;text-align:left;transition-duration:0s;width:100%;z-index:2}.p-accordion__tab[aria-expanded='true']{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E");background-size:.75rem}.p-accordion__tab[aria-expanded='false']{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E");background-size:.75rem}.p-accordion__tab:focus{outline:1px solid #007aa6;outline-offset:2px}.p-accordion__panel{border-bottom:1px solid #cdcdcd;margin:0;overflow:auto;padding-left:2rem}.p-accordion__panel[aria-hidden='true']{display:none}.p-accordion p{margin-bottom:.6rem}.p-aside{border-top:1px solid #cdcdcd;font-size:.875rem;padding:0 1.5rem}@media (min-width: 768px){.p-aside{border-left:1px solid #cdcdcd;border-top:0;padding:0 1rem}}.p-aside__header{color:#666;font-size:1rem;line-height:1.5;margin-bottom:1rem;text-transform:uppercase}.p-aside__section{padding:1rem 0}.p-aside__section:not(:last-child){border-bottom:1px dotted #cdcdcd}.p-aside__nav{list-style:none;margin:0;padding:0}.p-aside__nav .p-aside__link{border-bottom:0;color:#111;margin-bottom:.25rem}.p-aside__nav .p-aside__link:visited{color:#111}.p-aside__nav .p-aside__link:hover{color:#007aa6}.p-aside__nav .p-aside__link.is-active{font-weight:400;padding-left:.25rem}.p-breadcrumbs{list-style:none;margin:0;padding:0;width:100%}.p-breadcrumbs__item{display:inline-block;margin-bottom:.1rem}.p-breadcrumbs__item:not(:first-of-type){text-indent:1rem}.p-breadcrumbs__item:not(:first-of-type)::before{content:'\203A';margin-left:-.75rem;margin-right:.5rem}.p-button{background-color:#fff;border-color:#cdcdcd;color:#111}.p-button:visited{color:#111}.p-button:active,.p-button:hover{background-color:#f7f7f7;border-color:#cdcdcd}.p-button:disabled:active,.p-button:disabled:hover,.is--disabled.p-button:active,.is--disabled.p-button:hover{background-color:#fff;border-color:#fff}.p-button .p-link--external{color:currentColor}.p-button--neutral{background-color:#fff;border-color:#cdcdcd;color:#111}.p-button--neutral:visited{color:#111}.p-button--neutral:active,.p-button--neutral:hover{background-color:#dedede;border-color:#cdcdcd}.p-button--neutral:disabled:active,.p-button--neutral:disabled:hover,.is--disabled.p-button--neutral:active,.is--disabled.p-button--neutral:hover{background-color:transparent;border-color:#cdcdcd}.p-button--neutral .p-link--external{color:currentColor}.p-button--brand{background-color:#e95420;border-color:#e95420;color:#fff}.p-button--brand:visited{color:#fff}.p-button--brand:active,.p-button--brand:hover{background-color:#c34113;border-color:#c34113}.p-button--brand:disabled:active,.p-button--brand:disabled:hover,.is--disabled.p-button--brand:active,.is--disabled.p-button--brand:hover{background-color:#e95420;border-color:#e95420}.p-button--brand .p-link--external{color:currentColor}.p-button--positive{background-color:#0e8420;border-color:#0e8420;color:#fff}.p-button--positive:visited{color:#fff}.p-button--positive:active,.p-button--positive:hover{background-color:#095615;border-color:#095615}.p-button--positive:disabled:active,.p-button--positive:disabled:hover,.is--disabled.p-button--positive:active,.is--disabled.p-button--positive:hover{background-color:#0e8420;border-color:#0e8420}.p-button--positive .p-link--external{color:currentColor}.p-button--negative{background-color:#c7162b;border-color:#c7162b;color:#fff}.p-button--negative:visited{color:#fff}.p-button--negative:active,.p-button--negative:hover{background-color:#991121;border-color:#991121}.p-button--negative:disabled:active,.p-button--negative:disabled:hover,.is--disabled.p-button--negative:active,.is--disabled.p-button--negative:hover{background-color:#c7162b;border-color:#c7162b}.p-button--negative .p-link--external{color:currentColor}.p-button--base{background-color:transparent;border-color:transparent;color:#111}.p-button--base:visited{color:#111}.p-button--base:active,.p-button--base:hover{background-color:#f7f7f7;border-color:transparent}.p-button--base:disabled:active,.p-button--base:disabled:hover,.is--disabled.p-button--base:active,.is--disabled.p-button--base:hover{background-color:transparent;border-color:#cdcdcd}.p-button--base .p-link--external{color:currentColor}@media (min-width: 768px){[class^="p-button"].is-inline{margin-left:1rem;width:auto}}.p-card{padding:.4375rem}.p-card--overlay{background:rgba(255,255,255,0.9);color:#111;margin-bottom:1rem;overflow:auto;padding:.5rem}.p-card--muted{margin-bottom:1rem;overflow:auto;padding:.5rem}.p-card__image{margin-bottom:.5rem;vertical-align:top;width:100%}.p-card__content{margin-top:-1rem}.p-card__header{border-bottom:1px solid #cdcdcd;margin-bottom:.5rem}.p-card__header>.p-link--soft{display:inline-block;overflow:auto}.p-card__thumbnail{max-height:2rem}.p-card__content{margin-top:-1rem}.p-card__header{border-bottom:1px solid #cdcdcd;margin-bottom:.5rem}.p-card__header>.p-link--soft{display:inline-block;overflow:auto}.p-code-numbered{counter-reset:line-numbering;padding:0}.p-code-numbered .code-line{display:inline-block;padding:.5rem 1rem 0 5.5rem;position:relative;width:100%}.p-code-numbered .code-line:empty{display:block;min-height:2.5rem}.p-code-numbered .code-line:last-of-type,.p-code-numbered .code-line:last-of-type::before{padding-bottom:.5rem}.p-code-numbered .code-line::before{background:#fff;border-right:1px solid #cdcdcd;color:#666;content:counter(line-numbering);counter-increment:line-numbering;display:inline-block;height:100%;left:0;padding:.5rem 1rem 0 1rem;pointer-events:none;position:absolute;text-align:right;top:0;user-select:none;width:4.5rem}.p-code-snippet{background-color:#fff;border:1px solid #cdcdcd;border-radius:.125rem;color:#111;display:flex;overflow:hidden;padding-left:.5rem;padding-right:.5rem;position:relative;transition:border .2s, background-color .2s;width:100%}.p-code-snippet+.p-code-snippet{margin-top:0}.p-code-snippet__input{background-color:transparent;background-image:url('data:image/svg+xml;utf8, ');background-position:0 center;background-repeat:no-repeat;border:0;box-shadow:none;color:#666;font-family:"Ubuntu Mono", Consolas, Monaco, Courier, monospace;font-weight:300;line-height:1.5rem;margin-bottom:0;padding:0;padding-left:1.5rem;width:100%}.p-code-snippet__action{background-color:#f7f7f7;background-image:url('data:image/svg+xml;utf8, ');background-position:center;background-repeat:no-repeat;background-size:1rem;border-color:transparent;border-left:1px solid #cdcdcd;border-radius:0;display:block;height:100%;margin-bottom:0;margin-top:0;padding:0;position:absolute;right:0;text-indent:-9999px;top:0;width:40px}.p-code-snippet__action:hover{border-color:transparent;border-left:1px solid #cdcdcd}.p-contextual-menu,.p-contextual-menu--left,.p-contextual-menu--center,.p-cta,.p-table-menu{display:inline-block;margin:0;position:relative}.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown{display:none;margin:0;max-width:21rem;min-width:10rem;padding:0;position:absolute;right:0;top:calc(100% + .25rem);z-index:1}.p-contextual-menu__dropdown::before,.p-cta__dropdown::before,.p-table-menu .p-table-menu__dropdown::before,.p-contextual-menu__dropdown::after,.p-cta__dropdown::after,.p-table-menu .p-table-menu__dropdown::after{border-bottom:8px solid rgba(17,17,17,0.05);border-left:8px solid transparent;border-right:8px solid transparent;bottom:100%;content:'';height:0;pointer-events:none;position:absolute;right:.5rem;width:0}.p-contextual-menu__dropdown::after,.p-cta__dropdown::after,.p-table-menu .p-table-menu__dropdown::after{border-bottom:6px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;right:.6rem}.p-contextual-menu__dropdown[aria-hidden="false"],[aria-hidden="false"].p-cta__dropdown,.p-table-menu [aria-hidden="false"].p-table-menu__dropdown{display:block}.p-contextual-menu__group{display:block;padding:.125rem 0}.p-contextual-menu__group+.p-contextual-menu__group{border-top:1px solid #cdcdcd;margin:0}.p-contextual-menu__link,.p-cta__link,.p-table-menu .p-table-menu__link,.p-table-menu .p-table-menu__check-power,.p-table-menu .p-table-menu__power-on,.p-table-menu .p-table-menu__power-off{border:0;clear:both;color:#111;display:block;line-height:1.5rem;margin:0;overflow:hidden;padding:.125rem .5rem;text-align:left;text-overflow:ellipsis;white-space:nowrap;width:100%}.p-contextual-menu__link:hover,.p-cta__link:hover,.p-table-menu .p-table-menu__link:hover,.p-table-menu .p-table-menu__check-power:hover,.p-table-menu .p-table-menu__power-on:hover,.p-table-menu .p-table-menu__power-off:hover{background-color:#f7f7f7;text-decoration:none}.p-contextual-menu--left .p-contextual-menu__dropdown,.p-contextual-menu--left .p-cta__dropdown,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown{left:0}.p-contextual-menu--left .p-contextual-menu__dropdown::before,.p-contextual-menu--left .p-cta__dropdown::before,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown::before,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown::before,.p-contextual-menu--left .p-contextual-menu__dropdown::after,.p-contextual-menu--left .p-cta__dropdown::after,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown::after,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown::after{left:.5rem;right:initial}.p-contextual-menu--left .p-contextual-menu__dropdown::after,.p-contextual-menu--left .p-cta__dropdown::after,.p-contextual-menu--left .p-table-menu .p-table-menu__dropdown::after,.p-table-menu .p-contextual-menu--left .p-table-menu__dropdown::after{left:.6rem}.p-contextual-menu--center .p-contextual-menu__dropdown,.p-contextual-menu--center .p-cta__dropdown,.p-contextual-menu--center .p-table-menu .p-table-menu__dropdown,.p-table-menu .p-contextual-menu--center .p-table-menu__dropdown{left:50%;transform:translateX(-50%)}.p-contextual-menu--center .p-contextual-menu__dropdown::before,.p-contextual-menu--center .p-cta__dropdown::before,.p-contextual-menu--center .p-table-menu .p-table-menu__dropdown::before,.p-table-menu .p-contextual-menu--center .p-table-menu__dropdown::before,.p-contextual-menu--center .p-contextual-menu__dropdown::after,.p-contextual-menu--center .p-cta__dropdown::after,.p-contextual-menu--center .p-table-menu .p-table-menu__dropdown::after,.p-table-menu .p-contextual-menu--center .p-table-menu__dropdown::after{left:50%;right:initial;transform:translateX(-50%)}@media (min-width: 768px){.p-divider{display:flex}}@media (max-width: 768px){.p-divider__block{padding-bottom:1rem}.p-divider__block:not(:first-child){border-top:1px solid #cdcdcd;padding-top:.4375rem}}@media (min-width: 768px){.p-divider__block{padding-right:1rem}.p-divider__block:not(:nth-child(1))::before{border-left:1px solid #cdcdcd;bottom:.5rem;content:'';left:-1.5rem;position:absolute;top:.5rem}.p-divider__block:last-child{padding-right:0}}.p-footer{border-top:1px solid #cdcdcd;position:relative}@media only screen and (max-width: 1030px){.p-footer{padding-bottom:1.5rem;padding-top:1.5rem}}@media only screen and (min-width: 1030px){.p-footer{padding-bottom:3rem;padding-top:3rem}}.p-footer__copy{margin-bottom:0}.p-footer__links{margin:0;padding:0}@media (min-width: 768px){.p-footer__links{margin-top:0}}.p-footer__nav{margin-top:0}p+.p-footer__nav{margin-top:-1rem}.p-footer__item{display:block}@media (min-width: 768px){.p-footer__item{display:inline-block}}.p-footer__item:last-child a::after{opacity:0}.p-footer__link{border-bottom:0;color:#111;display:inline-block}.p-footer__link:visited{color:#000}.p-footer__link:hover{color:#007aa6}@media (min-width: 768px){.p-footer__link{margin-right:1rem;position:relative}.p-footer__link::after{content:'\00b7';display:inline-block;font-size:1.5rem;position:absolute;right:-.75rem;top:.4rem}}.p-footer__link:hover::after{color:#111}.p-form-help-text{color:#666}input+.p-form-help-text,.p-form-validation .p-form-help-text{margin-top:.05rem}.p-form-validation{color:#111;position:relative}.p-form-validation .p-form-validation__input{background-position:calc(100% - .5rem) 50%;background-repeat:no-repeat}.p-form-validation .p-form-validation__icon{position:relative}.p-form-validation .p-form-validation__icon::after{position:absolute;right:.5rem;top:calc(50% - .25rem)}input+.p-form-validation__message,.p-form-help-text .p-form-validation__message{margin-top:.05rem}.is-error .p-form-validation__input{background-image:url("/MAAS/static/assets/fonts/4b0cd7fc-icon-error.svg");border-color:#c7162b}.is-success .p-form-validation__input{background-image:url("/MAAS/static/assets/fonts/94949185-icon-success.svg");border-color:#0e8420}.is-caution .p-form-validation__input{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath stroke-linejoin='round' fill='%23f99b11' transform='matrix%282.28 0 0 2.437 -2180.8 -490.52%29' stroke='%23f99b11' stroke-width='.848' d='M963.07 207.03h-6.15l3.08-5.33z'/%3E%3Cpath d='M7 5v5h2V5H7zm0 6v2h2v-2H7z' fill='%23111'/%3E%3C/g%3E%3C/svg%3E");border-color:#f99b11}.p-form--stacked{width:100%}@media screen and (min-width: 768px){.p-form--stacked .p-form__group{align-items:baseline;display:flex;flex-flow:wrap}.p-form--stacked .p-form__group+.p-form__group{margin-top:.5rem}}@media screen and (min-width: 768px){.p-form--stacked .p-form__label{flex-basis:25%;flex-grow:1;margin:0;max-width:25%;padding-right:.5rem}}@media screen and (min-width: 768px){.p-form--stacked .p-form__control{flex-basis:75%;flex-grow:1;margin:0;max-width:75%}}@media screen and (min-width: 768px){.p-form--inline{align-items:baseline;display:inline-flex;flex-direction:row}.p-form--inline>*{margin:0}}@media screen and (min-width: 768px){.p-form--inline .p-form__group{display:inline-flex;width:auto}.p-form--inline .p-form__group+.p-form__group,.p-form--inline .p-form__group+[class*="p-button"]{margin-left:1.5rem}.p-form--inline .p-form__group .p-form__label,.p-form--inline .p-form__group .p-form__control,.p-form--inline .p-form__group .p-form-validation__message{align-self:baseline;box-sizing:border-box}.p-form--inline .p-form__group .p-form__label{flex-shrink:0;padding-right:1rem}.p-form--inline .p-form__group .p-form__control{display:inline-block}.p-form--inline .p-form__group .p-form-validation__message,.p-form--inline .p-form__group .p-form-help-text{clear:both;min-width:100%;width:0}}.p-form--inline [class*="p-button"]{flex:initial;flex-shrink:0;margin-top:0}form+[class*="p-button"]{margin-top:1.5rem}.row{width:100%}[grid-demo] [class*="col-"]{background:#cdcdcd;margin-bottom:.5rem}[grid-outline] [class*="col-"]{outline:1px solid #fff;padding:.5rem .5rem}@media screen and (max-width: 400px){@-ms-viewport{width:320px}}img{max-width:100%;height:auto}@media \0screen{img{width:auto}}.row{*zoom:1;margin-right:auto;margin-left:auto;max-width:90rem;padding-left:1.5rem;padding-right:1.5rem}.row:before,.row:after{display:table;content:" "}.row:after{clear:both}.row .row{margin-right:0;margin-left:0;max-width:none;padding-right:0;padding-left:0}.mobile-col-1,.mobile-col-2,.mobile-col-3{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:7.80829%}.row .mobile-col-1:first-child,.row .mobile-col-2:first-child,.row .mobile-col-3:first-child,.first-mobile-col{margin-left:0}.mobile-col-1{width:19.14379%}.mobile-col-2{width:46.09586%}.mobile-col-3{width:73.04793%}.mobile-prefix-1{padding-left:26.95207%}.mobile-prefix-2{padding-left:53.90414%}.mobile-prefix-3{padding-left:80.85621%}.mobile-suffix-1{padding-right:26.95207%}.mobile-suffix-2{padding-right:53.90414%}.mobile-suffix-3{padding-right:80.85621%}.mobile-push-1{left:26.95207%}.mobile-push-2{left:53.90414%}.mobile-push-3{left:80.85621%}.mobile-pull-1{right:26.95207%}.mobile-pull-2{right:53.90414%}.mobile-pull-3{right:80.85621%}@media screen and (min-width: 620px){.tablet-col-1,.tablet-col-2,.p-form--stacked .p-form__label,.tablet-col-3,.p-pod-summary__cpu,.p-pod-summary__ram,.tablet-col-4,.p-form--stacked .p-form__control,.tablet-col-5{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:4.93155%}.row .tablet-col-1:first-child,.row .tablet-col-2:first-child,.row .p-form--stacked .p-form__label:first-child,.p-form--stacked .row .p-form__label:first-child,.row .tablet-col-3:first-child,.row .p-pod-summary__cpu:first-child,.row .p-pod-summary__ram:first-child,.row .tablet-col-4:first-child,.row .p-form--stacked .p-form__control:first-child,.p-form--stacked .row .p-form__control:first-child,.row .tablet-col-5:first-child,.first-tablet-col{margin-left:0}.tablet-col-1{width:12.55704%}.tablet-col-2,.p-form--stacked .p-form__label{width:30.04563%}.tablet-col-3,.p-pod-summary__cpu,.p-pod-summary__ram{width:47.53423%}.tablet-col-4,.p-form--stacked .p-form__control{width:65.02282%}.tablet-col-5{width:82.51141%}.tablet-prefix-1{padding-left:17.48859%}.tablet-prefix-2{padding-left:34.97718%}.tablet-prefix-3{padding-left:52.46577%}.tablet-prefix-4{padding-left:69.95437%}.tablet-prefix-5{padding-left:87.44296%}.tablet-suffix-1{padding-right:17.48859%}.tablet-suffix-2{padding-right:34.97718%}.tablet-suffix-3{padding-right:52.46577%}.tablet-suffix-4{padding-right:69.95437%}.tablet-suffix-5{padding-right:87.44296%}.tablet-push-1{left:17.48859%}.tablet-push-2{left:34.97718%}.tablet-push-3{left:52.46577%}.tablet-push-4{left:69.95437%}.tablet-push-5{left:87.44296%}.tablet-pull-1{right:17.48859%}.tablet-pull-2{right:34.97718%}.tablet-pull-3{right:52.46577%}.tablet-pull-4{right:69.95437%}.tablet-pull-5{right:87.44296%}}@media screen and (min-width: 768px){.col-1,.col-2,.p-form--stacked .p-form__label,.col-3,.col-4,.p-form--stacked .p-form__control,.p-pod-summary__aside,.p-storage__name,.col-5,.col-6,.col-7,.col-8,.p-pod-summary__storage,.col-9,.col-10,.col-11{display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;margin-left:3.2877%}.row .col-1:first-child,.row .col-2:first-child,.row .p-form--stacked .p-form__label:first-child,.p-form--stacked .row .p-form__label:first-child,.row .col-3:first-child,.row .col-4:first-child,.row .p-form--stacked .p-form__control:first-child,.p-form--stacked .row .p-form__control:first-child,.row .p-pod-summary__aside:first-child,.row .p-storage__name:first-child,.row .col-5:first-child,.row .col-6:first-child,.row .col-7:first-child,.row .col-8:first-child,.row .p-pod-summary__storage:first-child,.row .col-9:first-child,.row .col-10:first-child,.row .col-11:first-child,.first-col{margin-left:0}.col-1{width:5.31961%}.col-2,.p-form--stacked .p-form__label{width:13.92692%}.col-3{width:22.53423%}.col-4,.p-form--stacked .p-form__control,.p-pod-summary__aside,.p-storage__name{width:31.14153%}.col-5{width:39.74884%}.col-6{width:48.35615%}.col-7{width:56.96346%}.col-8,.p-pod-summary__storage{width:65.57077%}.col-9{width:74.17808%}.col-10{width:82.78538%}.col-11{width:91.39269%}.prefix-1{padding-left:8.60731%}.prefix-2{padding-left:17.21462%}.prefix-3{padding-left:25.82192%}.prefix-4{padding-left:34.42923%}.prefix-5{padding-left:43.03654%}.prefix-6{padding-left:51.64385%}.prefix-7{padding-left:60.25116%}.prefix-8{padding-left:68.85847%}.prefix-9{padding-left:77.46577%}.prefix-10{padding-left:86.07308%}.prefix-11{padding-left:94.68039%}.suffix-1{padding-right:8.60731%}.suffix-2{padding-right:17.21462%}.suffix-3{padding-right:25.82192%}.suffix-4{padding-right:34.42923%}.suffix-5{padding-right:43.03654%}.suffix-6{padding-right:51.64385%}.suffix-7{padding-right:60.25116%}.suffix-8{padding-right:68.85847%}.suffix-9{padding-right:77.46577%}.suffix-10{padding-right:86.07308%}.suffix-11{padding-right:94.68039%}.push-1{left:8.60731%}.push-2{left:17.21462%}.push-3{left:25.82192%}.push-4{left:34.42923%}.push-5{left:43.03654%}.push-6{left:51.64385%}.push-7{left:60.25116%}.push-8{left:68.85847%}.push-9{left:77.46577%}.push-10{left:86.07308%}.push-11{left:94.68039%}.pull-1{right:8.60731%}.pull-2{right:17.21462%}.pull-3{right:25.82192%}.pull-4{right:34.42923%}.pull-5{right:43.03654%}.pull-6{right:51.64385%}.pull-7{right:60.25116%}.pull-8{right:68.85847%}.pull-9{right:77.46577%}.pull-10{right:86.07308%}.pull-11{right:94.68039%}.col-11 .col-1,.col-11 .col-2,.col-11 .p-form--stacked .p-form__label,.p-form--stacked .col-11 .p-form__label,.col-11 .col-3,.col-11 .col-4,.col-11 .p-form--stacked .p-form__control,.p-form--stacked .col-11 .p-form__control,.col-11 .p-pod-summary__aside,.col-11 .p-storage__name,.col-11 .col-5,.col-11 .col-6,.col-11 .col-7,.col-11 .col-8,.col-11 .p-pod-summary__storage,.col-11 .col-9,.col-11 .col-10{margin-left:3.59733%}.col-11 .col-1{width:5.82061%}.col-11 .col-2,.col-11 .p-form--stacked .p-form__label,.p-form--stacked .col-11 .p-form__label{width:15.23855%}.col-11 .col-3{width:24.65649%}.col-11 .col-4,.col-11 .p-form--stacked .p-form__control,.p-form--stacked .col-11 .p-form__control,.col-11 .p-pod-summary__aside,.col-11 .p-storage__name{width:34.07442%}.col-11 .col-5{width:43.49236%}.col-11 .col-6{width:52.9103%}.col-11 .col-7{width:62.32824%}.col-11 .col-8,.col-11 .p-pod-summary__storage{width:71.74618%}.col-11 .col-9{width:81.16412%}.col-11 .col-10{width:90.58206%}.col-11 .prefix-1{padding-left:9.41794%}.col-11 .prefix-2{padding-left:18.83588%}.col-11 .prefix-3{padding-left:28.25382%}.col-11 .prefix-4{padding-left:37.67176%}.col-11 .prefix-5{padding-left:47.0897%}.col-11 .prefix-6{padding-left:56.50764%}.col-11 .prefix-7{padding-left:65.92558%}.col-11 .prefix-8{padding-left:75.34351%}.col-11 .prefix-9{padding-left:84.76145%}.col-11 .prefix-10{padding-left:94.17939%}.col-11 .suffix-1{padding-right:9.41794%}.col-11 .suffix-2{padding-right:18.83588%}.col-11 .suffix-3{padding-right:28.25382%}.col-11 .suffix-4{padding-right:37.67176%}.col-11 .suffix-5{padding-right:47.0897%}.col-11 .suffix-6{padding-right:56.50764%}.col-11 .suffix-7{padding-right:65.92558%}.col-11 .suffix-8{padding-right:75.34351%}.col-11 .suffix-9{padding-right:84.76145%}.col-11 .suffix-10{padding-right:94.17939%}.col-11 .push-1{left:9.41794%}.col-11 .push-2{left:18.83588%}.col-11 .push-3{left:28.25382%}.col-11 .push-4{left:37.67176%}.col-11 .push-5{left:47.0897%}.col-11 .push-6{left:56.50764%}.col-11 .push-7{left:65.92558%}.col-11 .push-8{left:75.34351%}.col-11 .push-9{left:84.76145%}.col-11 .push-10{left:94.17939%}.col-11 .pull-1{right:9.41794%}.col-11 .pull-2{right:18.83588%}.col-11 .pull-3{right:28.25382%}.col-11 .pull-4{right:37.67176%}.col-11 .pull-5{right:47.0897%}.col-11 .pull-6{right:56.50764%}.col-11 .pull-7{right:65.92558%}.col-11 .pull-8{right:75.34351%}.col-11 .pull-9{right:84.76145%}.col-11 .pull-10{right:94.17939%}.col-10 .col-1,.col-10 .col-2,.col-10 .p-form--stacked .p-form__label,.p-form--stacked .col-10 .p-form__label,.col-10 .col-3,.col-10 .col-4,.col-10 .p-form--stacked .p-form__control,.p-form--stacked .col-10 .p-form__control,.col-10 .p-pod-summary__aside,.col-10 .p-storage__name,.col-10 .col-5,.col-10 .col-6,.col-10 .col-7,.col-10 .col-8,.col-10 .p-pod-summary__storage,.col-10 .col-9{margin-left:3.97135%}.col-10 .col-1{width:6.42578%}.col-10 .col-2,.col-10 .p-form--stacked .p-form__label,.p-form--stacked .col-10 .p-form__label{width:16.82292%}.col-10 .col-3{width:27.22005%}.col-10 .col-4,.col-10 .p-form--stacked .p-form__control,.p-form--stacked .col-10 .p-form__control,.col-10 .p-pod-summary__aside,.col-10 .p-storage__name{width:37.61719%}.col-10 .col-5{width:48.01432%}.col-10 .col-6{width:58.41146%}.col-10 .col-7{width:68.80859%}.col-10 .col-8,.col-10 .p-pod-summary__storage{width:79.20573%}.col-10 .col-9{width:89.60286%}.col-10 .prefix-1{padding-left:10.39714%}.col-10 .prefix-2{padding-left:20.79427%}.col-10 .prefix-3{padding-left:31.19141%}.col-10 .prefix-4{padding-left:41.58854%}.col-10 .prefix-5{padding-left:51.98568%}.col-10 .prefix-6{padding-left:62.38281%}.col-10 .prefix-7{padding-left:72.77995%}.col-10 .prefix-8{padding-left:83.17708%}.col-10 .prefix-9{padding-left:93.57422%}.col-10 .suffix-1{padding-right:10.39714%}.col-10 .suffix-2{padding-right:20.79427%}.col-10 .suffix-3{padding-right:31.19141%}.col-10 .suffix-4{padding-right:41.58854%}.col-10 .suffix-5{padding-right:51.98568%}.col-10 .suffix-6{padding-right:62.38281%}.col-10 .suffix-7{padding-right:72.77995%}.col-10 .suffix-8{padding-right:83.17708%}.col-10 .suffix-9{padding-right:93.57422%}.col-10 .push-1{left:10.39714%}.col-10 .push-2{left:20.79427%}.col-10 .push-3{left:31.19141%}.col-10 .push-4{left:41.58854%}.col-10 .push-5{left:51.98568%}.col-10 .push-6{left:62.38281%}.col-10 .push-7{left:72.77995%}.col-10 .push-8{left:83.17708%}.col-10 .push-9{left:93.57422%}.col-10 .pull-1{right:10.39714%}.col-10 .pull-2{right:20.79427%}.col-10 .pull-3{right:31.19141%}.col-10 .pull-4{right:41.58854%}.col-10 .pull-5{right:51.98568%}.col-10 .pull-6{right:62.38281%}.col-10 .pull-7{right:72.77995%}.col-10 .pull-8{right:83.17708%}.col-10 .pull-9{right:93.57422%}.col-9 .col-1,.col-9 .col-2,.col-9 .p-form--stacked .p-form__label,.p-form--stacked .col-9 .p-form__label,.col-9 .col-3,.col-9 .col-4,.col-9 .p-form--stacked .p-form__control,.p-form--stacked .col-9 .p-form__control,.col-9 .p-pod-summary__aside,.col-9 .p-storage__name,.col-9 .col-5,.col-9 .col-6,.col-9 .col-7,.col-9 .col-8,.col-9 .p-pod-summary__storage{margin-left:4.43217%}.col-9 .col-1{width:7.1714%}.col-9 .col-2,.col-9 .p-form--stacked .p-form__label,.p-form--stacked .col-9 .p-form__label{width:18.77498%}.col-9 .col-3{width:30.37855%}.col-9 .col-4,.col-9 .p-form--stacked .p-form__control,.p-form--stacked .col-9 .p-form__control,.col-9 .p-pod-summary__aside,.col-9 .p-storage__name{width:41.98213%}.col-9 .col-5{width:53.5857%}.col-9 .col-6{width:65.18928%}.col-9 .col-7{width:76.79285%}.col-9 .col-8,.col-9 .p-pod-summary__storage{width:88.39643%}.col-9 .prefix-1{padding-left:11.60357%}.col-9 .prefix-2{padding-left:23.20715%}.col-9 .prefix-3{padding-left:34.81072%}.col-9 .prefix-4{padding-left:46.4143%}.col-9 .prefix-5{padding-left:58.01787%}.col-9 .prefix-6{padding-left:69.62145%}.col-9 .prefix-7{padding-left:81.22502%}.col-9 .prefix-8{padding-left:92.8286%}.col-9 .suffix-1{padding-right:11.60357%}.col-9 .suffix-2{padding-right:23.20715%}.col-9 .suffix-3{padding-right:34.81072%}.col-9 .suffix-4{padding-right:46.4143%}.col-9 .suffix-5{padding-right:58.01787%}.col-9 .suffix-6{padding-right:69.62145%}.col-9 .suffix-7{padding-right:81.22502%}.col-9 .suffix-8{padding-right:92.8286%}.col-9 .push-1{left:11.60357%}.col-9 .push-2{left:23.20715%}.col-9 .push-3{left:34.81072%}.col-9 .push-4{left:46.4143%}.col-9 .push-5{left:58.01787%}.col-9 .push-6{left:69.62145%}.col-9 .push-7{left:81.22502%}.col-9 .push-8{left:92.8286%}.col-9 .pull-1{right:11.60357%}.col-9 .pull-2{right:23.20715%}.col-9 .pull-3{right:34.81072%}.col-9 .pull-4{right:46.4143%}.col-9 .pull-5{right:58.01787%}.col-9 .pull-6{right:69.62145%}.col-9 .pull-7{right:81.22502%}.col-9 .pull-8{right:92.8286%}.col-8 .col-1,.p-pod-summary__storage .col-1,.col-8 .col-2,.p-pod-summary__storage .col-2,.col-8 .p-form--stacked .p-form__label,.p-form--stacked .col-8 .p-form__label,.p-pod-summary__storage .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__storage .p-form__label,.col-8 .col-3,.p-pod-summary__storage .col-3,.col-8 .col-4,.p-pod-summary__storage .col-4,.col-8 .p-form--stacked .p-form__control,.p-form--stacked .col-8 .p-form__control,.p-pod-summary__storage .p-form--stacked .p-form__control,.p-form--stacked .p-pod-summary__storage .p-form__control,.col-8 .p-pod-summary__aside,.p-pod-summary__storage .p-pod-summary__aside,.col-8 .p-storage__name,.p-pod-summary__storage .p-storage__name,.col-8 .col-5,.p-pod-summary__storage .col-5,.col-8 .col-6,.p-pod-summary__storage .col-6,.col-8 .col-7,.p-pod-summary__storage .col-7{margin-left:5.01397%}.col-8 .col-1,.p-pod-summary__storage .col-1{width:8.11278%}.col-8 .col-2,.p-pod-summary__storage .col-2,.col-8 .p-form--stacked .p-form__label,.p-form--stacked .col-8 .p-form__label,.p-pod-summary__storage .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__storage .p-form__label{width:21.23952%}.col-8 .col-3,.p-pod-summary__storage .col-3{width:34.36627%}.col-8 .col-4,.p-pod-summary__storage .col-4,.col-8 .p-form--stacked .p-form__control,.p-form--stacked .col-8 .p-form__control,.p-pod-summary__storage .p-form--stacked .p-form__control,.p-form--stacked .p-pod-summary__storage .p-form__control,.col-8 .p-pod-summary__aside,.p-pod-summary__storage .p-pod-summary__aside,.col-8 .p-storage__name,.p-pod-summary__storage .p-storage__name{width:47.49301%}.col-8 .col-5,.p-pod-summary__storage .col-5{width:60.61976%}.col-8 .col-6,.p-pod-summary__storage .col-6{width:73.74651%}.col-8 .col-7,.p-pod-summary__storage .col-7{width:86.87325%}.col-8 .prefix-1,.p-pod-summary__storage .prefix-1{padding-left:13.12675%}.col-8 .prefix-2,.p-pod-summary__storage .prefix-2{padding-left:26.25349%}.col-8 .prefix-3,.p-pod-summary__storage .prefix-3{padding-left:39.38024%}.col-8 .prefix-4,.p-pod-summary__storage .prefix-4{padding-left:52.50699%}.col-8 .prefix-5,.p-pod-summary__storage .prefix-5{padding-left:65.63373%}.col-8 .prefix-6,.p-pod-summary__storage .prefix-6{padding-left:78.76048%}.col-8 .prefix-7,.p-pod-summary__storage .prefix-7{padding-left:91.88722%}.col-8 .suffix-1,.p-pod-summary__storage .suffix-1{padding-right:13.12675%}.col-8 .suffix-2,.p-pod-summary__storage .suffix-2{padding-right:26.25349%}.col-8 .suffix-3,.p-pod-summary__storage .suffix-3{padding-right:39.38024%}.col-8 .suffix-4,.p-pod-summary__storage .suffix-4{padding-right:52.50699%}.col-8 .suffix-5,.p-pod-summary__storage .suffix-5{padding-right:65.63373%}.col-8 .suffix-6,.p-pod-summary__storage .suffix-6{padding-right:78.76048%}.col-8 .suffix-7,.p-pod-summary__storage .suffix-7{padding-right:91.88722%}.col-8 .push-1,.p-pod-summary__storage .push-1{left:13.12675%}.col-8 .push-2,.p-pod-summary__storage .push-2{left:26.25349%}.col-8 .push-3,.p-pod-summary__storage .push-3{left:39.38024%}.col-8 .push-4,.p-pod-summary__storage .push-4{left:52.50699%}.col-8 .push-5,.p-pod-summary__storage .push-5{left:65.63373%}.col-8 .push-6,.p-pod-summary__storage .push-6{left:78.76048%}.col-8 .push-7,.p-pod-summary__storage .push-7{left:91.88722%}.col-8 .pull-1,.p-pod-summary__storage .pull-1{right:13.12675%}.col-8 .pull-2,.p-pod-summary__storage .pull-2{right:26.25349%}.col-8 .pull-3,.p-pod-summary__storage .pull-3{right:39.38024%}.col-8 .pull-4,.p-pod-summary__storage .pull-4{right:52.50699%}.col-8 .pull-5,.p-pod-summary__storage .pull-5{right:65.63373%}.col-8 .pull-6,.p-pod-summary__storage .pull-6{right:78.76048%}.col-8 .pull-7,.p-pod-summary__storage .pull-7{right:91.88722%}.col-7 .col-1,.col-7 .col-2,.col-7 .p-form--stacked .p-form__label,.p-form--stacked .col-7 .p-form__label,.col-7 .col-3,.col-7 .col-4,.col-7 .p-form--stacked .p-form__control,.p-form--stacked .col-7 .p-form__control,.col-7 .p-pod-summary__aside,.col-7 .p-storage__name,.col-7 .col-5,.col-7 .col-6{margin-left:5.77159%}.col-7 .col-1{width:9.33863%}.col-7 .col-2,.col-7 .p-form--stacked .p-form__label,.p-form--stacked .col-7 .p-form__label{width:24.44886%}.col-7 .col-3{width:39.55909%}.col-7 .col-4,.col-7 .p-form--stacked .p-form__control,.p-form--stacked .col-7 .p-form__control,.col-7 .p-pod-summary__aside,.col-7 .p-storage__name{width:54.66932%}.col-7 .col-5{width:69.77954%}.col-7 .col-6{width:84.88977%}.col-7 .prefix-1{padding-left:15.11023%}.col-7 .prefix-2{padding-left:30.22046%}.col-7 .prefix-3{padding-left:45.33068%}.col-7 .prefix-4{padding-left:60.44091%}.col-7 .prefix-5{padding-left:75.55114%}.col-7 .prefix-6{padding-left:90.66137%}.col-7 .suffix-1{padding-right:15.11023%}.col-7 .suffix-2{padding-right:30.22046%}.col-7 .suffix-3{padding-right:45.33068%}.col-7 .suffix-4{padding-right:60.44091%}.col-7 .suffix-5{padding-right:75.55114%}.col-7 .suffix-6{padding-right:90.66137%}.col-7 .push-1{left:15.11023%}.col-7 .push-2{left:30.22046%}.col-7 .push-3{left:45.33068%}.col-7 .push-4{left:60.44091%}.col-7 .push-5{left:75.55114%}.col-7 .push-6{left:90.66137%}.col-7 .pull-1{right:15.11023%}.col-7 .pull-2{right:30.22046%}.col-7 .pull-3{right:45.33068%}.col-7 .pull-4{right:60.44091%}.col-7 .pull-5{right:75.55114%}.col-7 .pull-6{right:90.66137%}.col-6 .col-1,.col-6 .col-2,.col-6 .p-form--stacked .p-form__label,.p-form--stacked .col-6 .p-form__label,.col-6 .col-3,.col-6 .col-4,.col-6 .p-form--stacked .p-form__control,.p-form--stacked .col-6 .p-form__control,.col-6 .p-pod-summary__aside,.col-6 .p-storage__name,.col-6 .col-5{margin-left:6.79893%}.col-6 .col-1{width:11.00089%}.col-6 .col-2,.col-6 .p-form--stacked .p-form__label,.p-form--stacked .col-6 .p-form__label{width:28.80072%}.col-6 .col-3{width:46.60054%}.col-6 .col-4,.col-6 .p-form--stacked .p-form__control,.p-form--stacked .col-6 .p-form__control,.col-6 .p-pod-summary__aside,.col-6 .p-storage__name{width:64.40036%}.col-6 .col-5{width:82.20018%}.col-6 .prefix-1{padding-left:17.79982%}.col-6 .prefix-2{padding-left:35.59964%}.col-6 .prefix-3{padding-left:53.39946%}.col-6 .prefix-4{padding-left:71.19928%}.col-6 .prefix-5{padding-left:88.99911%}.col-6 .suffix-1{padding-right:17.79982%}.col-6 .suffix-2{padding-right:35.59964%}.col-6 .suffix-3{padding-right:53.39946%}.col-6 .suffix-4{padding-right:71.19928%}.col-6 .suffix-5{padding-right:88.99911%}.col-6 .push-1{left:17.79982%}.col-6 .push-2{left:35.59964%}.col-6 .push-3{left:53.39946%}.col-6 .push-4{left:71.19928%}.col-6 .push-5{left:88.99911%}.col-6 .pull-1{right:17.79982%}.col-6 .pull-2{right:35.59964%}.col-6 .pull-3{right:53.39946%}.col-6 .pull-4{right:71.19928%}.col-6 .pull-5{right:88.99911%}.col-5 .col-1,.col-5 .col-2,.col-5 .p-form--stacked .p-form__label,.p-form--stacked .col-5 .p-form__label,.col-5 .col-3,.col-5 .col-4,.col-5 .p-form--stacked .p-form__control,.p-form--stacked .col-5 .p-form__control,.col-5 .p-pod-summary__aside,.col-5 .p-storage__name{margin-left:8.27118%}.col-5 .col-1{width:13.38305%}.col-5 .col-2,.col-5 .p-form--stacked .p-form__label,.p-form--stacked .col-5 .p-form__label{width:35.03729%}.col-5 .col-3{width:56.69153%}.col-5 .col-4,.col-5 .p-form--stacked .p-form__control,.p-form--stacked .col-5 .p-form__control,.col-5 .p-pod-summary__aside,.col-5 .p-storage__name{width:78.34576%}.col-5 .prefix-1{padding-left:21.65424%}.col-5 .prefix-2{padding-left:43.30847%}.col-5 .prefix-3{padding-left:64.96271%}.col-5 .prefix-4{padding-left:86.61695%}.col-5 .suffix-1{padding-right:21.65424%}.col-5 .suffix-2{padding-right:43.30847%}.col-5 .suffix-3{padding-right:64.96271%}.col-5 .suffix-4{padding-right:86.61695%}.col-5 .push-1{left:21.65424%}.col-5 .push-2{left:43.30847%}.col-5 .push-3{left:64.96271%}.col-5 .push-4{left:86.61695%}.col-5 .pull-1{right:21.65424%}.col-5 .pull-2{right:43.30847%}.col-5 .pull-3{right:64.96271%}.col-5 .pull-4{right:86.61695%}.col-4 .col-1,.p-form--stacked .p-form__control .col-1,.p-pod-summary__aside .col-1,.p-storage__name .col-1,.col-4 .col-2,.p-form--stacked .p-form__control .col-2,.p-pod-summary__aside .col-2,.p-storage__name .col-2,.col-4 .p-form--stacked .p-form__label,.p-form--stacked .col-4 .p-form__label,.p-form--stacked .p-form__control .p-form__label,.p-pod-summary__aside .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__aside .p-form__label,.p-storage__name .p-form--stacked .p-form__label,.p-form--stacked .p-storage__name .p-form__label,.col-4 .col-3,.p-form--stacked .p-form__control .col-3,.p-pod-summary__aside .col-3,.p-storage__name .col-3{margin-left:10.55728%}.col-4 .col-1,.p-form--stacked .p-form__control .col-1,.p-pod-summary__aside .col-1,.p-storage__name .col-1{width:17.08204%}.col-4 .col-2,.p-form--stacked .p-form__control .col-2,.p-pod-summary__aside .col-2,.p-storage__name .col-2,.col-4 .p-form--stacked .p-form__label,.p-form--stacked .col-4 .p-form__label,.p-form--stacked .p-form__control .p-form__label,.p-pod-summary__aside .p-form--stacked .p-form__label,.p-form--stacked .p-pod-summary__aside .p-form__label,.p-storage__name .p-form--stacked .p-form__label,.p-form--stacked .p-storage__name .p-form__label{width:44.72136%}.col-4 .col-3,.p-form--stacked .p-form__control .col-3,.p-pod-summary__aside .col-3,.p-storage__name .col-3{width:72.36068%}.col-4 .prefix-1,.p-form--stacked .p-form__control .prefix-1,.p-pod-summary__aside .prefix-1,.p-storage__name .prefix-1{padding-left:27.63932%}.col-4 .prefix-2,.p-form--stacked .p-form__control .prefix-2,.p-pod-summary__aside .prefix-2,.p-storage__name .prefix-2{padding-left:55.27864%}.col-4 .prefix-3,.p-form--stacked .p-form__control .prefix-3,.p-pod-summary__aside .prefix-3,.p-storage__name .prefix-3{padding-left:82.91796%}.col-4 .suffix-1,.p-form--stacked .p-form__control .suffix-1,.p-pod-summary__aside .suffix-1,.p-storage__name .suffix-1{padding-right:27.63932%}.col-4 .suffix-2,.p-form--stacked .p-form__control .suffix-2,.p-pod-summary__aside .suffix-2,.p-storage__name .suffix-2{padding-right:55.27864%}.col-4 .suffix-3,.p-form--stacked .p-form__control .suffix-3,.p-pod-summary__aside .suffix-3,.p-storage__name .suffix-3{padding-right:82.91796%}.col-4 .push-1,.p-form--stacked .p-form__control .push-1,.p-pod-summary__aside .push-1,.p-storage__name .push-1{left:27.63932%}.col-4 .push-2,.p-form--stacked .p-form__control .push-2,.p-pod-summary__aside .push-2,.p-storage__name .push-2{left:55.27864%}.col-4 .push-3,.p-form--stacked .p-form__control .push-3,.p-pod-summary__aside .push-3,.p-storage__name .push-3{left:82.91796%}.col-4 .pull-1,.p-form--stacked .p-form__control .pull-1,.p-pod-summary__aside .pull-1,.p-storage__name .pull-1{right:27.63932%}.col-4 .pull-2,.p-form--stacked .p-form__control .pull-2,.p-pod-summary__aside .pull-2,.p-storage__name .pull-2{right:55.27864%}.col-4 .pull-3,.p-form--stacked .p-form__control .pull-3,.p-pod-summary__aside .pull-3,.p-storage__name .pull-3{right:82.91796%}.col-3 .col-1,.col-3 .col-2,.col-3 .p-form--stacked .p-form__label,.p-form--stacked .col-3 .p-form__label{margin-left:14.5898%}.col-3 .col-1{width:23.6068%}.col-3 .col-2,.col-3 .p-form--stacked .p-form__label,.p-form--stacked .col-3 .p-form__label{width:61.8034%}.col-3 .prefix-1{padding-left:38.1966%}.col-3 .prefix-2{padding-left:76.3932%}.col-3 .suffix-1{padding-right:38.1966%}.col-3 .suffix-2{padding-right:76.3932%}.col-3 .push-1{left:38.1966%}.col-3 .push-2{left:76.3932%}.col-3 .pull-1{right:38.1966%}.col-3 .pull-2{right:76.3932%}.col-2 .col-1,.p-form--stacked .p-form__label .col-1{margin-left:23.6068%}.col-2 .col-1,.p-form--stacked .p-form__label .col-1{width:38.1966%}.col-2 .prefix-1,.p-form--stacked .p-form__label .prefix-1{padding-left:61.8034%}.col-2 .suffix-1,.p-form--stacked .p-form__label .suffix-1{padding-right:61.8034%}.col-2 .push-1,.p-form--stacked .p-form__label .push-1{left:61.8034%}.col-2 .pull-1,.p-form--stacked .p-form__label .pull-1{right:61.8034%}}.row .center-col{float:none;margin-left:auto !important;margin-right:auto}@media screen and (max-width: 619px){.hidden-mobile,.visible-tablet,.visible-desktop{display:none !important}}@media screen and (min-width: 620px) and (max-width: 767px){.visible-mobile,.hidden-tablet,.visible-desktop{display:none !important}}@media screen and (min-width: 768px){.visible-mobile,.visible-tablet,.hidden-desktop{display:none !important}}.p-heading-icon{margin-bottom:1rem}@media (min-width: 768px){.p-heading-icon{margin-bottom:0}}.p-heading-icon__header{display:flex;margin-bottom:.75rem}.p-heading-icon__title{margin-bottom:0;padding-top:0}.p-heading-icon__img{flex-shrink:0;height:2.5rem;margin-right:1rem;width:2.5rem}@media (min-width: 768px){.p-heading-icon__img{height:3.75rem;width:3.75rem}}.p-icon--plus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--plus,.p-icon--plus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23cdcdcd' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--minus{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--minus,.p-icon--minus.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23cdcdcd' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--expand{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23666' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23666' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23666' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--expand,.p-icon--expand.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23cdcdcd' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23cdcdcd' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23cdcdcd' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--collapse{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23666' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23666' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--collapse,.p-icon--collapse.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='15' width='15' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='none'/%3E%3Cpath stroke='%23cdcdcd' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23cdcdcd' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--chevron{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--chevron,.p-icon--chevron.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23cdcdcd' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-icon--close{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23666'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--close,.p-icon--close.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23cdcdcd'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--help{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' color='%23000' d='M-.003.002h16v16h-16z'/%3E%3Cpath d='M8.004 5.23q-.431 0-.825.11-.394.098-.825.332l-.419-1.145q.456-.258 1.035-.406.59-.16 1.206-.16.739 0 1.219.21.48.196.763.504.283.308.394.677.111.37.111.714 0 .419-.16.751-.148.333-.382.616t-.504.542q-.271.246-.505.517-.234.258-.394.554-.148.295-.148.664v.148q0 .074.012.148h-1.28q-.025-.123-.037-.259-.012-.147-.012-.27 0-.407.135-.727.136-.32.345-.59t.443-.506q.246-.234.456-.467.209-.234.344-.48.136-.247.136-.542 0-.407-.283-.665-.271-.271-.825-.271zM8.984 12.01q0 .43-.283.7-.284.272-.702.272-.406 0-.702-.271-.283-.271-.283-.702 0-.43.283-.702.296-.283.702-.283.418 0 .702.283.283.271.283.702z' fill='%23666'/%3E%3Cpath d='M2.064 1.002c-.591 0-1.067.476-1.067 1.067v11.867c0 .591.476 1.067 1.067 1.067H13.93c.591 0 1.067-.476 1.067-1.067V2.07c0-.591-.476-1.067-1.067-1.067zm-.067 1h12v12h-12z' fill='%23666' color='%23000'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--help,.p-icon--help.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' color='%23000' d='M-.003.002h16v16h-16z'/%3E%3Cpath d='M8.004 5.23q-.431 0-.825.11-.394.098-.825.332l-.419-1.145q.456-.258 1.035-.406.59-.16 1.206-.16.739 0 1.219.21.48.196.763.504.283.308.394.677.111.37.111.714 0 .419-.16.751-.148.333-.382.616t-.504.542q-.271.246-.505.517-.234.258-.394.554-.148.295-.148.664v.148q0 .074.012.148h-1.28q-.025-.123-.037-.259-.012-.147-.012-.27 0-.407.135-.727.136-.32.345-.59t.443-.506q.246-.234.456-.467.209-.234.344-.48.136-.247.136-.542 0-.407-.283-.665-.271-.271-.825-.271zM8.984 12.01q0 .43-.283.7-.284.272-.702.272-.406 0-.702-.271-.283-.271-.283-.702 0-.43.283-.702.296-.283.702-.283.418 0 .702.283.283.271.283.702z' fill='%23cdcdcd'/%3E%3Cpath d='M2.064 1.002c-.591 0-1.067.476-1.067 1.067v11.867c0 .591.476 1.067 1.067 1.067H13.93c.591 0 1.067-.476 1.067-1.067V2.07c0-.591-.476-1.067-1.067-1.067zm-.067 1h12v12h-12z' fill='%23cdcdcd' color='%23000'/%3E%3C/svg%3E")}.p-icon--information,.p-icon--info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath d='M2.07 1c-.59 0-1.066.475-1.066 1.066v11.867c0 .591.475 1.067 1.066 1.067h11.867c.591 0 1.066-.476 1.066-1.067V2.066c0-.59-.475-1.066-1.066-1.066zm-.066 1h12v12h-12z' fill='%23666'/%3E%3Cpath d='M7 4v2h2V4zm0 3v5h2V7z' fill='%23666'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--information,[class*="--dark"] .p-icon--info,.p-icon--information.is-light,.is-light.p-icon--info{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath d='M2.07 1c-.59 0-1.066.475-1.066 1.066v11.867c0 .591.475 1.067 1.066 1.067h11.867c.591 0 1.066-.476 1.066-1.067V2.066c0-.59-.475-1.066-1.066-1.066zm-.066 1h12v12h-12z' fill='%23cdcdcd'/%3E%3Cpath d='M7 4v2h2V4zm0 3v5h2V7z' fill='%23cdcdcd'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--delete{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M2 4v1h2V4H2zm11 0v1h2V4h-2zM2 6v8.506c0 .822.678 1.5 1.5 1.5h10c.822 0 1.5-.678 1.5-1.5V6h-2v7.506c0 .286-.214.5-.5.5h-8a.488.488 0 0 1-.5-.5V6H2z' fill='%23666'/%3E%3Cpath d='M6 0v3h1V1h3v2h1V0H6zM5 6h1v6H5zM8 6h1v6H8zM11 6h1v6h-1z' fill='%23666'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M3.5 2C2.678 2 2 2.678 2 3.5V5h13V3.5c0-.822-.678-1.5-1.5-1.5h-10zM2 6v8.006h2V6H2zm11 0v8.006h2V6h-2z' fill='%23666'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--delete,.p-icon--delete.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M2 4v1h2V4H2zm11 0v1h2V4h-2zM2 6v8.506c0 .822.678 1.5 1.5 1.5h10c.822 0 1.5-.678 1.5-1.5V6h-2v7.506c0 .286-.214.5-.5.5h-8a.488.488 0 0 1-.5-.5V6H2z' fill='%23cdcdcd'/%3E%3Cpath d='M6 0v3h1V1h3v2h1V0H6zM5 6h1v6H5zM8 6h1v6H8zM11 6h1v6h-1z' fill='%23cdcdcd'/%3E%3Cpath style='text-decoration-color:%23000;isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-decoration-style:solid;text-indent:0;text-transform:none' d='M3.5 2C2.678 2 2 2.678 2 3.5V5h13V3.5c0-.822-.678-1.5-1.5-1.5h-10zM2 6v8.006h2V6H2zm11 0v8.006h2V6h-2z' fill='%23cdcdcd'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--error{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' viewBox='0 0 16.000017 16.000017' width='16'%3E%3Cg stroke-width='1.5' color='%23000'%3E%3Cpath d='M8 0C3.5906 0 0 3.5906 0 8s3.5906 8 8 8 8-3.5906 8-8-3.5906-8-8-8z' fill='%23c7162b'/%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath d='M5 5l6 6m0-6l-6 6' stroke-dashoffset='.8' stroke='%23fff' fill='none'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--warning{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath stroke-linejoin='round' fill='%23f99b11' transform='matrix%282.28 0 0 2.437 -2180.8 -490.52%29' stroke='%23f99b11' stroke-width='.848' d='M963.07 207.03h-6.15l3.08-5.33z'/%3E%3Cpath d='M7 5v5h2V5H7zm0 6v2h2v-2H7z' fill='%23111'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--external-link{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' d='M.003.001h16v16h-16z'/%3E%3Cpath d='M8.581 2.068S12.208.631 16-.005c.002 0 .002 0 .002.002v.006h.002c-.708 3.964-2.08 7.406-2.08 7.406L8.58 2.069z' fill='%23666'/%3E%3Cpath stroke-linejoin='round' d='M7.87 8.128l4.446-4.445' stroke='%23666' stroke-width='2.00001' fill='none'/%3E%3Cpath d='M1.503 2.001c-.822 0-1.5.678-1.5 1.5v11c0 .823.678 1.5 1.5 1.5h11c.823 0 1.5-.677 1.5-1.5v-5.5h-2v4.5c0 .287-.214.5-.5.5h-9a.488.488 0 0 1-.5-.5v-9c0-.285.215-.5.5-.5h4.5v-2h-5.5z' fill='%23666'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--external-link,.p-icon--external-link.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' d='M.003.001h16v16h-16z'/%3E%3Cpath d='M8.581 2.068S12.208.631 16-.005c.002 0 .002 0 .002.002v.006h.002c-.708 3.964-2.08 7.406-2.08 7.406L8.58 2.069z' fill='%23cdcdcd'/%3E%3Cpath stroke-linejoin='round' d='M7.87 8.128l4.446-4.445' stroke='%23cdcdcd' stroke-width='2.00001' fill='none'/%3E%3Cpath d='M1.503 2.001c-.822 0-1.5.678-1.5 1.5v11c0 .823.678 1.5 1.5 1.5h11c.823 0 1.5-.677 1.5-1.5v-5.5h-2v4.5c0 .287-.214.5-.5.5h-9a.488.488 0 0 1-.5-.5v-9c0-.285.215-.5.5-.5h4.5v-2h-5.5z' fill='%23cdcdcd'/%3E%3C/svg%3E")}.p-icon--contextual-menu{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='14' width='6' viewBox='0 0 6 14'%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cpath d='M-10-6h26v26h-26z'/%3E%3Cpath fill-rule='nonzero' fill='%23666' d='M0 0v2h6V0M0 6v2h6V6m-6 6v2h6v-2'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--contextual-menu,.p-icon--contextual-menu.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='14' width='6' viewBox='0 0 6 14'%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cpath d='M-10-6h26v26h-26z'/%3E%3Cpath fill-rule='nonzero' fill='%23cdcdcd' d='M0 0v2h6V0M0 6v2h6V6m-6 6v2h6v-2'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--code{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.212' fill='none' d='M.005.002h16v16h-16z'/%3E%3Cpath d='M2.671 2.002c-1.778 0-2.666 0-2.666 2.068v8.866c0 2.067.888 2.066 2.666 2.066H13.34c1.778 0 2.666 0 2.666-2.066v-8.8c0-2.133-.888-2.134-2.666-2.134H2.671zm1.28 1.89h1.101v1.143c.339.028.642.078.91.148.268.064.48.128.635.192L6.333 6.42a6.601 6.601 0 0 0-.73-.222 3.858 3.858 0 0 0-.953-.106c-.382 0-.67.072-.86.213a.646.646 0 0 0-.285.56c0 .142.028.261.084.36a.875.875 0 0 0 .256.254c.113.07.25.142.412.213.162.063.346.13.55.201.29.113.561.233.815.36.261.12.487.266.678.435.19.163.34.356.445.582.113.226.17.494.17.805 0 .466-.144.868-.433 1.207-.29.339-.766.558-1.43.657v1.324H3.95V11.97c-.508-.036-.922-.103-1.24-.201a4.692 4.692 0 0 1-.697-.286l.36-1.005c.225.113.496.214.814.306.324.092.692.139 1.101.139.487 0 .823-.072 1.006-.213a.703.703 0 0 0 .287-.582.764.764 0 0 0-.117-.424 1.09 1.09 0 0 0-.328-.319 2.828 2.828 0 0 0-.508-.253c-.19-.078-.404-.158-.637-.243a8.505 8.505 0 0 1-.656-.265 2.866 2.866 0 0 1-.582-.36c-.17-.148-.306-.324-.412-.529s-.16-.456-.16-.752c0-.487.146-.901.435-1.24.29-.346.734-.567 1.334-.666V3.892zm4.054 8.096h3.99v.996h-3.99v-.996z' fill='%23666'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--code,.p-icon--code.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.212' fill='none' d='M.005.002h16v16h-16z'/%3E%3Cpath d='M2.671 2.002c-1.778 0-2.666 0-2.666 2.068v8.866c0 2.067.888 2.066 2.666 2.066H13.34c1.778 0 2.666 0 2.666-2.066v-8.8c0-2.133-.888-2.134-2.666-2.134H2.671zm1.28 1.89h1.101v1.143c.339.028.642.078.91.148.268.064.48.128.635.192L6.333 6.42a6.601 6.601 0 0 0-.73-.222 3.858 3.858 0 0 0-.953-.106c-.382 0-.67.072-.86.213a.646.646 0 0 0-.285.56c0 .142.028.261.084.36a.875.875 0 0 0 .256.254c.113.07.25.142.412.213.162.063.346.13.55.201.29.113.561.233.815.36.261.12.487.266.678.435.19.163.34.356.445.582.113.226.17.494.17.805 0 .466-.144.868-.433 1.207-.29.339-.766.558-1.43.657v1.324H3.95V11.97c-.508-.036-.922-.103-1.24-.201a4.692 4.692 0 0 1-.697-.286l.36-1.005c.225.113.496.214.814.306.324.092.692.139 1.101.139.487 0 .823-.072 1.006-.213a.703.703 0 0 0 .287-.582.764.764 0 0 0-.117-.424 1.09 1.09 0 0 0-.328-.319 2.828 2.828 0 0 0-.508-.253c-.19-.078-.404-.158-.637-.243a8.505 8.505 0 0 1-.656-.265 2.866 2.866 0 0 1-.582-.36c-.17-.148-.306-.324-.412-.529s-.16-.456-.16-.752c0-.487.146-.901.435-1.24.29-.346.734-.567 1.334-.666V3.892zm4.054 8.096h3.99v.996h-3.99v-.996z' fill='%23cdcdcd'/%3E%3C/svg%3E")}.p-icon--menu{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='19' width='25' viewBox='0 0 79 60'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23666' d='M.995 0h78v12h-78zm0 24h78v12h-78zm0 24h78v12h-78z'/%3E%3Cpath d='M-5.005-15h90v90h-90z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--menu,.p-icon--menu.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='19' width='25' viewBox='0 0 79 60'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath fill='%23cdcdcd' d='M.995 0h78v12h-78zm0 24h78v12h-78zm0 24h78v12h-78z'/%3E%3Cpath d='M-5.005-15h90v90h-90z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--copy{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='17' width='16'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M10.587 1.8h3.259c.472 0 .846.053 1.161.2s.567.412.716.748c.298.67.266 1.491.277 2.613V13.84c-.011 1.121.021 1.942-.277 2.613-.149.335-.401.6-.716.747s-.689.2-1.161.2H4.154c-.472 0-.846-.053-1.16-.2s-.568-.412-.717-.747c-.246-.554-.268-1.21-.273-2.053h.803c.016.854.058 1.428.178 1.707.072.166.151.26.336.348s.477.145.896.145h9.566c.42 0 .712-.057.897-.145a.602.602 0 0 0 .335-.348c.143-.331.175-1.081.185-2.222V5.309c-.01-1.137-.042-1.885-.185-2.216a.603.603 0 0 0-.335-.348c-.185-.088-.477-.145-.897-.145h-3.538c.182-.225.304-.5.342-.8zm-3.174 0c.038.3.16.575.341.8H4.217c-.42 0-.712.057-.896.145a.603.603 0 0 0-.336.348c-.143.33-.175 1.079-.185 2.216V10.8H2V5.361c.01-1.122-.021-1.942.277-2.613.149-.336.401-.601.716-.748s.689-.2 1.16-.2h3.26z'/%3E%3Cpath fill-rule='nonzero' d='M11.398 1.8v2.4H6.6V1.8h1.6c0 .447.353.8.8.8.445 0 .799-.353.799-.8h1.6z'/%3E%3Cpath fill-rule='nonzero' d='M10.6 1.6c0 .879-.722 1.6-1.6 1.6-.879 0-1.6-.721-1.6-1.6C7.4.72 8.121 0 9 0c.879 0 1.6.72 1.6 1.6zm-.8 0c0-.447-.354-.8-.8-.8-.447 0-.8.353-.8.8 0 .446.353.8.8.8.446 0 .8-.354.8-.8z'/%3E%3Cpath d='M8.4 7.2H14v1H8.4zM8.4 9.6H14v1H8.4zM10 12h4v1h-4z'/%3E%3Cpath fill-rule='nonzero' d='M4.4 10s2.134 1.026 4 2.505h-.002C6.427 14.03 4.4 15 4.4 15v-5z'/%3E%3Cpath d='M0 11.6h4.4v2H0z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--copy,.p-icon--copy.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='17' width='16'%3E%3Cg fill='%23cdcdcd' fill-rule='evenodd'%3E%3Cpath d='M10.587 1.8h3.259c.472 0 .846.053 1.161.2s.567.412.716.748c.298.67.266 1.491.277 2.613V13.84c-.011 1.121.021 1.942-.277 2.613-.149.335-.401.6-.716.747s-.689.2-1.161.2H4.154c-.472 0-.846-.053-1.16-.2s-.568-.412-.717-.747c-.246-.554-.268-1.21-.273-2.053h.803c.016.854.058 1.428.178 1.707.072.166.151.26.336.348s.477.145.896.145h9.566c.42 0 .712-.057.897-.145a.602.602 0 0 0 .335-.348c.143-.331.175-1.081.185-2.222V5.309c-.01-1.137-.042-1.885-.185-2.216a.603.603 0 0 0-.335-.348c-.185-.088-.477-.145-.897-.145h-3.538c.182-.225.304-.5.342-.8zm-3.174 0c.038.3.16.575.341.8H4.217c-.42 0-.712.057-.896.145a.603.603 0 0 0-.336.348c-.143.33-.175 1.079-.185 2.216V10.8H2V5.361c.01-1.122-.021-1.942.277-2.613.149-.336.401-.601.716-.748s.689-.2 1.16-.2h3.26z'/%3E%3Cpath fill-rule='nonzero' d='M11.398 1.8v2.4H6.6V1.8h1.6c0 .447.353.8.8.8.445 0 .799-.353.799-.8h1.6z'/%3E%3Cpath fill-rule='nonzero' d='M10.6 1.6c0 .879-.722 1.6-1.6 1.6-.879 0-1.6-.721-1.6-1.6C7.4.72 8.121 0 9 0c.879 0 1.6.72 1.6 1.6zm-.8 0c0-.447-.354-.8-.8-.8-.447 0-.8.353-.8.8 0 .446.353.8.8.8.446 0 .8-.354.8-.8z'/%3E%3Cpath d='M8.4 7.2H14v1H8.4zM8.4 9.6H14v1H8.4zM10 12h4v1h-4z'/%3E%3Cpath fill-rule='nonzero' d='M4.4 10s2.134 1.026 4 2.505h-.002C6.427 14.03 4.4 15 4.4 15v-5z'/%3E%3Cpath d='M0 11.6h4.4v2H0z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--search{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg transform='translate%28-74.67 -285.57%29 scale%28.66667%29' color='%23000'%3E%3Cpath opacity='.05' fill='none' d='M112 452.36h24v-24h-24z'/%3E%3Cpath style='isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M129.93 444.03l-2.27 2.273 6.07 6.07 2.27-2.27z' fill='%23666'/%3E%3Cellipse stroke-linejoin='round' stroke='%23666' rx='9.479' ry='9.479' cy='438.86' cx='122.5' stroke-linecap='round' stroke-width='2.041' fill='none'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--search,.p-icon--search.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg transform='translate%28-74.67 -285.57%29 scale%28.66667%29' color='%23000'%3E%3Cpath opacity='.05' fill='none' d='M112 452.36h24v-24h-24z'/%3E%3Cpath style='isolation:auto;mix-blend-mode:normal;block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M129.93 444.03l-2.27 2.273 6.07 6.07 2.27-2.27z' fill='%23cdcdcd'/%3E%3Cellipse stroke-linejoin='round' stroke='%23cdcdcd' rx='9.479' ry='9.479' cy='438.86' cx='122.5' stroke-linecap='round' stroke-width='2.041' fill='none'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--success,.p-icon--pass{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23fff' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--share{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath style='block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M11.43.012a2.48 2.48 0 0 0-1.5.597l-.952.797v.574a6.7 6.7 0 0 1-.154 1.489c-.102.452-.286.84-.543 1.158a2.333 2.333 0 0 1-.999.756c-.421.185-.953.278-1.59.278-.622 0-1.072-.04-1.568-.112-.929.544-1.363 1.382-1.363 2.493s.53 1.732 1.363 2.53a14.294 14.294 0 0 1 1.569-.077c.636 0 1.168.093 1.59.278.42.174.751.427.998.756.257.318.44.7.543 1.152.103.452.154.95.154 1.495v.414l.922.78a2.49 2.49 0 0 0 1.813.63 2.49 2.49 0 0 0 1.713-.866 2.49 2.49 0 0 0 .57-1.833 2.49 2.49 0 0 0-.923-1.684l-.65-.55h-1.696c-.44 0-.848-.06-1.229-.182a2.59 2.59 0 0 1-.993-.55 2.54 2.54 0 0 1-.65-.934c-.16-.372-.242-.818-.242-1.335s.083-.967.242-1.347c.16-.38.377-.69.65-.934.282-.243.613-.428.993-.55a4.25 4.25 0 0 1 1.23-.17h1.536l.821-.686c.798-.646 1.116-1.822.752-2.782-.363-.96-1.381-1.63-2.406-1.585z' fill='%23666'/%3E%3Cpath opacity='.1' fill='none' d='M-.003.005h16v16h-16z'/%3E%3C/g%3E%3C/svg%3E")}[class*="--dark"] .p-icon--share,.p-icon--share.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath style='block-progression:tb;text-decoration-line:none;text-indent:0;text-transform:none' d='M11.43.012a2.48 2.48 0 0 0-1.5.597l-.952.797v.574a6.7 6.7 0 0 1-.154 1.489c-.102.452-.286.84-.543 1.158a2.333 2.333 0 0 1-.999.756c-.421.185-.953.278-1.59.278-.622 0-1.072-.04-1.568-.112-.929.544-1.363 1.382-1.363 2.493s.53 1.732 1.363 2.53a14.294 14.294 0 0 1 1.569-.077c.636 0 1.168.093 1.59.278.42.174.751.427.998.756.257.318.44.7.543 1.152.103.452.154.95.154 1.495v.414l.922.78a2.49 2.49 0 0 0 1.813.63 2.49 2.49 0 0 0 1.713-.866 2.49 2.49 0 0 0 .57-1.833 2.49 2.49 0 0 0-.923-1.684l-.65-.55h-1.696c-.44 0-.848-.06-1.229-.182a2.59 2.59 0 0 1-.993-.55 2.54 2.54 0 0 1-.65-.934c-.16-.372-.242-.818-.242-1.335s.083-.967.242-1.347c.16-.38.377-.69.65-.934.282-.243.613-.428.993-.55a4.25 4.25 0 0 1 1.23-.17h1.536l.821-.686c.798-.646 1.116-1.822.752-2.782-.363-.96-1.381-1.63-2.406-1.585z' fill='%23cdcdcd'/%3E%3Cpath opacity='.1' fill='none' d='M-.003.005h16v16h-16z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--user{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.12' fill='none' color='%23000' d='M15.997 15.998v-16h-16v16z'/%3E%3Cpath style='text-decoration-color:%23000;font-variant-numeric:normal;text-decoration-line:none;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;text-decoration-style:solid;font-variant-ligatures:normal;isolation:auto;text-transform:none' d='M8 0c-.587 0-1.142.109-1.651.329-.508.209-.955.515-1.329.912h-.004a4.235 4.235 0 0 0-.844 1.426 5.128 5.128 0 0 0-.299 1.787c0 .653.098 1.256.3 1.802.199.539.48 1.012.843 1.41h.004c.25.264.531.49.841.676-.258.066-.701.144-.956.237-.878.322-1.617.766-2.196 1.334h-.004a5.586 5.586 0 0 0-1.286 2.03h-.002a7.541 7.541 0 0 0-.394 2.464v1.572L14.98 16v-1.572c0-.891-.139-1.7-.42-2.467a5.19 5.19 0 0 0-1.291-2.039c-.58-.567-1.316-1.011-2.194-1.333-.25-.093-.687-.17-.94-.236.31-.187.59-.414.834-.681.373-.397.661-.872.86-1.411a5.17 5.17 0 0 0 .3-1.803c0-.645-.098-1.243-.3-1.788a4.108 4.108 0 0 0-.86-1.427A3.652 3.652 0 0 0 9.652.33 4.14 4.14 0 0 0 8.001 0z' fill='%23666' color='%23000' white-space='normal'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--user,.p-icon--user.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath opacity='.12' fill='none' color='%23000' d='M15.997 15.998v-16h-16v16z'/%3E%3Cpath style='text-decoration-color:%23000;font-variant-numeric:normal;text-decoration-line:none;font-variant-position:normal;mix-blend-mode:normal;block-progression:tb;font-feature-settings:normal;shape-padding:0;font-variant-alternates:normal;text-indent:0;font-variant-caps:normal;text-decoration-style:solid;font-variant-ligatures:normal;isolation:auto;text-transform:none' d='M8 0c-.587 0-1.142.109-1.651.329-.508.209-.955.515-1.329.912h-.004a4.235 4.235 0 0 0-.844 1.426 5.128 5.128 0 0 0-.299 1.787c0 .653.098 1.256.3 1.802.199.539.48 1.012.843 1.41h.004c.25.264.531.49.841.676-.258.066-.701.144-.956.237-.878.322-1.617.766-2.196 1.334h-.004a5.586 5.586 0 0 0-1.286 2.03h-.002a7.541 7.541 0 0 0-.394 2.464v1.572L14.98 16v-1.572c0-.891-.139-1.7-.42-2.467a5.19 5.19 0 0 0-1.291-2.039c-.58-.567-1.316-1.011-2.194-1.333-.25-.093-.687-.17-.94-.236.31-.187.59-.414.834-.681.373-.397.661-.872.86-1.411a5.17 5.17 0 0 0 .3-1.803c0-.645-.098-1.243-.3-1.788a4.108 4.108 0 0 0-.86-1.427A3.652 3.652 0 0 0 9.652.33 4.14 4.14 0 0 0 8.001 0z' fill='%23cdcdcd' color='%23000' white-space='normal'/%3E%3C/svg%3E")}.p-icon--question{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cpath fill='none' color='%23000' d='M-.003.002h16v16h-16z'/%3E%3Cpath d='M7.997.002c-4.41 0-8 3.59-8 8s3.59 8 8 8 8-3.589 8-8-3.589-8-8-8z' fill='%23335280' color='%23000'/%3E%3Cpath d='M8.004 5.23q-.431 0-.825.11-.394.098-.825.332l-.419-1.145q.456-.258 1.035-.406.59-.16 1.206-.16.739 0 1.219.21.48.196.763.504.283.308.394.677.111.37.111.714 0 .419-.16.751-.148.333-.382.616t-.504.542q-.271.246-.505.517-.234.258-.394.554-.148.295-.148.664v.148q0 .074.012.148h-1.28q-.025-.123-.037-.259-.012-.147-.012-.27 0-.407.135-.727.136-.32.345-.59t.443-.506q.246-.234.456-.467.209-.234.344-.48.136-.247.136-.542 0-.407-.283-.665-.271-.271-.825-.271zM8.984 12.01q0 .43-.283.7-.284.272-.702.272-.406 0-.702-.271-.283-.271-.283-.702 0-.43.283-.702.296-.283.702-.283.418 0 .702.283.283.271.283.702z' fill='%23fff'/%3E%3C/svg%3E")}.p-icon--spinner{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}[class*="--dark"] .p-icon--spinner,.p-icon--spinner.is-light{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23cdcdcd' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--facebook{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' width='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Ccircle id='a' cx='20' cy='20' r='20'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cuse fill='%23666' fill-rule='nonzero' xlink:href='%23a'/%3E%3Cpath d='M30.037 10.001c-3.92 0-6.603 2.449-6.603 6.945v3.526H19v5.255h4.434V40c1.82-.246 3.6-.728 5.3-1.438V25.727h4.423l.66-5.255h-5.084V17.47c0-1.522.48-3.085 2.55-2.563H34v-4.7c-.47-.064-2.085-.207-3.963-.207v.001z' fill='%23FFF' fill-rule='nonzero' mask='url%28%23b%29'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--facebook:hover{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='40' width='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Ccircle id='a' cx='20' cy='20' r='20'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cuse fill='%233b5998' fill-rule='nonzero' xlink:href='%23a'/%3E%3Cpath d='M30.037 10.001c-3.92 0-6.603 2.449-6.603 6.945v3.526H19v5.255h4.434V40c1.82-.246 3.6-.728 5.3-1.438V25.727h4.423l.66-5.255h-5.084V17.47c0-1.522.48-3.085 2.55-2.563H34v-4.7c-.47-.064-2.085-.207-3.963-.207v.001z' fill='%23FFF' fill-rule='nonzero' mask='url%28%23b%29'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--google{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 0C8.955 0 0 8.955 0 20s8.955 20 20 20 20-8.955 20-20S31.045 0 20 0zm-4.862 26.805A6.799 6.799 0 0 1 8.333 20a6.799 6.799 0 0 1 6.805-6.805c1.839 0 3.374.67 4.559 1.778l-1.845 1.78c-.507-.486-1.39-1.05-2.714-1.05-2.323 0-4.218 1.925-4.218 4.299 0 2.373 1.897 4.298 4.218 4.298 2.694 0 3.707-1.937 3.86-2.937h-3.86V19.03h6.425c.06.34.107.68.107 1.128.002 3.887-2.605 6.647-6.532 6.647zm16.529-5.833H28.75v2.916h-1.945v-2.916h-2.917v-1.944h2.917v-2.916h1.945v2.916h2.917v1.944z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--google:hover{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 0C8.955 0 0 8.955 0 20s8.955 20 20 20 20-8.955 20-20S31.045 0 20 0zm-4.862 26.805A6.799 6.799 0 0 1 8.333 20a6.799 6.799 0 0 1 6.805-6.805c1.839 0 3.374.67 4.559 1.778l-1.845 1.78c-.507-.486-1.39-1.05-2.714-1.05-2.323 0-4.218 1.925-4.218 4.299 0 2.373 1.897 4.298 4.218 4.298 2.694 0 3.707-1.937 3.86-2.937h-3.86V19.03h6.425c.06.34.107.68.107 1.128.002 3.887-2.605 6.647-6.532 6.647zm16.529-5.833H28.75v2.916h-1.945v-2.916h-2.917v-1.944h2.917v-2.916h1.945v2.916h2.917v1.944z' fill='%23dd4b39' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--twitter{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23666'/%3E%3Cpath d='M16.34 30.55c8.87 0 13.72-7.35 13.72-13.72 0-.21 0-.42-.01-.62.94-.68 1.76-1.53 2.41-2.5-.86.38-1.79.64-2.77.76 1-.6 1.76-1.54 2.12-2.67-.93.55-1.96.95-3.06 1.17a4.799 4.799 0 0 0-3.52-1.52c-2.66 0-4.82 2.16-4.82 4.82 0 .38.04.75.13 1.1a13.68 13.68 0 0 1-9.94-5.04c-.41.71-.65 1.54-.65 2.42a4.8 4.8 0 0 0 2.15 4.01c-.79-.02-1.53-.24-2.18-.6v.06c0 2.34 1.66 4.28 3.87 4.73a4.807 4.807 0 0 1-2.18.08 4.815 4.815 0 0 0 4.5 3.35 9.693 9.693 0 0 1-7.14 1.99c2.11 1.38 4.65 2.18 7.37 2.18' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--twitter:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Ccircle cx='20' cy='20' r='20' fill='%231da1f2'/%3E%3Cpath d='M16.34 30.55c8.87 0 13.72-7.35 13.72-13.72 0-.21 0-.42-.01-.62.94-.68 1.76-1.53 2.41-2.5-.86.38-1.79.64-2.77.76 1-.6 1.76-1.54 2.12-2.67-.93.55-1.96.95-3.06 1.17a4.799 4.799 0 0 0-3.52-1.52c-2.66 0-4.82 2.16-4.82 4.82 0 .38.04.75.13 1.1a13.68 13.68 0 0 1-9.94-5.04c-.41.71-.65 1.54-.65 2.42a4.8 4.8 0 0 0 2.15 4.01c-.79-.02-1.53-.24-2.18-.6v.06c0 2.34 1.66 4.28 3.87 4.73a4.807 4.807 0 0 1-2.18.08 4.815 4.815 0 0 0 4.5 3.35 9.693 9.693 0 0 1-7.14 1.99c2.11 1.38 4.65 2.18 7.37 2.18' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--instagram{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 28.479h28.473V.009H0z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23666' fill-rule='nonzero'/%3E%3Cg transform='translate%286 6%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M14.237.009c-3.867 0-4.352.016-5.87.086-1.515.069-2.55.31-3.456.661-.936.364-1.73.851-2.522 1.642A6.978 6.978 0 0 0 .747 4.92C.395 5.826.155 6.86.086 8.376.016 9.894 0 10.379 0 14.246c0 3.866.016 4.35.086 5.87.069 1.515.31 2.55.661 3.455.364.936.851 1.73 1.642 2.522a6.98 6.98 0 0 0 2.522 1.642c.906.352 1.94.592 3.456.661 1.518.07 2.003.086 5.87.086 3.866 0 4.35-.016 5.87-.086 1.515-.069 2.55-.31 3.455-.661a6.98 6.98 0 0 0 2.522-1.642 6.98 6.98 0 0 0 1.642-2.522c.352-.905.592-1.94.661-3.456.07-1.518.086-2.003.086-5.87 0-3.866-.016-4.35-.086-5.87-.069-1.514-.31-2.55-.661-3.455a6.98 6.98 0 0 0-1.642-2.522A6.978 6.978 0 0 0 23.562.756c-.905-.352-1.94-.592-3.456-.661-1.518-.07-2.003-.086-5.87-.086zm0 2.565c3.8 0 4.251.015 5.752.083 1.388.063 2.142.295 2.644.49a4.41 4.41 0 0 1 1.637 1.065 4.41 4.41 0 0 1 1.065 1.637c.195.502.427 1.256.49 2.644.068 1.501.083 1.951.083 5.753 0 3.8-.015 4.251-.083 5.752-.063 1.388-.295 2.142-.49 2.644a4.41 4.41 0 0 1-1.065 1.637 4.41 4.41 0 0 1-1.637 1.065c-.502.195-1.256.427-2.644.49-1.5.068-1.95.083-5.752.083-3.802 0-4.252-.015-5.753-.083-1.388-.063-2.142-.295-2.644-.49a4.41 4.41 0 0 1-1.637-1.065 4.411 4.411 0 0 1-1.065-1.637c-.195-.502-.427-1.256-.49-2.644-.068-1.5-.083-1.951-.083-5.752 0-3.802.015-4.252.083-5.753.063-1.388.295-2.142.49-2.644a4.41 4.41 0 0 1 1.065-1.637A4.41 4.41 0 0 1 5.84 3.147c.502-.195 1.256-.427 2.644-.49 1.501-.068 1.951-.083 5.753-.083z' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath d='M20.24 24.991a4.746 4.746 0 1 1 0-9.49 4.746 4.746 0 0 1 0 9.49zm0-12.056a7.31 7.31 0 1 0 0 14.621 7.31 7.31 0 0 0 0-14.621zM29.54 12.646a1.708 1.708 0 1 1-3.416 0 1.708 1.708 0 0 1 3.417 0' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--instagram:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M0 28.479h28.473V.009H0z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='20' cy='20' r='20' fill='%23fb3958' fill-rule='nonzero'/%3E%3Cg transform='translate%286 6%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M14.237.009c-3.867 0-4.352.016-5.87.086-1.515.069-2.55.31-3.456.661-.936.364-1.73.851-2.522 1.642A6.978 6.978 0 0 0 .747 4.92C.395 5.826.155 6.86.086 8.376.016 9.894 0 10.379 0 14.246c0 3.866.016 4.35.086 5.87.069 1.515.31 2.55.661 3.455.364.936.851 1.73 1.642 2.522a6.98 6.98 0 0 0 2.522 1.642c.906.352 1.94.592 3.456.661 1.518.07 2.003.086 5.87.086 3.866 0 4.35-.016 5.87-.086 1.515-.069 2.55-.31 3.455-.661a6.98 6.98 0 0 0 2.522-1.642 6.98 6.98 0 0 0 1.642-2.522c.352-.905.592-1.94.661-3.456.07-1.518.086-2.003.086-5.87 0-3.866-.016-4.35-.086-5.87-.069-1.514-.31-2.55-.661-3.455a6.98 6.98 0 0 0-1.642-2.522A6.978 6.978 0 0 0 23.562.756c-.905-.352-1.94-.592-3.456-.661-1.518-.07-2.003-.086-5.87-.086zm0 2.565c3.8 0 4.251.015 5.752.083 1.388.063 2.142.295 2.644.49a4.41 4.41 0 0 1 1.637 1.065 4.41 4.41 0 0 1 1.065 1.637c.195.502.427 1.256.49 2.644.068 1.501.083 1.951.083 5.753 0 3.8-.015 4.251-.083 5.752-.063 1.388-.295 2.142-.49 2.644a4.41 4.41 0 0 1-1.065 1.637 4.41 4.41 0 0 1-1.637 1.065c-.502.195-1.256.427-2.644.49-1.5.068-1.95.083-5.752.083-3.802 0-4.252-.015-5.753-.083-1.388-.063-2.142-.295-2.644-.49a4.41 4.41 0 0 1-1.637-1.065 4.411 4.411 0 0 1-1.065-1.637c-.195-.502-.427-1.256-.49-2.644-.068-1.5-.083-1.951-.083-5.752 0-3.802.015-4.252.083-5.753.063-1.388.295-2.142.49-2.644a4.41 4.41 0 0 1 1.065-1.637A4.41 4.41 0 0 1 5.84 3.147c.502-.195 1.256-.427 2.644-.49 1.501-.068 1.951-.083 5.753-.083z' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath d='M20.24 24.991a4.746 4.746 0 1 1 0-9.49 4.746 4.746 0 0 1 0 9.49zm0-12.056a7.31 7.31 0 1 0 0 14.621 7.31 7.31 0 0 0 0-14.621zM29.54 12.646a1.708 1.708 0 1 1-3.416 0 1.708 1.708 0 0 1 3.417 0' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--linkedin{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%23666' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg fill='%23FFFFFE'%3E%3Cpath d='M11.07 8.406a2.743 2.743 0 0 1 2.731 2.75c0 1.52-1.225 2.753-2.731 2.753a2.743 2.743 0 0 1-2.734-2.752 2.742 2.742 0 0 1 2.734-2.751zM8.712 31.268h4.713V15.997H8.712v15.271zM16.382 15.997h4.52v2.087h.064c.63-1.201 2.167-2.467 4.46-2.467 4.773 0 5.654 3.163 5.654 7.274v8.377h-4.71v-7.426c0-1.771-.032-4.05-2.45-4.05-2.452 0-2.828 1.93-2.828 3.921v7.555h-4.71V15.997'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}.p-icon--linkedin:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%230071a1' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg fill='%23FFFFFE'%3E%3Cpath d='M11.07 8.406a2.743 2.743 0 0 1 2.731 2.75c0 1.52-1.225 2.753-2.731 2.753a2.743 2.743 0 0 1-2.734-2.752 2.742 2.742 0 0 1 2.734-2.751zM8.712 31.268h4.713V15.997H8.712v15.271zM16.382 15.997h4.52v2.087h.064c.63-1.201 2.167-2.467 4.46-2.467 4.773 0 5.654 3.163 5.654 7.274v8.377h-4.71v-7.426c0-1.771-.032-4.05-2.45-4.05-2.452 0-2.828 1.93-2.828 3.921v7.555h-4.71V15.997'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}.p-icon--youtube{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M.009 18.367V.006h26.06v18.36z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%23666' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg transform='translate%287 11%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M25.524 2.868A3.275 3.275 0 0 0 23.22.548C21.187 0 13.034 0 13.034 0S4.882 0 2.85.548a3.275 3.275 0 0 0-2.305 2.32C0 4.914 0 9.183 0 9.183s0 4.27.545 6.316a3.276 3.276 0 0 0 2.305 2.32c2.032.548 10.184.548 10.184.548s8.153 0 10.185-.548a3.276 3.276 0 0 0 2.305-2.32c.545-2.047.545-6.316.545-6.316s0-4.269-.545-6.315' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath fill='%23666' d='M17.368 24.06l6.814-3.876-6.814-3.877v7.753'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--youtube:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath id='a' d='M.009 18.367V.006h26.06v18.36z'/%3E%3C/defs%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle fill='%23d9252a' fill-rule='nonzero' cx='20' cy='20' r='20'/%3E%3Cg transform='translate%287 11%29'%3E%3Cmask id='b' fill='%23fff'%3E%3Cuse xlink:href='%23a'/%3E%3C/mask%3E%3Cpath d='M25.524 2.868A3.275 3.275 0 0 0 23.22.548C21.187 0 13.034 0 13.034 0S4.882 0 2.85.548a3.275 3.275 0 0 0-2.305 2.32C0 4.914 0 9.183 0 9.183s0 4.27.545 6.316a3.276 3.276 0 0 0 2.305 2.32c2.032.548 10.184.548 10.184.548s8.153 0 10.185-.548a3.276 3.276 0 0 0 2.305-2.32c.545-2.047.545-6.316.545-6.316s0-4.269-.545-6.315' fill='%23FFF' mask='url%28%23b%29'/%3E%3C/g%3E%3Cpath fill='%23d9252a' d='M17.368 24.06l6.814-3.876-6.814-3.877v7.753'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--canonical{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 32.735c-7.036 0-12.736-5.7-12.736-12.736 0-7.034 5.7-12.734 12.736-12.734 7.036 0 12.736 5.7 12.736 12.734 0 7.036-5.7 12.736-12.736 12.736zM40 20c0 11.045-8.955 20-20 20S0 31.045 0 20C0 8.954 8.955 0 20 0s20 8.954 20 20zM20 4.865C11.636 4.865 4.864 11.642 4.864 20c0 8.36 6.772 15.135 15.136 15.135 8.364 0 15.136-6.775 15.136-15.135 0-8.358-6.772-15.135-15.136-15.135z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--canonical:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cpath d='M20 32.735c-7.036 0-12.736-5.7-12.736-12.736 0-7.034 5.7-12.734 12.736-12.734 7.036 0 12.736 5.7 12.736 12.734 0 7.036-5.7 12.736-12.736 12.736zM40 20c0 11.045-8.955 20-20 20S0 31.045 0 20C0 8.954 8.955 0 20 0s20 8.954 20 20zM20 4.865C11.636 4.865 4.864 11.642 4.864 20c0 8.36 6.772 15.135 15.136 15.135 8.364 0 15.136-6.775 15.136-15.135 0-8.358-6.772-15.135-15.136-15.135z' fill='%23772953' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-icon--ubuntu{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Cpath d='M39.906 20.013c0 10.987-8.905 19.893-19.892 19.893C9.028 39.906.122 31 .122 20.013.122 9.028 9.028.122 20.014.122c10.987 0 19.892 8.905 19.892 19.891z' fill='%23666'/%3E%3Cpath d='M9.69 20.013a2.558 2.558 0 1 1-5.116 0 2.558 2.558 0 0 1 5.116 0zM24.241 32.45a2.559 2.559 0 0 0 4.43-2.558 2.557 2.557 0 1 0-4.43 2.558zm4.429-22.313a2.557 2.557 0 1 0-4.43-2.556 2.557 2.557 0 0 0 4.43 2.556zm-8.656 2.584a7.292 7.292 0 0 1 7.265 6.648l3.701-.059a10.954 10.954 0 0 0-3.227-7.094 3.591 3.591 0 0 1-3.097-.24A3.592 3.592 0 0 1 22.9 9.41c-.92-.25-1.888-.384-2.886-.384-1.75 0-3.404.41-4.874 1.137l1.801 3.234a7.278 7.278 0 0 1 3.073-.677zm-7.294 7.293a7.283 7.283 0 0 1 3.102-5.967l-1.9-3.177a11.005 11.005 0 0 0-4.533 6.341 3.59 3.59 0 0 1 1.343 2.803 3.592 3.592 0 0 1-1.343 2.804 11.01 11.01 0 0 0 4.532 6.343l1.9-3.177a7.286 7.286 0 0 1-3.1-5.97zm7.294 7.295a7.267 7.267 0 0 1-3.073-.678l-1.8 3.234a10.938 10.938 0 0 0 4.873 1.137c.998 0 1.966-.132 2.886-.383a3.587 3.587 0 0 1 1.756-2.564 3.591 3.591 0 0 1 3.097-.24 10.958 10.958 0 0 0 3.227-7.096l-3.701-.058a7.293 7.293 0 0 1-7.265 6.648z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--ubuntu:hover{background-image:url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' %3E%3Cg fill-rule='nonzero' fill='none'%3E%3Cpath d='M39.906 20.013c0 10.987-8.905 19.893-19.892 19.893C9.028 39.906.122 31 .122 20.013.122 9.028 9.028.122 20.014.122c10.987 0 19.892 8.905 19.892 19.891z' fill='%23e95420'/%3E%3Cpath d='M9.69 20.013a2.558 2.558 0 1 1-5.116 0 2.558 2.558 0 0 1 5.116 0zM24.241 32.45a2.559 2.559 0 0 0 4.43-2.558 2.557 2.557 0 1 0-4.43 2.558zm4.429-22.313a2.557 2.557 0 1 0-4.43-2.556 2.557 2.557 0 0 0 4.43 2.556zm-8.656 2.584a7.292 7.292 0 0 1 7.265 6.648l3.701-.059a10.954 10.954 0 0 0-3.227-7.094 3.591 3.591 0 0 1-3.097-.24A3.592 3.592 0 0 1 22.9 9.41c-.92-.25-1.888-.384-2.886-.384-1.75 0-3.404.41-4.874 1.137l1.801 3.234a7.278 7.278 0 0 1 3.073-.677zm-7.294 7.293a7.283 7.283 0 0 1 3.102-5.967l-1.9-3.177a11.005 11.005 0 0 0-4.533 6.341 3.59 3.59 0 0 1 1.343 2.803 3.592 3.592 0 0 1-1.343 2.804 11.01 11.01 0 0 0 4.532 6.343l1.9-3.177a7.286 7.286 0 0 1-3.1-5.97zm7.294 7.295a7.267 7.267 0 0 1-3.073-.678l-1.8 3.234a10.938 10.938 0 0 0 4.873 1.137c.998 0 1.966-.132 2.886-.383a3.587 3.587 0 0 1 1.756-2.564 3.591 3.591 0 0 1 3.097-.24 10.958 10.958 0 0 0 3.227-7.096l-3.701-.058a7.293 7.293 0 0 1-7.265 6.648z' fill='%23FFF'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--medium{height:1.25rem;width:1.25rem}.p-icon--large{height:1.5rem;width:1.5rem}.p-icon--x-large{height:1.75rem;width:1.75rem}.p-icon--x-large{height:2.25rem;width:2.25rem}.p-icon--xx-large{height:3rem;width:3rem}[class*="p-button-"] [class*="p-icon-"]{top:-1px;vertical-align:middle}.p-image--bordered{border-color:#cdcdcd;border-style:solid;border-width:1px}.p-image--shadowed{box-shadow:0 1px 5px 1px rgba(205,205,205,0.2)}.p-inline-images{display:block;list-style:none;margin-left:0;padding-left:0;text-align:center}.p-inline-images__item{display:inline-block;margin:1rem;overflow:hidden;text-align:center;vertical-align:middle}@media only screen and (min-width: 768px){.p-inline-images__item{margin:1.875rem}}.p-inline-images__logo,.p-inline-images__item img{max-height:3rem;max-width:7rem;width:auto}@media screen and (min-width: 768px){.p-inline-images__logo,.p-inline-images__item img{max-height:5.5rem;max-width:9rem}}.p-inline-images__img{display:inline-block;margin:2rem;max-width:6rem;text-align:center;vertical-align:middle;width:100%}@media (min-width: 768px){.p-inline-images__img{margin:3rem;max-width:11.25rem}}.p-link--soft{color:#111}.p-link--soft:visited{color:#111;text-decoration:none}.p-link--soft:hover{color:#007aa6}.p-link--soft.is-selected{font-weight:400}.p-link--strong{color:currentColor;font-weight:400}.p-link--strong:visited{color:currentColor}.p-link--strong:hover{color:#007aa6;text-decoration:underline}.p-link--inverted{color:#f7f7f7;font-weight:400}.p-link--inverted:hover{color:#f7f7f7}.p-link--inverted:visited{color:#dedede}@supports (mask-size: 1em) or (-webkit-mask-size: 1em){.p-link--external::after{-webkit-mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E") no-repeat 0 0/cover;background-color:currentColor;content:'';margin:0 0 0 .25em;mask:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E") no-repeat 0 0/cover;padding-right:.75em}.p-link--no-underline{border:0}}@supports not ((mask-size: 1em) or (-webkit-mask-size: 1em)){.p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");background-position:top right;background-repeat:no-repeat;background-size:.75em;margin-top:-.25em;padding:.25em 1em 0 0}.p-link--external.p-link--strong,.p-link--external.p-link--soft,.p-link--external.sidebar__link{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.p-link--soft:hover,.p-link--external.sidebar__link:hover{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.p-link--inverted{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23f7f7f7' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23f7f7f7' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23f7f7f7' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.p-link--inverted:visited{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23dedede' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23dedede' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23dedede' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-link--external.sidebar__link{display:inline-block;padding:0 1em 1em 0}.p-link--no-underline{border:0}.p-button .p-link--external,.p-button--neutral .p-link--external,.p-button--base .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23111' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23111' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-button--positive .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-button--negative .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-button--brand .p-link--external{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E");padding-top:0}.p-strip--dark * .p-link--external.p-link--soft,.p-strip--accent * .p-link--external.p-link--soft,.p-strip--image.is-dark * .p-link--external.p-link--soft{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-strip--dark * .p-link--external.p-link--soft:hover,.p-strip--accent * .p-link--external.p-link--soft:hover,.p-strip--image.is-dark * .p-link--external.p-link--soft:hover{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23007aa6' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23007aa6' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}.p-strip--dark * .p-link--external.p-link--strong,.p-strip--accent * .p-link--external.p-link--strong,.p-strip--image.is-dark * .p-link--external.p-link--strong{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='0.75em' height='0.75em' viewBox='0 0 16 16' %3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M4.867 1.313C.6 1.32.067 1.443.067 4.51v6.4c0 3.2.533 3.2 5.333 3.2h2.133c4.8 0 5.334 0 5.334-3.2v-1.6h-1.6v1.068c0 2.133 0 2.133-4.267 2.133H5.933c-4.266 0-4.266 0-4.266-2.132V5.044c0-1.93.034-2.112 3.2-2.13v-1.6z'/%3E%3Cpath d='M-1-1h16v16H-1'/%3E%3Cpath fill='%23fff' d='M6.435 2.16c.11-.446 7.113-2.196 7.448-1.86.335.334-1.416 7.335-1.863 7.447-.447.112-5.697-5.14-5.586-5.586z'/%3E%3Cpath fill='%23fff' d='M9.032 3.38L4.705 7.708l1.767 1.767L10.8 5.148'/%3E%3C/g%3E%3C/svg%3E")}}.p-top{border-bottom:1px dotted #cdcdcd;clear:both;margin:20px 0}.p-top__link{background:#fff;color:#111;float:right;margin-right:5px;padding:0 5px;position:relative;text-decoration:none;top:-.725rem}.p-list-tree__item--group::after,.p-list-tree .p-list-tree[aria-hidden="false"]::after{background-position:center;background-repeat:no-repeat;content:' ';display:block;height:.9375rem;left:-.75rem;pointer-events:none;position:absolute;top:.4rem;width:.9375rem}.p-list-tree{border-left:1px solid #cdcdcd;list-style-type:none;margin-left:1rem;padding:0 0 0 .25rem}.p-list-tree__item{margin-top:.125rem;padding-left:.8rem;position:relative}.p-list-tree__item::before{background:#cdcdcd;content:' ';display:block;height:1px;left:-.25rem;pointer-events:none;position:absolute;top:.8rem;width:.625rem}.p-list-tree__item--group::after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='15' width='15' viewBox='0 0 15 15'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='%23FFF'/%3E%3Cpath stroke='%23888' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23888' d='M7 4h1v7H7z'/%3E%3Cpath fill='%23888' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E")}.p-list-tree__toggle{background:transparent;border:0;font-weight:normal;margin:0 0 0 -1.75rem;padding:0 0 0 1.75rem;transition-duration:0s;width:auto}.p-list-tree__toggle:hover{background:transparent;color:#007aa6;text-decoration:underline}.p-list-tree__toggle:focus{background:transparent;outline:1px dotted #cdcdcd}.p-list-tree .p-list-tree{display:none;margin-left:0}.p-list-tree .p-list-tree[aria-hidden="false"]{display:block}.p-list-tree .p-list-tree[aria-hidden="false"]::after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='15' width='15' viewBox='0 0 15 15'%3E%3Cdefs%3E%3Cpath id='a' d='M0 0h15v15H0z'/%3E%3C/defs%3E%3Cg fill-rule='evenodd' fill='none'%3E%3Cuse xlink:href='%23a' fill='%23FFF'/%3E%3Cpath stroke='%23888' d='M.5.5h14v14H.5z'/%3E%3Cpath fill='%23888' d='M4 8V7h7v1z'/%3E%3C/g%3E%3C/svg%3E");z-index:1}.p-list-step,.p-stepped-list--detailed{counter-reset:li;list-style:none;margin-left:4rem;padding-left:0}.p-list-step>li::before,.p-stepped-list--detailed>li::before{background-color:#666;border-radius:100%;color:#fff;content:counter(li);counter-increment:li;direction:rtl;display:inline-block;margin-left:-4rem;margin-top:0.1rem;padding:0;position:absolute;text-align:center;width:2.5rem}@media (max-width: 768px){.p-list-step>li::before,.p-stepped-list--detailed>li::before{margin-top:0;width:2rem}}.p-list{list-style:none;margin-left:0;padding-left:0}.p-list .p-list__item{padding-bottom:.125rem;padding-top:.125rem}form .p-list .p-list__item{padding-bottom:0;padding-top:0}form .p-list .p-list__item label{margin-bottom:.1rem}.p-list--divided{list-style:none;margin-left:0;padding-left:0}.p-list--divided .p-list__item{padding-bottom:.125rem;padding-top:.125rem}form .p-list--divided .p-list__item{padding-bottom:0;padding-top:0}form .p-list--divided .p-list__item label{margin-bottom:.1rem}.p-list--divided .p-list__item{position:relative}.p-list--divided .p-list__item::after{border-bottom:1px dotted #cdcdcd;bottom:0;content:'';height:1px;left:0;position:absolute;right:0}.p-list--divided .p-list__item:last-of-type::after,.p-list--divided .p-list__item .last-item::after{border-bottom:0}.p-list--divided.is-split .p-list__item:last-of-type{border-bottom:1px dotted #cdcdcd}.is-ticked{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 14 14'%3E%3Ccircle fill='%23e95420' cx='7' cy='7' r='7'/%3E%3Cpath fill='%23fff' d='M6.1 10.813L2.41 8.105l1.184-1.613L5.9 8.187l4.393-4.394 1.414 1.414z' /%3E%3C/svg%3E");background-position-y:.4375rem;background-repeat:no-repeat;padding-left:2rem}.p-inline-list{margin-left:0;padding-left:0}.p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem}.p-inline-list__item:last-of-type,.p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot{margin-left:0;padding-left:0}.p-inline-list--middot .p-inline-list__item{display:inline;list-style:none;margin-right:1.25rem;margin-right:1.25em;position:relative}.p-inline-list--middot .p-inline-list__item:last-of-type,.p-inline-list--middot .p-inline-list__item .last-item{margin-right:0}.p-inline-list--middot .p-inline-list__item::after{color:#666;content:'\00b7';font-size:1.4em;line-height:0;position:absolute;right:-.5em;top:.4em}.p-inline-list--middot .p-inline-list__item:hover::after{color:#666}.p-inline-list--middot .p-inline-list__item:last-of-type::after,.p-inline-list--middot .p-inline-list__item .last-item::after{content:''}.p-list-step__item{float:none;margin-left:0;width:100%}.p-list-step__content{margin-top:-1rem}.p-list-step__bullet{display:none}@media (min-width: 768px){.p-stepped-list--detailed .p-list-step__content{margin-top:0}.p-stepped-list--detailed .p-list-step__item{display:flex;margin:0}.p-stepped-list--detailed .p-list-step__item>:nth-child(2n){display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;width:48.35615%;margin-left:3.2877%}.p-stepped-list--detailed .p-list-step__item>:nth-child(2n+1){display:block;float:left;min-height:1px;position:relative;*margin-right:-1px;width:48.35615%}}@media (min-width: 768px){@supports (columns: 1){[class*='p-list'].is-split{column-gap:2rem;columns:2}[class*='p-list'].is-split .p-list__item{display:inline-block;width:100%}}@supports not (columns: 1){[class*='p-list'].is-split{display:flex;flex-wrap:wrap}[class*='p-list'].is-split .p-list__item{width:calc(50% - .5rem)}}[class*='p-list'].is-split:nth-child(2n-1){margin-right:1rem}}.p-matrix{display:flex;flex-wrap:wrap;list-style:none;margin-bottom:1rem;margin-left:0;padding-left:0}.p-matrix__item{border-top:1px solid #cdcdcd;display:flex;flex:1 1 auto;padding-bottom:.5rem;padding-top:.4375rem}@media (min-width: 620px){.p-matrix__item{display:flex;flex-wrap:wrap;width:33.333%}}@media (min-width: 620px) and (max-width: 1030px){.p-matrix__item{flex-direction:column}}@media (min-width: 768px){.p-matrix__item{border-right:1px solid #cdcdcd;padding-left:.5rem;padding-right:.5rem;width:33.333%}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(3n+1){padding-left:0}.p-matrix__item:nth-child(3n+3){border-right:0}.p-matrix__item:nth-child(1),.p-matrix__item:nth-child(2),.p-matrix__item:nth-child(3){border-top:0}}@media (min-width: 1030px){.p-matrix__item{border-right:1px solid #cdcdcd;padding:.5rem;width:33.333%}.p-matrix__item:empty{display:block}.p-matrix__item:nth-child(3n+1){padding-left:0}.p-matrix__item:nth-child(3n+3){border-right:0;padding-right:0}.p-matrix__item:nth-last-child(1),.p-matrix__item:nth-last-child(2),.p-matrix__item:nth-last-child(3){border-bottom:0}}.p-matrix__img{align-self:flex-start;margin-bottom:.5rem;margin-right:1rem;max-height:3rem;max-width:3rem;width:auto}.p-matrix__content{display:flex;flex:1 1 auto;flex-direction:column;padding-right:1rem}@media (min-width: 1030px){.p-matrix__content{width:calc(100% - 4rem)}}.p-matrix__title{margin-top:-.5rem}.p-matrix__desc{margin-bottom:.1rem;margin-top:-1rem}.p-matrix__desc>p:last-child{margin-bottom:0}.p-matrix__desc+.p-matrix__desc{margin-top:0}@media (max-width: 768px){.p-matrix__desc{margin-top:-.5rem}}.p-media-object__image{align-self:flex-start;border-radius:.125rem;flex-basis:inherit;flex-shrink:0;margin-right:1rem;max-height:5rem;max-width:5rem;vertical-align:middle;width:auto}.p-media-object__content{margin-bottom:.6rem;margin-top:0}.p-media-object__image.is-round{border-radius:50%}.p-media-object__title{margin-bottom:.2rem;margin-top:-.5rem}@media only screen and (min-width: 768px){.p-media-object__title{margin-bottom:-0.05rem}}.p-media-object__meta-list{list-style:none;margin:0;padding-left:0;padding-top:.5rem}.p-media-object__meta-list-item--date{background-image:url('data:image/svg+xml;utf8,')}.p-media-object__meta-list-item--location{background-image:url('data:image/svg+xml;utf8,')}.p-media-object__meta-list-item--venue{background-image:url('data:image/svg+xml;utf8,')}.p-media-object--large .p-media-object__image{max-height:6rem;max-width:6rem}.p-media-object--large .p-media-object__title{margin-bottom:.3rem;margin-top:-.5rem}@media only screen and (min-width: 768px){.p-media-object--large .p-media-object__title{margin-bottom:-0.2rem}}.p-modal{align-items:center;background:rgba(17,17,17,0.85);content:'';display:flex;height:100vh;justify-content:center;left:0;margin:0;overflow:scroll;padding:1.5rem;position:absolute;top:0;width:100%}.p-modal__dialog{bottom:1.5rem;left:1.5rem;max-width:90rem;overflow:scroll;position:absolute;right:1.5rem;top:1.5rem;width:auto}@media screen and (min-width: 768px){.p-modal__dialog{bottom:initial;left:initial;overflow:visible;position:relative;right:initial;top:initial}}.p-modal__header{display:flex;justify-content:space-between}.p-modal__title{align-self:flex-end}.p-modal__close{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='90' width='90'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h90v90H0z'/%3E%3Cpath d='M14.52 6L6 14.52 36.48 45 6 75.49 14.52 84 45 53.52 75.48 84 84 75.49 53.52 45 84 14.52 75.48 6 45 36.49z' fill='%23888'/%3E%3C/g%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;background-size:1rem;border:0;box-sizing:content-box;height:1rem;margin:-1rem -1rem 0 0;padding:1rem;text-indent:-999em;width:1rem}.p-modal__close:focus{outline:1px solid #007aa6;outline-offset:2px}.p-notification,.p-notification--positive,.p-notification--caution,.p-notification--negative,.p-notification--information{display:flex;padding:0}.p-notification{position:relative}.p-notification::before{top:0;background-color:#666;content:'';position:absolute}.p-notification::before{height:.1875rem;width:auto;left:0;right:0}.p-notification+.p-notification{margin-top:1rem}.p-notification__response{background-position:1rem .9rem;background-repeat:no-repeat;background-size:1rem;padding:.65rem 1rem .25rem}.p-notification__status::after,.p-notification__action::before{content:' '}.p-notification .p-icon--close{background-color:transparent;background-size:1rem;border:0;margin:1.1875rem 1rem auto auto;padding:.5rem}.p-notification__response,.p-notification--floating{max-width:60em}.p-notification--positive{position:relative}.p-notification--positive::before{top:0;background-color:#0e8420;content:'';position:absolute}.p-notification--positive::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--positive .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='17px' height='17px' viewBox='0 0 17 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='notification-success' transform='translate(1.000000, 1.000000)'%3E%3Cg id='Page-3---colours'%3E%3Cg id='Notifications---single'%3E%3Cg id='Group'%3E%3Cg id='ICON'%3E%3Ccircle id='circle6710' stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.2500086' cy='7.2500086' r='7.2500086'%3E%3C/circle%3E%3Cpolygon id='path6712' fill='%23fff' points='11.0502986 4.1734486 10.9843986 4.2311486 6.2496486 8.3783686 3.4740786 5.9974286 2.6350186 6.9463086 6.2503386 10.7500186 11.7500086 4.9627786 11.0502986 4.1734886'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--caution{position:relative}.p-notification--caution::before{top:0;background-color:#f99b11;content:'';position:absolute}.p-notification--caution::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--caution .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='16' width='16'%3E%3Cg color='%23000'%3E%3Cpath fill='none' d='M0 0h16v16H0z'/%3E%3Cpath stroke-linejoin='round' fill='%23f99b11' transform='matrix%282.28 0 0 2.437 -2180.8 -490.52%29' stroke='%23f99b11' stroke-width='.848' d='M963.07 207.03h-6.15l3.08-5.33z'/%3E%3Cpath d='M7 5v5h2V5H7zm0 6v2h2v-2H7z' fill='%23111'/%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--negative{position:relative}.p-notification--negative::before{top:0;background-color:#c7162b;content:'';position:absolute}.p-notification--negative::before{height:.1875rem;width:auto;left:0;right:0}.p-notification--negative .p-notification__response{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='17px' viewBox='0 0 16 17' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cg id='Page-3---colours' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='Notifications---single' transform='translate(-215.000000, -271.000000)'%3E%3Cg id='Group' transform='translate(205.000000, 254.000000)'%3E%3Cg id='ICON' transform='translate(10.000000, 17.000000)'%3E%3Crect id='rect6415' x='0' y='0.36218' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle6417' stroke='%23c7162b' stroke-width='1.5' fill='%23c7162b' cx='8' cy='8.36218' r='7.2500086'%3E%3C/circle%3E%3Cpath d='M5.00001,5.36218 L11.00001,11.36218' id='path6479-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3Cpath d='M11.00001,5.36218 L5.00001,11.36218' id='path6481-8' stroke='%23fff' stroke-width='1.5'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");padding-left:3rem}.p-notification--information{position:relative}.p-notification--information::before{top:0;background-color:#335280;content:'';position:absolute}.p-notification--information::before{height:.1875rem;width:auto;left:0;right:0}.p-pagination__link--previous::before,.p-pagination__link--next::after{color:#666;content:'\203A';font-size:2rem;position:absolute;top:1rem}.p-pagination{display:flex;width:100%}.p-pagination__link,.p-pagination__link--previous,.p-pagination__link--next{margin-top:0;padding:1rem;position:relative;width:50%}.p-pagination__link:hover,.p-pagination__link--previous:hover,.p-pagination__link--next:hover{background:#f7f7f7;text-decoration:none}.p-pagination__link--previous{padding-left:2.5rem;text-align:left}@media (max-width: 460px){.p-pagination__link--previous{width:auto}.p-pagination__link--previous:only-child{width:100%}.p-pagination__link--previous:not(:only-child) *{display:none;max-width:.25rem;padding-left:1.5rem}}.p-pagination__link--previous::before{left:.5rem;transform:scaleX(-1)}.p-pagination__link--next{padding-right:2.5rem;text-align:right}@media (max-width: 460px){.p-pagination__link--next{width:100%}}.p-pagination__link--next:only-child{margin-left:auto}.p-pagination__link--next::after{right:.5rem}.p-pagination__label,.p-pagination__title{color:#111;display:block;margin-top:0;width:100%}.p-pagination__label{margin-bottom:.25rem}.p-pagination__title{font-size:1.125rem}@media (min-width: 620px){.p-pagination__title{font-size:1.25rem}}.p-pull-quote{border:0;margin:1rem 0 1.5rem;overflow:visible;padding:0 2rem;position:relative}.p-pull-quote>p:first-of-type::before{color:#cdcdcd;display:inline-block;font-size:3rem;font-weight:bold;max-width:1.25rem;position:absolute;content:'\201C\2002';left:.25rem;top:.05rem}@media (max-width: 768px){.p-pull-quote>p:first-of-type::before{font-size:2.75rem}}@media (max-width: 768px){.p-pull-quote>p:first-of-type::before{top:.3rem}}.p-pull-quote>p:last-of-type{margin-bottom:0}.p-pull-quote>p:last-of-type::after{color:#cdcdcd;display:inline-block;font-size:3rem;font-weight:bold;max-width:1.25rem;position:absolute;content:'\2002\201E';margin-left:.25rem;margin-top:-2.2rem}@media (max-width: 768px){.p-pull-quote>p:last-of-type::after{font-size:2.75rem}}@media (max-width: 768px){.p-pull-quote>p:last-of-type::after{margin-top:-1.7rem}}.p-pull-quote__citation{font-style:italic;margin-top:.25rem}.p-search-box__button,.p-search-box__reset{background:#fff;border:1px solid #cdcdcd;display:block;height:100%;margin:0;padding:0 .5rem;position:absolute;top:0;width:2.5rem}.p-search-box__button:hover,.p-search-box__reset:hover{background:inherit}.p-search-box__button:hover:disabled,.p-search-box__reset:hover:disabled{cursor:not-allowed}.p-search-box{box-shadow:inset 0 1px 2px rgba(0,0,0,0.12);display:flex;margin-bottom:1.2rem;position:relative}.p-search-box__input{box-shadow:none;flex-grow:2;margin-bottom:0}.p-search-box__input::-webkit-search-cancel-button{-webkit-appearance:none}.p-search-box__input:not(:valid) ~ .p-search-box__reset{display:none}.p-search-box__button{right:0}.p-search-box__reset{border-left:0;border-right:0;right:2.5rem}.p-slider{appearance:none;border-radius:3px;margin:.5rem 0;padding:0;width:100%}.p-slider::-webkit-slider-runnable-track{border:1px solid #cdcdcd;border-radius:3px;height:6px}.p-slider::-webkit-slider-thumb{appearance:none;background:#fff;border:0;border-radius:2px;box-shadow:0 0 2px 1px rgba(0,0,0,0.2);height:24px;margin-top:-10.5px;width:24px}.p-slider::-webkit-slider-thumb:hover{cursor:pointer}.p-slider::-moz-range-track{background:#fff;border:1px solid #cdcdcd;border-radius:2px;height:4px}.p-slider::-moz-range-progress{background-color:#335280;border-radius:2px;height:4px}.p-slider::-moz-range-thumb{background:#fff;border:0;border-radius:2px;box-shadow:0 0 2px 1px rgba(0,0,0,0.2);height:24px;width:24px}.p-slider::-moz-range-thumb:hover{cursor:pointer}.p-slider::-moz-focus-outer{border:0}.p-slider::-ms-track{background:transparent;border-color:transparent;border-width:12px;color:transparent;height:6px;width:calc(100% - ($thumb-size / 2))}.p-slider::-ms-fill-lower{background:#335280;border:1px solid #cdcdcd;border-radius:2px}.p-slider::-ms-fill-upper{background:#fff;border:1px solid #cdcdcd;border-radius:2px}.p-slider::-ms-thumb{background:#fff;border:0;border-radius:2px;box-shadow:0 0 2px 1px rgba(0,0,0,0.2);height:24px;margin:0 2px;width:24px}.p-slider::-ms-thumb:hover{cursor:pointer}.p-slider::-ms-tooltip{display:none}.p-slider:focus{outline:none}.p-slider:focus::-webkit-slider-thumb{outline:1px solid #19b6ee;outline-offset:2px}.p-slider:focus::-moz-range-thumb{outline:1px solid #19b6ee;outline-offset:2px}.p-slider:focus::-ms-thumb{outline:1px solid #19b6ee;outline-offset:2px}.p-slider:disabled{opacity:.5}.p-slider__wrapper{align-items:center;display:inline-flex;width:100%}.p-slider__input{height:2.625em;margin:0 0 0 1rem;min-width:3.5em;text-align:center;width:5%}@media only screen and (max-width: 1030px){[class^='p-strip'].is-shallow{padding-bottom:.5rem;padding-top:.5rem}}@media only screen and (min-width: 1030px){[class^='p-strip'].is-shallow{padding-bottom:1rem;padding-top:1rem}}@media only screen and (max-width: 1030px){.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image{padding-bottom:1.5rem;padding-top:1.5rem}}@media only screen and (min-width: 1030px){.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image{padding-bottom:3rem;padding-top:3rem}}@media only screen and (max-width: 1030px){[class^='p-strip'].is-deep{padding:2.5rem 0 2.5rem}}@media only screen and (min-width: 1030px){[class^='p-strip'].is-deep{padding:5rem 0}}.p-strip,.p-strip--light,.p-strip--dark,.p-strip--accent,.p-strip--image{clear:both;width:100%}.p-strip{background-color:transparent}.p-strip--light{background-color:#f7f7f7}.p-strip--dark{background-color:#111;color:#f7f7f7}.p-strip--accent{background-color:#e95420;color:#111}.p-strip--image{background-repeat:no-repeat;background-size:cover}.p-strip--image.is-light{color:#000}.p-strip--image.is-dark{color:#fff}[class^='p-strip'].is-bordered{border-bottom:1px solid #cdcdcd;margin-bottom:-.0625rem}.p-switch{height:1.5rem;margin:0;position:relative;width:3rem}.p-switch:checked+.p-switch__slider::before{left:50%}.p-switch:focus{outline:0}.p-switch:focus+.p-switch__slider{outline:1px solid #19b6ee;outline-offset:2px}.p-switch__slider{background:linear-gradient(to right, #335280 50%, #cdcdcd 50%);box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);height:1.5rem;margin:.1rem 0 .5rem 0;position:relative;width:3rem}.p-switch__slider::before{transition-duration:0.5s;transition-property:all;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:#fff;content:"";height:1.5rem;left:0;position:absolute;width:1.5rem}button.p-switch{align-items:stretch;border:0;display:inline-flex;height:1.5rem;padding:initial;width:3rem}button.p-switch :first-child,button.p-switch :last-child{box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);line-height:1.5rem;margin:0;text-align:center;width:50%}button.p-switch :first-child{background-color:#335280;border-radius:2px 0 0 2px;color:#fff}button.p-switch :last-child{background-color:#cdcdcd;border-radius:0 2px 2px 0}button.p-switch::before{transition-duration:0.5s;transition-property:all;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:inherit;background-color:#fff;border-radius:.125rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);content:'';display:block;height:100%;left:0;max-height:2rem;padding:0;position:absolute;top:0;width:50%}button.p-switch::after{display:none}button.p-switch[aria-checked='true']::before{left:50%}.p-table-expanding{display:flex;flex-flow:column nowrap;justify-content:space-between}.p-table-expanding tbody{margin:0}.p-table-expanding tr{display:flex;flex-flow:row;flex-wrap:wrap;margin:0;width:100%}.p-table-expanding tr+tr{margin:0}.p-table-expanding th,.p-table-expanding td{display:flex;flex-basis:0;flex-flow:row nowrap;flex-grow:1;margin:0;word-break:break-word}.p-table-expanding th.p-table-expanding__panel,.p-table-expanding th.p-table-expanding__panel--bordered,.p-table-expanding td.p-table-expanding__panel,.p-table-expanding td.p-table-expanding__panel--bordered{flex-basis:100%;max-width:100%}.p-table-expanding th.p-table-expanding__panel[aria-hidden="true"],.p-table-expanding th[aria-hidden="true"].p-table-expanding__panel--bordered,.p-table-expanding td.p-table-expanding__panel[aria-hidden="true"],.p-table-expanding td[aria-hidden="true"].p-table-expanding__panel--bordered{display:none}.p-table-expanding th.p-table-expanding__panel .row,.p-table-expanding th.p-table-expanding__panel--bordered .row,.p-table-expanding td.p-table-expanding__panel .row,.p-table-expanding td.p-table-expanding__panel--bordered .row{max-width:100%;padding:0;width:100%}@media screen and (max-width: 1030px){.p-table--mobile-card thead{display:none}.p-table--mobile-card tbody{display:flex;flex-wrap:wrap;justify-content:space-between}.p-table--mobile-card tr{border-top:1px solid #cdcdcd;display:flex;flex-wrap:wrap;margin:-1px 0 .5rem;width:100%}.p-table--mobile-card td,.p-table--mobile-card tbody th{align-items:flex-start;display:flex;flex:0 1 auto;flex-direction:column;justify-content:left !important;margin:0;overflow:visible;padding-bottom:0;padding-top:0;text-align:left !important;width:25%}.p-table--mobile-card td[aria-label],.p-table--mobile-card tbody th[aria-label]{text-align:right}.p-table--mobile-card td[aria-label]::before,.p-table--mobile-card tbody th[aria-label]::before{content:attr(aria-label);display:block;flex:0 1 auto;margin-bottom:0;width:100%}.p-table--mobile-card td.u-align--right,.p-table--mobile-card tbody th.u-align--right{justify-content:unset !important}.p-table--mobile-card .p-contextual-menu,.p-table--mobile-card .p-contextual-menu--left,.p-table--mobile-card .p-contextual-menu--center,.p-table--mobile-card .p-cta,.p-table--mobile-card .p-table-menu{width:100%}.p-table--mobile-card .p-contextual-menu [role="menuitem"],.p-table--mobile-card .p-contextual-menu--left [role="menuitem"],.p-table--mobile-card .p-contextual-menu--center [role="menuitem"],.p-table--mobile-card .p-cta [role="menuitem"],.p-table--mobile-card .p-table-menu [role="menuitem"]{display:none}.p-table--mobile-card .p-contextual-menu__dropdown,.p-table--mobile-card .p-cta__dropdown,.p-table--mobile-card .p-table-menu .p-table-menu__dropdown,.p-table-menu .p-table--mobile-card .p-table-menu__dropdown{box-shadow:none;display:block;max-width:100%;position:relative}.p-table--mobile-card .p-contextual-menu__dropdown::before,.p-table--mobile-card .p-cta__dropdown::before,.p-table--mobile-card .p-table-menu .p-table-menu__dropdown::before,.p-table-menu .p-table--mobile-card .p-table-menu__dropdown::before{display:none}.p-table--mobile-card .p-contextual-menu__group{padding:0}.p-table--mobile-card .p-contextual-menu__group+.p-contextual-menu__group{margin-top:.5rem;padding-top:.5rem}.p-table--mobile-card .p-contextual-menu__link,.p-table--mobile-card .p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__power-off{border-color:#cdcdcd;border-radius:.125rem;border-style:solid;border-width:1px;box-sizing:border-box;color:#000;cursor:pointer;display:block;line-height:1rem;outline:none;padding:.5rem 1.5rem;text-align:center;text-decoration:none;width:100%}.p-table--mobile-card .p-contextual-menu__link+.p-contextual-menu__link,.p-table--mobile-card .p-cta__link+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-contextual-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-contextual-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-contextual-menu__link,.p-table--mobile-card .p-contextual-menu__link+.p-cta__link,.p-table--mobile-card .p-cta__link+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-cta__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-cta__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-cta__link,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__link,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__link,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__check-power,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__check-power,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__power-on,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__power-on,.p-table--mobile-card .p-table-menu .p-contextual-menu__link+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-contextual-menu__link+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-cta__link+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-cta__link+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__link+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__link+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__check-power+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__check-power+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__power-on+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__power-on+.p-table-menu__power-off,.p-table--mobile-card .p-table-menu .p-table-menu__power-off+.p-table-menu__power-off,.p-table-menu .p-table--mobile-card .p-table-menu__power-off+.p-table-menu__power-off{margin-top:.25rem}}.p-table--sortable th[role="columnheader"][aria-sort="ascending"]::after,.p-table--sortable [role="columnheader"].is-sorted.sort-asc::after,.p-table--sortable th[role="columnheader"][aria-sort="descending"]::after,.p-table--sortable [role="columnheader"].is-sorted.sort-desc::after{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10' viewBox='0 0 10 4'%3E%3Cpath d='M3.637 3.138c-.518-.365-1.052-.778-1.6-1.238C1.486 1.44.946.948.414.423.273.283.135.14 0 0h1.54c.305.29.62.57.948.846.138.116.277.23.417.34.163.13.328.257.495.38.085.062.17.123.257.184.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.4-.28.79-.583 1.17-.904C7.837.57 8.153.29 8.457 0h1.54c-.134.14-.272.282-.414.422C9.05.948 8.51 1.442 7.963 1.9c-.55.46-1.084.873-1.602 1.238S5.39 3.79 5 4c-.39-.21-.845-.497-1.363-.862z' fill='%23888' fill-rule='evenodd'/%3E%3C/svg%3E");background-position:center;background-repeat:no-repeat;background-size:100%;content:'';display:inline-block;height:.4rem;margin-left:.25rem;vertical-align:middle;width:1rem}.p-table--sortable{table-layout:fixed}.p-table--sortable th[role="columnheader"][aria-sort],.p-table--sortable [role="columnheader"]{align-items:center;cursor:pointer;white-space:nowrap}.p-table--sortable th[role="columnheader"][aria-sort="descending"]::after,.p-table--sortable [role="columnheader"].is-sorted.sort-desc::after{transform:rotate(180deg)}.p-table--sortable th[role="columnheader"][aria-sort]:hover,.p-table--sortable [role="columnheader"]:hover{color:#007aa6;text-decoration:underline}.p-tabs{border-radius:0;overflow:hidden;padding:0;position:relative}.p-tabs::before{bottom:0;color:#666;content:'\203A';display:block;font-size:2rem;line-height:1.5rem;padding-right:1.5rem;pointer-events:none;position:absolute;right:.5rem;text-align:right;top:15%;width:1rem;z-index:10}@media screen and (min-width: 768px){.p-tabs::before{display:none}}.p-tabs__list{margin:0 auto .5rem;overflow-x:scroll;padding:0;position:relative;white-space:nowrap;width:100%}@media screen and (min-width: 768px){.p-tabs__list{max-width:90rem;overflow:hidden}}.p-tabs__item{display:inline-block;float:none;margin:0;padding:0;width:auto}@media screen and (min-width: 768px){.p-tabs__item{float:left}}.p-tabs__item:last-child{margin-right:3rem}@media screen and (min-width: 768px){.p-tabs__item:last-child{margin-right:0}}.p-tabs__link{color:#000;display:inline-block;padding:.75rem 1rem}.p-tabs__link:visited,.p-tabs__link:active,.p-tabs__link:hover{color:#000;text-decoration:none}.p-tabs__link:hover,.p-tabs__link[aria-selected="true"],.p-tabs__link.is-active{position:relative}.p-tabs__link:hover::before,.p-tabs__link[aria-selected="true"]::before,.p-tabs__link.is-active::before{bottom:0;background-color:#666;content:'';position:absolute}.p-tabs__link:hover::before,.p-tabs__link[aria-selected="true"]::before,.p-tabs__link.is-active::before{height:.1875rem;width:auto;left:-1px;right:-1px;z-index:1}.p-tooltip{position:relative}.p-tooltip__message{background-color:#111;border:0;border-radius:.125rem;color:#fff;display:none;left:0;margin-bottom:0;min-width:155px;padding:.5rem 1rem;position:absolute;text-align:left;text-decoration:initial;top:100%;transform:translateX(0%) translateY(13px);white-space:pre;z-index:1}.p-tooltip__message::before{border-bottom:8px solid #111;border-left:8px solid transparent;border-right:8px solid transparent;bottom:100%;content:'';height:0;left:1rem;pointer-events:none;position:absolute;width:0}.p-tooltip:focus .p-tooltip__message,.p-tooltip:hover .p-tooltip__message{display:inline;text-decoration:initial}.p-tooltip--btm-center .p-tooltip__message{bottom:inherit;left:50%;top:100%;transform:translateX(-50%) translateY(13px)}.p-tooltip--btm-center .p-tooltip__message::before{left:50%;transform:translateX(-50%)}.p-tooltip--btm-right .p-tooltip__message{bottom:inherit;left:initial;right:0;top:100%;transform:translateY(13px)}.p-tooltip--btm-right .p-tooltip__message::before{left:initial;right:.5rem}.p-tooltip--top-left .p-tooltip__message{bottom:100%;left:0;top:initial;transform:translateX(0%) translateY(-13px)}.p-tooltip--top-left .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #111;bottom:-1rem;left:.5rem}.p-tooltip--top-center .p-tooltip__message{bottom:100%;left:50%;top:initial;transform:translateX(-50%) translateY(-13px)}.p-tooltip--top-center .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #111;bottom:-1rem;left:50%;transform:translateX(-50%)}.p-tooltip--top-right .p-tooltip__message{bottom:100%;left:initial;right:0;top:initial;transform:translateX(0%) translateY(-13px)}.p-tooltip--top-right .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid transparent;border-top:8px solid #111;bottom:-1rem;left:initial;right:.5rem}.p-tooltip--right .p-tooltip__message{bottom:inherit;left:100%;top:50%;transform:translateX(14px) translateY(-50%)}.p-tooltip--right .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid transparent;border-right:8px solid #111;border-top:8px solid transparent;bottom:inherit;left:0;top:50%;transform:translateX(-16px) translateY(-50%)}.p-tooltip--left .p-tooltip__message{bottom:inherit;left:-16px;top:50%;transform:translateX(-100%) translateY(-50%)}.p-tooltip--left .p-tooltip__message::before{border-bottom:8px solid transparent;border-left:8px solid #111;border-right:8px solid transparent;border-top:8px solid transparent;bottom:inherit;left:100%;top:50%;transform:translateX(0) translateY(-50%)}.u-align--center{justify-content:center !important;text-align:center !important}.u-align--left{justify-content:flex-start !important;text-align:left !important}.u-align--right{justify-content:flex-end !important;text-align:right !important}.u-align--bottom{margin-top:auto !important}.u-align-text--center{margin-left:auto !important;margin-right:auto !important;text-align:center !important}.u-align-text--left{margin-right:auto !important;text-align:left !important}.u-align-text--right{margin-left:auto !important;text-align:right !important}.u-animation--spin{animation:spin 1s infinite linear}@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.u-baseline-grid{position:relative}.u-baseline-grid::after{background:linear-gradient(to top, rgba(255,0,0,0.3), rgba(255,0,0,0.3) 1px, transparent 1px, transparent);background-size:100% .5rem;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:100}.u-baseline-grid__toggle{bottom:1rem;position:fixed;right:1.5rem;z-index:101}.u-embedded-media{height:0;margin-top:.5rem;max-width:100%;overflow:hidden;padding-bottom:56.25%;position:relative}.u-embedded-media__element{height:100%;left:0;position:absolute;top:0;width:100%}@media only screen and (min-width: 768px){.u-equal-height{display:flex}}.u-float--right{float:right !important}.u-float--left{float:left !important}.u-float-right{float:right !important}@media (max-width: 620px){.u-float-right--small{float:right !important}}@media (min-width: 768px) and (max-width: 1030px){.u-float-right--medium{float:right !important}}@media (min-width: 1030px){.u-float-right--large{float:right !important}}.u-float-left{float:left !important}@media (max-width: 620px){.u-float-left--small{float:left !important}}@media (min-width: 768px) and (max-width: 1030px){.u-float-left--medium{float:left !important}}@media (min-width: 1030px){.u-float-left--large{float:left !important}}.u-hide{display:none !important}@media screen and (max-width: 768px){.u-hide--small{display:none !important}}@media (min-width: 768px) and (max-width: 1030px){.u-hide--medium{display:none !important}}@media screen and (min-width: 1030px){.u-hide--large{display:none !important}}@media (min-width: 768px){.u-image-position{overflow:hidden;position:relative}.u-image-position .u-image-position--top,.u-image-position .u-image-position--bottom,.u-image-position .u-image-position--left,.u-image-position .u-image-position--right{margin:0;position:absolute}.u-image-position [class*='col-']{position:static}.u-image-position--top{top:0}.u-image-position--bottom{bottom:0}.u-image-position--left{left:0}.u-image-position--right{right:0}}.u-no-margin{margin:0 !important}.u-no-margin--top{margin-top:0 !important}.u-no-margin--right{margin-right:0 !important}.u-no-margin--bottom{margin-bottom:0 !important}.u-no-margin--left{margin-left:0 !important}.u-off-screen{height:1px !important;left:-10000px !important;overflow:hidden !important;position:absolute !important;top:auto !important;width:1px !important}.u-no-padding{padding:0 !important}.u-no-padding--top{padding-top:0 !important}.u-no-padding--right{padding-right:0 !important}.u-no-padding--bottom{padding-bottom:0 !important}.u-no-padding--left{padding-left:0 !important}.u-show{display:inherit !important}@media screen and (max-width: 768px){.u-show--small{display:inherit !important}}@media (min-width: 768px) and (max-width: 1030px){.u-show--medium{display:inherit !important}}@media screen and (min-width: 1030px){.u-show--large{display:inherit !important}}.u-sv-3::after,.u-sv-2::after,.u-sv-1::after,.u-sv0::after,.u-sv1::after,.u-sv2::after,.u-sv3::after{content:'';display:block;height:.0625rem;position:relative}.u-sv-3::after{margin-top:-1.5625rem}.u-sv-2::after{margin-top:-1.0625rem}.u-sv-1::after{margin-top:-.5625rem}.u-sv0::after{margin-top:-.0625rem}.u-sv1::after{margin-top:.4375rem}.u-sv2::after{margin-top:.9375rem}.u-sv3::after{margin-top:1.4375rem}@media (min-width: 768px){.u-vertically-center{align-items:center !important;display:flex !important}.u-vertically-center>img{align-self:center !important}}.u-hidden{display:none !important}@media screen and (max-width: 768px){.u-hidden--small{display:none !important}}@media (min-width: 768px) and (max-width: 1030px){.u-hidden--medium{display:none !important}}@media screen and (min-width: 1030px){.u-hidden--large{display:none !important}}.u-visible{display:inherit !important}@media screen and (max-width: 768px){.u-visible--small{display:inherit !important}}@media (min-width: 768px) and (max-width: 1030px){.u-visible--medium{display:inherit !important}}@media screen and (min-width: 1030px){.u-visible--large{display:inherit !important}}.u-position-relative{position:relative}.u-width--auto{width:auto !important}.u-flex--no-wrap{display:flex;flex-wrap:wrap}.u-text--light{color:#666}.u-td-outdent--left{margin-left:-.5rem}.u-td-outdent--right{margin-right:-.5rem}.u-td-outdent-focusable--left{margin-left:-.3125rem}.u-td-outdent-focusable--right{margin-right:-.3125rem}.u-valign--middle{vertical-align:middle}.u-space-between{display:flex;justify-content:space-between}.u-disable{opacity:0.5;pointer-events:none !important;user-select:none}.u-mirror--y{transform:rotate(180deg)}.u-rotate{transform:rotate(180deg)}.hide-create-tag-label .create-tag-label{display:none}.p-table--controllers-commissioning .is-active .p-icon--chevron{transform:rotate(180deg)}.u-text-overflow{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table thead th{font-size:.76562rem;font-weight:400;margin-bottom:.1rem;padding-top:.15rem}h4+h4,.p-heading--four+h4,h4+.p-heading--four,.p-heading--four+.p-heading--four{margin-top:-1rem !important}.default-text{display:block}.p-p-small,.p-p-small--align-with-p{display:block}.p-p-small--align-with-p{padding-top:0.55rem}.p-p-compact{display:block;margin-bottom:.6rem}.u-no-max-width{max-width:unset}@media only screen and (max-width: 460px){button,[type="submit"],.p-button,.p-button--neutral,.p-button--brand,.p-button--positive,.p-button--negative,.p-button--base{width:auto}}.p-key-icon--free::before,.p-key-icon--used::before,.p-key-icon--requests::before,.p-key-icon--other::before{content:"•";float:left;font-size:2rem;line-height:1.5rem;margin-right:.5rem;padding-top:.4rem;width:.5rem}.p-key-icon--free{background-color:rgba(0,122,166,0.2)}.p-key-icon--used{background-color:#007aa6}.p-key-icon--requests{background-color:#0e8420}.p-key-icon--other{background-color:rgba(14,132,32,0.2)}.p-slider{-webkit-appearance:none;-moz-appearance:none}.p-slider::-webkit-slider-thumb{-webkit-appearance:none;-webkit-box-shadow:0 0 2px 1px rgba(0,0,0,0.2)}.p-slider__wrapper{-webkit-box-align:center;-ms-flex-align:center}.p-slider{margin:0 0 1rem 0}.p-slider__wrapper{margin-top:-1rem}.p-slider__input{height:inherit !important}.p-slider+button{margin-left:1rem}.u-baseline-grid{position:relative}.u-baseline-grid::after{background:linear-gradient(to top, rgba(255,0,0,0.3), rgba(255,0,0,0.3) 1px, transparent 1px, transparent);background-size:100% .5rem;bottom:0;content:"";display:block;left:0;pointer-events:none;position:absolute;right:0;top:0;z-index:100}.u-baseline-grid__toggle{bottom:1rem;position:fixed;right:1.5rem;z-index:101}.p-footer__link{color:#007aa6}.p-footer__link::after{color:#111}body{background-color:#f7f7f7}maas-obj-field,maas-obj-errors,maas-obj-form,maas-machines-table,storage-disks-partitions,storage-filesystems{display:block;position:relative}maas-obj-field[type="text"],maas-obj-field[type="password"]{background-color:transparent;border:0;border-radius:0;box-shadow:none;padding:0}textarea{min-height:175px}.field-description textarea{min-height:initial}.p-table--mobile-card td[aria-label]{min-height:2rem}@media (max-width: 768px){.p-table--mobile-card td,.p-table--mobile-card tr{overflow-x:visible}}.u-float-none{float:none !important}p:empty,ul:empty,label:empty{margin:0;padding:0}.tags .input{width:100% !important}dl dt:first-of-type{margin-top:0;padding-top:0}.p-navigation--sidebar{background:#fff}.p-navigation--sidebar .sidebar__content{top:0;padding:1rem}.sidebar__content .sidebar__second-level .is-active{background-position-y:0.9rem}.u-float--none{float:none !important}.u-remove-max-width{max-width:none}.p-navigation--sidebar{margin-bottom:1rem}.p-navigation--sidebar::after{background:transparent}.p-form__control{margin-top:0;white-space:normal}.p-form-help-text{display:inline-block}select,.p-option-selector__input{-moz-appearance:none;-webkit-appearance:none;appearance:none;padding-right:2.3rem}maas-obj-field[type="password"]{background:transparent;border:0}.p-list-tree .p-list-tree::after{display:none !important}.editable{padding:0.5rem 1rem;border:1px solid transparent}.editable:hover,.editable.editmode{border:1px solid #cdcdcd}.page-header__title-domain{display:inline-block;width:auto}.col-12{width:100%}.errorlist{list-style:none;display:inline-block;margin-left:0}.sidebar__second-level .is-active{background:transparent url("../assets/images/89c10794-remove.svg") top .5rem right 1.5rem no-repeat;background-size:12px}.p-form__control [type="checkbox"]{height:auto;min-height:auto}.p-script-expander{border-color:#cdcdcd;border-bottom-style:solid;border-bottom-width:1px}.p-script-expander__content{overflow:hidden;width:100%;margin-top:0}.p-script-expander__controls{vertical-align:top}.p-contextual-menu__dropdown,.p-cta__dropdown,.p-table-menu .p-table-menu__dropdown{display:block;min-width:13rem;width:100%}.p-tooltip__message{margin:0;z-index:100}.u-upper-case--first{text-transform:capitalize}.u-no-wrap{white-space:nowrap}.u-wrap{white-space:normal}.p-form__group .col-1:first-child,.p-form__group .col-2:first-child,.p-form__group .p-form--stacked .p-form__label:first-child,.p-form--stacked .p-form__group .p-form__label:first-child,.p-form__group .col-3:first-child,.p-form__group .col-4:first-child,.p-form__group .p-form--stacked .p-form__control:first-child,.p-form--stacked .p-form__group .p-form__control:first-child,.p-form__group .p-pod-summary__aside:first-child,.p-form__group .p-storage__name:first-child,.p-form__group .col-5:first-child,.p-form__group .col-6:first-child,.p-form__group .col-7:first-child,.p-form__group .col-8:first-child,.p-form__group .p-pod-summary__storage:first-child,.p-form__group .col-9:first-child,.p-form__group .col-10:first-child,.p-form__group .col-11:first-child,.p-form__group .col-12:first-child{margin-left:0}.flex-row{display:flex;justify-content:space-between}@media (max-width: 768px){.flex-row{flex-direction:column}}.flex-item{flex:1}.p-inline-list--settings .p-inline-list__item{display:inline-block;margin-right:1.5rem;vertical-align:top}.p-inline-list--settings [type="checkbox"]{float:none}.p-inline-list--settings label{display:inline}.p-inline-list__item div{display:inline-block}.p-list--divided .p-list__item::after{border-bottom-style:solid;border-bottom-color:#e5e5e5}@media (min-width: 620px){table th,table td,table p{text-overflow:ellipsis;overflow-x:hidden;overflow-y:visible;white-space:nowrap}}table{overflow-x:visible}table input[type="radio"],table input[type="checkbox"]{float:none}table form input[type="radio"],table form input[type="checkbox"]{float:left}table thead th{margin-bottom:0;padding-bottom:.35rem;text-transform:uppercase}table th,table td{display:table-cell !important;flex-basis:auto !important;flex-grow:0;vertical-align:top}table th:first-of-type,table td:first-of-type{padding-left:.5rem}table th:not(:last-child),table td:not(:last-child){padding-right:.5rem}tr.is-active{background-color:#fff}tr.is-suppressed>td:nth-child(2),tr.is-suppressed>td:nth-child(3),tr.is-suppressed>td:nth-child(4),tr.is-suppressed>td:nth-child(5),tr.is-suppressed>td:nth-child(6){opacity:0.5}thead tr{border-bottom-color:#cdcdcd}tbody tr:not(:first-child){border-top-color:#e5e5e5}tr.ng-hide+tr{border:0}.p-table--action-cell{overflow:visible}maas-obj-field{margin-bottom:0 !important;padding:0 !important}[type="text"].u-min-margin--bottom,[type="date"].u-min-margin--bottom,[type="datetime"].u-min-margin--bottom,[type="datatime-local"].u-min-margin--bottom,[type="month"].u-min-margin--bottom,[type="time"].u-min-margin--bottom,[type="week"].u-min-margin--bottom,[type="color"].u-min-margin--bottom,[type="number"].u-min-margin--bottom,[type="search"].u-min-margin--bottom,[type="password"].u-min-margin--bottom,[type="email"].u-min-margin--bottom,[type="url"].u-min-margin--bottom,[type="tel"].u-min-margin--bottom,textarea.u-min-margin--bottom,select.u-min-margin--bottom,.u-min-margin--bottom.p-option-selector__input,.p-table--pod-networking-config input,.p-table--pod-storage-config input,.p-table--pod-networking-config select,.p-table--pod-networking-config .p-option-selector__input{margin-bottom:.2rem !important}table [type="text"],table [type="date"],table [type="datetime"],table [type="datatime-local"],table [type="month"],table [type="time"],table [type="week"],table [type="color"],table [type="number"],table [type="search"],table [type="password"],table [type="email"],table [type="url"],table [type="tel"],table textarea,table select,table .p-option-selector__input{margin-bottom:.7rem;padding-bottom:.3375rem;padding-top:.3375rem;min-height:2.3rem;min-width:auto}.is-small [type="text"],.is-small [type="date"],.is-small [type="datetime"],.is-small [type="datatime-local"],.is-small [type="month"],.is-small [type="time"],.is-small [type="week"],.is-small [type="color"],.is-small [type="number"],.is-small [type="search"],.is-small [type="password"],.is-small [type="email"],.is-small [type="url"],.is-small [type="tel"],.is-small textarea,.is-small select,.is-small .p-option-selector__input{margin-bottom:.1rem;padding-bottom:.0875rem;padding-top:.0875rem}p.u-min-margin--bottom{margin-bottom:.1rem}input[type="checkbox"]+label::after{top:13px}th:not(.p-table__group-label) input[type="checkbox"]+label::before{top:0.65em}th:not(.p-table__group-label) input[type="checkbox"]+label::after{top:11px}.p-inline-list__item input[type="checkbox"]+label::before{top:0.5rem}.p-inline-list__item input[type="checkbox"]+label::after{top:11px}.u-full-width-input .p-form__control{width:100%}.obj-saving{margin-right:0.5rem}a:visited{color:#007aa6}.p-action-button{position:relative}.p-action-button::before{background-size:1rem;content:"";height:1rem;left:1rem;position:absolute;top:.5875rem;width:1rem}.p-action-button.is-indeterminate,.p-action-button.is-done{padding-left:2.5rem}.p-action-button.is-indeterminate::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23666' fill-rule='nonzero'/%3E%3C/svg%3E");animation:spin 1s infinite linear}.p-action-button.is-indeterminate.p-button--positive::before,.p-action-button.is-indeterminate.p-button--negative::before{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='24' width='24' viewBox='0 0 24 24'%3E%3Ctitle%3Espinner-dark-grey%3C/title%3E%3Cpath d='M7.49 23.123c2.78 1.125 5.978 1.213 8.975 0 4.247-1.72 6.972-5.603 7.424-9.87l-1.136-.118c-.408 3.86-2.875 7.374-6.717 8.93-2.71 1.098-5.605 1.018-8.118 0l-.43 1.058zm-2.21-1.176c-1.913-1.29-3.475-3.148-4.404-5.45C-1.284 11.146.686 5.15 5.28 2.05l.638.946C1.76 5.802-.02 11.228 1.934 16.068c.84 2.086 2.254 3.766 3.985 4.933l-.64.947zm18.61-11.2c-.115-1.088-.38-2.178-.81-3.242-2.478-6.142-9.457-9.11-15.59-6.628l.43 1.057c5.546-2.245 11.86.44 14.103 5.998.388.963.63 1.95.733 2.933l1.134-.12z' fill='%23fff' fill-rule='nonzero'/%3E%3C/svg%3E")}.p-action-button.is-done::before{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%230e8420' stroke-width='1.5' fill='%230e8420' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23fff' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E");animation:none}.p-action-button.is-done.p-button--positive::before{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23fff' stroke-width='1.5' fill='%23fff' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%230e8420' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E")}.p-action-button.is-done.p-button--negative::before{background-image:url("data:image/svg+xml,%3Csvg width='17' height='17' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform='translate%281 1%29' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23fff' stroke-width='1.5' fill='%23fff' cx='7.25' cy='7.25' r='7.25'/%3E%3Cpath fill='%23c7162b' d='M11.05 4.173l-.066.058L6.25 8.378l-2.776-2.38-.839.948L6.25 10.75l5.5-5.787-.7-.79z'/%3E%3C/g%3E%3C/svg%3E")}.p-icon{padding-right:.5rem}.p-icon--edit{background-image:url("data:image/svg+xml;charset=UTF-8,%3csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3e%3ctitle%3eedit%3c/title%3e%3cg fill='%23666666' fill-rule='evenodd'%3e%3cpath d='M17 15h5v1h-5zm-3 3h8v1h-8zm-3 3h11v1H11zm5.75-21L3.47 13.517S.956 17.465 0 21.987v.004l.002.004V22c4.532-.955 8.48-3.472 8.48-3.472L22 5.25 16.75 0zM4.51 14.555L7.454 17.5c-.2.114-2.99 2.064-5.544 2.602V20.093l-.002-.003c.537-2.546 2.485-5.334 2.602-5.537v.002z'/%3e%3cpath d='M2.234 18l1.85 1.85L1 21'/%3e%3c/g%3e%3c/svg%3e")}[class$="--dark"] .p-icon--edit{background-image:url("data:image/svg+xml;charset=UTF-8,%3csvg width='22' height='22' viewBox='0 0 22 22' xmlns='http://www.w3.org/2000/svg'%3e%3ctitle%3eedit%3c/title%3e%3cg fill='%23CDCDCD' fill-rule='evenodd'%3e%3cpath d='M17 15h5v1h-5zm-3 3h8v1h-8zm-3 3h11v1H11zm5.75-21L3.47 13.517S.956 17.465 0 21.987v.004l.002.004V22c4.532-.955 8.48-3.472 8.48-3.472L22 5.25 16.75 0zM4.51 14.555L7.454 17.5c-.2.114-2.99 2.064-5.544 2.602V20.093l-.002-.003c.537-2.546 2.485-5.334 2.602-5.537v.002z'/%3e%3cpath d='M2.234 18l1.85 1.85L1 21'/%3e%3c/g%3e%3c/svg%3e")}.p-icon--status-failed{background:url('data:image/svg+xml;utf8,%3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23C7162B" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23C7162B" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-in-progress{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23335280" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23335280" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-queued{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23666666" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23666666" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-succeeded{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%230E8420" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%230E8420" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-waiting{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23F99B11" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23F99B11" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--timed-out{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='16px' width='16px' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 16 16'%3E%3Ctitle%3Etimed out%3C/title%3E%3Cg id='Page-1' fill-rule='evenodd' fill='none'%3E%3Cg id='smoke-testing-status' transform='translate%28-62 -206%29'%3E%3Cg id='timed-out' transform='translate%2850 191%29'%3E%3Cg transform='translate%2812 15%29'%3E%3Crect id='rect4970' y='0.00002' x='0' height='16' width='16'/%3E%3Ccircle id='circle4972' stroke-width='1.5' cy='8' stroke='%23E95420' cx='8' r='7.25'/%3E%3Cpolyline id='path839' stroke='%23E95420' stroke-width='2' points='11.8 11.8 7.9999 8 7.9999 3'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A")}.p-icon--success-muted{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='17' height='17' viewBox='0 0 17 17'%3E%3Cg transform='translate(1 1)' fill='none' fill-rule='evenodd'%3E%3Ccircle stroke='%23CDCDCD' stroke-width='1.5' fill='%23CDCDCD' cx='7.25001' cy='7.25001' r='7.25001'/%3E%3Cpath fill='%23fff' d='M11.0503 4.17345l-.0659.0577-4.73475 4.14722-2.77557-2.38094-.83906.94888 3.61532 3.80373L11.75 4.96278l-.6997-.7893'/%3E%3C/g%3E%3C/svg%3E")}.p-icon--locked{background-image:url("data:image/svg+xml,%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3Cpath d='M8,3e-05 C5.7926,3e-05 4,1.79263 4,4.00003 L4,7.00003 L2,7.00003 L2,15.99953 L14,15.99953 L14,7.00003 L12,7.00003 L12,4.00003 C12,1.79263 10.207,3e-05 8,3e-05 L8,0 L8,3e-05 Z M8,1.00003 C9.6706,1.00003 11,2.32933 11,4.00003 L11,7.00003 L5,7.00003 L5,4.00003 C5,2.32933 6.3293,1.00003 8,1.00003 Z M9,9.50003 L9,13.99953 L7,13.99953 L7,9.99953 L9,9.50003 L9,9.50003 Z' id='padlock-icon'%3E%3C/path%3E%3C/defs%3E%3Cg id='padlock-16'%3E%3Cuse id='lock-icon' fill='%23666666' fill-rule='nonzero' xlink:href='%23padlock-icon'%3E%3C/use%3E%3C/g%3E%3C/svg%3E%0A")}.p-icon--status-waiting{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23666666" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23666666" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-succeeded{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%230E8420" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%230E8420" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-queued{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23CDCDCD" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23CDCDCD" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-in-progress{background:url('data:image/svg+xml;utf8, %3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23335280" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23335280" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--status-failed{background:url('data:image/svg+xml;utf8,%3Csvg width="16px" height="16px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"%3E%3Cg id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"%3E%3Cg id="status-queued" transform="translate(1, 1)"%3E%3Cg id="Page-1"%3E%3Cg id="status-queued"%3E%3Cg id="Group"%3E%3Ccircle id="Oval" stroke="%23C7162B" stroke-width="2" cx="6" cy="6" r="6"%3E%3C/circle%3E%3Ccircle id="Oval-Copy" class="status-circle" fill="%23C7162B" cx="6" cy="6" r="4"%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E')}.p-icon--compose-machine{background:url('data:image/svg+xml;utf8,%3Csvg width="149" height="105" viewBox="0 0 149 105" xmlns="http://www.w3.org/2000/svg"%3E%3Ctitle%3Ecompose-machine%3C/title%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cpath d="M9.746 1.845H37.3c5.98 0 7.723 1.735 7.723 7.68v85.64c0 5.946-1.744 7.68-7.723 7.68H9.746c-5.98 0-7.723-1.734-7.723-7.68V9.525c0-5.945 1.744-7.68 7.723-7.68z" stroke="%23CDCDCD" stroke-width="3"/%3E%3Cpath d="M13.49 86.845c-2.48 0-4.502 2.018-4.502 4.5 0 2.48 2.02 4.5 4.502 4.5 2.48 0 4.498-2.02 4.498-4.5 0-2.482-2.017-4.5-4.498-4.5z" stroke="%23CDCDCD" stroke-width="2"/%3E%3Cpath fill="%23CDCDCD" d="M8.476 8.346v1H38.54v-1M8.436 13.372v1H38.5v-1M8.476 18.346v1H38.54v-1M8.436 23.372v1H38.5v-1"/%3E%3Cg%3E%3Cpath d="M111.746 1.845H139.3c5.98 0 7.723 1.735 7.723 7.68v85.64c0 5.946-1.744 7.68-7.723 7.68h-27.554c-5.98 0-7.723-1.734-7.723-7.68V9.525c0-5.945 1.744-7.68 7.723-7.68z" stroke="%23CDCDCD" stroke-width="3"/%3E%3Cpath d="M115.49 86.845c-2.48 0-4.502 2.018-4.502 4.5 0 2.48 2.02 4.5 4.502 4.5 2.48 0 4.498-2.02 4.498-4.5 0-2.482-2.017-4.5-4.498-4.5z" stroke="%23CDCDCD" stroke-width="2"/%3E%3Cpath fill="%23CDCDCD" d="M110.476 8.346v1h30.065v-1M110.436 13.372v1H140.5v-1M110.476 18.346v1h30.065v-1M110.436 23.372v1H140.5v-1"/%3E%3C/g%3E%3Cg%3E%3Cpath d="M60.746 1.845H88.3c5.98 0 7.723 1.735 7.723 7.68v85.64c0 5.946-1.744 7.68-7.723 7.68H60.746c-5.98 0-7.723-1.734-7.723-7.68V9.525c0-5.945 1.744-7.68 7.723-7.68z" stroke="%23CDCDCD" stroke-width="3"/%3E%3Cpath d="M64.49 86.845c-2.48 0-4.502 2.018-4.502 4.5 0 2.48 2.02 4.5 4.502 4.5 2.48 0 4.498-2.02 4.498-4.5 0-2.482-2.017-4.5-4.498-4.5z" stroke="%23CDCDCD" stroke-width="2"/%3E%3Cpath fill="%23CDCDCD" d="M59.476 8.346v1H89.54v-1M59.436 13.372v1H89.5v-1M59.476 18.346v1H89.54v-1M59.436 23.372v1H89.5v-1"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');background-size:100% 100%}.p-icon--account{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal' d='M48 6C24.828 6 6 24.83 6 48s18.828 41.998 42 41.998S90 71.17 90 48C90 24.83 71.172 6 48 6zm0 4c21.01 0 37.998 16.99 37.998 38 0 9.324-3.35 17.852-8.908 24.46a25.598 25.598 0 0 0-4.94-7.495c-1.955-2.062-4.308-3.79-7.017-5.192a24.975 24.975 0 0 1-3.697 2.682c3.188 1.365 5.775 3.117 7.81 5.264h.002c2.162 2.274 3.75 4.902 4.81 7.94C67.26 82.07 58.1 86 48 86c-10.135 0-19.325-3.96-26.133-10.412 1.123-3.013 2.706-5.623 4.78-7.875 2.208-2.328 5.055-4.2 8.626-5.606 3.507-1.38 7.732-2.106 12.696-2.11L48 60c2.886 0 5.613-.508 8.12-1.54l.017-.01.017-.01c2.49-1.078 4.66-2.615 6.45-4.575 1.84-1.96 3.258-4.302 4.242-6.967.994-2.693 1.476-5.672 1.476-8.898v-.002c0-3.18-.484-6.132-1.476-8.822-.98-2.706-2.397-5.073-4.24-7.037-1.79-1.966-3.973-3.483-6.47-4.515C53.623 16.54 50.89 16 48 16c-2.892 0-5.624.54-8.135 1.625a18.703 18.703 0 0 0-6.54 4.5l-.01.012-.01.01c-1.792 1.968-3.176 4.333-4.155 7.037l-.002.004c-.99 2.686-1.47 5.634-1.47 8.81 0 3.226.48 6.207 1.474 8.9.98 2.657 2.366 4.993 4.153 6.954l.01.01.01.01a19.447 19.447 0 0 0 4.152 3.345c-1.274.325-2.5.71-3.668 1.17-4.03 1.587-7.416 3.776-10.074 6.58l-.01.012-.01.01c-2.007 2.177-3.62 4.66-4.855 7.41a37.86 37.86 0 0 1-8.858-24.4c0-21.01 16.99-38 37.998-38zm0 9.998c2.408 0 4.573.438 6.562 1.3l.018.01.016.006c1.995.823 3.66 1.983 5.066 3.526l.01.012.01.01c1.453 1.548 2.587 3.42 3.406 5.685l.002.008.004.006c.81 2.195 1.226 4.662 1.226 7.44 0 2.83-.418 5.323-1.226 7.514-.818 2.216-1.953 4.07-3.412 5.623l-.01.01-.01.013c-1.408 1.546-3.083 2.735-5.086 3.606C52.584 55.583 50.412 56 48 56c-2.413 0-4.586-.416-6.578-1.234a15.41 15.41 0 0 1-5.168-3.62c-1.412-1.553-2.528-3.41-3.348-5.632-.808-2.19-1.226-4.684-1.226-7.514 0-2.778.417-5.245 1.226-7.44l.002-.005.004-.008c.82-2.27 1.936-4.147 3.342-5.693a14.599 14.599 0 0 1 5.15-3.54l.016-.007.017-.008c1.99-.864 4.156-1.302 6.563-1.302z' font-family='sans-serif' white-space='normal' overflow='visible' solid-color='%23FFFFFF' fill='%23666666'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--mount{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333493 4.2333317'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath d='M0 .265v.793h4.233V.265zm3.44.264h.529v.265h-.53zM3.175 2.117v.793h-.794v.265h.794v.794h.265v-.794h.793V2.91H3.44v-.793z' style='marker:none' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--unmount{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333493 4.2333317'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath d='M0 .265v.793h4.233V.265zm3.44.264h.529v.265h-.53zM2.381 2.91v.265h1.852V2.91z' style='marker:none' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--partition{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333398 4.2333315'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath d='M0 .794V3.44h1.852v-.265H.265V1.058h1.587V.794zM3.175 1.323v.794h-.794v.264h.794v.794h.265v-.794h.793v-.264H3.44v-.794zM1.852.264h.265v.53h-.265z' style='marker:none' color='%23000' overflow='visible' fill='gray'/%3E%3Cpath style='marker:none' color='%23000' overflow='visible' fill='gray' d='M1.852 1.058h.265v.53h-.265zM1.852 1.852h.265v.53h-.265zM1.852 2.645h.265v.53h-.265zM1.852 3.44h.265v.528h-.265z'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--debug{background-image:url("data:image/svg+xml,%3Csvg width='16' height='16' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3Edebug icon 8@2x%3C/title%3E%3Cpath d='M11.673 12.994L14.68 16l1.313-1.316-3.004-3.005A5.501 5.501 0 0 0 8.5 3C5.46 3 3 5.463 3 8.5 3 11.54 5.46 14 8.5 14c1.18 0 2.276-.37 3.173-1.006zM4.25 8.5a4.25 4.25 0 1 1 8.5 0 4.25 4.25 0 0 1-8.5 0zM6 9h5v1H6V9zm0-2h5v1H6V7zM4.71 0C2.845 0 1.646.095.87.87.093 1.648 0 2.847 0 4.716v4.568c0 1.87.094 3.068.87 3.844.667.668 1.68.824 3.13.857v-1.002c-1.327-.044-2.075-.21-2.424-.56-.41-.41-.576-1.318-.576-3.14V4.716c0-1.82.167-2.727.576-3.137C1.986 1.168 2.892 1 4.71 1h4.577c1.82 0 2.726.17 3.135.578.348.35.514 1.097.558 2.422h1.006c-.033-1.45-.19-2.46-.857-3.13C12.352.096 11.153 0 9.286 0H4.71z' fill='gray' fill-rule='evenodd'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--remove{background-image:url("data:image/svg+xml,%3Csvg width='18' height='18' viewBox='0 0 18 18' xmlns='http://www.w3.org/2000/svg'%3E%3Ctitle%3Eremove%3C/title%3E%3Cg fill='gray' fill-rule='evenodd'%3E%3Cpath d='M16.36 0L18 1.64 1.64 18 0 16.36z'/%3E%3Cpath d='M18 16.36L16.36 18 0 1.64 1.64 0z'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--settings{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath style='marker:none' overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M79.197 66.033c9.916-17.191 3.984-39.246-13.223-49.19-17.207-9.944-39.253-4.057-49.17 13.134C6.89 47.168 12.82 69.222 30.027 79.166c17.207 9.944 39.255 4.058 49.17-13.133zm-3.465-2.002c-8.834 15.316-28.37 20.535-43.708 11.67-15.338-8.863-20.59-28.407-11.756-43.723 8.834-15.316 28.37-20.535 43.708-11.671 15.338 8.864 20.59 28.408 11.756 43.724z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3Cpath style='marker:none' d='M54.902 5.582L41.098 7.645v7.097a34.033 33.971 43.146 0 1 13.804.028V5.582zm-28.246 6.244L16.14 20.768l5.181 6.175a34.033 33.971 43.146 0 1 10.58-8.867l-5.244-6.25zm42.631.067L64.08 18.1a34.033 33.971 43.146 0 1 .895.474 34.033 33.971 43.146 0 1 9.673 8.406l5.272-6.28-10.633-8.807zM8.398 34.008l-2.31 13.611 7.947 1.402a34.033 33.971 43.146 0 1 2.387-13.597l-8.024-1.416zm79.118.015l-7.987 1.409a34.033 33.971 43.146 0 1 2.432 13.588L90 47.604l-2.484-13.58zM15.758 58.645L8.67 62.738h-.002l6.98 11.91 7.018-4.05a34.033 33.971 43.146 0 1-6.908-11.953zm64.512.015a34.033 33.971 43.146 0 1-2.805 6.371 34.033 33.971 43.146 0 1-4.059 5.608l7.022 4.054 6.826-12-6.984-4.033zM30.148 76.867l-2.804 7.703 13.004 4.639 2.757-7.58a34.033 33.971 43.146 0 1-12.08-4.195 34.033 33.971 43.146 0 1-.877-.567zm35.733.08a34.033 33.971 43.146 0 1-12.979 4.707l2.782 7.639 12.941-4.807-2.744-7.539z' overflow='visible' fill='gray'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M68.805 60.027c6.61-11.46 2.652-26.178-8.816-32.806-11.47-6.628-26.185-2.7-32.795 8.76-6.61 11.459-2.651 26.18 8.817 32.808 11.47 6.628 26.184 2.697 32.794-8.762zm-3.463-2.001C59.814 67.61 47.61 70.87 38.01 65.324c-9.6-5.548-12.88-17.757-7.351-27.341 5.528-9.585 17.732-12.846 27.332-7.298 9.6 5.547 12.88 17.757 7.351 27.34z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--sync{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath style='marker:none' overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath d='M84.93 14.958L67.965 31.934a177.473 177.473 0 0 0 11.842 3.925A211.649 211.649 0 0 0 92 39c-.936-3.985-1.999-8.035-3.187-12.147a206.82 206.82 0 0 0-3.882-11.894zM11.035 81L28 64.024A177.472 177.472 0 0 0 16.158 60.1a211.647 211.647 0 0 0-12.193-3.141c.936 3.985 1.999 8.035 3.187 12.147 1.217 4.136 2.511 8.1 3.882 11.894z' style='marker:none' overflow='visible' fill='gray'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M48 8C25.932 8 8 25.932 8 48c0 .652.02 1.3.05 1.945 1.356.336 2.717.686 4.083 1.053A36.54 36.54 0 0 1 12 48c0-19.906 16.094-36 36-36 13.005 0 24.38 6.872 30.705 17.186l2.86-2.862C74.444 15.31 62.076 8 48 8zm35.863 36.969c.084 1 .137 2.009.137 3.031 0 19.906-16.094 36-36 36-13.025 0-24.416-6.892-30.736-17.232l-2.88 2.88C21.512 80.682 33.908 88 48 88c22.068 0 40-17.932 40-40a2 2 0 0 0-.041-.406 40.381 40.381 0 0 0-.057-1.584 211.753 211.753 0 0 1-4.039-1.041z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--system-shutdown{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='96' height='96' viewBox='0 0 96 96.000001'%3E%3Cg color='%23000'%3E%3Cpath style='marker:none' overflow='visible' fill='none' d='M96 0v96H0V0z'/%3E%3Cpath d='M46 6l4-1v35h-4z' style='marker:none' overflow='visible' fill='gray'/%3E%3Cpath style='line-height:normal;font-variant-ligatures:none;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;text-transform:none;block-progression:tb;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none' d='M30.006 16.798c-14.099 8.144-20.983 24.771-16.77 40.504C17.45 73.034 31.72 83.989 48 83.989s30.55-10.955 34.763-26.687c4.214-15.733-2.669-32.36-16.767-40.504l-2.002 3.463C76.54 27.509 82.65 42.263 78.9 56.266 75.15 70.27 62.488 79.99 48 79.99S20.85 70.27 17.1 56.266c-3.75-14.003 2.357-28.757 14.905-36.005l-2-3.463z' font-weight='400' font-family='sans-serif' white-space='normal' overflow='visible' fill='gray'/%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--tags{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 15.999999'%3E%3Cpath opacity='.212' fill='none' d='M0 0h16v16H0z'/%3E%3Cpath style='marker:none' d='M8.691 0L.775 7.918c-1.218 1.218-.913 1.522.305 2.74l2.13 2.131 2.132 2.13c1.218 1.219 1.522 1.524 2.74.306L16 7.309V.914c0-.304-.076-.533-.228-.686C15.619.076 15.39 0 15.086 0zm.418 1.008h5.883V6.89l-7.525 7.523c-.795.839-1.472-.388-2.074-.87a46.125 46.125 0 0 0-1.47-1.468 45.917 45.917 0 0 0-1.468-1.469c-.481-.602-1.708-1.28-.87-2.074L9.11 1.008z' color='%23000' overflow='visible' fill='gray'/%3E%3Cpath style='marker:none' d='M12.518 2.25a1.25 1.25 0 0 1 .867.365 1.25 1.25 0 0 1 0 1.77 1.25 1.25 0 0 1-1.77 0 1.25 1.25 0 0 1 0-1.77 1.25 1.25 0 0 1 .903-.365z' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--logical-volume{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 4.2333398 4.2333403'%3E%3Cpath style='marker:none' color='%23000' overflow='visible' opacity='.12' fill='none' d='M4.233 4.233V0H0v4.233z'/%3E%3Cpath style='marker:none' d='M3.175 2.381v.794h-.794v.265h.794v.793h.265V3.44h.793v-.265H3.44v-.794zM2.381 0v1.852h1.852V0zm.265.265h1.323v1.323H2.646zM0 0v1.852h1.852V0zm.265.265h1.323v1.323H.264zM0 2.381v1.852h1.852V2.381zm.265.265h1.323v1.323H.264z' color='%23000' overflow='visible' fill='gray'/%3E%3C/svg%3E");background-size:100% 100%}.p-icon--pending{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3Epending%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='smoke-testing-status' transform='translate%28-62.000000, -112.000000%29'%3E%3Cg id='pending' transform='translate%2850.000000, 97.000000%29'%3E%3Cg transform='translate%2812.000000, 15.000000%29'%3E%3Crect id='rect4970' x='0' y='1.99999999e-05' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle4972' stroke='%23F99B11' stroke-width='1.5' cx='8' cy='8.00002' r='7.2500086'%3E%3C/circle%3E%3Crect id='rect4980' fill='%23F99B11' fill-rule='nonzero' x='7' y='7.00002' width='2' height='2'%3E%3C/rect%3E%3Crect id='rect4982' fill='%23F99B11' fill-rule='nonzero' x='10' y='7.00002' width='2' height='2'%3E%3C/rect%3E%3Crect id='rect4984' fill='%23F99B11' fill-rule='nonzero' x='4' y='7.00002' width='2' height='2'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--running{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3Erunning%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='smoke-testing-status' transform='translate%28-62.000000, -159.000000%29'%3E%3Cg id='running' transform='translate%2850.000000, 144.000000%29'%3E%3Cg transform='translate%2812.000000, 15.000000%29'%3E%3Crect id='rect6425' x='9.99999997e-06' y='9.99999989e-06' width='16' height='16'%3E%3C/rect%3E%3Ccircle id='circle6427' stroke='%230E8420' stroke-width='1.5' fill='%230E8420' fill-rule='nonzero' cx='8.00001' cy='8.00001' r='7.2500086'%3E%3C/circle%3E%3Cpolygon id='path6429' fill='%23FFFFFF' fill-rule='nonzero' points='6.00002 12.00001 6.00002 4.00001 12.00002 8.00001'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:100% 100%}.p-icon--timed-out{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg xmlns='http://www.w3.org/2000/svg' height='16px' width='16px' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 16 16'%3E%3C!-- Generator: Sketch 42 %2836781%29 - http://www.bohemiancoding.com/sketch --%3E%3Ctitle%3Etimed out%3C/title%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cg id='Page-1' fill-rule='evenodd' fill='none'%3E%3Cg id='smoke-testing-status' transform='translate%28-62 -206%29'%3E%3Cg id='timed-out' transform='translate%2850 191%29'%3E%3Cg transform='translate%2812 15%29'%3E%3Crect id='rect4970' y='0.00002' x='0' height='16' width='16'/%3E%3Ccircle id='circle4972' stroke-width='1.5' cy='8' stroke='%23E95420' cx='8' r='7.25'/%3E%3Cpolyline id='path839' stroke='%23E95420' stroke-width='2' points='11.8 11.8 7.9999 8 7.9999 3'/%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A");background-size:100% 100%}.p-icon--power-error{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%23C7162B%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-icon--power-on{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%230E8420%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-icon--power-off{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-icon--power-unknown{background-image:url("data:image/svg+xml,%0A%3Csvg width='7px' height='12px' viewBox='0 0 7 12' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: Sketch 50.2 %2855047%29 - http://www.bohemiancoding.com/sketch --%3E%3Cdesc%3ECreated with Sketch.%3C/desc%3E%3Cdefs%3E%3C/defs%3E%3Cg id='prototype--tables' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='207-group-selection' transform='translate%28-333.000000, -143.000000%29'%3E%3Cg id='Group' transform='translate%28333.000000, 141.000000%29'%3E%3Crect id='Rectangle' x='0' y='0' width='16' height='16'%3E%3C/rect%3E%3Cpath d='M2.79224377,3.71191136 C2.40443019,3.71191136 2.03324277,3.7590023 1.67867036,3.8531856 C1.32409795,3.94736889 0.952910526,4.09972194 0.565096953,4.31024931 L0,2.76454294 C0.409974349,2.53185479 0.878113712,2.34626108 1.40443213,2.20775623 C1.93075055,2.06925139 2.47091136,2 3.02493075,2 C3.68975402,2 4.23822499,2.09141183 4.67036011,2.27423823 C5.10249524,2.45706463 5.44598211,2.68697922 5.70083102,2.96398892 C5.95567994,3.24099861 6.13296349,3.54570471 6.23268698,3.87811634 C6.33241047,4.21052798 6.38227147,4.5318544 6.38227147,4.84210526 C6.38227147,5.21883845 6.31302008,5.55678521 6.17451524,5.85595568 C6.03601039,6.15512615 5.8614969,6.43213169 5.65096953,6.68698061 C5.44044216,6.94182953 5.21329762,7.18282435 4.96952909,7.4099723 C4.72576055,7.63712025 4.49861601,7.8698049 4.28808864,8.10803324 C4.07756127,8.34626158 3.90304778,8.59833662 3.76454294,8.86426593 C3.62603809,9.13019524 3.5567867,9.42936122 3.5567867,9.76177285 L3.5567867,9.95290859 C3.5567867,10.0249311 3.56232681,10.0941825 3.5734072,10.1606648 L1.84487535,10.1606648 C1.82271457,10.0498609 1.80609424,9.93074856 1.79501385,9.8033241 C1.78393346,9.67589964 1.77839335,9.55678726 1.77839335,9.44598338 C1.77839335,9.08033058 1.83933457,8.75346404 1.96121884,8.46537396 C2.0831031,8.17728388 2.2382262,7.91135856 2.4265928,7.66759003 C2.61495939,7.4238215 2.81717343,7.19667695 3.033241,6.98614958 C3.24930856,6.77562222 3.4515226,6.56509801 3.6398892,6.35457064 C3.82825579,6.14404327 3.98337889,5.92797895 4.10526316,5.70637119 C4.22714742,5.48476343 4.28808864,5.24099856 4.28808864,4.97506925 C4.28808864,4.60941645 4.16343615,4.30748042 3.91412742,4.06925208 C3.6648187,3.83102374 3.29086122,3.71191136 2.79224377,3.71191136 Z M4.08864266,12.6869806 C4.08864266,13.0747942 3.96122011,13.3905805 3.70637119,13.634349 C3.45152227,13.8781176 3.13573596,14 2.75900277,14 C2.39334997,14 2.08033371,13.8781176 1.8199446,13.634349 C1.55955548,13.3905805 1.42936288,13.0747942 1.42936288,12.6869806 C1.42936288,12.299167 1.55955548,11.9806107 1.8199446,11.7313019 C2.08033371,11.4819932 2.39334997,11.3573407 2.75900277,11.3573407 C3.13573596,11.3573407 3.45152227,11.4819932 3.70637119,11.7313019 C3.96122011,11.9806107 4.08864266,12.299167 4.08864266,12.6869806 Z' id='%3F' fill='%23CDCDCD'%3E%3C/path%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E")}[class^="p-icon"]{margin-right:0.25em}[class^="p-icon"].on-right{margin-left:0.25em;margin-right:0}.p-icon--lock{background-image:url("https://assets.ubuntu.com/v1/5d2e0e21-padlock.svg");background-size:1rem 1rem;display:block;position:relative;margin-right:1rem;top:0 !important}.p-icon--lock.is-open{background-image:url("https://assets.ubuntu.com/v1/a6c61cd6-padlock_open.svg")}.p-icon--x{background-size:1rem 1rem;display:block;position:relative}.p-icon--tick{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='22px' height='16px' viewBox='0 0 22 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='confirm-tick' transform='translate(-1.000000, -1.000000)'%3E%3Cpolygon id='Shape' points='0 0 24 0 24 24 0 24'%3E%3C/polygon%3E%3Cpolygon id='Shape' fill-opacity='0.999998987' fill='%23666666' fill-rule='nonzero' points='3.872 6.93333333 1.6 9.20533333 9.33333333 16.9386667 22.4 3.872 20.128 1.6 9.33333333 12.3973333'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:1rem 1rem;display:block;position:relative}[class^="p-icon"]{height:1em;width:1em}.p-dropdown.active .p-navigation__toggle--open{display:none}@media (max-width: 870px){.p-dropdown.active .p-navigation__toggle--close{display:inline-block}}.p-dropdown.active .p-navigation__nav{display:block}@media (max-width: 870px){.p-navigation__banner{overflow:hidden;position:relative}}.p-navigation .p-navigation__links,.p-navigation .p-navigation__links--right{z-index:6}@media (min-width: 870px){.p-navigation .p-navigation__links--right{right:0}}.p-navigation .p-navigation__links .p-navigation__link:hover,.p-navigation .p-navigation__links--right .p-navigation__link:hover,.p-navigation .p-navigation__links .p-navigation__link:active,.p-navigation .p-navigation__links--right .p-navigation__link:active{background-color:#2b2b2b}@media (min-width: 870px){.p-navigation .p-navigation__links .p-navigation__link.is-selected>a,.p-navigation .p-navigation__links--right .p-navigation__link.is-selected>a{border-bottom-color:#e95420}}@media (max-width: 870px){.p-navigation .p-navigation__links .p-navigation__link.is-selected>a,.p-navigation .p-navigation__links--right .p-navigation__link.is-selected>a{border-bottom:0}}.p-dropdown{height:3rem}.p-dropdown__toggle{background-color:#333}.p-dropdown .p-icon--chevron{margin-bottom:-2px;margin-left:10px}.p-dropdown .active{background-color:#2b2b2b}.p-dropdown .active .p-icon--chevron{transform:rotate(180deg)}.p-navigation .p-navigation__links .p-dropdown__menu,.p-navigation .p-navigation__links--right .p-dropdown__menu{background-color:#2b2b2b;margin:0;padding:0}.p-navigation .p-navigation__links .p-dropdown__menu .p-navigation__link,.p-navigation .p-navigation__links--right .p-dropdown__menu .p-navigation__link{border-left:0;width:100%;float:none}@media (min-width: 1071px){.u-hide-nav-viewport--large{display:none !important}}@media (max-width: 1070px) and (min-width: 871px){.u-hide-nav-viewport--medium{display:none !important}}@media (max-width: 870px){.u-hide-nav-viewport--small{display:none !important}}.page-header{padding:1.5rem 0;position:sticky;top:0;z-index:5}.page-header__title{display:inline-block}.page-header__status{color:#666;display:inline;margin-left:1rem;position:relative;width:auto}.page-header__status [class^="p-icon"]{margin-bottom:0.1rem}.page-header__controls{float:right}@media (max-width: 620px){.page-header__controls{float:none}}.page-header.is-not-sticky{position:absolute}.page-header__controls--discoveries button:last-child{margin-left:1.5rem}.page-header__controls--discoveries .maas-p-switch{position:relative;top:0.4rem}@media screen and (max-width: 986px){.p-tabs.p-tabs--machine-details::before{display:block}}.p-tabs::before{display:none;background:-moz-linear-gradient(left, rgba(255,255,255,0) 0%, #fff 100%);background:-webkit-linear-gradient(left, rgba(255,255,255,0) 0%, #fff 100%);background:linear-gradient(to right, rgba(255,255,255,0) 0%, #fff 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#ffffff',GradientType=1 );padding-left:1.5rem;padding-right:0;right:1.5rem;width:2.05rem;z-index:1}.p-tabs__list{margin-bottom:0;font-size:0;overflow-x:auto}.p-tabs__list::after{content:none}.p-tabs__item{float:none;font-size:16px}.maas-p-switch{height:1.5rem;margin:0 auto;position:relative;width:3rem;display:inline-block}.maas-p-switch--input{cursor:pointer;height:100% !important;left:0;position:absolute;top:0;width:100%}.maas-p-switch--mask{background:#f7f7f7;height:100%;line-height:1.5rem;margin-top:0;pointer-events:none;position:relative}.maas-p-switch--mask::before{transition-duration:0.333s;transition-property:background-color;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);background:#cdcdcd;content:"";display:block;height:100%;text-align:right;width:100%;padding:0 0.3rem 0 0.45rem;box-shadow:inset 0 2px 5px 0 rgba(17,17,17,0.2);border-radius:3px;font-size:0.875rem}.maas-p-switch--mask::after{transition-duration:0.333s;transition-property:left;transition-timing-function:cubic-bezier(0.215, 0.61, 0.355, 1);width:50%;display:block;position:absolute;top:0;left:0;height:100%;background:#fff;content:" ";box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);border-radius:3px}.maas-p-switch--input:checked+.maas-p-switch--mask::before{text-align:left;content:"";color:#fff;background:#335280}.maas-p-switch--input:checked+.maas-p-switch--mask::after{left:50%}.maas-p-switch--input:disabled{cursor:not-allowed}.maas-p-switch--input:disabled+.maas-p-switch--mask::before{background:#f3f3f3;color:#999}.maas-p-switch--input:disabled:checked+.maas-p-switch--mask::before{background:#809fcc;color:#fff}.p-table-expanding .p-table-expanding__panel,.p-table-expanding .p-table-expanding__panel--bordered{background-color:#fff;margin-bottom:0;margin-left:0 !important;padding:.5rem;width:100%;white-space:wrap}.p-table-expanding .p-table-expanding__panel.is-active,.p-table-expanding .is-active.p-table-expanding__panel--bordered{flex-grow:1}.p-table-expanding .p-table-expanding__panel--bordered{border-top:1px solid #cdcdcd}.p-table-expanding td{display:table-cell}.p-table-expanding td[colspan="2"]{flex:2}.p-table-expanding tr{display:table-row}.p-table-expanding tr .is-active{background-color:#fff}.p-table-expanding>thead>tr,.p-table-expanding>tbody>tr{display:flex}.p-table-expanding .tags-input .tags .input{margin-bottom:0}.p-card--highlighted.is-error{border-top:3px solid #c7162b}div[class*="p-card--"],.p-card{padding:1rem}.p-card__content{margin-top:0}h2+.p-card__content,.p-heading--2+.p-card__content{margin-top:-1rem}.p-card__content .muted-label{color:#666}.p-card__header{border-bottom:0}.p-meter__container,.p-meter--cpu-cores{padding-top:0.2rem;margin-bottom:-0.2rem}.p-meter{border-radius:.5rem;display:block;height:1rem;position:relative;overflow:hidden;width:100%;-moz-appearance:none;-webkit-appearance:none;background:none;background-color:rgba(0,122,166,0.2)}.p-meter__container{position:relative}.p-meter.is-over{background-color:#f99b11}.p-meter.is-over::-webkit-meter-bar{background-color:#f99b11}.p-meter.is-over::-webkit-meter-optimum-value{background-color:#f99b11}.p-meter.is-over::-moz-meter-bar{background-color:#f99b11}.p-meter::-webkit-meter-inner-element{display:block}.p-meter::-webkit-meter-bar{background:rgba(0,122,166,0.2)}.p-meter::-webkit-meter-optimum-value{background:#007aa6}.p-meter::-moz-meter-bar{background:#007aa6}.p-meter--kvm::-webkit-meter-bar{background-color:transparent}.p-meter--kvm::-webkit-meter-optimum-value{background-color:transparent}.p-meter--kvm::-moz-meter-bar{background:#007aa6}.p-meter--cpu-cores__container{border-radius:.5rem;display:flex;position:relative;overflow:hidden;width:100%}.p-meter--cpu-cores__container.is-over span{background-color:#f99b11}.p-meter--cpu-cores .p-meter--cpu-cores__core--used,.p-meter--cpu-cores .p-meter--cpu-cores__core--available{border-right:1px solid #fff;display:block;float:left;height:1rem;position:relative;flex:1}.p-meter--cpu-cores__core--used{background:#007aa6}.p-meter--cpu-cores__core--available{background:rgba(0,122,166,0.2)}.p-meter__graph{position:absolute;top:0;width:100%}.p-meter__graph-content{background-image:linear-gradient(90deg, #007aa6 0%, #007aa6 100%, transparent 100%, transparent 100%);background-size:100% 100%;display:block;text-indent:-9999px;height:inherit}.p-legend{list-style:none;margin-bottom:0;padding:0}.p-legend--numbers{margin-left:0;padding-left:0}.p-legend::after{content:unset}.p-legend__item{display:block;float:left}.p-legend__item:not(:first-child){margin-left:1rem;float:right}.p-legend__item::before{content:"•";padding-top:.4rem;float:left;font-size:2rem;line-height:1.5rem;display:inline-block;margin-right:.5rem;width:.5rem}.p-legend__item--requests::before{color:#007aa6}.p-legend__item--used::before{color:#007aa6}.p-legend__item--free::before{color:rgba(0,122,166,0.2)}.p-legend__text{float:left;margin-bottom:.6rem}.p-strip{padding:1rem 0}@media only screen and (min-width: 1030px){.p-strip{padding:1.5rem 0}}.p-strip--light{padding:1rem 0;background-color:#fff}@media only screen and (min-width: 1030px){.p-strip--light{padding:1.5rem 0}}.p-strip--dark{padding:1rem 0}@media only screen and (min-width: 1030px){.p-strip--dark{padding:1.5rem 0}}.p-strip.is-shallow,.p-strip--light.is-shallow,.p-strip--dark.is-shallow{padding:.5rem 0}@media only screen and (min-width: 1030px){.p-strip.is-shallow,.p-strip--light.is-shallow,.p-strip--dark.is-shallow{padding:1rem 0}}tags-input{display:block}tags-input .autocomplete{margin-top:-1px;position:absolute;padding:5px 0;z-index:999;width:100%;background-color:#fff;border-radius:0 0 3px 3px;border:1px solid rgba(0,0,0,0.2);-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);max-height:300px;transition:max-height 0.3s ease-in;overflow-y:scroll;overflow-x:visible}tags-input .autocomplete.no-suggestion{overflow-y:visible}tags-input .autocomplete .suggestion-list{margin:0;clear:both;min-width:160px;width:100%;box-sizing:border-box}tags-input .autocomplete .suggestion-list .suggestion-item{float:left;margin:0;padding:5px 10px;border-top:1px solid #d2d2d2;width:100%}tags-input .autocomplete .suggestion-list .suggestion-item:first-child{border-top:0}tags-input .autocomplete .suggestion-list .suggestion-item:hover{background-color:#f7f7f7}.autocomplete.no-suggestion .suggestion-list .suggestion-item{color:#666}.autocomplete.no-suggestion .suggestion-list .suggestion-item:hover{background-color:#fff}#tags{display:block}.tags--inline .host{position:relative;margin-bottom:5px;height:100%}.tags--inline .tags{border:1px solid #d2d2d2;border-radius:2px;-moz-appearance:textfield;-webkit-appearance:textfield;padding:1px;overflow:hidden;word-wrap:break-word;cursor:text;background-color:#fff;height:100%}.tags--inline .tags:focus,.tags--inline .tags.focused{border-color:#888;outline:none}.tags--inline .tags .tag-list{margin:0;padding:0;list-style-type:none}.tags--inline .tags .tag-list .tag-item{line-height:24px;margin:4px}.tags--inline .tags .input{border:0;outline:0;margin:2px;padding:0 0 0 5px;height:30px;box-shadow:none}.tags--inline .tags .input:placeholder{color:transparent}.tags{width:100%}.tags .tag-list{width:100%;margin:0;overflow:hidden}.tags .tag-list .tag-item{float:left;line-height:36px;margin-bottom:0;margin-top:0;margin-right:10px;word-wrap:break-word}.tags .tag-list .tag-item .remove-button{border-bottom:0}.tags .input{width:100% !important}.tag-item{display:inline-block;background-color:#f7f7f7;padding:0 5px}table button,table [class^="p-button"]{padding-bottom:.3375rem;padding-top:.3375rem}.p-button--narrow{padding-left:.75rem;padding-right:.75rem}.p-button--min-margin-bottom,.p-button--icon{margin-bottom:.1rem}.p-button--base.is-small{padding:.5rem;margin:0}.p-button--close{padding:.3375rem .5rem}.p-button--lock{margin-left:-0.5rem}.p-button--icon{padding:.5rem .5rem}input+.p-button--icon{margin-left:1rem}.p-button--close{align-self:flex-start;border:0;float:right;margin:0 0 auto auto;width:auto}.p-button--close [class^="p-icon"]{margin-right:0}[class*="p-button"] [class^="p-icon"],button [class^="p-icon"]{margin-right:0}.p-table-expanding__panel *[class*="p-button"],.p-table-expanding .p-table-expanding__panel--bordered *[class*="p-button"]{margin-bottom:.2rem}.p-table-expanding__panel *[class*="p-button"]:not(.p-button--close),.p-table-expanding .p-table-expanding__panel--bordered *[class*="p-button"]:not(.p-button--close){padding:.3375rem 1rem}*[class*="p-button"].is-small{padding:.3375rem 1rem}.p-cta__toggle::after{background-position-y:center;background-repeat:no-repeat;background-size:1rem;content:"";height:1rem;position:absolute;width:1rem}.p-cta__toggle{margin-bottom:.25rem;padding-right:3rem}.p-cta__toggle::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E");right:1rem;top:.675rem}.p-cta__toggle.p-button--positive::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23fff' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-cta__toggle.is-selected::after{transform:rotate(180deg)}.p-cta__dropdown{min-width:100%;top:100%;width:auto;z-index:2}.p-cta__group+.p-cta__group .p-cta__link:first-child{border-top:1px solid #cdcdcd}.p-cta__link{display:flex;justify-content:space-between;padding:.25rem 1rem;transition-duration:0s}.p-cta__link.is-unavailable{opacity:0.5;cursor:not-allowed}.p-cta__count{padding-left:1rem}.p-form--stacked .p-form__group{align-items:flex-start}.p-form--stacked .p-form__label,.p-form--stacked .p-form__control{flex:0 0 auto;max-width:none}.p-form--stacked .p-form__control>.p-control-text{display:block}.p-form--stacked .p-form__control--placeholder{display:block;margin-bottom:.7rem;min-height:2.3rem;padding-bottom:.3375rem;padding-top:.3375rem}.p-form--inline .p-form__group .p-form__label{flex-shrink:1}.p-form--inline,.p-form--inline .p-form__group{width:100%}.p-form--stacked .p-form__group+.p-form__group{margin-top:0}.form__group-input input.in-warning{border-color:#f99b11 !important;padding-right:2rem}.p-form__label{color:#111}.p-form__label.is-disabled{color:#666}maas-obj-form[disabled="disabled"] .p-form__label{color:#666}.u-hr--fixed-width{position:relative}.u-hr--fixed-width::after{left:1.5rem;margin:0 auto;max-width:87rem;right:1.5rem}.p-notification__response{max-width:none}.p-notification--group>[class^="p-notification"]{flex-direction:column}.p-notification--group .p-list--divided{margin-top:.5rem;border-top:1px dotted #cdcdcd}[class*="p-notification"].is-subtle{background:transparent;box-shadow:none}[class*="p-notification"].is-subtle::before{height:0}.p-input--overcommit{float:left;width:3rem;max-width:3rem !important;min-width:3rem}.p-pod-summary{display:flex}@media (max-width: 1030px){.p-pod-summary{flex-direction:column}}@media (min-width: 1030px){.p-pod-summary{flex-direction:row;flex-wrap:wrap}}.p-pod-summary__cpu,.p-pod-summary__ram,.p-pod-summary__aside,.p-pod-summary__storage{flex:0 0 auto}@media (min-width: 1030px){.p-pod-summary__cpu,.p-pod-summary__ram{margin-bottom:1rem}}.p-pod-summary__ram{position:relative}@media (min-width: 620px) and (max-width: 1030px){.p-pod-summary__ram::after{content:unset}}.p-pod-summary__storage{position:relative}@media (max-width: 1030px){.p-pod-summary__storage{width:100%;margin-left:0}}@media (min-width: 1030px){.p-pod-summary__storage::after{content:unset}.p-pod-summary__storage::before{background-color:#cdcdcd;content:"";height:100%;left:-2.73045%;position:absolute;width:1px}}@media (max-width: 1029px){.p-pod-summary__aside{width:100%}}@media (min-width: 1030px){.p-pod-summary__cpu,.p-pod-summary__ram{width:100%;margin-left:0}}.p-storage{margin-bottom:.0625rem;padding-top:.5rem}@media (max-width: 1029px){.p-storage::after{background:transparent}}.p-storage__name{margin-top:-.5rem}@media (max-width: 1030px){.p-storage__meter{float:left}}@media (max-width: 620px){.p-storage__meter{width:100%}}.p-storage__disk-name{float:left;margin-bottom:.1rem;margin-right:-5rem;width:calc(100% - 5rem)}.p-storage__info{float:right;text-align:right;width:5rem}.p-storage__path{display:block;color:#666;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.faded{opacity:0.5}.p-overcommit-switch{float:left;width:100%}.p-pod-edit{align-items:flex-start;display:flex;justify-content:space-between}@media (max-width: 620px){.p-pod-edit__label{display:none}}.p-pod-edit__label,.p-pod-edit .p-button{flex:0 0 auto}.p-pod-edit *+*{margin-left:1rem}.p-pod-edit .p-code-snippet-wrapper{flex:1 0 auto}@media (max-width: 620px){.p-pod-edit .p-code-snippet-wrapper{margin-left:0}}.p-search-box{width:85%;box-shadow:none}.p-search-box.u-min-margin--bottom{margin-bottom:.2rem}.p-search-box.is-full-width{width:100%}.p-footer{margin-top:auto}.p-footer__logo{margin:0.5rem 0;height:1rem}html,body{height:100%}.has-sticky-footer{display:flex;flex-direction:column}.p-filter .p-filter__dropdown-button::after,.p-filter .p-button--base.is-active::after{background-position-y:center;background-repeat:no-repeat;background-size:1rem;content:"";height:1rem;position:absolute;width:1rem}.p-filter .p-filter__dropdown-button,.p-filter .p-button--base{margin:0;text-align:left;width:100%}.p-filter{position:relative}.p-filter .p-filter__dropdown{position:absolute;top:0;z-index:2}.p-filter .p-filter__dropdown-button::after{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='4' width='10'%3E%3Cpath d='M3.637 3.138A26.335 26.335 0 0 1 0 0h1.541a21.242 21.242 0 0 0 1.364 1.187 16.899 16.899 0 0 0 .752.563c.397.282.935.626 1.315.848h.054c.38-.222.918-.566 1.315-.848.398-.28.788-.583 1.169-.904.327-.275.643-.557.947-.846h1.541a26.335 26.335 0 0 1-3.637 3.138c-.519.365-.973.652-1.362.862-.39-.21-.844-.497-1.362-.862z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E");right:1rem;top:.675rem}.p-filter .p-filter__dropdown-button.is-selected+.p-accordion{display:block}.p-filter .p-filter__dropdown-button.is-selected::after{transform:rotate(180deg)}.p-filter .p-accordion{border-top:0;display:none;max-height:66vh;overflow-y:auto}.p-filter .p-accordion__list{margin-bottom:0}.p-filter .p-accordion__tab{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cg fill='%23666' fill-rule='evenodd'%3E%3Cpath d='M4 0h1v9H4z'/%3E%3Cpath d='M0 5V4h9v1z'/%3E%3C/g%3E%3C/svg%3E");background-position:right 1rem center;background-size:.75rem;border-bottom:0 !important;padding:.5rem 1rem}.p-filter .p-accordion__tab:focus{outline:1px solid #19b6ee;outline-offset:2px}.p-filter .p-accordion__tab.is-selected{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-filter .p-accordion__tab.is-selected+.p-accordion__panel{display:block}.p-filter .p-accordion__panel{border:0;display:none;padding:0}.p-filter .p-list{margin:0}.p-filter .p-button--base{padding:0 2.5rem;position:relative;transition-duration:0s}.p-filter .p-button--base.is-active{font-weight:400}.p-filter .p-button--base.is-active::after{background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='22px' height='16px' viewBox='0 0 22 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='confirm-tick' transform='translate(-1.000000, -1.000000)'%3E%3Cpolygon id='Shape' points='0 0 24 0 24 24 0 24'%3E%3C/polygon%3E%3Cpolygon id='Shape' fill-opacity='0.999998987' fill='%23666666' fill-rule='nonzero' points='3.872 6.93333333 1.6 9.20533333 9.33333333 16.9386667 22.4 3.872 20.128 1.6 9.33333333 12.3973333'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E");left:1rem;top:.25rem}.p-table--machines tbody .p-table__row:hover{background-color:#fff;box-shadow:0 1px 3px 0 rgba(17,17,17,0.2)}.p-table--machines tbody .p-table__row:hover .p-table-menu__toggle{display:inline-block}.p-table--machines tbody .p-table__row:hover .p-table-menu:not(.is-hidden) .p-double-row__main-row .u-text-overflow{max-width:calc(100% - 1.5rem)}.p-table__row--muted{background:#f7f7f7}.p-table--network-discovery tr{justify-content:space-between}.p-table--network-discovery th,.p-table--network-discovery td{flex:0 0 auto}.p-table--network-discovery__name{width:15%}.p-table--network-discovery__mac{width:20%}.p-table--network-discovery__ip{width:25%}.p-table--network-discovery__rack{width:15%}.p-table--network-discovery__last-seen{width:calc(25% - 50px)}.p-table--network-discovery__chevron{flex:0 0 auto;width:50px}.p-table--pods .p-table__row .p-table__cell:nth-child(1){width:18%}.p-table--pods .p-table__row .p-table__cell:nth-child(2){width:9%}.p-table--pods .p-table__row .p-table__cell:nth-child(3){width:14%}.p-table--pods .p-table__row .p-table__cell:nth-child(4){width:14%}.p-table--pods .p-table__row .p-table__cell:nth-child(5){width:10%}.p-table--pods .p-table__row .p-table__cell:nth-child(6){width:14%}.p-table--pods .p-table__row .p-table__cell:nth-child(7){width:15%}.p-table--pod-networking-config input,.p-table--pod-storage-config input{min-width:auto}@media (min-width: 620px){.p-table--pod-networking-config{margin-bottom:0}.p-table--pod-networking-config .p-table__row th:nth-child(1),.p-table--pod-networking-config .p-table__row td:nth-child(1){width:3.125rem}.p-table--pod-networking-config .p-table__row th:nth-child(2),.p-table--pod-networking-config .p-table__row td:nth-child(2){width:10%}.p-table--pod-networking-config .p-table__row th:nth-child(3),.p-table--pod-networking-config .p-table__row td:nth-child(3){width:25%}.p-table--pod-networking-config .p-table__row th:nth-child(4),.p-table--pod-networking-config .p-table__row td:nth-child(4){width:15%}.p-table--pod-networking-config .p-table__row th:nth-child(5),.p-table--pod-networking-config .p-table__row td:nth-child(5){width:23%}.p-table--pod-networking-config .p-table__row th:nth-child(6),.p-table--pod-networking-config .p-table__row td:nth-child(6){width:10%}.p-table--pod-networking-config .p-table__row th:nth-child(7),.p-table--pod-networking-config .p-table__row td:nth-child(7){width:12%}.p-table--pod-networking-config .p-table__row th:nth-child(8),.p-table--pod-networking-config .p-table__row td:nth-child(8){width:5%}}@media (min-width: 620px){.p-table--pod-storage-config{margin-bottom:0}.p-table--pod-storage-config .p-table__row th:nth-child(1),.p-table--pod-storage-config .p-table__row td:nth-child(1){width:3.125rem}.p-table--pod-storage-config .p-table__row th:nth-child(2),.p-table--pod-storage-config .p-table__row td:nth-child(2){width:10%}.p-table--pod-storage-config .p-table__row th:nth-child(3),.p-table--pod-storage-config .p-table__row td:nth-child(3){width:40%}.p-table--pod-storage-config .p-table__row th:nth-child(4),.p-table--pod-storage-config .p-table__row td:nth-child(4){width:40%}.p-table--pod-storage-config .p-table__row th:nth-child(5),.p-table--pod-storage-config .p-table__row td:nth-child(5){width:10%}}.p-table--pod-networking-config--message{margin-left:35%}.p-table--devices .p-table__row .p-table__cell:nth-child(1){width:33%}.p-table--devices .p-table__row .p-table__cell:nth-child(2){width:17%}.p-table--devices .p-table__row .p-table__cell:nth-child(3){width:15%}@media (max-width: 1000px){.p-table--devices .p-table__row .p-table__cell:nth-child(3){display:none !important}}.p-table--devices .p-table__row .p-table__cell:nth-child(4){width:20%}.p-table--devices .p-table__row .p-table__cell:nth-child(5){width:15%}.p-table--controllers .p-table__row .p-table__cell:nth-child(1){width:30%}.p-table--controllers .p-table__row .p-table__cell:nth-child(2){width:10%}.p-table--controllers .p-table__row .p-table__cell:nth-child(3){width:20%}.p-table--controllers .p-table__row .p-table__cell:nth-child(4){width:15%}.p-table--controllers .p-table__row .p-table__cell:nth-child(5){width:20%}.p-table--controllers .p-table__row .p-table__cell:nth-child(6){width:15%}.p-table--images .p-table__row .p-table__cell:nth-child(1){width:20%}.p-table--images .p-table__row .p-table__cell:nth-child(2){width:15%}.p-table--images .p-table__row .p-table__cell:nth-child(3){width:15%}.p-table--images .p-table__row .p-table__cell:nth-child(4){width:35%}.p-table--images .p-table__row .p-table__cell:nth-child(5){width:15%}.p-table--disks-partitions .p-table__row .p-table__cell,.p-table--used-disks .p-table__row .p-table__cell{flex:0 0 auto !important}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(1),.p-table--used-disks .p-table__row .p-table__cell:nth-child(1){width:15%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(2),.p-table--used-disks .p-table__row .p-table__cell:nth-child(2){width:15%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(3),.p-table--used-disks .p-table__row .p-table__cell:nth-child(3){width:7%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(4),.p-table--used-disks .p-table__row .p-table__cell:nth-child(4){width:9%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(5),.p-table--used-disks .p-table__row .p-table__cell:nth-child(5){width:22%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(6),.p-table--used-disks .p-table__row .p-table__cell:nth-child(6){width:22%}.p-table--disks-partitions .p-table__row .p-table__cell:nth-child(7),.p-table--used-disks .p-table__row .p-table__cell:nth-child(7){width:10%}.p-table--used-disks .p-table__row .p-table__cell:nth-child(6){width:7%}.p-table--used-disks .p-table__row .p-table__cell:nth-child(7){width:25%}.p-table--datastores{flex:0 0 auto !important}.p-table--datastores .p-table__row .p-table__cell{flex:0 0 auto !important}.p-table--datastores .p-table__row .p-table__cell:nth-child(1){width:15%}.p-table--datastores .p-table__row .p-table__cell:nth-child(2){width:22%}.p-table--datastores .p-table__row .p-table__cell:nth-child(3){width:9%}.p-table--datastores .p-table__row .p-table__cell:nth-child(4){width:44%}.p-table--datastores .p-table__row .p-table__cell:nth-child(5){width:10%}.p-table--machines{margin:0;position:relative}.p-table--machines .p-table__header{border-bottom:1px solid #cdcdcd}.p-table--machines .p-table__group{border:0;position:relative}.p-table--machines .p-table__group .p-table__group-label{color:#111;font-size:1rem;padding:.5rem 0 .5rem .5rem;text-transform:none}.p-table--machines .p-table__group .p-table__group-toggle{padding:0 .25rem;position:absolute;right:.75rem;top:1rem}.p-table--machines .p-table__group.is-open{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='9' width='9'%3E%3Cpath d='M0 5V4h9v1z' fill='%23666' fill-rule='evenodd'/%3E%3C/svg%3E")}.p-table--machines .p-table__row{position:relative}.p-table--machines .p-table__row::after{content:""}.p-table--machines .p-table__row.is-grouped{border:0}.p-table--machines .p-table__row.is-grouped::after{position:absolute;left:2.5rem;right:0;height:.0625rem;background-color:#e5e5e5}.p-table--machines .p-table__row.is-grouped td:first-child{padding-left:2.5rem}.p-table--machines .p-table__row td{vertical-align:top}.p-table--machines .p-table__row .p-table__col--name{position:relative}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--name{width:46%}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--name{width:30%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--name{width:22%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--name{width:20%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--name{width:17%}}.p-table--machines .p-table__row .p-table__col--name .p-tooltip{position:static}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--power{width:8%}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--power{width:8%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--power{width:4%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--power{width:10%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--power{width:9%}}.p-table--machines .p-table__row .p-table__col--status{position:relative}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--status{width:46%}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--status{width:44%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--status{width:22%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--status{width:22%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--status{width:18%}}.p-table--machines .p-table__row .p-table__col--status .p-tooltip{position:static}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--owner{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--owner{width:18%}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--owner{width:8%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--owner{width:10%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--owner{width:9%}}.p-table--machines .p-table__row .p-table__col--pool{overflow:visible}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--pool{display:none !important}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--pool{width:7%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--zone{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--zone{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--zone{display:none !important}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--zone{width:10%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--zone{width:9%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--fabric{display:none !important}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--fabric{width:8%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--cores{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--cores{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--cores{width:10%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--cores{width:6%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--cores{width:5%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--ram{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--ram{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--ram{width:12%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--ram{width:8%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--ram{width:7%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--disks{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--disks{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--disks{width:10%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--disks{width:6%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--disks{width:5%}}@media (max-width: 599px){.p-table--machines .p-table__row .p-table__col--storage{display:none !important}}@media (min-width: 600px) and (max-width: 899px){.p-table--machines .p-table__row .p-table__col--storage{display:none !important}}@media (min-width: 900px) and (max-width: 1029px){.p-table--machines .p-table__row .p-table__col--storage{width:10%}}@media (min-width: 1030px) and (max-width: 1359px){.p-table--machines .p-table__row .p-table__col--storage{width:8%}}@media (min-width: 1360px){.p-table--machines .p-table__row .p-table__col--storage{width:6%}}.p-table--machines .p-table__placeholder *{height:0 !important;padding:0 !important;visibility:hidden}.p-table--machines .p-icon--placeholder{height:1rem;margin-right:.5rem;width:1rem}.p-table--machines .p-tooltip__message--latest-event{max-width:500px;white-space:inherit}.p-table--machines:last-of-type::after{display:none}.p-table--controller-interfaces .p-table--is-device th:nth-child(1),.p-table--controller-interfaces .p-table--is-device td:nth-child(1){width:30%}.p-table--controller-interfaces .p-table--is-device th:nth-child(2),.p-table--controller-interfaces .p-table--is-device td:nth-child(2){width:25%}.p-table--controller-interfaces .p-table--is-device th:nth-child(3),.p-table--controller-interfaces .p-table--is-device td:nth-child(3){width:25%}.p-table--controller-interfaces .p-table--is-device th:nth-child(4),.p-table--controller-interfaces .p-table--is-device td:nth-child(4){width:15%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(1),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(1){width:20%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(2),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(2){width:6%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(3),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(3){width:10%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(4),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(4){width:14%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(5),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(5){width:16%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(6),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(6){width:28%}.p-table--controller-interfaces .p-table--is-not-device th:nth-child(7),.p-table--controller-interfaces .p-table--is-not-device td:nth-child(7){width:6%}.p-table--controllers-commissioning .p-table__row th:nth-child(1),.p-table--controllers-commissioning .p-table__row td:nth-child(1){width:15%}.p-table--controllers-commissioning .p-table__row th:nth-child(2),.p-table--controllers-commissioning .p-table__row td:nth-child(2){width:15%}.p-table--controllers-commissioning .p-table__row th:nth-child(3),.p-table--controllers-commissioning .p-table__row td:nth-child(3){width:20%}.p-table--controllers-commissioning .p-table__row th:nth-child(4),.p-table--controllers-commissioning .p-table__row td:nth-child(4){width:20%}.p-table--controllers-commissioning .p-table__row th:nth-child(5),.p-table--controllers-commissioning .p-table__row td:nth-child(5){width:25%}.p-table--controllers-commissioning .p-table__row th:nth-child(6),.p-table--controllers-commissioning .p-table__row td:nth-child(6){width:5%}@media (min-width: 620px){.p-table--controller-vlans .p-table__row th:nth-child(1),.p-table--controller-vlans .p-table__row td:nth-child(1){width:15%}.p-table--controller-vlans .p-table__row th:nth-child(2),.p-table--controller-vlans .p-table__row td:nth-child(2){width:15%}.p-table--controller-vlans .p-table__row th:nth-child(3),.p-table--controller-vlans .p-table__row td:nth-child(3){width:10%}.p-table--controller-vlans .p-table__row th:nth-child(4),.p-table--controller-vlans .p-table__row td:nth-child(4){width:20%}.p-table--controller-vlans .p-table__row th:nth-child(5),.p-table--controller-vlans .p-table__row td:nth-child(5){width:20%}.p-table--controller-vlans .p-table__row th:nth-child(6),.p-table--controller-vlans .p-table__row td:nth-child(6){width:20%}}@media (max-width: 768px){.p-table--create-raid__name{width:50%}}@media (min-width: 768px){.p-table--create-raid__name{width:30%}}.p-table--create-raid__size{width:10%}.p-table--create-raid__type{width:20%}.p-table--create-raid__active{width:10%}.p-table--create-raid__spare{width:10%}@media (max-width: 768px){.p-table--create-volume-group__name{width:50%}}@media (min-width: 768px){.p-table--create-volume-group__name{width:30%}}.p-table--create-volume-group__size{width:30%}.p-table--create-volume-group__type{width:20%}.p-table--create-volume-group__empty{width:10%}@media (max-width: 768px){.p-table--bcache__name{width:50%}}@media (min-width: 768px){.p-table--bcache__name{width:30%}}.p-table--bcache__size{width:30%}.p-table--bcache__type{width:20%}.p-table--bcache__empty{width:10%}.p-double-row{overflow:visible}.p-double-row .p-double-row__checkbox,.p-double-row .p-double-row__icon-container{display:block;float:left;width:1rem}.p-double-row .p-double-row__main-row,.p-double-row .p-double-row__muted-row{align-items:center;display:flex;white-space:nowrap;width:100%}.p-double-row .p-double-row__checkbox{margin-right:1rem}.p-double-row .p-double-row__icon-container{margin-right:.5rem}.p-double-row .p-double-row__rows-container--icon{float:left;width:calc(100% - 1.5rem)}.p-double-row .p-double-row__rows-container--checkbox{float:left;width:calc(100% - 2rem)}.p-double-row .p-double-row__muted-row{color:#666;margin-bottom:.2rem}.p-checkbox--action.actionable::before{background-color:#0e8420}.p-checkbox--action.not-actionable::before{background-color:#f99b11}.p-checkbox--action.actionable::after,.p-checkbox--action.not-actionable::after{color:#fff}.p-muted-text{color:#666;margin:0;padding:0}.p-table__group-label .p-muted-text{font-weight:300;padding-left:2rem}.p-link--muted:visited{color:#666}.p-link--muted:hover{color:#007aa6}.p-domain-name{display:inline-block}.p-domain-name .p-domain-name__host{font-weight:400}.p-domain-name .p-domain-name__tld{margin-bottom:.2rem}.p-table-menu{margin-bottom:-.5rem;width:100%}.p-table-menu .p-table-menu__link,.p-table-menu .p-table-menu__check-power,.p-table-menu .p-table-menu__power-on,.p-table-menu .p-table-menu__power-off{padding:.5rem 1rem;position:relative;transition:0s}.p-table-menu .p-table-menu__link::before,.p-table-menu .p-table-menu__check-power::before,.p-table-menu .p-table-menu__power-on::before,.p-table-menu .p-table-menu__power-off::before{background-position:center;background-repeat:no-repeat;background-size:1rem;content:"";height:17px;left:1rem;position:absolute;top:.75rem;width:1rem}.p-table-menu .p-table-menu__title,.p-table-menu .p-table-menu__title--icon{border-bottom:1px solid #e5e5e5;color:#666;font-size:0.75rem;font-weight:400;padding:.25rem 1rem;text-transform:uppercase}.p-table-menu .p-table-menu__title--icon{padding-left:2.5rem}.p-table-menu .p-table-menu__footer{border-top:1px solid #e5e5e5;color:#666;padding:.25rem 2.5rem}.p-table-menu .p-table-menu__toggle{background-color:rgba(255,255,255,0.75);border:0;cursor:pointer;display:none;left:.25rem;opacity:0.25;top:1px}td:hover .p-table-menu .p-table-menu__toggle{opacity:1}.p-table-menu .p-table-menu__dropdown{left:-1rem;max-width:none;min-width:100%;top:2rem;width:-moz-max-content;width:max-content}.p-table-menu .p-table-menu__dropdown .p-contextual-menu__group{border-color:#e5e5e5}.p-table-menu .p-table-menu__check-power{padding-left:2.5rem;padding-right:2.5rem}.p-table-menu .p-table-menu__power-on{padding-left:2.5rem;padding-right:2.5rem}.p-table-menu .p-table-menu__power-on::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%230E8420%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-table-menu .p-table-menu__power-off{padding-left:2.5rem;padding-right:2.5rem}.p-table-menu .p-table-menu__power-off::before{background-image:url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2215%22%20width%3D%2214%22%3E%3Cpath%20d%3D%22M11.04%202.323l-.324%202.268a5.017%205.017%200%200%201%201.352%203.426c0%202.787-2.274%205.056-5.068%205.056s-5.067-2.269-5.067-5.056a5.02%205.02%200%200%201%201.351-3.426L2.96%202.323A6.935%206.935%200%200%200%200%208.017C0%2011.868%203.14%2015%207%2015s7-3.132%207-6.983a6.933%206.933%200%200%200-2.96-5.694zM6%200h2v7H6V0z%22%20fill%3D%22%23CDCDCD%22%20fill-rule%3D%22evenodd%22%2F%3E%3C%2Fsvg%3E")}.p-table-menu .p-double-row__icon-container{cursor:pointer}@media (max-width: 599px){.u-hide--br1{display:none !important}}@media (max-width: 899px){.u-hide--br2{display:none !important}}@media (max-width: 1029px){.u-hide--br3{display:none !important}}@media (max-width: 1359px){.u-hide--br4{display:none !important}}.p-space-between{display:flex;flex-wrap:wrap;justify-content:space-between}@media (max-width: 620px){.p-space-between{flex-direction:column}}@media (max-width: 620px){.p-space-between .p-space-between__align-right{align-self:flex-end}}.p-chart{background-color:rgba(0,122,166,0.2);position:relative;overflow:hidden;width:100%;height:1rem;border-radius:1rem;background-color:rgba(0,122,166,0.2)}.p-chart__container{padding-top:.6rem;margin-bottom:-.1rem}.p-chart__bar,.p-chart__bar--used,.p-chart__bar--other,.p-chart__bar--requests{bottom:0;left:0;position:absolute;top:0}.p-chart__bar--used{background-color:#007aa6;border-right:1px solid #fff}.p-chart__bar--other{background-color:#1baf66;border-right:1px solid #fff}.p-chart__bar--requests{background-color:#0e8420;opacity:0.15}.p-chart__bar--requests.is-selected{opacity:1}.p-chart__bar--requests.is-over{background-color:#f99b11}.is-over .p-chart__bar--requests{background-color:#f99b11}.p-key-list{display:flex;list-style:none;margin-left:0;padding-left:0;position:relative}.p-key-list__item--requests,.p-key-list__item--other-requests,.p-key-list__item--used,.p-key-list__item--free{display:flex;flex:1;padding-right:1rem}.p-key-list__item--requests::before,.p-key-list__item--other-requests::before,.p-key-list__item--used::before,.p-key-list__item--free::before{content:"•";float:left;font-size:2rem;line-height:1.5rem;margin-right:.5rem;padding-top:.4rem;width:.5rem}.p-key-list__item--requests:last-of-type,.p-key-list__item--other-requests:last-of-type,.p-key-list__item--used:last-of-type,.p-key-list__item--free:last-of-type{text-align:right;justify-content:flex-end}.p-key-list__item--requests::before{color:#0e8420}.p-key-list__item--other-requests{padding-right:0.25rem}.p-key-list__item--other-requests::before{color:#1aaf65}.p-key-list__item--used::before{color:#007aa6}.p-key-list__item--free::before{color:rgba(0,122,166,0.2)}.p-option-selector{position:relative}.p-option-selector__header{padding-top:.25rem}.p-option-selector__header .p-button--close{margin-left:.5rem;float:none}.p-option-selector__title{padding:.75rem 1rem 0}.p-option-selector__input{color:#111 !important;cursor:pointer}.p-option-selector__input.in-warning{border-color:#f99b11 !important}.p-option-selector [readonly][type="text"].p-option-selector__input{color:#111;border-color:#cdcdcd}@media (min-width: 768px){.p-option-selector .p-option-selector-subnets__options{width:70vw}}@media (min-width: 1030px){.p-option-selector .p-option-selector-subnets__options{width:650px}}.p-option-selector .p-option-selector__options-key{padding:0 1rem}.p-option-selector__option{cursor:pointer;display:flex;width:100%;position:relative}.p-option-selector__option:focus{outline:1px solid #19b6ee;outline-offset:2px}.p-option-selector__option:hover{background-color:#f7f7f7}.p-option-selector__option.is-over{opacity:0.5;pointer-events:none;cursor:not-allowed}.p-option-selector__option.is-selected{background-color:#f7f7f7;background-image:url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='22px' height='16px' viewBox='0 0 22 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Cdefs%3E%3C/defs%3E%3Cg id='Page-1' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='confirm-tick' transform='translate(-1.000000, -1.000000)'%3E%3Cpolygon id='Shape' points='0 0 24 0 24 24 0 24'%3E%3C/polygon%3E%3Cpolygon id='Shape' fill-opacity='0.999998987' fill='%23666666' fill-rule='nonzero' points='3.872 6.93333333 1.6 9.20533333 9.33333333 16.9386667 22.4 3.872 20.128 1.6 9.33333333 12.3973333'%3E%3C/polygon%3E%3C/g%3E%3C/g%3E%3C/svg%3E");background-size:1rem 1rem;background-repeat:no-repeat;background-position:1rem center}.p-option-selector__option+.p-option-selector__option::after{background-color:#e5e5e5}.p-option-selector__option+.p-option-selector__option::after{background-color:#e5e5e5}.p-table--pod-networking-config .p-option-selector__option+.p-option-selector__option::after{left:1rem;right:1rem}.p-table--pod-storage-config .p-option-selector__option+.p-option-selector__option::after{left:3rem;right:1rem}.p-table--pod-networking-config .p-option-selector__options{position:absolute;top:2.25rem;padding:1rem 0 0;border:1px solid #cdcdcd;border-radius:.125rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);background-color:#fff;z-index:10;width:100%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__options{width:70vw}}@media (min-width: 1030px){.p-table--pod-networking-config .p-option-selector__options{left:0;right:-126%;width:auto}}.p-table--pod-networking-config .p-option-selector__options-key{padding:0 1rem}.p-table--pod-networking-config .p-option-selector__option-cell{padding:.5rem 1rem}.p-table--pod-networking-config .p-option-selector__option-cell:first-child{width:70%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__option-cell:first-child{width:35%}}@media (min-width: 1030px){.p-table--pod-networking-config .p-option-selector__option-cell:first-child{width:40%}}.p-table--pod-networking-config .p-option-selector__option-cell>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(2){width:30%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(2){width:20%}}@media (min-width: 1030px){.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(2){width:15%}}.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(3){width:100%}@media (min-width: 768px){.p-table--pod-networking-config .p-option-selector__option-cell:nth-child(3){width:45%}}.p-option-selector-subnets__option-cell{padding:.5rem 1rem}.p-option-selector-subnets__option-cell:first-child{width:70%}@media (min-width: 768px){.p-option-selector-subnets__option-cell:first-child{width:35%}}@media (min-width: 1030px){.p-option-selector-subnets__option-cell:first-child{width:44%}}.p-option-selector-subnets__option-cell>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-option-selector-subnets__option-cell:nth-child(2){width:55%}@media (min-width: 768px){.p-option-selector-subnets__option-cell:nth-child(2){width:50%}}@media (min-width: 1030px){.p-option-selector-subnets__option-cell:nth-child(2){width:25%}}.p-option-selector-subnets__option-cell:nth-child(3){width:100%}@media (min-width: 768px){.p-option-selector-subnets__option-cell:nth-child(3){width:35%}}@media (min-width: 1030px){.p-option-selector-subnets__option-cell:nth-child(3){width:31%}}.p-table--pod-storage-config .p-option-selector__options{background-color:#fff;border-radius:.125rem;box-shadow:0 1px 5px 1px rgba(17,17,17,0.2);position:absolute;top:2.25rem;width:100vw;z-index:10}@media (min-width: 620px){.p-table--pod-storage-config .p-option-selector__options{width:70vw}}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__options{width:70vw}}@media (min-width: 1030px){.p-table--pod-storage-config .p-option-selector__options{width:750px}}.p-table--pod-storage-config .p-option-selector__options-key{padding:0 1rem}.p-table--pod-storage-config .p-option-selector__option-cell{padding:0 1rem 0 0}.p-table--pod-storage-config .p-option-selector__option-cell:first-child{padding-left:3rem;width:70%}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__option-cell:first-child{width:35%}}@media (min-width: 1030px){.p-table--pod-storage-config .p-option-selector__option-cell:first-child{width:40%}}.p-table--pod-storage-config .p-option-selector__option-cell>*{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(2){width:30%}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(2){width:20%}}@media (min-width: 1030px){.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(2){width:15%}}.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(3){width:100%}@media (min-width: 768px){.p-table--pod-storage-config .p-option-selector__option-cell:nth-child(3){width:45%}} diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/add_device.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/add_device.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/add_device.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/add_device.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,267 +6,288 @@ /* @ngInject */ function AddDeviceController( - $scope, DevicesManager, SubnetsManager, DomainsManager, - ManagerHelperService, ValidationService) { - // Set the addDeviceScope in the parent, so it can call functions - // in this controller. - var parentScope = $scope.$parent; - parentScope.addDeviceScope = $scope; - - // Set initial values. - $scope.subnets = SubnetsManager.getItems(); - $scope.domains = DomainsManager.getItems(); - $scope.viewable = false; - $scope.error = null; - - // Device ip assignment options. - $scope.ipAssignments = [ - { - name: "external", - title: "External" - }, - { - name: "dynamic", - title: "Dynamic" - }, - { - name: "static", - title: "Static" - } - ]; - - // Makes a new interface. - function makeInterface() { - return { - mac: "", - ipAssignment: null, - subnetId: null, - ipAddress: "" - }; - } - - // Makes a new device. - function newDevice(cloneDevice) { - if (angular.isObject(cloneDevice)) { - return { - name: "", - domain: cloneDevice.domain, - interfaces: [makeInterface()] - }; - } else { - return { - name: "", - domain: DomainsManager.getDefaultDomain(), - interfaces: [makeInterface()] - }; - } + $scope, + DevicesManager, + SubnetsManager, + DomainsManager, + ManagerHelperService, + ValidationService +) { + // Set the addDeviceScope in the parent, so it can call functions + // in this controller. + var parentScope = $scope.$parent; + parentScope.addDeviceScope = $scope; + + // Set initial values. + $scope.subnets = SubnetsManager.getItems(); + $scope.domains = DomainsManager.getItems(); + $scope.viewable = false; + $scope.error = null; + + // Device ip assignment options. + $scope.ipAssignments = [ + { + name: "external", + title: "External" + }, + { + name: "dynamic", + title: "Dynamic" + }, + { + name: "static", + title: "Static" } + ]; - // Input values. - $scope.device = null; + // Makes a new interface. + function makeInterface() { + return { + mac: "", + ipAssignment: null, + subnetId: null, + ipAddress: "" + }; + } + + // Makes a new device. + function newDevice(cloneDevice) { + if (angular.isObject(cloneDevice)) { + return { + name: "", + domain: cloneDevice.domain, + interfaces: [makeInterface()] + }; + } else { + return { + name: "", + domain: DomainsManager.getDefaultDomain(), + interfaces: [makeInterface()] + }; + } + } - // Converts the device information from how it is held in the UI to - // how it is handled over the websocket. - function convertDeviceToProtocol(device) { - // Return the new object. - var convertedDevice = { - hostname: device.name, - domain: device.domain, - primary_mac: device.interfaces[0].mac, - extra_macs: [], - interfaces: [] - }; - var i; - for (i = 1; i < device.interfaces.length; i++) { - convertedDevice.extra_macs.push(device.interfaces[i].mac); - } - angular.forEach(device.interfaces, function(nic) { - convertedDevice.interfaces.push({ - mac: nic.mac, - ip_assignment: nic.ipAssignment.name, - ip_address: nic.ipAddress, - "subnet": nic.subnetId - }); - }); - return convertedDevice; - } - - // Called by the parent scope when this controller is viewable. - $scope.show = function() { - // Exit early if already viewable. - if ($scope.viewable) { - return; - } - // Load subnets to get the available subnets. - ManagerHelperService.loadManagers( - $scope, [SubnetsManager, DomainsManager]).then(function() { - $scope.device = newDevice($scope.device); - $scope.viewable = true; - }); - }; + // Input values. + $scope.device = null; - // Called by the parent scope when this controller is hidden. - $scope.hide = function() { - $scope.viewable = false; - - ManagerHelperService.unloadManagers( - $scope, [SubnetsManager, DomainsManager]); - // Emit the hidden event. - $scope.$emit('addDeviceHidden'); - }; + // Converts the device information from how it is held in the UI to + // how it is handled over the websocket. + function convertDeviceToProtocol(device) { + // Return the new object. + var convertedDevice = { + hostname: device.name, + domain: device.domain, + primary_mac: device.interfaces[0].mac, + extra_macs: [], + interfaces: [] + }; + var i; + for (i = 1; i < device.interfaces.length; i++) { + convertedDevice.extra_macs.push(device.interfaces[i].mac); + } + angular.forEach(device.interfaces, function(nic) { + convertedDevice.interfaces.push({ + mac: nic.mac, + ip_assignment: nic.ipAssignment.name, + ip_address: nic.ipAddress, + subnet: nic.subnetId + }); + }); + return convertedDevice; + } + + // Called by the parent scope when this controller is viewable. + $scope.show = function() { + // Exit early if already viewable. + if ($scope.viewable) { + return; + } + // Load subnets to get the available subnets. + ManagerHelperService.loadManagers($scope, [ + SubnetsManager, + DomainsManager + ]).then(function() { + $scope.device = newDevice($scope.device); + $scope.viewable = true; + }); + }; - // Returns true if the name is in error. - $scope.nameHasError = function() { - // If the name is empty don't show error. - if ($scope.device === null || $scope.device.name.length === 0) { - return false; - } - return !ValidationService.validateHostname($scope.device.name); - }; + // Called by the parent scope when this controller is hidden. + $scope.hide = function() { + $scope.viewable = false; - // Returns true if the MAC is in error. - $scope.macHasError = function(deviceInterface) { - // If the MAC is empty don't show error. - if (deviceInterface.mac.length === 0) { - return false; - } - // If the MAC is invalid show error. - if (!ValidationService.validateMAC(deviceInterface.mac)) { - return true; - } - // If the MAC is the same as another MAC show error. - var i; - for (i = 0; i < $scope.device.interfaces.length; i++) { - var isSelf = $scope.device.interfaces[i] === deviceInterface; - if (!isSelf && - $scope.device.interfaces[i].mac === deviceInterface.mac) { - return true; - } - } - return false; - }; + ManagerHelperService.unloadManagers($scope, [ + SubnetsManager, + DomainsManager + ]); + // Emit the hidden event. + $scope.$emit("addDeviceHidden"); + }; + + // Returns true if the name is in error. + $scope.nameHasError = function() { + // If the name is empty don't show error. + if ($scope.device === null || $scope.device.name.length === 0) { + return false; + } + return !ValidationService.validateHostname($scope.device.name); + }; - // Returns true if the IP address is in error. - $scope.ipHasError = function(deviceInterface) { - // If the IP is empty don't show error. - if (deviceInterface.ipAddress.length === 0) { - return false; - } - // If ip address is invalid, then exit early. - if (!ValidationService.validateIP(deviceInterface.ipAddress)) { - return true; - } - var i, inNetwork; - if (angular.isObject(deviceInterface.ipAssignment)) { - if (deviceInterface.ipAssignment.name === "external") { - // External IP address cannot be within a known subnet. - for (i = 0; i < $scope.subnets.length; i++) { - inNetwork = ValidationService.validateIPInNetwork( - deviceInterface.ipAddress, - $scope.subnets[i].cidr); - if (inNetwork) { - return true; - } - } - } else if (deviceInterface.ipAssignment.name === "static" && - angular.isNumber(deviceInterface.subnetId)) { - // Static IP address must be within a subnet. - var subnet = SubnetsManager.getItemFromList( - deviceInterface.subnetId); - inNetwork = ValidationService.validateIPInNetwork( - deviceInterface.ipAddress, subnet.cidr); - if (!inNetwork) { - return true; - } - } - } - return false; - }; + // Returns true if the MAC is in error. + $scope.macHasError = function(deviceInterface) { + // If the MAC is empty don't show error. + if (deviceInterface.mac.length === 0) { + return false; + } + // If the MAC is invalid show error. + if (!ValidationService.validateMAC(deviceInterface.mac)) { + return true; + } + // If the MAC is the same as another MAC show error. + var i; + for (i = 0; i < $scope.device.interfaces.length; i++) { + var isSelf = $scope.device.interfaces[i] === deviceInterface; + if (!isSelf && $scope.device.interfaces[i].mac === deviceInterface.mac) { + return true; + } + } + return false; + }; - // Return true when the device is missing information or invalid - // information. - $scope.deviceHasError = function() { - if ($scope.device === null || $scope.device.name === '' || - $scope.nameHasError()) { + // Returns true if the IP address is in error. + $scope.ipHasError = function(deviceInterface) { + // If the IP is empty don't show error. + if (deviceInterface.ipAddress.length === 0) { + return false; + } + // If ip address is invalid, then exit early. + if (!ValidationService.validateIP(deviceInterface.ipAddress)) { + return true; + } + var i, inNetwork; + if (angular.isObject(deviceInterface.ipAssignment)) { + if (deviceInterface.ipAssignment.name === "external") { + // External IP address cannot be within a known subnet. + for (i = 0; i < $scope.subnets.length; i++) { + inNetwork = ValidationService.validateIPInNetwork( + deviceInterface.ipAddress, + $scope.subnets[i].cidr + ); + if (inNetwork) { return true; + } } - - var i; - for (i = 0; i < $scope.device.interfaces.length; i++) { - var deviceInterface = $scope.device.interfaces[i]; - if (deviceInterface.mac === '' || - $scope.macHasError(deviceInterface) || - !angular.isObject(deviceInterface.ipAssignment)) { - return true; - } - var externalIpError = ( - deviceInterface.ipAssignment.name === "external" && ( - deviceInterface.ipAddress === '' || - $scope.ipHasError(deviceInterface))); - var staticIpError = ( - deviceInterface.ipAssignment.name === "static" && ( - !angular.isNumber(deviceInterface.subnetId) || - $scope.ipHasError(deviceInterface))); - if (externalIpError || staticIpError) { - return true; - } + } else if ( + deviceInterface.ipAssignment.name === "static" && + angular.isNumber(deviceInterface.subnetId) + ) { + // Static IP address must be within a subnet. + var subnet = SubnetsManager.getItemFromList(deviceInterface.subnetId); + inNetwork = ValidationService.validateIPInNetwork( + deviceInterface.ipAddress, + subnet.cidr + ); + if (!inNetwork) { + return true; } - return false; - }; + } + } + return false; + }; - // Adds new interface to device. - $scope.addInterface = function() { - $scope.device.interfaces.push(makeInterface()); - }; + // Return true when the device is missing information or invalid + // information. + $scope.deviceHasError = function() { + if ( + $scope.device === null || + $scope.device.name === "" || + $scope.nameHasError() + ) { + return true; + } - // Returns true if the first interface in the device interfaces array. - $scope.isPrimaryInterface = function(deviceInterface) { - return $scope.device.interfaces.indexOf(deviceInterface) === 0; - }; + var i; + for (i = 0; i < $scope.device.interfaces.length; i++) { + var deviceInterface = $scope.device.interfaces[i]; + if ( + deviceInterface.mac === "" || + $scope.macHasError(deviceInterface) || + !angular.isObject(deviceInterface.ipAssignment) + ) { + return true; + } + var externalIpError = + deviceInterface.ipAssignment.name === "external" && + (deviceInterface.ipAddress === "" || + $scope.ipHasError(deviceInterface)); + var staticIpError = + deviceInterface.ipAssignment.name === "static" && + (!angular.isNumber(deviceInterface.subnetId) || + $scope.ipHasError(deviceInterface)); + if (externalIpError || staticIpError) { + return true; + } + } + return false; + }; - // Removes the interface from the devices interfaces array. - $scope.deleteInterface = function(deviceInterface) { - // Don't remove the primary. - if ($scope.isPrimaryInterface(deviceInterface)) { - return; - } - $scope.device.interfaces.splice( - $scope.device.interfaces.indexOf(deviceInterface), 1); - }; + // Adds new interface to device. + $scope.addInterface = function() { + $scope.device.interfaces.push(makeInterface()); + }; + + // Returns true if the first interface in the device interfaces array. + $scope.isPrimaryInterface = function(deviceInterface) { + return $scope.device.interfaces.indexOf(deviceInterface) === 0; + }; + + // Removes the interface from the devices interfaces array. + $scope.deleteInterface = function(deviceInterface) { + // Don't remove the primary. + if ($scope.isPrimaryInterface(deviceInterface)) { + return; + } + $scope.device.interfaces.splice( + $scope.device.interfaces.indexOf(deviceInterface), + 1 + ); + }; - // Called when cancel clicked. - $scope.cancel = function() { - $scope.error = null; - $scope.device = newDevice(); - $scope.hide(); - }; + // Called when cancel clicked. + $scope.cancel = function() { + $scope.error = null; + $scope.device = newDevice(); + $scope.hide(); + }; + + // Called when save is clicked. + $scope.save = function(addAnother) { + // Do nothing if device in error. + if ($scope.deviceHasError()) { + return; + } - // Called when save is clicked. - $scope.save = function(addAnother) { - // Do nothing if device in error. - if ($scope.deviceHasError()) { - return; - } + // Clear the error so it can be set again, if it fails to save + // the device. + $scope.error = null; - // Clear the error so it can be set again, if it fails to save - // the device. - $scope.error = null; - - // Create the device. - var device = convertDeviceToProtocol($scope.device); - DevicesManager.create(device).then(function(device) { - if (addAnother) { - $scope.device = newDevice($scope.device); - } else { - $scope.device = newDevice(); - // Hide the scope if not adding another. - $scope.hide(); - } - }, function(error) { - $scope.error = - ManagerHelperService.parseValidationError(error); - }); - }; + // Create the device. + var device = convertDeviceToProtocol($scope.device); + DevicesManager.create(device).then( + function(device) { + if (addAnother) { + $scope.device = newDevice($scope.device); + } else { + $scope.device = newDevice(); + // Hide the scope if not adding another. + $scope.hide(); + } + }, + function(error) { + $scope.error = ManagerHelperService.parseValidationError(error); + } + ); + }; } export default AddDeviceController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/add_domain.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/add_domain.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/add_domain.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/add_domain.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,104 +5,110 @@ */ /* @ngInject */ -function AddDomainController($scope, DomainsManager, - ManagerHelperService, ValidationService) { - // Set the addDomainScope in the parent, so it can call functions - // in this controller. - var parentScope = $scope.$parent; - parentScope.addDomainScope = $scope; +function AddDomainController( + $scope, + DomainsManager, + ManagerHelperService, + ValidationService +) { + // Set the addDomainScope in the parent, so it can call functions + // in this controller. + var parentScope = $scope.$parent; + parentScope.addDomainScope = $scope; + + // Set initial values. + $scope.viewable = false; + $scope.error = null; + + // Makes a new domain. + function makeDomain() { + return { + name: "", + authoritative: true + }; + } + + // Initial domain. + $scope.domain = makeDomain(); + + // Converts the domain information from how it is held in the UI to + // how it is handled over the websocket. Since they're identical, we + // just return a copy: some day, they might be different, so we retain + // the function against that day. + function convertDomainToProtocol(domain) { + return angular.copy(domain); + } + + // Called by the parent scope when this controller is viewable. + $scope.show = function() { + // Exit early if already viewable. + if ($scope.viewable) { + return; + } + $scope.domain = makeDomain(); + $scope.viewable = true; + }; - // Set initial values. + // Called by the parent scope when this controller is hidden. + $scope.hide = function() { $scope.viewable = false; - $scope.error = null; - // Makes a new domain. - function makeDomain() { - return { - name: "", - authoritative: true - }; + // Emit the hidden event. + $scope.$emit("addDomainHidden"); + }; + + // Returns true if the name is in error. + $scope.nameHasError = function() { + // If the name is empty don't show error. + if ($scope.domain.name.length === 0) { + return false; } + return !ValidationService.validateDomainName($scope.domain.name); + }; - // Initial domain. - $scope.domain = makeDomain(); - - // Converts the domain information from how it is held in the UI to - // how it is handled over the websocket. Since they're identical, we - // just return a copy: some day, they might be different, so we retain - // the function against that day. - function convertDomainToProtocol(domain) { - return angular.copy(domain); + // Return true when the domain is missing information or invalid + // information. + $scope.domainHasError = function() { + if ($scope.domain.name === "" || $scope.nameHasError()) { + return true; } - // Called by the parent scope when this controller is viewable. - $scope.show = function() { - // Exit early if already viewable. - if ($scope.viewable) { - return; - } - $scope.domain = makeDomain(); - $scope.viewable = true; - }; - - // Called by the parent scope when this controller is hidden. - $scope.hide = function() { - $scope.viewable = false; + return false; + }; - // Emit the hidden event. - $scope.$emit('addDomainHidden'); - }; - - // Returns true if the name is in error. - $scope.nameHasError = function() { - // If the name is empty don't show error. - if ($scope.domain.name.length === 0) { - return false; - } - return !ValidationService.validateDomainName($scope.domain.name); - }; + // Called when cancel clicked. + $scope.cancel = function() { + $scope.error = null; + $scope.domain = makeDomain(); + $scope.hide(); + }; - // Return true when the domain is missing information or invalid - // information. - $scope.domainHasError = function() { - if ($scope.domain.name === '' || $scope.nameHasError()) { - return true; - } + // Called when save is clicked. + $scope.save = function(addAnother) { + // Do nothing if domain in error. + if ($scope.domainHasError()) { + return; + } - return false; - }; + // Clear the error so it can be set again, if it fails to save + // the domain. + $scope.error = null; - // Called when cancel clicked. - $scope.cancel = function() { - $scope.error = null; + // Create the domain. + var domain = convertDomainToProtocol($scope.domain); + DomainsManager.create(domain).then( + function() { $scope.domain = makeDomain(); - $scope.hide(); - }; - - // Called when save is clicked. - $scope.save = function(addAnother) { - // Do nothing if domain in error. - if ($scope.domainHasError()) { - return; + if (!addAnother) { + // Hide the scope if not adding another. + $scope.hide(); } - - // Clear the error so it can be set again, if it fails to save - // the domain. - $scope.error = null; - - // Create the domain. - var domain = convertDomainToProtocol($scope.domain); - DomainsManager.create(domain).then(function() { - $scope.domain = makeDomain(); - if (!addAnother) { - // Hide the scope if not adding another. - $scope.hide(); - } - }, function(error) { - $scope.error = - ManagerHelperService.parseValidationError(error); - }); - }; + }, + function(error) { + $scope.error = ManagerHelperService.parseValidationError(error); + } + ); + }; } export default AddDomainController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/add_hardware.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/add_hardware.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/add_hardware.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/add_hardware.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,637 +6,658 @@ /* @ngInject */ function AddHardwareController( - $q, $scope, $http, ZonesManager, ResourcePoolsManager, - MachinesManager, GeneralManager, DomainsManager, - ManagerHelperService, ValidationService) { - // Set the addHardwareScope in the parent, so it can call functions - // in this controller. - var parentScope = $scope.$parent; - parentScope.addHardwareScope = $scope; - - // Set initial values. - $scope.machineManager = MachinesManager; - $scope.newMachineObj = {}; - $scope.viewable = false; - $scope.model = 'machine'; - $scope.zones = ZonesManager.getItems(); - $scope.pools = ResourcePoolsManager.getItems(); - $scope.domains = DomainsManager.getItems(); - $scope.architectures = GeneralManager.getData("architectures"); - $scope.architectures.push("Choose an architecture"); - $scope.hwe_kernels = GeneralManager.getData("hwe_kernels"); - $scope.default_min_hwe_kernel = GeneralManager.getData( - "default_min_hwe_kernel"); - $scope.power_types = GeneralManager.getData("power_types"); - $scope.error = null; - $scope.macAddressRegex = /^([0-9A-F]{2}[::]){5}([0-9A-F]{2})$/gmi; + $q, + $scope, + $http, + ZonesManager, + ResourcePoolsManager, + MachinesManager, + GeneralManager, + DomainsManager, + ManagerHelperService, + ValidationService +) { + // Set the addHardwareScope in the parent, so it can call functions + // in this controller. + var parentScope = $scope.$parent; + parentScope.addHardwareScope = $scope; + + // Set initial values. + $scope.machineManager = MachinesManager; + $scope.newMachineObj = {}; + $scope.viewable = false; + $scope.model = "machine"; + $scope.zones = ZonesManager.getItems(); + $scope.pools = ResourcePoolsManager.getItems(); + $scope.domains = DomainsManager.getItems(); + $scope.architectures = GeneralManager.getData("architectures"); + $scope.architectures.push("Choose an architecture"); + $scope.hwe_kernels = GeneralManager.getData("hwe_kernels"); + $scope.default_min_hwe_kernel = GeneralManager.getData( + "default_min_hwe_kernel" + ); + $scope.power_types = GeneralManager.getData("power_types"); + $scope.error = null; + $scope.macAddressRegex = /^([0-9A-F]{2}[::]){5}([0-9A-F]{2})$/gim; + + // Input values. + $scope.machine = null; + $scope.chassis = null; + + // Hard coded chassis types. This is because there is no method in + // MAAS to get a full list of supported chassis. This needs to be + // fixed ASAP. + var virshFields = [ + { + name: "hostname", + label: "Address", + field_type: "string", + default: "", // Using "default" to make lint happy. + choices: [], + required: true + }, + { + name: "password", + label: "Password", + field_type: "string", + default: "", + choices: [], + required: false + }, + { + name: "prefix_filter", + label: "Prefix filter", + field_type: "string", + default: "", + choices: [], + required: false + } + ]; + $scope.chassisPowerTypes = [ + { + name: "mscm", + description: "Moonshot Chassis Manager", + fields: [ + { + name: "hostname", + label: "Host", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "username", + label: "Username", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "password", + label: "Password", + field_type: "string", + default: "", + choices: [], + required: true + } + ] + }, + { + name: "powerkvm", + description: "PowerKVM", + fields: virshFields + }, + { + name: "recs_box", + description: "Christmann RECS|Box", + fields: [ + { + name: "hostname", + label: "Hostname", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "port", + label: "Port", + field_type: "string", + default: "80", + choices: [], + required: false + }, + { + name: "username", + label: "Username", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "password", + label: "Password", + field_type: "string", + default: "", + choices: [], + required: true + } + ] + }, + { + name: "seamicro15k", + description: "SeaMicro 15000", + fields: [ + { + name: "hostname", + label: "Hostname", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "username", + label: "Username", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "password", + label: "Password", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "power_control", + label: "Power Control", + field_type: "choice", + default: "restapi2", + choices: [ + ["restapi2", "REST API V2.0"], + ["restapi", "REST API V0.9"], + ["ipmi", "IPMI"] + ], + required: true + } + ] + }, + { + name: "ucsm", + description: "UCS Chassis Manager", + fields: [ + { + name: "hostname", + label: "URL", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "username", + label: "Username", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "password", + label: "Password", + field_type: "string", + default: "", + choices: [], + required: true + } + ] + }, + { + name: "virsh", + description: "Virsh (virtual systems)", + fields: virshFields + }, + { + name: "vmware", + description: "VMware", + fields: [ + { + name: "hostname", + label: "Host", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "username", + label: "Username", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "password", + label: "Password", + field_type: "string", + default: "", + choices: [], + required: true + }, + { + name: "prefix_filter", + label: "Prefix filter", + field_type: "string", + default: "", + choices: [], + required: false + } + ] + } + ]; - // Input values. - $scope.machine = null; - $scope.chassis = null; - - // Hard coded chassis types. This is because there is no method in - // MAAS to get a full list of supported chassis. This needs to be - // fixed ASAP. - var virshFields = [ - { - name: 'hostname', - label: 'Address', - field_type: 'string', - "default": '', // Using "default" to make lint happy. - choices: [], - required: true - }, - { - name: 'password', - label: 'Password', - field_type: 'string', - "default": '', - choices: [], - required: false - }, - { - name: 'prefix_filter', - label: 'Prefix filter', - field_type: 'string', - "default": '', - choices: [], - required: false - } - ]; - $scope.chassisPowerTypes = [ - { - name: 'mscm', - description: 'Moonshot Chassis Manager', - fields: [ - { - name: 'hostname', - label: 'Host', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'username', - label: 'Username', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'password', - label: 'Password', - field_type: 'string', - "default": '', - choices: [], - required: true - } - ] - }, - { - name: 'powerkvm', - description: 'PowerKVM', - fields: virshFields - }, - { - name: 'recs_box', - description: 'Christmann RECS|Box', - fields: [ - { - name: 'hostname', - label: 'Hostname', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'port', - label: 'Port', - field_type: 'string', - "default": '80', - choices: [], - required: false - }, - { - name: 'username', - label: 'Username', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'password', - label: 'Password', - field_type: 'string', - "default": '', - choices: [], - required: true - } - ] - }, - { - name: 'seamicro15k', - description: 'SeaMicro 15000', - fields: [ - { - name: 'hostname', - label: 'Hostname', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'username', - label: 'Username', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'password', - label: 'Password', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'power_control', - label: 'Power Control', - field_type: 'choice', - "default": 'restapi2', - choices: [ - ['restapi2', 'REST API V2.0'], - ['restapi', 'REST API V0.9'], - ['ipmi', 'IPMI'] - ], - required: true - } - ] - }, - { - name: 'ucsm', - description: 'UCS Chassis Manager', - fields: [ - { - name: 'hostname', - label: 'URL', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'username', - label: 'Username', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'password', - label: 'Password', - field_type: 'string', - "default": '', - choices: [], - required: true - } - ] - }, - { - name: 'virsh', - description: 'Virsh (virtual systems)', - fields: virshFields - }, - { - name: 'vmware', - description: 'VMware', - fields: [ - { - name: 'hostname', - label: 'Host', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'username', - label: 'Username', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'password', - label: 'Password', - field_type: 'string', - "default": '', - choices: [], - required: true - }, - { - name: 'prefix_filter', - label: 'Prefix filter', - field_type: 'string', - "default": '', - choices: [], - required: false - } - ] - } - ]; - - // Get the default zone from the loaded zones. - function defaultZone() { - if ($scope.zones.length === 0) { - return null; - } else { - return $scope.zones[0]; + // Get the default zone from the loaded zones. + function defaultZone() { + if ($scope.zones.length === 0) { + return null; + } else { + return $scope.zones[0]; + } + } + + // Get the default resource pools from loaded pools. + function defaultResourcePool() { + if ($scope.pools.length === 0) { + return null; + } else { + return $scope.pools[0]; + } + } + + // Get the default architecture from the loaded architectures. + function defaultArchitecture() { + if ($scope.architectures.length === 0) { + return ""; + } else { + // Return amd64/generic first if available. + var i; + for (i = 0; i < $scope.architectures.length; i++) { + if ($scope.architectures[i] === "amd64/generic") { + return $scope.architectures[i]; } + } + return $scope.architectures[0]; } + } - // Get the default resource pools from loaded pools. - function defaultResourcePool() { - if ($scope.pools.length === 0) { - return null; - } else { - return $scope.pools[0]; + // Return a new MAC address object. + function newMAC() { + return { + mac: "", + error: false + }; + } + + // Return a new machine object. + function newMachine(cloneMachine) { + // Clone the machine instead of just creating a new one. + // This helps the user by already having the previous selected + // items selected for the new machine. + if (angular.isObject(cloneMachine)) { + return { + name: "", + domain: cloneMachine.domain, + macs: [newMAC()], + zone: cloneMachine.zone, + pool: cloneMachine.pool, + architecture: cloneMachine.architecture, + min_hwe_kernel: cloneMachine.min_hwe_kernel, + power: { + type: cloneMachine.power.type, + parameters: {} } + }; } - // Get the default architecture from the loaded architectures. - function defaultArchitecture() { - if ($scope.architectures.length === 0) { - return ''; - } else { - // Return amd64/generic first if available. - var i; - for (i = 0; i < $scope.architectures.length; i++) { - if ($scope.architectures[i] === "amd64/generic") { - return $scope.architectures[i]; - } - } - return $scope.architectures[0]; - } - } - - // Return a new MAC address object. - function newMAC() { - return { - mac: '', - error: false - }; - } - - // Return a new machine object. - function newMachine(cloneMachine) { - // Clone the machine instead of just creating a new one. - // This helps the user by already having the previous selected - // items selected for the new machine. - if (angular.isObject(cloneMachine)) { - return { - name: '', - domain: cloneMachine.domain, - macs: [newMAC()], - zone: cloneMachine.zone, - pool: cloneMachine.pool, - architecture: cloneMachine.architecture, - min_hwe_kernel: cloneMachine.min_hwe_kernel, - power: { - type: cloneMachine.power.type, - parameters: {} - } - }; - } - - // No clone machine. So create a new blank machine. - return { - name: '', - domain: DomainsManager.getDefaultDomain(), - macs: [newMAC()], - zone: defaultZone(), - pool: defaultResourcePool(), - architecture: defaultArchitecture(), - min_hwe_kernel: $scope.default_min_hwe_kernel.text, - power: { - type: null, - parameters: {} - } - }; - } - - // Return a new chassis object. - function newChassis(cloneChassis) { - // Clone the chassis instead of just creating a new one. - // This helps the user by already having the previous selected - // items selected for the new machine. - if (angular.isObject(cloneChassis)) { - return { - domain: cloneChassis.domain, - power: { - type: null, - parameters: {} - } - }; - } else { - return { - domain: DomainsManager.getDefaultDomain(), - power: { - type: null, - parameters: {} - } - }; - } - } - - // Validate that all the parameters are there for the given power type. - function powerParametersHasError(power_type, parameters) { - var i; - for (i = 0; i < power_type.fields.length; i++) { - var field = power_type.fields[i]; - var value = parameters[field.name]; - if (field.required) { - if (angular.isUndefined(value) || value === '') { - return true; - } - } - } - return false; - } - - // Called by the parent scope when this controller is viewable. - $scope.show = function(mode) { - $scope.mode = mode; - - // Exit early if already viewable. - if ($scope.viewable) { - return; - } - - var loadedItems = false, loadedManagers = false; - var defer = $q.defer(); - defer.promise.then(function() { - // Add the first machine and chassis. - $scope.machine = newMachine($scope.machine); - $scope.chassis = newChassis($scope.chassis); - $scope.error = null; - - // If the machine doesn't have an architecture - // set then it was created before all of the - // architectures were loaded. Set the default - // architecture for that machine. - if (angular.isObject($scope.machine) && - $scope.machine.architecture === '') { - $scope.machine.architecture = defaultArchitecture(); - } - $scope.viewable = true; - }); - - // The parent scope has already loaded the GeneralManager. If the - // general manager is reloaded all items from the parents scope - // will be reloaded as well. Call loadItems so only the items - // the add hardware form cares about are loaded. - GeneralManager.loadItems([ - "architectures", "hwe_kernels", "default_min_hwe_kernel" - ]).then(function() { - loadedItems = true; - if (loadedManagers) { - defer.resolve(); - } - }); - ManagerHelperService.loadManagers( - $scope, [ZonesManager, DomainsManager]).then(function() { - loadedManagers = true; - if (loadedItems) { - defer.resolve(); - } - }); - }; + // No clone machine. So create a new blank machine. + return { + name: "", + domain: DomainsManager.getDefaultDomain(), + macs: [newMAC()], + zone: defaultZone(), + pool: defaultResourcePool(), + architecture: defaultArchitecture(), + min_hwe_kernel: $scope.default_min_hwe_kernel.text, + power: { + type: null, + parameters: {} + } + }; + } + + // Return a new chassis object. + function newChassis(cloneChassis) { + // Clone the chassis instead of just creating a new one. + // This helps the user by already having the previous selected + // items selected for the new machine. + if (angular.isObject(cloneChassis)) { + return { + domain: cloneChassis.domain, + power: { + type: null, + parameters: {} + } + }; + } else { + return { + domain: DomainsManager.getDefaultDomain(), + power: { + type: null, + parameters: {} + } + }; + } + } - // Called by the parent scope when this controller is hidden. - $scope.hide = function() { - $scope.viewable = false; + // Validate that all the parameters are there for the given power type. + function powerParametersHasError(power_type, parameters) { + var i; + for (i = 0; i < power_type.fields.length; i++) { + var field = power_type.fields[i]; + var value = parameters[field.name]; + if (field.required) { + if (angular.isUndefined(value) || value === "") { + return true; + } + } + } + return false; + } - ManagerHelperService.unloadManagers( - $scope, [ZonesManager, DomainsManager]); + // Called by the parent scope when this controller is viewable. + $scope.show = function(mode) { + $scope.mode = mode; + + // Exit early if already viewable. + if ($scope.viewable) { + return; + } - // Emit the hidden event. - $scope.$emit('addHardwareHidden'); - }; + var loadedItems = false, + loadedManagers = false; + var defer = $q.defer(); + defer.promise.then(function() { + // Add the first machine and chassis. + $scope.machine = newMachine($scope.machine); + $scope.chassis = newChassis($scope.chassis); + $scope.error = null; + + // If the machine doesn't have an architecture + // set then it was created before all of the + // architectures were loaded. Set the default + // architecture for that machine. + if ( + angular.isObject($scope.machine) && + $scope.machine.architecture === "" + ) { + $scope.machine.architecture = defaultArchitecture(); + } + $scope.viewable = true; + }); + + // The parent scope has already loaded the GeneralManager. If the + // general manager is reloaded all items from the parents scope + // will be reloaded as well. Call loadItems so only the items + // the add hardware form cares about are loaded. + GeneralManager.loadItems([ + "architectures", + "hwe_kernels", + "default_min_hwe_kernel" + ]).then(function() { + loadedItems = true; + if (loadedManagers) { + defer.resolve(); + } + }); + ManagerHelperService.loadManagers($scope, [ + ZonesManager, + DomainsManager + ]).then(function() { + loadedManagers = true; + if (loadedItems) { + defer.resolve(); + } + }); + }; - // Return True when architectures loaded and in machine mode. - $scope.showMachine = function() { - if ($scope.architectures.length === 0) { - return false; - } - return $scope.mode === "machine"; - }; + // Called by the parent scope when this controller is hidden. + $scope.hide = function() { + $scope.viewable = false; - // Return True when architectures loaded and in chassis mode. - $scope.showChassis = function() { - if ($scope.architectures.length === 0) { - return false; - } - return $scope.mode === "chassis"; - }; + ManagerHelperService.unloadManagers($scope, [ZonesManager, DomainsManager]); - // Add a new MAC address to the machine. - $scope.addMac = function() { - $scope.machine.macs.push(newMAC()); - }; + // Emit the hidden event. + $scope.$emit("addHardwareHidden"); + }; + + // Return True when architectures loaded and in machine mode. + $scope.showMachine = function() { + if ($scope.architectures.length === 0) { + return false; + } + return $scope.mode === "machine"; + }; - // Remove a MAC address to the machine. - $scope.removeMac = function(mac) { - var idx = $scope.machine.macs.indexOf(mac); - if (idx > -1) { - $scope.machine.macs.splice(idx, 1); - } - }; + // Return True when architectures loaded and in chassis mode. + $scope.showChassis = function() { + if ($scope.architectures.length === 0) { + return false; + } + return $scope.mode === "chassis"; + }; - // Return true if the machine name is invalid. - $scope.invalidName = function() { - // Not invalid if empty. - if ($scope.machine.name.length === 0) { - return false; - } - return !ValidationService.validateHostname($scope.machine.name); - }; + // Add a new MAC address to the machine. + $scope.addMac = function() { + $scope.machine.macs.push(newMAC()); + }; + + // Remove a MAC address to the machine. + $scope.removeMac = function(mac) { + var idx = $scope.machine.macs.indexOf(mac); + if (idx > -1) { + $scope.machine.macs.splice(idx, 1); + } + }; - // Validate that the mac address is valid. - $scope.validateMac = function(mac) { - if (mac.mac === '') { - mac.error = false; - } else { - mac.error = !ValidationService.validateMAC(mac.mac); - } - }; + // Return true if the machine name is invalid. + $scope.invalidName = function() { + // Not invalid if empty. + if ($scope.machine.name.length === 0) { + return false; + } + return !ValidationService.validateHostname($scope.machine.name); + }; - // Return true when the machine is missing information or invalid - // information. - $scope.machineHasError = function() { - // Early-out for errors. - let in_error = ( - $scope.machine === null || - $scope.machine.zone === null || - $scope.machine.pool === null || - ($scope.machine.architecture === 'Choose an architecture' && - $scope.machine.power.type.name !== 'ipmi') || - $scope.machine.power.type === null || - $scope.invalidName($scope.machine)); - if (in_error) { - return in_error; - } - - // Make sure none of the mac addresses are in error. The first one - // cannot be blank the remaining are allowed to be empty. - if (($scope.machine.macs[0].mac === '' && - $scope.machine.power.type.name !== 'ipmi') || - $scope.machine.macs[0].error) { - return true; - } - var i; - for (i = 1; i < $scope.machine.macs.length; i++) { - var mac = $scope.machine.macs[i]; - if (mac.mac !== '' && mac.error) { - return true; - } - } - return false; - }; + // Validate that the mac address is valid. + $scope.validateMac = function(mac) { + if (mac.mac === "") { + mac.error = false; + } else { + mac.error = !ValidationService.validateMAC(mac.mac); + } + }; - // Return true if the chassis has errors. - $scope.chassisHasErrors = function() { - // Early-out for errors. - let in_error = ( - $scope.chassis === null || - $scope.chassis.power.type === null); - if (in_error) { - return in_error; - } - return powerParametersHasError( - $scope.chassis.power.type, $scope.chassis.power.parameters); - }; + // Return true when the machine is missing information or invalid + // information. + $scope.machineHasError = function() { + // Early-out for errors. + let in_error = + $scope.machine === null || + $scope.machine.zone === null || + $scope.machine.pool === null || + ($scope.machine.architecture === "Choose an architecture" && + $scope.machine.power.type.name !== "ipmi") || + $scope.machine.power.type === null || + $scope.invalidName($scope.machine); + if (in_error) { + return in_error; + } - // Called when the cancel button is pressed. - $scope.cancel = function() { - $scope.machine = newMachine(); - $scope.chassis = newChassis(); + // Make sure none of the mac addresses are in error. The first one + // cannot be blank the remaining are allowed to be empty. + if ( + ($scope.machine.macs[0].mac === "" && + $scope.machine.power.type.name !== "ipmi") || + $scope.machine.macs[0].error + ) { + return true; + } + var i; + for (i = 1; i < $scope.machine.macs.length; i++) { + var mac = $scope.machine.macs[i]; + if (mac.mac !== "" && mac.error) { + return true; + } + } + return false; + }; - // Hide the controller. - $scope.hide(); + // Return true if the chassis has errors. + $scope.chassisHasErrors = function() { + // Early-out for errors. + let in_error = + $scope.chassis === null || $scope.chassis.power.type === null; + if (in_error) { + return in_error; + } + return powerParametersHasError( + $scope.chassis.power.type, + $scope.chassis.power.parameters + ); + }; + + // Called when the cancel button is pressed. + $scope.cancel = function() { + $scope.machine = newMachine(); + $scope.chassis = newChassis(); + + // Hide the controller. + $scope.hide(); + + $scope.showErrors = false; + }; + + // Converts machine information for macs and power from how + // it is held in the UI to how it is handled over the websocket. + function convertMachineToProtocol(machine) { + // Convert the mac addresses. + var macs = angular.copy(machine.macs); + var pxe_mac = macs.shift().mac; + var extra_macs = macs.map(function(mac) { + return mac.mac; + }); + + // Return the new object. + return { + name: machine.name, + domain: machine.domain, + architecture: machine.architecture, + min_hwe_kernel: machine.min_hwe_kernel, + pxe_mac: pxe_mac, + extra_macs: extra_macs, + power_type: machine.power.type.name, + power_parameters: angular.copy(machine.power.parameters), + zone: { + id: machine.zone.id, + name: machine.zone.name + }, + pool: { + id: machine.pool.id, + name: machine.pool.name + } + }; + } + + // Called to update maas-obj-form state with protocol + // for macs and power. + $scope.saveMachine = function(addAnother) { + $scope.addAnother = addAnother; + $scope.showErrors = true; + + // set maas-obj-form object; + $scope.newMachineObj = Object.assign( + $scope.newMachineObj, + convertMachineToProtocol($scope.machine) + ); + }; + + // maas-obj-form after-save callback + $scope.afterSaveMachine = function() { + if ($scope.addAnother) { + $scope.machine = newMachine($scope.machine); + } else { + $scope.machine = newMachine(); - $scope.showErrors = false; - }; + // Hide the scope if not adding another. + $scope.hide(); + } + }; - // Converts machine information for macs and power from how - // it is held in the UI to how it is handled over the websocket. - function convertMachineToProtocol(machine) { - // Convert the mac addresses. - var macs = angular.copy(machine.macs); - var pxe_mac = macs.shift().mac; - var extra_macs = macs.map(function(mac) { return mac.mac; }); - - // Return the new object. - return { - name: machine.name, - domain: machine.domain, - architecture: machine.architecture, - min_hwe_kernel: machine.min_hwe_kernel, - pxe_mac: pxe_mac, - extra_macs: extra_macs, - power_type: machine.power.type.name, - power_parameters: angular.copy(machine.power.parameters), - zone: { - id: machine.zone.id, - name: machine.zone.name - }, - pool: { - id: machine.pool.id, - name: machine.pool.name - }, - }; - } - - // Called to update maas-obj-form state with protocol - // for macs and power. - $scope.saveMachine = function(addAnother) { - $scope.addAnother = addAnother; - $scope.showErrors = true; - - // set maas-obj-form object; - $scope.newMachineObj = Object.assign( - $scope.newMachineObj, - convertMachineToProtocol($scope.machine)); - } - - // maas-obj-form after-save callback - $scope.afterSaveMachine = function() { - if ($scope.addAnother) { - $scope.machine = newMachine($scope.machine); - } else { - $scope.machine = newMachine(); + // Called to perform the saving of the chassis. + $scope.saveChassis = function(addAnother) { + // Does nothing if error exists. + if ($scope.chassisHasErrors()) { + return; + } - // Hide the scope if not adding another. - $scope.hide(); - } - }; + // Clear the error so it can be set again, if it fails to save + // the device. + $scope.error = null; - // Called to perform the saving of the chassis. - $scope.saveChassis = function(addAnother) { - // Does nothing if error exists. - if ($scope.chassisHasErrors()) { - return; - } - - // Clear the error so it can be set again, if it fails to save - // the device. - $scope.error = null; - - // Create the parameters. - var params = angular.copy($scope.chassis.power.parameters); - params.chassis_type = $scope.chassis.power.type.name; - params.domain = $scope.chassis.domain.name; - - // XXX ltrager 24-02-2016: Something is adding the username field - // even though its not defined in virshFields. The API rejects - // requests with improper fields so remove it before we send the - // request. - if ( - params.chassis_type === "powerkvm" || - params.chassis_type === "virsh") { - delete params.username; - } - // Add the chassis. For now we use the API as the websocket doesn't - // support probe and enlist. - $http({ - method: 'POST', - url: 'api/2.0/machines/?op=add_chassis', - data: $.param(params), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }).then(function() { - if (addAnother) { - $scope.chassis = newChassis($scope.chassis); - } else { - $scope.chassis = newChassis(); - // Hide the scope if not adding another. - $scope.hide(); - } - }, function(error) { - $scope.error = - ManagerHelperService.parseValidationError(error.data); - }); - }; + // Create the parameters. + var params = angular.copy($scope.chassis.power.parameters); + params.chassis_type = $scope.chassis.power.type.name; + params.domain = $scope.chassis.domain.name; + + // XXX ltrager 24-02-2016: Something is adding the username field + // even though its not defined in virshFields. The API rejects + // requests with improper fields so remove it before we send the + // request. + if (params.chassis_type === "powerkvm" || params.chassis_type === "virsh") { + delete params.username; + } + // Add the chassis. For now we use the API as the websocket doesn't + // support probe and enlist. + $http({ + method: "POST", + url: "api/2.0/machines/?op=add_chassis", + data: $.param(params), + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }).then( + function() { + if (addAnother) { + $scope.chassis = newChassis($scope.chassis); + } else { + $scope.chassis = newChassis(); + // Hide the scope if not adding another. + $scope.hide(); + } + }, + function(error) { + $scope.error = ManagerHelperService.parseValidationError(error.data); + } + ); + }; } export default AddHardwareController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/dashboard.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/dashboard.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/dashboard.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/dashboard.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,328 +5,342 @@ */ /* @ngInject */ -function DashboardController($scope, $rootScope, $location, - DiscoveriesManager, DomainsManager, MachinesManager, - DevicesManager, SubnetsManager, VLANsManager, ConfigsManager, - ManagerHelperService, SearchService) { - - // Default device IP options. - var deviceIPOptions = [ - ['static', 'Static'], - ['dynamic', 'Dynamic'], - ['external', 'External'] - ]; - - // Set title and page. - $rootScope.title = "Dashboard"; - $rootScope.page = "dashboard"; - - // Set initial values. - $scope.loaded = false; - $scope.discoveredDevices = DiscoveriesManager.getItems(); - $scope.domains = DomainsManager.getItems(); - $scope.machines = MachinesManager.getItems(); - $scope.devices = DevicesManager.getItems(); - $scope.configManager = ConfigsManager; - $scope.networkDiscovery = null; - $scope.column = 'mac'; - $scope.selectedDevice = null; - $scope.convertTo = null; - $scope.showClearDiscoveriesPanel = false; - $scope.removingDevices = false; - $scope.MAAS_VERSION_NUMBER = DiscoveriesManager - .formatMAASVersionNumber(); - $scope.search = ""; - $scope.searchValid = true; - $scope.actionOption = null; - $scope.filters = SearchService.getEmptyFilter(); - $scope.metadata = {}; - $scope.filteredDevices = []; - - $scope.clearSearch = function() { - $scope.search = ""; - $scope.updateFilters(); - }; +function DashboardController( + $scope, + $rootScope, + $location, + DiscoveriesManager, + DomainsManager, + MachinesManager, + DevicesManager, + SubnetsManager, + VLANsManager, + ConfigsManager, + ManagerHelperService, + SearchService +) { + // Default device IP options. + var deviceIPOptions = [ + ["static", "Static"], + ["dynamic", "Dynamic"], + ["external", "External"] + ]; + + // Set title and page. + $rootScope.title = "Dashboard"; + $rootScope.page = "dashboard"; + + // Set initial values. + $scope.loaded = false; + $scope.discoveredDevices = DiscoveriesManager.getItems(); + $scope.domains = DomainsManager.getItems(); + $scope.machines = MachinesManager.getItems(); + $scope.devices = DevicesManager.getItems(); + $scope.configManager = ConfigsManager; + $scope.networkDiscovery = null; + $scope.column = "mac"; + $scope.selectedDevice = null; + $scope.convertTo = null; + $scope.showClearDiscoveriesPanel = false; + $scope.removingDevices = false; + $scope.MAAS_VERSION_NUMBER = DiscoveriesManager.formatMAASVersionNumber(); + $scope.search = ""; + $scope.searchValid = true; + $scope.actionOption = null; + $scope.filters = SearchService.getEmptyFilter(); + $scope.metadata = {}; + $scope.filteredDevices = []; - $scope.filteredDevices = []; + $scope.clearSearch = function() { + $scope.search = ""; + $scope.updateFilters(); + }; - $scope.updateFilters = function() { - var searchQuery = $scope.search; - var filters = SearchService.getCurrentFilters(searchQuery); - if (filters === null) { - $scope.filters = SearchService.getEmptyFilter(); - $scope.searchValid = false; - } else { - $scope.filters = filters; - $scope.searchValid = true; - } - }; + $scope.filteredDevices = []; - $scope.dedupeMetadata = function(prop) { - return $scope.discoveredDevices.filter(function(item, pos, arr) { - return arr.map(function(obj) { - return obj[prop]; - }).indexOf(item[prop]) === pos; - }); + $scope.updateFilters = function() { + var searchQuery = $scope.search; + var filters = SearchService.getCurrentFilters(searchQuery); + if (filters === null) { + $scope.filters = SearchService.getEmptyFilter(); + $scope.searchValid = false; + } else { + $scope.filters = filters; + $scope.searchValid = true; } + }; - $scope.getCount = function(prop, value) { - return $scope.discoveredDevices.filter(function(item) { - return item[prop] === value; - }).length; - }; - - $scope.setMetadata = function() { - var fabrics = $scope.dedupeMetadata("fabric_name") - .map(function(item) { - return { - name: item.fabric_name, - count: $scope.getCount("fabric_name", item.fabric_name) - }; - }); - - var vlans = $scope.dedupeMetadata("vlan") - .map(function(item) { - return { - name: item.vlan, - count: $scope.getCount("vlan", item.vlan) - }; - }); - - var racks = $scope.dedupeMetadata("observer_hostname") - .map(function(item) { - return { - name: item.observer_hostname, - count: $scope - .getCount("observer_hostname", item.observer_hostname) - }; - }); - - var subnets = $scope.dedupeMetadata("subnet_cidr") - .map(function(item) { - return { - name: item.subnet_cidr, - count: $scope.getCount("subnet_cidr", item.subnet_cidr) - }; - }); - - $scope.metadata = { - fabric: fabrics, - vlan: vlans, - rack: racks, - subnet: subnets - }; - }; - - // Adds or removes a filter to the search. - $scope.toggleFilter = function(type, value) { - $scope.filters = - SearchService.toggleFilter($scope.filters, type, value, true); - $scope.search = SearchService.filtersToString($scope.filters); - }; - - // Return True if the filter is active. - $scope.isFilterActive = function(type, value) { - return SearchService - .isFilterActive($scope.filters, type, value, true); - }; - - $scope.formatMAASVersionNumber = function() { - if (MAAS_config.version) { - var versionWithPoint = MAAS_config.version.split(" ")[0]; - - if (versionWithPoint) { - if (versionWithPoint.split(".")[2] === "0") { - return versionWithPoint.split(".")[0] - + "." - + versionWithPoint.split(".")[1]; - } else { - return versionWithPoint; - } - } + $scope.dedupeMetadata = function(prop) { + return $scope.discoveredDevices.filter(function(item, pos, arr) { + return ( + arr + .map(function(obj) { + return obj[prop]; + }) + .indexOf(item[prop]) === pos + ); + }); + }; + + $scope.getCount = function(prop, value) { + return $scope.discoveredDevices.filter(function(item) { + return item[prop] === value; + }).length; + }; + + $scope.setMetadata = function() { + var fabrics = $scope.dedupeMetadata("fabric_name").map(function(item) { + return { + name: item.fabric_name, + count: $scope.getCount("fabric_name", item.fabric_name) + }; + }); + + var vlans = $scope.dedupeMetadata("vlan").map(function(item) { + return { + name: item.vlan, + count: $scope.getCount("vlan", item.vlan) + }; + }); + + var racks = $scope.dedupeMetadata("observer_hostname").map(function(item) { + return { + name: item.observer_hostname, + count: $scope.getCount("observer_hostname", item.observer_hostname) + }; + }); + + var subnets = $scope.dedupeMetadata("subnet_cidr").map(function(item) { + return { + name: item.subnet_cidr, + count: $scope.getCount("subnet_cidr", item.subnet_cidr) + }; + }); + + $scope.metadata = { + fabric: fabrics, + vlan: vlans, + rack: racks, + subnet: subnets + }; + }; + + // Adds or removes a filter to the search. + $scope.toggleFilter = function(type, value) { + $scope.filters = SearchService.toggleFilter( + $scope.filters, + type, + value, + true + ); + $scope.search = SearchService.filtersToString($scope.filters); + }; + + // Return True if the filter is active. + $scope.isFilterActive = function(type, value) { + return SearchService.isFilterActive($scope.filters, type, value, true); + }; + + $scope.formatMAASVersionNumber = function() { + if (MAAS_config.version) { + var versionWithPoint = MAAS_config.version.split(" ")[0]; + + if (versionWithPoint) { + if (versionWithPoint.split(".")[2] === "0") { + return ( + versionWithPoint.split(".")[0] + + "." + + versionWithPoint.split(".")[1] + ); + } else { + return versionWithPoint; } - }; - - $scope.MAAS_VERSION_NUMBER = $scope.formatMAASVersionNumber(); - - // Set default predicate to last_seen. - $scope.predicate = $scope.last_seen; - - // Open clear devices panel - $scope.openClearDiscoveriesPanel = function() { - $scope.showClearDiscoveriesPanel = true; - }; - - // Close clear devices panel - $scope.closeClearDiscoveriesPanel = function() { - $scope.showClearDiscoveriesPanel = false; - }; - - // Sorts the table by predicate. - $scope.sortTable = function(predicate) { - $scope.predicate = predicate; - $scope.reverse = !$scope.reverse; - }; + } + } + }; - // Proxy manager that the maas-obj-form directive uses to call the - // correct method based on current type. - $scope.proxyManager = { - updateItem: function(params) { - if ($scope.convertTo.type === 'device') { - return DevicesManager.createItem(params); - } else if ($scope.convertTo.type === 'interface') { - return DevicesManager.createInterface(params); - } else { - throw new Error("Unknown type: " + $scope.convertTo.type); - } - } - }; + $scope.MAAS_VERSION_NUMBER = $scope.formatMAASVersionNumber(); - // Return the name name for the Discovery. - $scope.getDiscoveryName = function(discovery) { - if (discovery.hostname === null) { - return 'unknown'; - } - else { - return discovery.hostname; - } - }; + // Set default predicate to last_seen. + $scope.predicate = $scope.last_seen; - // Get the name of the subnet from its ID. - $scope.getSubnetName = function(subnetId) { - var subnet = SubnetsManager.getItemFromList(subnetId); - return SubnetsManager.getName(subnet); - }; + // Open clear devices panel + $scope.openClearDiscoveriesPanel = function() { + $scope.showClearDiscoveriesPanel = true; + }; - // Get the name of the VLAN from its ID. - $scope.getVLANName = function(vlanId) { - var vlan = VLANsManager.getItemFromList(vlanId); - return VLANsManager.getName(vlan); - }; + // Close clear devices panel + $scope.closeClearDiscoveriesPanel = function() { + $scope.showClearDiscoveriesPanel = false; + }; - // Remove device - $scope.removeDevice = function(device) { - device.isBeingRemoved = true; - DiscoveriesManager - .removeDevice(device); - }; + // Sorts the table by predicate. + $scope.sortTable = function(predicate) { + $scope.predicate = predicate; + $scope.reverse = !$scope.reverse; + }; + + // Proxy manager that the maas-obj-form directive uses to call the + // correct method based on current type. + $scope.proxyManager = { + updateItem: function(params) { + if ($scope.convertTo.type === "device") { + return DevicesManager.createItem(params); + } else if ($scope.convertTo.type === "interface") { + return DevicesManager.createInterface(params); + } else { + throw new Error("Unknown type: " + $scope.convertTo.type); + } + } + }; - // Remove all devices - $scope.removeAllDevices = function() { - $scope.removingDevices = true; - DiscoveriesManager - .removeDevices($scope.discoveredDevices) - .then(function() { - $scope.discoveredDevices = DiscoveriesManager.getItems(); - }); - }; + // Return the name name for the Discovery. + $scope.getDiscoveryName = function(discovery) { + if (discovery.hostname === null) { + return "unknown"; + } else { + return discovery.hostname; + } + }; - // Sets selected device - $scope.toggleSelected = function(deviceId) { - if ($scope.selectedDevice === deviceId) { - $scope.selectedDevice = null; - } else { - var discovered = DiscoveriesManager.getItemFromList(deviceId); - var hostname = $scope.getDiscoveryName(discovered); - var domain; - if (hostname === 'unknown') { - hostname = ''; - } - if (hostname.indexOf('.') > 0) { - domain = DomainsManager.getDomainByName( - hostname.slice(hostname.indexOf('.') + 1)); - hostname = hostname.split(".", 1)[0]; - if (domain === null) { - domain = DomainsManager.getDefaultDomain(); - } - } else { - domain = DomainsManager.getDefaultDomain(); - } - $scope.convertTo = { - type: 'device', - hostname: hostname, - domain: domain, - parent: null, - ip_assignment: 'dynamic', - goTo: false, - saved: false, - deviceIPOptions: deviceIPOptions.filter( - function(option) { - // Filter the options to not include static if - // a subnet is not defined for this discovered - // item. - if (option[0] === 'static' && - !angular.isNumber(discovered.subnet)) { - return false; - } else { - return true; - } - }) - }; - $scope.selectedDevice = deviceId; + // Get the name of the subnet from its ID. + $scope.getSubnetName = function(subnetId) { + var subnet = SubnetsManager.getItemFromList(subnetId); + return SubnetsManager.getName(subnet); + }; + + // Get the name of the VLAN from its ID. + $scope.getVLANName = function(vlanId) { + var vlan = VLANsManager.getItemFromList(vlanId); + return VLANsManager.getName(vlan); + }; + + // Remove device + $scope.removeDevice = function(device) { + device.isBeingRemoved = true; + DiscoveriesManager.removeDevice(device); + }; + + // Remove all devices + $scope.removeAllDevices = function() { + $scope.removingDevices = true; + DiscoveriesManager.removeDevices($scope.discoveredDevices).then(function() { + $scope.discoveredDevices = DiscoveriesManager.getItems(); + }); + }; + + // Sets selected device + $scope.toggleSelected = function(deviceId) { + if ($scope.selectedDevice === deviceId) { + $scope.selectedDevice = null; + } else { + var discovered = DiscoveriesManager.getItemFromList(deviceId); + var hostname = $scope.getDiscoveryName(discovered); + var domain; + if (hostname === "unknown") { + hostname = ""; + } + if (hostname.indexOf(".") > 0) { + domain = DomainsManager.getDomainByName( + hostname.slice(hostname.indexOf(".") + 1) + ); + hostname = hostname.split(".", 1)[0]; + if (domain === null) { + domain = DomainsManager.getDefaultDomain(); } - }; + } else { + domain = DomainsManager.getDefaultDomain(); + } + $scope.convertTo = { + type: "device", + hostname: hostname, + domain: domain, + parent: null, + ip_assignment: "dynamic", + goTo: false, + saved: false, + deviceIPOptions: deviceIPOptions.filter(function(option) { + // Filter the options to not include static if + // a subnet is not defined for this discovered + // item. + if (option[0] === "static" && !angular.isNumber(discovered.subnet)) { + return false; + } else { + return true; + } + }) + }; + $scope.selectedDevice = deviceId; + } + }; - // Called before the createItem is called to adjust the values - // passed over the call. - $scope.preProcess = function(item) { - var discovered = DiscoveriesManager.getItemFromList( - $scope.selectedDevice); - item = angular.copy(item); - if ($scope.convertTo.type === 'device') { - item.primary_mac = discovered.mac_address; - item.extra_macs = []; - item.interfaces = [{ - mac: discovered.mac_address, - ip_assignment: item.ip_assignment, - ip_address: discovered.ip, - subnet: discovered.subnet - }]; - } else if ($scope.convertTo.type === 'interface') { - item.mac_address = discovered.mac_address; - item.ip_address = discovered.ip; - item.subnet = discovered.subnet; + // Called before the createItem is called to adjust the values + // passed over the call. + $scope.preProcess = function(item) { + var discovered = DiscoveriesManager.getItemFromList($scope.selectedDevice); + item = angular.copy(item); + if ($scope.convertTo.type === "device") { + item.primary_mac = discovered.mac_address; + item.extra_macs = []; + item.interfaces = [ + { + mac: discovered.mac_address, + ip_assignment: item.ip_assignment, + ip_address: discovered.ip, + subnet: discovered.subnet } - return item; - }; + ]; + } else if ($scope.convertTo.type === "interface") { + item.mac_address = discovered.mac_address; + item.ip_address = discovered.ip; + item.subnet = discovered.subnet; + } + return item; + }; - // Called after the createItem has been successful. - $scope.afterSave = function(obj) { - DiscoveriesManager._removeItem($scope.selectedDevice); - $scope.selectedDevice = null; - $scope.convertTo.hostname = obj.hostname; - $scope.convertTo.parent = obj.parent; - $scope.convertTo.saved = true; - if ($scope.convertTo.goTo) { - if (angular.isString(obj.parent)) { - $location.path('/device/' + obj.parent); - } else { - $location.path('/devices/'); - } - } - }; + // Called after the createItem has been successful. + $scope.afterSave = function(obj) { + DiscoveriesManager._removeItem($scope.selectedDevice); + $scope.selectedDevice = null; + $scope.convertTo.hostname = obj.hostname; + $scope.convertTo.parent = obj.parent; + $scope.convertTo.saved = true; + if ($scope.convertTo.goTo) { + if (angular.isString(obj.parent)) { + $location.path("/device/" + obj.parent); + } else { + $location.path("/devices/"); + } + } + }; - // Load all the managers and get the network discovery config item. - ManagerHelperService.loadManagers($scope, [ - DiscoveriesManager, DomainsManager, MachinesManager, - DevicesManager, SubnetsManager, VLANsManager, ConfigsManager]).then( - function() { - $scope.loaded = true; - $scope.networkDiscovery = ConfigsManager.getItemFromList( - 'network_discovery'); - - $scope.setMetadata(); - - $scope.discoveredDevices.forEach(function(device) { - var date = new Date(device.last_seen); - device.last_seen_timestamp = date.getTime(); - }); - - $scope.$watchCollection('discoveredDevices', function() { - $scope.removingDevices = false; - $scope.closeClearDiscoveriesPanel(); - }); - }); + // Load all the managers and get the network discovery config item. + ManagerHelperService.loadManagers($scope, [ + DiscoveriesManager, + DomainsManager, + MachinesManager, + DevicesManager, + SubnetsManager, + VLANsManager, + ConfigsManager + ]).then(function() { + $scope.loaded = true; + $scope.networkDiscovery = ConfigsManager.getItemFromList( + "network_discovery" + ); + + $scope.setMetadata(); + + $scope.discoveredDevices.forEach(function(device) { + var date = new Date(device.last_seen); + device.last_seen_timestamp = date.getTime(); + }); + + $scope.$watchCollection("discoveredDevices", function() { + $scope.removingDevices = false; + $scope.closeClearDiscoveriesPanel(); + }); + }); } export default DashboardController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/domain_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/domain_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/domain_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/domain_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,171 +6,193 @@ /* @ngInject */ function DomainDetailsController( - $scope, $rootScope, $routeParams, $location, - DomainsManager, UsersManager, ManagerHelperService, ErrorService) { - // Set title and page. - $rootScope.title = "Loading..."; - - // Note: this value must match the top-level tab, in order for - // highlighting to occur properly. - $rootScope.page = "domains"; - - // Initial values. - $scope.loaded = false; - $scope.domain = null; + $scope, + $rootScope, + $routeParams, + $location, + DomainsManager, + UsersManager, + ManagerHelperService, + ErrorService +) { + // Set title and page. + $rootScope.title = "Loading..."; + + // Note: this value must match the top-level tab, in order for + // highlighting to occur properly. + $rootScope.page = "domains"; + + // Initial values. + $scope.loaded = false; + $scope.domain = null; + $scope.editSummary = false; + $scope.predicate = "name"; + $scope.reverse = false; + $scope.action = null; + $scope.editRow = null; + $scope.deleteRow = null; + + $scope.domainsManager = DomainsManager; + $scope.newObject = {}; + + $scope.supportedRecordTypes = [ + "A", + "AAAA", + "CNAME", + "MX", + "NS", + "SRV", + "SSHFP", + "TXT" + ]; + + // Set default predicate to name. + $scope.predicate = "name"; + + // Sorts the table by predicate. + $scope.sortTable = function(predicate) { + $scope.predicate = predicate; + $scope.reverse = !$scope.reverse; + }; + + $scope.enterEditSummary = function() { + $scope.editSummary = true; + }; + + // Called when the "cancel" button is clicked in the domain summary. + $scope.exitEditSummary = function() { $scope.editSummary = false; - $scope.predicate = "name"; - $scope.reverse = false; - $scope.action = null; - $scope.editRow = null; + }; + + $scope.isRecordAutogenerated = function(row) { + // We can't edit records that don't have a dnsresource_id. + // (If the row doesn't have one, it has probably been automatically + // generated by means of a deployed node, or some other reason.) + return !row.dnsresource_id; + }; + + $scope.editRecord = function(row) { + $scope.editRow = row; + // We'll need the original values to determine if an update is + // required. + row.previous_rrdata = row.rrdata; + row.previous_rrtype = row.rrtype; + row.previous_name = row.name; + row.previous_ttl = row.ttl; $scope.deleteRow = null; + }; - $scope.domainsManager = DomainsManager; - $scope.newObject = {}; + $scope.removeRecord = function(row) { + $scope.deleteRow = row; + $scope.editRow = null; + }; - $scope.supportedRecordTypes = [ - 'A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'SSHFP', 'TXT' - ]; - - // Set default predicate to name. - $scope.predicate = 'name'; - - // Sorts the table by predicate. - $scope.sortTable = function(predicate) { - $scope.predicate = predicate; - $scope.reverse = !$scope.reverse; - }; - - $scope.enterEditSummary = function() { - $scope.editSummary = true; - }; - - // Called when the "cancel" button is clicked in the domain summary. - $scope.exitEditSummary = function() { - $scope.editSummary = false; - }; - - $scope.isRecordAutogenerated = function(row) { - // We can't edit records that don't have a dnsresource_id. - // (If the row doesn't have one, it has probably been automatically - // generated by means of a deployed node, or some other reason.) - return !row.dnsresource_id; - }; - - $scope.editRecord = function(row) { - $scope.editRow = row; - // We'll need the original values to determine if an update is - // required. - row.previous_rrdata = row.rrdata; - row.previous_rrtype = row.rrtype; - row.previous_name = row.name; - row.previous_ttl = row.ttl; - $scope.deleteRow = null; - }; - - $scope.removeRecord = function(row) { - $scope.deleteRow = row; - $scope.editRow = null; - }; - - $scope.confirmRemoveRecord = function(row) { - // The websocket handler needs the domain ID, so add it in. - row.domain = $scope.domain.id; - $scope.domainsManager.deleteDNSRecord(row); - $scope.stopEditingRow(); - }; - - $scope.stopEditingRow = function() { - $scope.editRow = null; - $scope.deleteRow = null; - }; - - // Updates the page title. - function updateTitle() { - $rootScope.title = $scope.domain.displayname; - } + $scope.confirmRemoveRecord = function(row) { + // The websocket handler needs the domain ID, so add it in. + row.domain = $scope.domain.id; + $scope.domainsManager.deleteDNSRecord(row); + $scope.stopEditingRow(); + }; - // Called when the domain has been loaded. - function domainLoaded(domain) { - $scope.domain = domain; - $scope.loaded = true; + $scope.stopEditingRow = function() { + $scope.editRow = null; + $scope.deleteRow = null; + }; - updateTitle(); + // Updates the page title. + function updateTitle() { + $rootScope.title = $scope.domain.displayname; + } + + // Called when the domain has been loaded. + function domainLoaded(domain) { + $scope.domain = domain; + $scope.loaded = true; + + updateTitle(); + } + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Return true if this is the default domain. + $scope.isDefaultDomain = function() { + if (angular.isObject($scope.domain)) { + return $scope.domain.id === 0; } + return false; + }; - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Return true if this is the default domain. - $scope.isDefaultDomain = function() { - if (angular.isObject($scope.domain)) { - return $scope.domain.id === 0; - } - return false; - }; - - // Called to check if the space can be deleted. - $scope.canBeDeleted = function() { - if (angular.isObject($scope.domain)) { - return $scope.domain.rrsets.length === 0; - } - return false; - }; - - // Called when the delete domain button is pressed. - $scope.deleteButton = function() { - $scope.error = null; - $scope.actionInProgress = true; - $scope.action = 'delete'; - }; - - // Called when the add record button is pressed. - $scope.addRecordButton = function() { - $scope.error = null; - $scope.actionInProgress = true; - $scope.action = 'add_record'; - }; + // Called to check if the space can be deleted. + $scope.canBeDeleted = function() { + if (angular.isObject($scope.domain)) { + return $scope.domain.rrsets.length === 0; + } + return false; + }; - // Called when the cancel delete domain button is pressed. - $scope.cancelAction = function() { + // Called when the delete domain button is pressed. + $scope.deleteButton = function() { + $scope.error = null; + $scope.actionInProgress = true; + $scope.action = "delete"; + }; + + // Called when the add record button is pressed. + $scope.addRecordButton = function() { + $scope.error = null; + $scope.actionInProgress = true; + $scope.action = "add_record"; + }; + + // Called when the cancel delete domain button is pressed. + $scope.cancelAction = function() { + $scope.actionInProgress = false; + }; + + // Called when the confirm delete domain button is pressed. + $scope.deleteConfirmButton = function() { + DomainsManager.deleteDomain($scope.domain).then( + function() { $scope.actionInProgress = false; - }; - - // Called when the confirm delete domain button is pressed. - $scope.deleteConfirmButton = function() { - DomainsManager.deleteDomain($scope.domain).then(function() { - $scope.actionInProgress = false; - $location.path("/domains"); - }, function(error) { - $scope.error = - ManagerHelperService.parseValidationError(error); - }); - }; - - // Load all the required managers. - ManagerHelperService.loadManagers( - $scope, [DomainsManager, UsersManager]).then(function() { - // Possibly redirected from another controller that already had - // this domain set to active. Only call setActiveItem if not - // already the activeItem. - var activeDomain = DomainsManager.getActiveItem(); - var requestedDomain = parseInt($routeParams.domain_id, 10); - if (isNaN(requestedDomain)) { - ErrorService.raiseError("Invalid domain identifier."); - } else if (angular.isObject(activeDomain) && - activeDomain.id === requestedDomain) { - domainLoaded(activeDomain); - } else { - DomainsManager.setActiveItem( - requestedDomain).then(function(domain) { - domainLoaded(domain); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); + $location.path("/domains"); + }, + function(error) { + $scope.error = ManagerHelperService.parseValidationError(error); + } + ); + }; + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ + DomainsManager, + UsersManager + ]).then(function() { + // Possibly redirected from another controller that already had + // this domain set to active. Only call setActiveItem if not + // already the activeItem. + var activeDomain = DomainsManager.getActiveItem(); + var requestedDomain = parseInt($routeParams.domain_id, 10); + if (isNaN(requestedDomain)) { + ErrorService.raiseError("Invalid domain identifier."); + } else if ( + angular.isObject(activeDomain) && + activeDomain.id === requestedDomain + ) { + domainLoaded(activeDomain); + } else { + DomainsManager.setActiveItem(requestedDomain).then( + function(domain) { + domainLoaded(domain); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } + }); } export default DomainDetailsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/domains_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/domains_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/domains_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/domains_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,61 +6,65 @@ /* @ngInject */ function DomainsListController( - $scope, $rootScope, DomainsManager, - UsersManager, ManagerHelperService) { + $scope, + $rootScope, + DomainsManager, + UsersManager, + ManagerHelperService +) { + // Load the filters that are used inside the controller. + + // Set title and page. + $rootScope.title = "DNS"; + $rootScope.page = "domains"; + + // Set initial values. + $scope.domains = DomainsManager.getItems(); + $scope.currentpage = "domains"; + $scope.predicate = "name"; + $scope.reverse = false; + $scope.loading = true; + $scope.confirmSetDefaultRow = null; + + // This will hold the AddDomainController once it's initialized. The + // controller will set this variable as it's always a child of this + // scope. + $scope.addDomainScope = null; + + // Called when the add domain button is pressed. + $scope.addDomain = function() { + $scope.addDomainScope.show(); + }; + + // Called when the cancel add domain button is pressed. + $scope.cancelAddDomain = function() { + $scope.addDomainScope.cancel(); + }; + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + $scope.confirmSetDefault = function(domain) { + $scope.confirmSetDefaultRow = domain; + }; - // Load the filters that are used inside the controller. + $scope.cancelSetDefault = function() { + $scope.confirmSetDefaultRow = null; + }; - // Set title and page. - $rootScope.title = "DNS"; - $rootScope.page = "domains"; - - // Set initial values. - $scope.domains = DomainsManager.getItems(); - $scope.currentpage = "domains"; - $scope.predicate = "name"; - $scope.reverse = false; - $scope.loading = true; + $scope.setDefault = function(domain) { + DomainsManager.setDefault(domain); $scope.confirmSetDefaultRow = null; + }; - // This will hold the AddDomainController once it's initialized. The - // controller will set this variable as it's always a child of this - // scope. - $scope.addDomainScope = null; - - // Called when the add domain button is pressed. - $scope.addDomain = function() { - $scope.addDomainScope.show(); - }; - - // Called when the cancel add domain button is pressed. - $scope.cancelAddDomain = function() { - $scope.addDomainScope.cancel(); - }; - - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - $scope.confirmSetDefault = function(domain) { - $scope.confirmSetDefaultRow = domain; - }; - - $scope.cancelSetDefault = function() { - $scope.confirmSetDefaultRow = null; - }; - - $scope.setDefault = function(domain) { - DomainsManager.setDefault(domain); - $scope.confirmSetDefaultRow = null; - }; - - ManagerHelperService.loadManagers( - $scope, [DomainsManager, UsersManager]).then( - function() { - $scope.loading = false; - }); + ManagerHelperService.loadManagers($scope, [ + DomainsManager, + UsersManager + ]).then(function() { + $scope.loading = false; + }); } export default DomainsListController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/fabric_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/fabric_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/fabric_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/fabric_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,175 +6,198 @@ /* @ngInject */ function FabricDetailsController( - $scope, $rootScope, $routeParams, $filter, $location, - FabricsManager, VLANsManager, SubnetsManager, SpacesManager, - ControllersManager, UsersManager, ManagerHelperService, - ErrorService) { - // Set title and page. - $rootScope.title = "Loading..."; - - // Note: this value must match the top-level tab, in order for - // highlighting to occur properly. - $rootScope.page = "networks"; - - // Initial values. - $scope.fabric = null; - $scope.fabricManager = FabricsManager; + $scope, + $rootScope, + $routeParams, + $filter, + $location, + FabricsManager, + VLANsManager, + SubnetsManager, + SpacesManager, + ControllersManager, + UsersManager, + ManagerHelperService, + ErrorService +) { + // Set title and page. + $rootScope.title = "Loading..."; + + // Note: this value must match the top-level tab, in order for + // highlighting to occur properly. + $rootScope.page = "networks"; + + // Initial values. + $scope.fabric = null; + $scope.fabricManager = FabricsManager; + $scope.editSummary = false; + $scope.vlans = VLANsManager.getItems(); + $scope.subnets = SubnetsManager.getItems(); + $scope.controllers = ControllersManager.getItems(); + $scope.loaded = false; + + // Updates the page title. + function updateTitle() { + $rootScope.title = $scope.fabric.name; + } + + // Called when the "edit" button is cliked in the fabric summary + $scope.enterEditSummary = function() { + $scope.editSummary = true; + }; + + // Called when the "cancel" button is cliked in the fabric summary + $scope.exitEditSummary = function() { $scope.editSummary = false; - $scope.vlans = VLANsManager.getItems(); - $scope.subnets = SubnetsManager.getItems(); - $scope.controllers = ControllersManager.getItems(); - $scope.loaded = false; - - // Updates the page title. - function updateTitle() { - $rootScope.title = $scope.fabric.name; - } + }; - // Called when the "edit" button is cliked in the fabric summary - $scope.enterEditSummary = function() { - $scope.editSummary = true; - }; - - // Called when the "cancel" button is cliked in the fabric summary - $scope.exitEditSummary = function() { - $scope.editSummary = false; - }; - - // Called when the fabric has been loaded. - function fabricLoaded(fabric) { - if (angular.isObject(fabric)) { - $scope.fabric = fabric; - updateTitle(); - $scope.$watch("vlans", updateVLANs, true); - $scope.$watch("subnets", updateVLANs, true); - $scope.$watch("controllers", updateVLANs, true); - $scope.loaded = true; - // Initial table sort order. - $scope.predicate = "['vlan_name', 'vlan.id', 'subnet_name']"; - } + // Called when the fabric has been loaded. + function fabricLoaded(fabric) { + if (angular.isObject(fabric)) { + $scope.fabric = fabric; + updateTitle(); + $scope.$watch("vlans", updateVLANs, true); + $scope.$watch("subnets", updateVLANs, true); + $scope.$watch("controllers", updateVLANs, true); + $scope.loaded = true; + // Initial table sort order. + $scope.predicate = "['vlan_name', 'vlan.id', 'subnet_name']"; } + } - // Generate a table that can easily be rendered in the view. - function updateVLANs() { - var rows = []; - var racks = {}; - angular.forEach($filter('filter')( - $scope.vlans, { fabric: $scope.fabric.id }, true), - function(vlan) { - var subnets = - $filter('filter')($scope.subnets, { vlan: vlan.id }, true); - if (subnets.length > 0) { - angular.forEach(subnets, function(subnet) { - var space = SpacesManager.getItemFromList(subnet.space); - var space_name = (space === null) ? - "(undefined)" : space.name; - var row = { - vlan: vlan, - vlan_name: VLANsManager.getName(vlan), - subnet: subnet, - subnet_name: SubnetsManager.getName(subnet), - space: space, - space_name: space_name - }; - rows.push(row); - }); - } else { - // If there are no subnets, populate a row based on the - // information we have (just the VLAN). - var row = { - vlan: vlan, - vlan_name: VLANsManager.getName(vlan), - subnet: null, - subnet_name: null, - space: null, - space_name: null - }; - rows.push(row); - } - // Enumerate racks for vlan. - angular.forEach(vlan.rack_sids, function(rack_sid) { - var rack = ControllersManager.getItemFromList(rack_sid); - if (angular.isObject(rack)) { - racks[rack.system_id] = rack; - } - }); - }); - $scope.rows = rows; - $scope.racks = Object.keys(racks).map(function(key) { - return racks[key]; + // Generate a table that can easily be rendered in the view. + function updateVLANs() { + var rows = []; + var racks = {}; + angular.forEach( + $filter("filter")($scope.vlans, { fabric: $scope.fabric.id }, true), + function(vlan) { + var subnets = $filter("filter")( + $scope.subnets, + { vlan: vlan.id }, + true + ); + if (subnets.length > 0) { + angular.forEach(subnets, function(subnet) { + var space = SpacesManager.getItemFromList(subnet.space); + var space_name = space === null ? "(undefined)" : space.name; + var row = { + vlan: vlan, + vlan_name: VLANsManager.getName(vlan), + subnet: subnet, + subnet_name: SubnetsManager.getName(subnet), + space: space, + space_name: space_name + }; + rows.push(row); + }); + } else { + // If there are no subnets, populate a row based on the + // information we have (just the VLAN). + var row = { + vlan: vlan, + vlan_name: VLANsManager.getName(vlan), + subnet: null, + subnet_name: null, + space: null, + space_name: null + }; + rows.push(row); + } + // Enumerate racks for vlan. + angular.forEach(vlan.rack_sids, function(rack_sid) { + var rack = ControllersManager.getItemFromList(rack_sid); + if (angular.isObject(rack)) { + racks[rack.system_id] = rack; + } }); + } + ); + $scope.rows = rows; + $scope.racks = Object.keys(racks).map(function(key) { + return racks[key]; + }); + } + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Return true if this is the default Fabric + $scope.isDefaultFabric = function() { + if (!angular.isObject($scope.fabric)) { + return false; } + return $scope.fabric.id === 0; + }; - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Return true if this is the default Fabric - $scope.isDefaultFabric = function() { - if (!angular.isObject($scope.fabric)) { - return false; - } - return $scope.fabric.id === 0; - }; - - // Called to check if the space can be deleted. - $scope.canBeDeleted = function() { - if (angular.isObject($scope.fabric)) { - return $scope.fabric.id !== 0; - } - return false; - }; - - // Called when the delete fabric button is pressed. - $scope.deleteButton = function() { - $scope.error = null; - $scope.confirmingDelete = true; - }; + // Called to check if the space can be deleted. + $scope.canBeDeleted = function() { + if (angular.isObject($scope.fabric)) { + return $scope.fabric.id !== 0; + } + return false; + }; - // Called when the cancel delete fabric button is pressed. - $scope.cancelDeleteButton = function() { + // Called when the delete fabric button is pressed. + $scope.deleteButton = function() { + $scope.error = null; + $scope.confirmingDelete = true; + }; + + // Called when the cancel delete fabric button is pressed. + $scope.cancelDeleteButton = function() { + $scope.confirmingDelete = false; + }; + + // Called when the confirm delete fabric button is pressed. + $scope.deleteConfirmButton = function() { + FabricsManager.deleteFabric($scope.fabric).then( + function() { $scope.confirmingDelete = false; - }; - - // Called when the confirm delete fabric button is pressed. - $scope.deleteConfirmButton = function() { - FabricsManager.deleteFabric($scope.fabric).then(function() { - $scope.confirmingDelete = false; - $location.path("/networks"); - $location.search('by', 'fabric'); - }, function(reply) { - $scope.error = - ManagerHelperService.parseValidationError(reply.error); - }); - }; - - // Load all the required managers. - ManagerHelperService.loadManagers($scope, [ - FabricsManager, VLANsManager, SubnetsManager, SpacesManager, - ControllersManager, UsersManager]).then( - function() { - // Possibly redirected from another controller that already had - // this fabric set to active. Only call setActiveItem if not - // already the activeItem. - var activeFabric = FabricsManager.getActiveItem(); - var requestedFabric = parseInt($routeParams.fabric_id, 10); - - if (isNaN(requestedFabric)) { - ErrorService.raiseError("Invalid fabric identifier."); - } else if (angular.isObject(activeFabric) && - activeFabric.id === requestedFabric) { - fabricLoaded(activeFabric); - } else { - FabricsManager.setActiveItem( - requestedFabric).then(function(fabric) { - fabricLoaded(fabric); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); + $location.path("/networks"); + $location.search("by", "fabric"); + }, + function(reply) { + $scope.error = ManagerHelperService.parseValidationError(reply.error); + } + ); + }; + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ + FabricsManager, + VLANsManager, + SubnetsManager, + SpacesManager, + ControllersManager, + UsersManager + ]).then(function() { + // Possibly redirected from another controller that already had + // this fabric set to active. Only call setActiveItem if not + // already the activeItem. + var activeFabric = FabricsManager.getActiveItem(); + var requestedFabric = parseInt($routeParams.fabric_id, 10); + + if (isNaN(requestedFabric)) { + ErrorService.raiseError("Invalid fabric identifier."); + } else if ( + angular.isObject(activeFabric) && + activeFabric.id === requestedFabric + ) { + fabricLoaded(activeFabric); + } else { + FabricsManager.setActiveItem(requestedFabric).then( + function(fabric) { + fabricLoaded(fabric); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } + }); } export default FabricDetailsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/images.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/images.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/images.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/images.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,37 +6,45 @@ /* @ngInject */ function ImagesController( - $rootScope, $scope, BootResourcesManager, - ConfigsManager, UsersManager, ManagerHelperService) { - $rootScope.page = "images"; - $rootScope.title = "Loading..."; - - $scope.loading = true; - $scope.bootResources = BootResourcesManager.getData(); - $scope.configManager = ConfigsManager; - $scope.autoImport = null; - - // Return true if the user is a super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Load the required managers. - ManagerHelperService.loadManagers( - $scope, [ConfigsManager, UsersManager]).then(function() { - $scope.autoImport = ConfigsManager.getItemFromList( - "boot_images_auto_import"); - }); - - // The boot-images directive will load the bootResources manager, - // we just watch until resources is set. That means the page is - // loaded. - $scope.$watch("bootResources.resources", function() { - if (angular.isArray($scope.bootResources.resources)) { - $scope.loading = false; - $rootScope.title = "Images"; - } - }); + $rootScope, + $scope, + BootResourcesManager, + ConfigsManager, + UsersManager, + ManagerHelperService +) { + $rootScope.page = "images"; + $rootScope.title = "Loading..."; + + $scope.loading = true; + $scope.bootResources = BootResourcesManager.getData(); + $scope.configManager = ConfigsManager; + $scope.autoImport = null; + + // Return true if the user is a super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Load the required managers. + ManagerHelperService.loadManagers($scope, [ + ConfigsManager, + UsersManager + ]).then(function() { + $scope.autoImport = ConfigsManager.getItemFromList( + "boot_images_auto_import" + ); + }); + + // The boot-images directive will load the bootResources manager, + // we just watch until resources is set. That means the page is + // loaded. + $scope.$watch("bootResources.resources", function() { + if (angular.isArray($scope.bootResources.resources)) { + $scope.loading = false; + $rootScope.title = "Images"; + } + }); } export default ImagesController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/intro.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/intro.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/intro.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/intro.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,123 +6,124 @@ /* @ngInject */ function IntroController( - $rootScope, $scope, $window, $location, - ConfigsManager, PackageRepositoriesManager, BootResourcesManager, - ManagerHelperService) { - - $rootScope.page = "intro"; - $rootScope.title = "Welcome"; - - $scope.loading = true; - $scope.configManager = ConfigsManager; - $scope.repoManager = PackageRepositoriesManager; - $scope.bootResources = BootResourcesManager.getData(); - $scope.hasImages = false; - $scope.maasName = null; - $scope.upstreamDNS = null; - $scope.mainArchive = null; - $scope.portsArchive = null; - $scope.httpProxy = null; - - // Set the skip function on the rootScope to allow skipping the - // intro view. - $rootScope.skip = function() { - $scope.clickContinue(true); - }; - - // Return true if the welcome section is not in error. - $scope.welcomeInError = function() { - var form = $scope.maasName.$maasForm; - if (angular.isObject(form)) { - return form.hasErrors(); - } else { - return false; - } - }; + $rootScope, + $scope, + $window, + $location, + ConfigsManager, + PackageRepositoriesManager, + BootResourcesManager, + ManagerHelperService +) { + $rootScope.page = "intro"; + $rootScope.title = "Welcome"; + + $scope.loading = true; + $scope.configManager = ConfigsManager; + $scope.repoManager = PackageRepositoriesManager; + $scope.bootResources = BootResourcesManager.getData(); + $scope.hasImages = false; + $scope.maasName = null; + $scope.upstreamDNS = null; + $scope.mainArchive = null; + $scope.portsArchive = null; + $scope.httpProxy = null; + + // Set the skip function on the rootScope to allow skipping the + // intro view. + $rootScope.skip = function() { + $scope.clickContinue(true); + }; + + // Return true if the welcome section is not in error. + $scope.welcomeInError = function() { + var form = $scope.maasName.$maasForm; + if (angular.isObject(form)) { + return form.hasErrors(); + } else { + return false; + } + }; + + // Return true if the network section is in error. + $scope.networkInError = function() { + var inError = false; + var objs = [ + $scope.upstreamDNS, + $scope.mainArchive, + $scope.portsArchive, + $scope.httpProxy + ]; + angular.forEach(objs, function(obj) { + var form = obj.$maasForm; + if (angular.isObject(form) && form.hasErrors()) { + inError = true; + } + }); + return inError; + }; + + // Return true if continue can be clicked. + $scope.canContinue = function() { + return ( + !$scope.welcomeInError() && !$scope.networkInError() && $scope.hasImages + ); + }; + + // Called when continue button is clicked. + $scope.clickContinue = function(force) { + if (angular.isUndefined(force)) { + force = false; + } + if (force || $scope.canContinue()) { + ConfigsManager.updateItem({ + name: "completed_intro", + value: true + }).then(function() { + // Reload the whole page so that the MAAS_config will be + // set to the new value. + $window.location.reload(); + }); + } + }; - // Return true if the network section is in error. - $scope.networkInError = function() { - var inError = false; - var objs = [ - $scope.upstreamDNS, - $scope.mainArchive, - $scope.portsArchive, - $scope.httpProxy]; - angular.forEach(objs, function(obj) { - var form = obj.$maasForm; - if (angular.isObject(form) && form.hasErrors()) { - inError = true; - } - }); - return inError; - }; - - // Return true if continue can be clicked. - $scope.canContinue = function() { - return ( - !$scope.welcomeInError() && - !$scope.networkInError() && - $scope.hasImages); - }; - - // Called when continue button is clicked. - $scope.clickContinue = function(force) { - if (angular.isUndefined(force)) { - force = false; + // If intro has been completed redirect to '/'. + if (MAAS_config.completed_intro) { + $location.path("/"); + } else { + // Load the required managers. + var managers = [ConfigsManager, PackageRepositoriesManager]; + ManagerHelperService.loadManagers($scope, managers).then(function() { + $scope.loading = false; + $scope.maasName = ConfigsManager.getItemFromList("maas_name"); + $scope.upstreamDNS = ConfigsManager.getItemFromList("upstream_dns"); + $scope.httpProxy = ConfigsManager.getItemFromList("http_proxy"); + $scope.mainArchive = PackageRepositoriesManager.getItems().filter( + function(repo) { + return repo["default"] && repo.name === "main_archive"; } - if (force || $scope.canContinue()) { - ConfigsManager.updateItem({ - 'name': 'completed_intro', - 'value': true - }).then(function() { - // Reload the whole page so that the MAAS_config will be - // set to the new value. - $window.location.reload(); - }); + )[0]; + $scope.portsArchive = PackageRepositoriesManager.getItems().filter( + function(repo) { + return repo["default"] && repo.name === "ports_archive"; } - }; + )[0]; + }); - // If intro has been completed redirect to '/'. - if (MAAS_config.completed_intro) { - $location.path('/'); - } else { - // Load the required managers. - var managers = [ConfigsManager, PackageRepositoriesManager]; - ManagerHelperService.loadManagers( - $scope, managers).then(function() { - $scope.loading = false; - $scope.maasName = ConfigsManager.getItemFromList( - "maas_name"); - $scope.upstreamDNS = ConfigsManager.getItemFromList( - "upstream_dns"); - $scope.httpProxy = ConfigsManager.getItemFromList( - "http_proxy"); - $scope.mainArchive = ( - PackageRepositoriesManager.getItems().filter( - function(repo) { - return (repo['default'] && - repo.name === "main_archive"); - })[0]); - $scope.portsArchive = ( - PackageRepositoriesManager.getItems().filter( - function(repo) { - return (repo['default'] && - repo.name === "ports_archive"); - })[0]); - }); - - // Don't load the boot resources as the boot-images directive - // performs that action. Just watch and make sure that - // at least one resource exists before continuing. - $scope.$watch("bootResources.resources", function() { - if (angular.isArray($scope.bootResources.resources) && - $scope.bootResources.resources.length > 0) { - $scope.hasImages = true; - } else { - $scope.hasImages = false; - } - }); - } + // Don't load the boot resources as the boot-images directive + // performs that action. Just watch and make sure that + // at least one resource exists before continuing. + $scope.$watch("bootResources.resources", function() { + if ( + angular.isArray($scope.bootResources.resources) && + $scope.bootResources.resources.length > 0 + ) { + $scope.hasImages = true; + } else { + $scope.hasImages = false; + } + }); + } } export default IntroController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/intro_user.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/intro_user.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/intro_user.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/intro_user.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,56 +6,59 @@ /* @ngInject */ function IntroUserController( - $rootScope, $scope, $window, $location, - UsersManager, ManagerHelperService) { - - $rootScope.page = "intro"; - $rootScope.title = "Welcome"; - - $scope.loading = true; - $scope.user = null; - - // Set the skip function on the rootScope to allow skipping the - // intro view. - $rootScope.skip = function() { - $scope.clickContinue(true); - }; - - // Return true if super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Return true if continue can be clicked. - $scope.canContinue = function() { - return $scope.user.sshkeys_count > 0; - }; - - // Called when continue button is clicked. - $scope.clickContinue = function(force) { - if (angular.isUndefined(force)) { - force = false; - } - if (force || $scope.canContinue()) { - UsersManager.markIntroComplete().then(function() { - // Reload the whole page so that the MAAS_config will - // be set to the new value. - $window.location.reload(); - }); - } - }; - - // If intro has been completed redirect to '/'. - if (MAAS_config.user_completed_intro) { - $location.path('/'); - } else { - // Load the required managers. - ManagerHelperService.loadManager( - $scope, UsersManager).then(function() { - $scope.loading = false; - $scope.user = UsersManager.getAuthUser(); - }); + $rootScope, + $scope, + $window, + $location, + UsersManager, + ManagerHelperService +) { + $rootScope.page = "intro"; + $rootScope.title = "Welcome"; + + $scope.loading = true; + $scope.user = null; + + // Set the skip function on the rootScope to allow skipping the + // intro view. + $rootScope.skip = function() { + $scope.clickContinue(true); + }; + + // Return true if super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Return true if continue can be clicked. + $scope.canContinue = function() { + return $scope.user.sshkeys_count > 0; + }; + + // Called when continue button is clicked. + $scope.clickContinue = function(force) { + if (angular.isUndefined(force)) { + force = false; } + if (force || $scope.canContinue()) { + UsersManager.markIntroComplete().then(function() { + // Reload the whole page so that the MAAS_config will + // be set to the new value. + $window.location.reload(); + }); + } + }; + + // If intro has been completed redirect to '/'. + if (MAAS_config.user_completed_intro) { + $location.path("/"); + } else { + // Load the required managers. + ManagerHelperService.loadManager($scope, UsersManager).then(function() { + $scope.loading = false; + $scope.user = UsersManager.getAuthUser(); + }); + } } export default IntroUserController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/networks_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/networks_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/networks_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/networks_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,305 +5,317 @@ */ /* @ngInject */ -function NetworksListController($scope, $rootScope, $filter, $location, - SubnetsManager, FabricsManager, SpacesManager, VLANsManager, - UsersManager, ManagerHelperService) { - - // Load the filters that are used inside the controller. - var filterByVLAN = $filter('filterByVLAN'); - var filterByFabric = $filter('filterByFabric'); - var filterBySpace = $filter('filterBySpace'); - var filterByNullSpace = $filter('filterByNullSpace'); - - // Set title and page. - $rootScope.title = "Subnets"; - $rootScope.page = "networks"; - - // Set the initial value of $scope.groupBy based on the URL - // parameters, but default to the 'fabric' groupBy if it's not found. - $scope.getURLParameters = function() { - if (angular.isString($location.search().by)) { - $scope.groupBy = $location.search().by; - } else { - $scope.groupBy = 'fabric'; - } - }; - - $scope.ADD_FABRIC_ACTION = { - name: "add_fabric", - title: "Fabric", - selectedTitle: "Add fabric", - objectName: 'fabric' - }; - $scope.ADD_VLAN_ACTION = { - name: "add_vlan", - title: "VLAN", - selectedTitle: "Add VLAN", - objectName: 'vlan' - }; - $scope.ADD_SPACE_ACTION = { - name: "add_space", - title: "Space", - selectedTitle: "Add space", - objectName: 'space' - }; - $scope.ADD_SUBNET_ACTION = { - name: "add_subnet", - title: "Subnet", - selectedTitle: "Add subnet", - objectName: 'subnet' - }; - - $scope.getURLParameters(); - - // Set initial values. - $scope.subnetManager = SubnetsManager; - $scope.subnets = SubnetsManager.getItems(); - $scope.fabricManager = FabricsManager; - $scope.fabrics = FabricsManager.getItems(); - $scope.spaceManager = SpacesManager; - $scope.spaces = SpacesManager.getItems(); - $scope.vlanManager = VLANsManager; - $scope.vlans = VLANsManager.getItems(); - $scope.loading = true; - - $scope.group = {}; - // Used when grouping by fabrics. - $scope.group.fabrics = {}; - // User when grouping by spaces. - $scope.group.spaces = {}; - $scope.group.spaces.orphanVLANs = []; - - // Initializers for action objects. - var actionObjectInitializers = { - fabric: function() { - return {}; - }, - vlan: function() { - // Set initial fabric. - return { - fabric: $scope.fabrics[0].id - }; - }, - space: function() { - return {}; - }, - subnet: function() { - // Set initial VLAN and space. - return { - vlan: $scope.fabrics[0].vlan_ids[0] - }; - } - }; - - // Return the name of the subnet. - function getSubnetName(subnet) { - return SubnetsManager.getName(subnet); - } - - // Generate a table that can be easily rendered in the view. - // Traverses the fabrics and VLANs in-order so that if previous - // fabrics and VLANs' names are identical, they can be hidden from - // the table cell. - function updateFabricsGroupBy() { - var rows = []; - var previous_fabric = { id: -1 }; - var previous_vlan = { id: -1 }; - var fabrics = $filter('orderBy')($scope.fabrics, ['name']); - angular.forEach(fabrics, function(fabric) { - var vlans = filterByFabric($scope.vlans, fabric); - vlans = $filter('orderBy')(vlans, ['vid']); - angular.forEach(vlans, function(vlan) { - var subnets = filterByVLAN($scope.subnets, vlan); - if (subnets.length > 0) { - angular.forEach(subnets, function(subnet) { - var space = SpacesManager.getItemFromList( - subnet.space); - var row = { - fabric: fabric, - fabric_name: "", - vlan: vlan, - vlan_name: "", - space: space, - subnet: subnet, - subnet_name: getSubnetName(subnet) - }; - if (fabric.id !== previous_fabric.id) { - previous_fabric.id = fabric.id; - row.fabric_name = fabric.name; - } - if (vlan.id !== previous_vlan.id) { - previous_vlan.id = vlan.id; - row.vlan_name = $scope.getVLANName(vlan); - } - rows.push(row); - }); - } else { - var row = { - fabric: fabric, - fabric_name: "", - vlan: vlan, - vlan_name: $scope.getVLANName(vlan) - }; - if (fabric.id !== previous_fabric.id) { - previous_fabric.id = fabric.id; - row.fabric_name = fabric.name; - } - rows.push(row); - } - }); - }); - $scope.group.fabrics.rows = rows; +function NetworksListController( + $scope, + $rootScope, + $filter, + $location, + SubnetsManager, + FabricsManager, + SpacesManager, + VLANsManager, + UsersManager, + ManagerHelperService +) { + // Load the filters that are used inside the controller. + var filterByVLAN = $filter("filterByVLAN"); + var filterByFabric = $filter("filterByFabric"); + var filterBySpace = $filter("filterBySpace"); + var filterByNullSpace = $filter("filterByNullSpace"); + + // Set title and page. + $rootScope.title = "Subnets"; + $rootScope.page = "networks"; + + // Set the initial value of $scope.groupBy based on the URL + // parameters, but default to the 'fabric' groupBy if it's not found. + $scope.getURLParameters = function() { + if (angular.isString($location.search().by)) { + $scope.groupBy = $location.search().by; + } else { + $scope.groupBy = "fabric"; } + }; - // Generate a table that can be easily rendered in the view. - // Traverses the spaces in-order so that if the previous space's name - // is identical, it can be hidden from the table cell. - // Note that this view only shows items that can be related to a space. - // That is, VLANs and fabrics with no corresponding subnets (and - // therefore spaces) cannot be shown in this table. - function updateSpacesGroupBy() { - var rows = []; - var spaces = $filter('orderBy')($scope.spaces, ['name']); - var previous_space = { id: -1 }; - angular.forEach(spaces, function(space) { - var subnets = filterBySpace($scope.subnets, space); - subnets = $filter('orderBy')(subnets, ['cidr']); - var index = 0; - angular.forEach(subnets, function(subnet) { - index++; - var vlan = VLANsManager.getItemFromList(subnet.vlan); - var fabric = FabricsManager.getItemFromList(vlan.fabric); - var row = { - fabric: fabric, - vlan: vlan, - vlan_name: $scope.getVLANName(vlan), - subnet: subnet, - subnet_name: getSubnetName(subnet), - space: space, - space_name: "" - }; - if (space.id !== previous_space.id) { - previous_space.id = space.id; - row.space_name = space.name; - } - rows.push(row); - }); - if (index === 0) { - rows.push({ - space: space, - space_name: space.name - }); - } - }); - $scope.group.spaces.rows = rows; + $scope.ADD_FABRIC_ACTION = { + name: "add_fabric", + title: "Fabric", + selectedTitle: "Add fabric", + objectName: "fabric" + }; + $scope.ADD_VLAN_ACTION = { + name: "add_vlan", + title: "VLAN", + selectedTitle: "Add VLAN", + objectName: "vlan" + }; + $scope.ADD_SPACE_ACTION = { + name: "add_space", + title: "Space", + selectedTitle: "Add space", + objectName: "space" + }; + $scope.ADD_SUBNET_ACTION = { + name: "add_subnet", + title: "Subnet", + selectedTitle: "Add subnet", + objectName: "subnet" + }; + + $scope.getURLParameters(); + + // Set initial values. + $scope.subnetManager = SubnetsManager; + $scope.subnets = SubnetsManager.getItems(); + $scope.fabricManager = FabricsManager; + $scope.fabrics = FabricsManager.getItems(); + $scope.spaceManager = SpacesManager; + $scope.spaces = SpacesManager.getItems(); + $scope.vlanManager = VLANsManager; + $scope.vlans = VLANsManager.getItems(); + $scope.loading = true; + + $scope.group = {}; + // Used when grouping by fabrics. + $scope.group.fabrics = {}; + // User when grouping by spaces. + $scope.group.spaces = {}; + $scope.group.spaces.orphanVLANs = []; + + // Initializers for action objects. + var actionObjectInitializers = { + fabric: function() { + return {}; + }, + vlan: function() { + // Set initial fabric. + return { + fabric: $scope.fabrics[0].id + }; + }, + space: function() { + return {}; + }, + subnet: function() { + // Set initial VLAN and space. + return { + vlan: $scope.fabrics[0].vlan_ids[0] + }; } + }; - function updateOrphanVLANs() { - var rows = []; - var subnets = filterByNullSpace($scope.subnets); - subnets = $filter('orderBy')(subnets, ['cidr']); - angular.forEach(subnets, function(subnet) { - var vlan = VLANsManager.getItemFromList(subnet.vlan); - var fabric = FabricsManager.getItemFromList(vlan.fabric); + // Return the name of the subnet. + function getSubnetName(subnet) { + return SubnetsManager.getName(subnet); + } + + // Generate a table that can be easily rendered in the view. + // Traverses the fabrics and VLANs in-order so that if previous + // fabrics and VLANs' names are identical, they can be hidden from + // the table cell. + function updateFabricsGroupBy() { + var rows = []; + var previous_fabric = { id: -1 }; + var previous_vlan = { id: -1 }; + var fabrics = $filter("orderBy")($scope.fabrics, ["name"]); + angular.forEach(fabrics, function(fabric) { + var vlans = filterByFabric($scope.vlans, fabric); + vlans = $filter("orderBy")(vlans, ["vid"]); + angular.forEach(vlans, function(vlan) { + var subnets = filterByVLAN($scope.subnets, vlan); + if (subnets.length > 0) { + angular.forEach(subnets, function(subnet) { + var space = SpacesManager.getItemFromList(subnet.space); var row = { - fabric: fabric, - vlan: vlan, - vlan_name: $scope.getVLANName(vlan), - subnet: subnet, - subnet_name: getSubnetName(subnet), - space: null + fabric: fabric, + fabric_name: "", + vlan: vlan, + vlan_name: "", + space: space, + subnet: subnet, + subnet_name: getSubnetName(subnet) }; + if (fabric.id !== previous_fabric.id) { + previous_fabric.id = fabric.id; + row.fabric_name = fabric.name; + } + if (vlan.id !== previous_vlan.id) { + previous_vlan.id = vlan.id; + row.vlan_name = $scope.getVLANName(vlan); + } rows.push(row); - }); - $scope.group.spaces.orphanVLANs = rows; - } - - // Update the "Group by" selection. This is called from a few places: - // * When the $watch notices data has changed - // * When the URL bar is updated, after the URL is parsed and - // $scope.groupBy is updated - // * When the drop-down "Group by" selection box changes - $scope.updateGroupBy = function() { - var groupBy = $scope.groupBy; - if (groupBy === 'space') { - $location.search('by', 'space'); - updateSpacesGroupBy(); - updateOrphanVLANs(); + }); } else { - // The only other option is 'fabric', but in case the user - // made a typo on the URL bar we just assume it was 'fabric' - // as a fallback. - $location.search('by', 'fabric'); - updateFabricsGroupBy(); + var row = { + fabric: fabric, + fabric_name: "", + vlan: vlan, + vlan_name: $scope.getVLANName(vlan) + }; + if (fabric.id !== previous_fabric.id) { + previous_fabric.id = fabric.id; + row.fabric_name = fabric.name; + } + rows.push(row); } - }; - - // Called when the managers load to populate the actions the user - // is allowed to perform. - $scope.updateActions = function() { - if (UsersManager.isSuperUser()) { - $scope.actionOptions = [ - $scope.ADD_FABRIC_ACTION, - $scope.ADD_VLAN_ACTION, - $scope.ADD_SPACE_ACTION, - $scope.ADD_SUBNET_ACTION - ]; - } else { - $scope.actionOptions = []; + }); + }); + $scope.group.fabrics.rows = rows; + } + + // Generate a table that can be easily rendered in the view. + // Traverses the spaces in-order so that if the previous space's name + // is identical, it can be hidden from the table cell. + // Note that this view only shows items that can be related to a space. + // That is, VLANs and fabrics with no corresponding subnets (and + // therefore spaces) cannot be shown in this table. + function updateSpacesGroupBy() { + var rows = []; + var spaces = $filter("orderBy")($scope.spaces, ["name"]); + var previous_space = { id: -1 }; + angular.forEach(spaces, function(space) { + var subnets = filterBySpace($scope.subnets, space); + subnets = $filter("orderBy")(subnets, ["cidr"]); + var index = 0; + angular.forEach(subnets, function(subnet) { + index++; + var vlan = VLANsManager.getItemFromList(subnet.vlan); + var fabric = FabricsManager.getItemFromList(vlan.fabric); + var row = { + fabric: fabric, + vlan: vlan, + vlan_name: $scope.getVLANName(vlan), + subnet: subnet, + subnet_name: getSubnetName(subnet), + space: space, + space_name: "" + }; + if (space.id !== previous_space.id) { + previous_space.id = space.id; + row.space_name = space.name; } - }; + rows.push(row); + }); + if (index === 0) { + rows.push({ + space: space, + space_name: space.name + }); + } + }); + $scope.group.spaces.rows = rows; + } + + function updateOrphanVLANs() { + var rows = []; + var subnets = filterByNullSpace($scope.subnets); + subnets = $filter("orderBy")(subnets, ["cidr"]); + angular.forEach(subnets, function(subnet) { + var vlan = VLANsManager.getItemFromList(subnet.vlan); + var fabric = FabricsManager.getItemFromList(vlan.fabric); + var row = { + fabric: fabric, + vlan: vlan, + vlan_name: $scope.getVLANName(vlan), + subnet: subnet, + subnet_name: getSubnetName(subnet), + space: null + }; + rows.push(row); + }); + $scope.group.spaces.orphanVLANs = rows; + } + + // Update the "Group by" selection. This is called from a few places: + // * When the $watch notices data has changed + // * When the URL bar is updated, after the URL is parsed and + // $scope.groupBy is updated + // * When the drop-down "Group by" selection box changes + $scope.updateGroupBy = function() { + var groupBy = $scope.groupBy; + if (groupBy === "space") { + $location.search("by", "space"); + updateSpacesGroupBy(); + updateOrphanVLANs(); + } else { + // The only other option is 'fabric', but in case the user + // made a typo on the URL bar we just assume it was 'fabric' + // as a fallback. + $location.search("by", "fabric"); + updateFabricsGroupBy(); + } + }; + + // Called when the managers load to populate the actions the user + // is allowed to perform. + $scope.updateActions = function() { + if (UsersManager.isSuperUser()) { + $scope.actionOptions = [ + $scope.ADD_FABRIC_ACTION, + $scope.ADD_VLAN_ACTION, + $scope.ADD_SPACE_ACTION, + $scope.ADD_SUBNET_ACTION + ]; + } else { + $scope.actionOptions = []; + } + }; - // Called when a action is selected. - $scope.actionChanged = function() { - $scope.newObject = ( - actionObjectInitializers[$scope.actionOption.objectName]()); - }; - - // Called when the "Cancel" button is pressed. - $scope.cancelAction = function() { - $scope.actionOption = null; - $scope.newObject = null; - }; - - // Return the name name for the VLAN. - $scope.getVLANName = function(vlan) { - return VLANsManager.getName(vlan); - }; - - // Return the name of the fabric from its given ID. - $scope.getFabricNameById = function(fabricId) { - return FabricsManager.getName( - FabricsManager.getItemFromList(fabricId)); - }; - - // Called before the subnet object is saved. Sets the fabric - // field to be the fabric for the selected VLAN. - $scope.actionSubnetPreSave = function(obj) { - obj.fabric = VLANsManager.getItemFromList(obj.vlan).fabric; - return obj; - }; - - ManagerHelperService.loadManagers($scope, [ - SubnetsManager, FabricsManager, SpacesManager, VLANsManager, - UsersManager]).then( - function() { - $scope.loading = false; - - $scope.updateActions(); - - $scope.$watch( - "[subnets, fabrics, spaces, vlans]", - $scope.updateGroupBy, true); - - // If the route has been updated, a new search string must - // potentially be rendered. - $scope.$on("$routeUpdate", function() { - $scope.getURLParameters(); - $scope.updateGroupBy(); - }); - $scope.updateGroupBy(); - }); + // Called when a action is selected. + $scope.actionChanged = function() { + $scope.newObject = actionObjectInitializers[ + $scope.actionOption.objectName + ](); + }; + + // Called when the "Cancel" button is pressed. + $scope.cancelAction = function() { + $scope.actionOption = null; + $scope.newObject = null; + }; + + // Return the name name for the VLAN. + $scope.getVLANName = function(vlan) { + return VLANsManager.getName(vlan); + }; + + // Return the name of the fabric from its given ID. + $scope.getFabricNameById = function(fabricId) { + return FabricsManager.getName(FabricsManager.getItemFromList(fabricId)); + }; + + // Called before the subnet object is saved. Sets the fabric + // field to be the fabric for the selected VLAN. + $scope.actionSubnetPreSave = function(obj) { + obj.fabric = VLANsManager.getItemFromList(obj.vlan).fabric; + return obj; + }; + + ManagerHelperService.loadManagers($scope, [ + SubnetsManager, + FabricsManager, + SpacesManager, + VLANsManager, + UsersManager + ]).then(function() { + $scope.loading = false; + + $scope.updateActions(); + + $scope.$watch( + "[subnets, fabrics, spaces, vlans]", + $scope.updateGroupBy, + true + ); + + // If the route has been updated, a new search string must + // potentially be rendered. + $scope.$on("$routeUpdate", function() { + $scope.getURLParameters(); + $scope.updateGroupBy(); + }); + $scope.updateGroupBy(); + }); } export default NetworksListController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,1212 +4,1370 @@ * MAAS Node Details Controller */ +import { NodeTypes } from "../enum"; + /* @ngInject */ function NodeDetailsController( - $scope, $rootScope, $routeParams, $location, DevicesManager, - MachinesManager, ControllersManager, ZonesManager, GeneralManager, - UsersManager, TagsManager, DomainsManager, ManagerHelperService, - ServicesManager, ErrorService, ValidationService, ScriptsManager, - ResourcePoolsManager, VLANsManager) { - - // Mapping of device.ip_assignment to viewable text. - var DEVICE_IP_ASSIGNMENT = { - external: "External", - dynamic: "Dynamic", - "static": "Static" - }; - - // Set title and page. - $rootScope.title = "Loading..."; - - // Initial values. - $scope.MAAS_config = MAAS_config; - $scope.loaded = false; - $scope.node = null; - $scope.action = { - option: null, - allOptions: null, - availableOptions: [], - error: null, - showing_confirmation: false, - confirmation_message: '', - confirmation_details: [] - }; - $scope.power_types = GeneralManager.getData("power_types"); - $scope.osinfo = GeneralManager.getData("osinfo"); - $scope.section = { - area: angular.isString( - $routeParams.area) ? $routeParams.area : "summary" - }; - $scope.osSelection = { - osystem: null, - release: null, - hwe_kernel: null - }; - $scope.commissionOptions = { - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }; - $scope.deployOptions = { - installKVM: false, - }; - $scope.commissioningSelection = []; - $scope.testSelection = []; - $scope.releaseOptions = {}; - $scope.checkingPower = false; - $scope.devices = []; - $scope.scripts = ScriptsManager.getItems(); - $scope.vlans = VLANsManager.getItems(); - - // Node header section. - $scope.header = { - editing: false, - editing_domain: false, - hostname: { - value: "" - }, - domain: { - selected: null, - options: DomainsManager.getItems() - } - }; - - // Summary section. - $scope.summary = { - editing: false, - architecture: { - selected: null, - options: GeneralManager.getData("architectures") - }, - min_hwe_kernel: { - selected: null, - options: GeneralManager.getData("min_hwe_kernels") - }, - zone: { - selected: null, - options: ZonesManager.getItems() - }, - pool: { - selected: null, - options: ResourcePoolsManager.getItems() - }, - tags: [] - }; - - // Service monitor section (for controllers). - $scope.services = {}; - - // Power section. - $scope.power = { - editing: false, - type: null, - bmc_node_count: 0, - parameters: {}, - in_pod: false - }; - - // Get the display text for device ip assignment type. - $scope.getDeviceIPAssignment = function(ipAssignment) { - return DEVICE_IP_ASSIGNMENT[ipAssignment]; - }; - - // Events section. - $scope.events = { - limit: 10 - }; - - // Updates the page title. - function updateTitle() { - if ($scope.node && $scope.node.fqdn) { - $rootScope.title = $scope.node.fqdn; - } + $scope, + $rootScope, + $routeParams, + $location, + DevicesManager, + MachinesManager, + ControllersManager, + ZonesManager, + GeneralManager, + UsersManager, + TagsManager, + DomainsManager, + ManagerHelperService, + ServicesManager, + ErrorService, + ValidationService, + ScriptsManager, + ResourcePoolsManager, + VLANsManager, + $log, + $window +) { + // Mapping of device.ip_assignment to viewable text. + var DEVICE_IP_ASSIGNMENT = { + external: "External", + dynamic: "Dynamic", + static: "Static" + }; + + // Set title and page. + $rootScope.title = "Loading..."; + + // Initial values. + $scope.MAAS_config = $window.MAAS_config; + $scope.loaded = false; + $scope.node = null; + $scope.action = { + option: null, + allOptions: null, + availableOptions: [], + error: null, + showing_confirmation: false, + confirmation_message: "", + confirmation_details: [] + }; + $scope.power_types = GeneralManager.getData("power_types"); + $scope.osinfo = GeneralManager.getData("osinfo"); + $scope.section = { + area: angular.isString($routeParams.area) ? $routeParams.area : "summary" + }; + $scope.osSelection = { + osystem: null, + release: null, + hwe_kernel: null + }; + $scope.commissionOptions = { + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }; + $scope.deployOptions = { + installKVM: false + }; + $scope.commissioningSelection = []; + $scope.testSelection = []; + $scope.releaseOptions = {}; + $scope.checkingPower = false; + $scope.devices = []; + $scope.scripts = ScriptsManager.getItems(); + $scope.vlans = VLANsManager.getItems(); + $scope.hideHighAvailabilityNotification = false; + $scope.failedUpdateError = ""; + + // Node header section. + $scope.header = { + editing: false, + editing_domain: false, + hostname: { + value: "" + }, + domain: { + selected: null, + options: DomainsManager.getItems() } + }; - function updateHeader() { - // Don't update the value if in editing mode. As this would - // overwrite the users changes. - if ($scope.header.editing || $scope.header.editing_domain) { - return; - } - $scope.header.hostname.value = $scope.node.fqdn; - // DomainsManager gives us all Domain information while the node - // only contains the name and id. Because of this we need to loop - // through the DomainsManager options and find the option with the - // id matching the node id. Otherwise we end up setting our - // selected field to an option not from DomainsManager which - // doesn't work. - for (let i = 0; i < $scope.header.domain.options.length; i++) { - let option = $scope.header.domain.options[i]; - if (option.id === $scope.node.domain.id) { - $scope.header.domain.selected = option; - break; - } - } + // Summary section. + $scope.summary = { + editing: false, + architecture: { + selected: null, + options: GeneralManager.getData("architectures") + }, + min_hwe_kernel: { + selected: null, + options: GeneralManager.getData("min_hwe_kernels") + }, + zone: { + selected: null, + options: ZonesManager.getItems() + }, + pool: { + selected: null, + options: ResourcePoolsManager.getItems() + }, + tags: [] + }; + + // Service monitor section (for controllers). + $scope.services = {}; + + // Power section. + $scope.power = { + editing: false, + type: null, + bmc_node_count: 0, + parameters: {}, + in_pod: false + }; + + // Dismiss high availability notification + $scope.dismissHighAvailabilityNotification = function() { + $scope.hideHighAvailabilityNotification = true; + localStorage.setItem( + `hideHighAvailabilityNotification-${$scope.vlan.id}`, + true + ); + }; + + $scope.getNotificationVLANText = function() { + if ($scope.node.vlan.name) { + return $scope.node.vlan.name; + } else { + return $scope.node.vlan.id; } + }; - // Update the available action options for the node. - function updateAvailableActionOptions() { - if (!angular.isObject($scope.node)) { - return; - } - const actionTypeForNodeType = { - 0: "machine_actions", - 1: "device_actions", - 2: "rack_controller_actions", - 3: "region_controller_actions", - 4: "region_and_rack_controller_actions" - }; - if (GeneralManager.isDataLoaded( - actionTypeForNodeType[$scope.node.node_type])) { - // Build the available action options control from the - // allowed actions, except set-zone which does not make - // sense in this view because the form has this - // functionality - $scope.action.allOptions = GeneralManager.getData( - actionTypeForNodeType[$scope.node.node_type]); - $scope.action.availableOptions.length = 0; - angular.forEach($scope.action.allOptions, function(option) { - if ($scope.node.actions.indexOf(option.name) >= 0 - && option.name !== "set-zone" - && option.name !== "set-pool" - && option.name !== "tag") { - $scope.action.availableOptions.push(option); - } - }); - } else { - // The GeneralManager only loads data requested on load. This - // isn't called until after load as its triggered on the loaded - // node's actions. If the node's action list isn't loaded load - // it then update the available options. - GeneralManager.loadItems( - [actionTypeForNodeType[$scope.node.node_type]]).then( - updateAvailableActionOptions); - } + $scope.showHighAvailabilityNotification = function() { + if ( + !$scope.hideHighAvailabilityNotification && + $scope.node.dhcp_on && + $scope.vlan && + $scope.vlan.rack_sids.length > 1 && + !$scope.vlan.secondary_rack && + $scope.node.node_type !== NodeTypes.REGION_CONTROLLER + ) { + if ( + $scope.section.area === "summary" || + $scope.section.area === "vlans" + ) { + return true; + } } - // Updates the currently selected items in the power section. - function updatePower() { - // Do not update the selected items, when editing this would - // cause the users selection to change. - if ($scope.power.editing) { - return; - } + return false; + }; - $scope.power.type = null; - for (let i = 0; i < $scope.power_types.length; i++) { - if ($scope.node.power_type === $scope.power_types[i].name) { - $scope.power.type = $scope.power_types[i]; - break; - } - } - - $scope.power.bmc_node_count = $scope.node.power_bmc_node_count; - - $scope.power.parameters = angular.copy( - $scope.node.power_parameters); - if (!angular.isObject($scope.power.parameters)) { - $scope.power.parameters = {}; - } - - // Force editing mode on, if the power_type is missing for a - // machine. This is placed at the bottom because we wanted the - // selected items to be filled in at least once. - if ($scope.canEdit() && $scope.node.power_type === "" && - $scope.node.node_type === 0) { - $scope.power.editing = true; - } - - $scope.power.in_pod = angular.isDefined($scope.node.pod); + // Get the display text for device ip assignment type. + $scope.getDeviceIPAssignment = function(ipAssignment) { + return DEVICE_IP_ASSIGNMENT[ipAssignment]; + }; + + // Events section. + $scope.events = { + limit: 10 + }; + + // Add parameters to URL so tab state persists + $scope.openSection = function(sectionName) { + $scope.section.area = sectionName; + $location.search("area", sectionName); + }; + + // Updates the page title. + function updateTitle() { + if ($scope.node && $scope.node.fqdn) { + $rootScope.title = $scope.node.fqdn; } + } - // Updates the currently selected items in the summary section. - function updateSummary() { - // Do not update the selected items, when editing this would - // cause the users selection to change. - if ($scope.summary.editing) { - return; - } - - if (angular.isObject($scope.node.zone)) { - $scope.summary.zone.selected = ZonesManager.getItemFromList( - $scope.node.zone.id); - } - if (angular.isObject($scope.node.pool)) { - $scope.summary.pool.selected = ( - ResourcePoolsManager.getItemFromList($scope.node.pool.id)); - } - $scope.summary.architecture.selected = $scope.node.architecture; - $scope.summary.description = $scope.node.description; - $scope.summary.min_hwe_kernel.selected = $scope.node.min_hwe_kernel; - $scope.summary.tags = angular.copy($scope.node.tags); - - // Force editing mode on, if the architecture is invalid. This is - // placed at the bottom because we wanted the selected items to - // be filled in at least once. - if ($scope.canEdit() && - $scope.hasUsableArchitectures() && - $scope.hasInvalidArchitecture()) { - $scope.summary.editing = true; - } + function updateHeader() { + // Don't update the value if in editing mode. As this would + // overwrite the users changes. + if ($scope.header.editing || $scope.header.editing_domain) { + return; + } + $scope.header.hostname.value = $scope.node.fqdn; + // DomainsManager gives us all Domain information while the node + // only contains the name and id. Because of this we need to loop + // through the DomainsManager options and find the option with the + // id matching the node id. Otherwise we end up setting our + // selected field to an option not from DomainsManager which + // doesn't work. + for (let i = 0; i < $scope.header.domain.options.length; i++) { + let option = $scope.header.domain.options[i]; + if (option.id === $scope.node.domain.id) { + $scope.header.domain.selected = option; + break; + } } + } - // Updates the service monitor section. - function updateServices() { - if ($scope.isController) { - $scope.services = {}; - angular.forEach(ControllersManager.getServices( - $scope.node), function(service) { - $scope.services[service.name] = service; - }); + // Update the available action options for the node. + function updateAvailableActionOptions() { + if (!angular.isObject($scope.node)) { + return; + } + const actionTypeForNodeType = { + 0: "machine_actions", + 1: "device_actions", + 2: "rack_controller_actions", + 3: "region_controller_actions", + 4: "region_and_rack_controller_actions" + }; + if ( + GeneralManager.isDataLoaded(actionTypeForNodeType[$scope.node.node_type]) + ) { + // Build the available action options control from the + // allowed actions, except set-zone which does not make + // sense in this view because the form has this + // functionality + $scope.action.allOptions = GeneralManager.getData( + actionTypeForNodeType[$scope.node.node_type] + ); + $scope.action.availableOptions.length = 0; + angular.forEach($scope.action.allOptions, function(option) { + if ( + $scope.node.actions.indexOf(option.name) >= 0 && + option.name !== "set-zone" && + option.name !== "set-pool" && + option.name !== "tag" + ) { + $scope.action.availableOptions.push(option); } + }); + } else { + // The GeneralManager only loads data requested on load. This + // isn't called until after load as its triggered on the loaded + // node's actions. If the node's action list isn't loaded load + // it then update the available options. + GeneralManager.loadItems([ + actionTypeForNodeType[$scope.node.node_type] + ]).then(updateAvailableActionOptions); } + } - // Update the devices array on the scope based on the device children - // on the node. - function updateDevices() { - $scope.devices = []; - angular.forEach($scope.node.devices, function(child) { - var device = { - name: child.fqdn - }; - - // Add the interfaces to the device object if any exists. - if (angular.isArray(child.interfaces) && - child.interfaces.length > 0) { - angular.forEach(child.interfaces, function(nic, nicIdx) { - var deviceWithMAC = angular.copy(device); - deviceWithMAC.mac_address = nic.mac_address; - - // Remove device name so it is not duplicated in the - // table since this is another MAC address on this - // device. - if (nicIdx > 0) { - deviceWithMAC.name = ""; - } - - // Add this links to the device object if any exists. - if (angular.isArray(nic.links) && - nic.links.length > 0) { - angular.forEach(nic.links, function(link, lIdx) { - var deviceWithLink = angular.copy( - deviceWithMAC); - deviceWithLink.ip_address = link.ip_address; - - // Remove the MAC address so it is not - // duplicated in the table since this is - // another link on this interface. - if (lIdx > 0) { - deviceWithLink.mac_address = ""; - } - - $scope.devices.push(deviceWithLink); - }); - } else { - $scope.devices.push(deviceWithMAC); - } - }); - } else { - $scope.devices.push(device); - } - }); + // Updates the currently selected items in the power section. + function updatePower() { + // Do not update the selected items, when editing this would + // cause the users selection to change. + if ($scope.power.editing) { + return; } - // Starts the watchers on the scope. - function startWatching() { - // Update the title and name when the node fqdn changes. - $scope.$watch("node.fqdn", function() { - updateTitle(); - updateHeader(); - }); - - // Update the devices on the node. - $scope.$watch("node.devices", updateDevices); - - // Update the availableActionOptions when the node actions change. - $scope.$watch("node.actions", updateAvailableActionOptions); - - // Update the summary when the node or architectures list is - // updated. - $scope.$watch("node.architecture", updateSummary); - $scope.$watchCollection( - $scope.summary.architecture.options, updateSummary); - - // Uppdate the summary when min_hwe_kernel is updated. - $scope.$watch("node.min_hwe_kernel", updateSummary); - $scope.$watchCollection( - $scope.summary.min_hwe_kernel.options, updateSummary); - - // Update the summary when the node or zone list is - // updated. - $scope.$watch("node.zone.id", updateSummary); - $scope.$watchCollection( - $scope.summary.zone.options, updateSummary); - - // Update the summary when the node or the resouce pool list is - // updated. - $scope.$watch("node.pool.id", updateSummary); - $scope.$watchCollection( - $scope.summary.pool.options, updateSummary); - - // Update the power when the node power_type or power_parameters - // are updated. - $scope.$watch("node.power_type", updatePower); - $scope.$watch("node.power_parameters", updatePower); - $scope.$watchCollection("power_types", updatePower); - - // Update the services when the services list is updated. - $scope.$watch("node.service_ids", updateServices); - } - - // Called when the node has been loaded. - function nodeLoaded(node) { - $scope.node = node; - $scope.loaded = true; + $scope.power.type = null; + for (let i = 0; i < $scope.power_types.length; i++) { + if ($scope.node.power_type === $scope.power_types[i].name) { + $scope.power.type = $scope.power_types[i]; + break; + } + } - updateTitle(); - updateSummary(); - updateServices(); - startWatching(); + $scope.power.bmc_node_count = $scope.node.power_bmc_node_count; - // Tell the storageController and networkingController that the - // node has been loaded. - if (angular.isObject($scope.storageController)) { - $scope.storageController.nodeLoaded(); - } - if (angular.isObject($scope.networkingController)) { - $scope.networkingController.nodeLoaded(); - } + $scope.power.parameters = angular.copy($scope.node.power_parameters); + if (!angular.isObject($scope.power.parameters)) { + $scope.power.parameters = {}; } - // Update the node with new data on the region. - $scope.updateNode = function(node, queryPower) { - if (angular.isUndefined(queryPower)) { - queryPower = false; - } - return $scope.nodesManager.updateItem(node).then(function(node) { - updateHeader(); - updateSummary(); - if (queryPower) { - $scope.checkPowerState(); - } - }, function(error) { - console.log(error); - updateHeader(); - updateSummary(); - }); - }; - - // Called for autocomplete when the user is typing a tag name. - $scope.tagsAutocomplete = function(query) { - return TagsManager.autocomplete(query); - }; + // Force editing mode on, if the power_type is missing for a + // machine. This is placed at the bottom because we wanted the + // selected items to be filled in at least once. + if ( + $scope.canEdit() && + $scope.node.power_type === "" && + $scope.node.node_type === 0 + ) { + $scope.power.editing = true; + } - $scope.getPowerStateClass = function() { - // This will get called very early and node can be empty. - // In that case just return an empty string. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return ""; - } + $scope.power.in_pod = angular.isDefined($scope.node.pod); + } - if ($scope.checkingPower) { - return "checking"; - } else { - return $scope.node.power_state; - } - }; + // Updates the currently selected items in the summary section. + function updateSummary() { + // Do not update the selected items, when editing this would + // cause the users selection to change. + if ($scope.summary.editing) { + return; + } - // Get the power state text to show. - $scope.getPowerStateText = function() { - // This will get called very early and node can be empty. - // In that case just return an empty string. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return ""; - } + if (angular.isObject($scope.node.zone)) { + $scope.summary.zone.selected = ZonesManager.getItemFromList( + $scope.node.zone.id + ); + } + if (angular.isObject($scope.node.pool)) { + $scope.summary.pool.selected = ResourcePoolsManager.getItemFromList( + $scope.node.pool.id + ); + } + $scope.summary.architecture.selected = $scope.node.architecture; + $scope.summary.description = $scope.node.description; + $scope.summary.min_hwe_kernel.selected = $scope.node.min_hwe_kernel; + $scope.summary.tags = angular.copy($scope.node.tags); + + // Force editing mode on, if the architecture is invalid. This is + // placed at the bottom because we wanted the selected items to + // be filled in at least once. + if ( + $scope.canEdit() && + $scope.hasUsableArchitectures() && + $scope.hasInvalidArchitecture() + ) { + $scope.summary.editing = true; + } + } - if ($scope.checkingPower) { - return "Checking power"; - } else if ($scope.node.power_state === "unknown") { - return ""; - } else { - return "Power " + $scope.node.power_state; - } - }; + // Updates the service monitor section. + function updateServices() { + if ($scope.isController) { + $scope.services = {}; + angular.forEach(ControllersManager.getServices($scope.node), function( + service + ) { + $scope.services[service.name] = service; + }); + } + } - // Returns true when the "check now" button for updating the power - // state should be shown. - $scope.canCheckPowerState = function() { - // This will get called very early and node can be empty. - // In that case just return false. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return false; - } - return ( - $scope.node.power_state !== "unknown" && - !$scope.checkingPower); - }; + // Update the devices array on the scope based on the device children + // on the node. + function updateDevices() { + $scope.devices = []; + angular.forEach($scope.node.devices, function(child) { + var device = { + name: child.fqdn + }; + + // Add the interfaces to the device object if any exists. + if (angular.isArray(child.interfaces) && child.interfaces.length > 0) { + angular.forEach(child.interfaces, function(nic, nicIdx) { + var deviceWithMAC = angular.copy(device); + deviceWithMAC.mac_address = nic.mac_address; + + // Remove device name so it is not duplicated in the + // table since this is another MAC address on this + // device. + if (nicIdx > 0) { + deviceWithMAC.name = ""; + } + + // Add this links to the device object if any exists. + if (angular.isArray(nic.links) && nic.links.length > 0) { + angular.forEach(nic.links, function(link, lIdx) { + var deviceWithLink = angular.copy(deviceWithMAC); + deviceWithLink.ip_address = link.ip_address; + + // Remove the MAC address so it is not + // duplicated in the table since this is + // another link on this interface. + if (lIdx > 0) { + deviceWithLink.mac_address = ""; + } - // Check the power state of the node. - $scope.checkPowerState = function() { - $scope.checkingPower = true; - $scope.nodesManager.checkPowerState($scope.node).then(function() { - $scope.checkingPower = false; + $scope.devices.push(deviceWithLink); + }); + } else { + $scope.devices.push(deviceWithMAC); + } }); - }; - - $scope.isUbuntuOS = function() { - // This will get called very early and node can be empty. - // In that case just return an empty string. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return false; - } + } else { + $scope.devices.push(device); + } + }); + } - if ($scope.node.osystem === "ubuntu") { - return true; - } - return false; - }; + // Starts the watchers on the scope. + function startWatching() { + if (angular.isObject($scope.node)) { + // Update the title and name when the node fqdn changes. + $scope.$watch("node.fqdn", function() { + updateTitle(); + updateHeader(); + }); - $scope.isUbuntuCoreOS = function() { - // This will get called very early and node can be empty. - // In that case just return an empty string. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return false; - } + // Update the devices on the node. + $scope.$watch("node.devices", updateDevices); - if ($scope.node.osystem === "ubuntu-core") { - return true; - } - return false; - }; + // Update the availableActionOptions when the node actions change. + $scope.$watch("node.actions", updateAvailableActionOptions); - $scope.isCentOS = function() { - // This will get called very early and node can be empty. - // In that case just return an empty string. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return false; - } + // Update the summary when the node or architectures list is + // updated. + $scope.$watch("node.architecture", updateSummary); + $scope.$watchCollection( + $scope.summary.architecture.options, + updateSummary + ); + + // Uppdate the summary when min_hwe_kernel is updated. + $scope.$watch("node.min_hwe_kernel", updateSummary); + $scope.$watchCollection( + $scope.summary.min_hwe_kernel.options, + updateSummary + ); + + // Update the summary when the node or zone list is + // updated. + $scope.$watch("node.zone.id", updateSummary); + $scope.$watchCollection($scope.summary.zone.options, updateSummary); + + // Update the summary when the node or the resouce pool list is + // updated. + $scope.$watch("node.pool.id", updateSummary); + $scope.$watchCollection($scope.summary.pool.options, updateSummary); + + // Update the power when the node power_type or power_parameters + // are updated. + $scope.$watch("node.power_type", updatePower); + $scope.$watch("node.power_parameters", updatePower); + $scope.$watchCollection("power_types", updatePower); - if ($scope.node.osystem === "centos" || - $scope.node.osystem === "rhel") { - return true; - } - return false; - }; + // Update the services when the services list is updated. + $scope.$watch("node.service_ids", updateServices); + } + } - $scope.isCustomOS = function() { - // This will get called very early and node can be empty. - // In that case just return an empty string. It will be - // called again to show the correct information. - if (!angular.isObject($scope.node)) { - return false; - } + // Called when the node has been loaded. + function nodeLoaded(node) { + $scope.node = node; + $scope.loaded = true; + + updateTitle(); + updateSummary(); + updateServices(); + startWatching(); + + // Tell the storageController and networkingController that the + // node has been loaded. + if (angular.isObject($scope.storageController)) { + $scope.storageController.nodeLoaded(); + } + if (angular.isObject($scope.networkingController)) { + $scope.networkingController.nodeLoaded(); + } - if ($scope.node.osystem === "custom") { - return true; - } - return false; - }; + if (angular.isObject($scope.node.vlan)) { + $scope.vlan = VLANsManager.getItemFromList($scope.node.vlan.id); + } + } - // Return true if there is an action error. - $scope.isActionError = function() { - return $scope.action.error !== null; - }; + // Update the node with new data on the region. + $scope.updateNode = function(node, queryPower) { + if (angular.isUndefined(queryPower)) { + queryPower = false; + } - // Return True if in deploy action and the osinfo is missing. - $scope.isDeployError = function() { - // Never a deploy error when there is an action error. - if ($scope.isActionError()) { - return false; + return $scope.nodesManager + .updateItem(node) + .then(function() { + updateHeader(); + updateSummary(); + if (queryPower) { + $scope.checkPowerState(); } + $scope.failedUpdateError = ""; + }) + .catch(function(error) { + $log.error(error); + updateHeader(); + updateSummary(); + $scope.node.power_parameters = {}; + $scope.failedUpdateError = error; + }); + }; + + // Called for autocomplete when the user is typing a tag name. + $scope.tagsAutocomplete = function(query) { + return TagsManager.autocomplete(query); + }; + + $scope.getPowerStateClass = function() { + // This will get called very early and node can be empty. + // In that case just return an empty string. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return ""; + } - var missing_osinfo = ( - angular.isUndefined($scope.osinfo.osystems) || - $scope.osinfo.osystems.length === 0); - if (angular.isObject($scope.action.option) && - $scope.action.option.name === "deploy" && - missing_osinfo) { - return true; - } - return false; - }; + if ($scope.checkingPower) { + return "checking"; + } else { + return $scope.node.power_state; + } + }; - // Return True if unable to deploy because of missing ssh keys. - $scope.isSSHKeyError = function() { - // Never a deploy error when there is an action error. - if ($scope.isActionError()) { - return false; - } - if (angular.isObject($scope.action.option) && - $scope.action.option.name === "deploy" && - UsersManager.getSSHKeyCount() === 0) { - return true; - } - return false; - }; + // Get the power state text to show. + $scope.getPowerStateText = function() { + // This will get called very early and node can be empty. + // In that case just return an empty string. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return ""; + } - // Called when the actionOption has changed. - $scope.action.optionChanged = function() { - // Clear the action error. - $scope.action.error = null; - $scope.action.showing_confirmation = false; - $scope.action.confirmation_message = ''; - $scope.action.confirmation_details = []; - }; + if ($scope.checkingPower) { + return "Checking power"; + } else if ($scope.node.power_state === "unknown") { + return ""; + } else { + return "Power " + $scope.node.power_state; + } + }; - // Cancel the action. - $scope.actionCancel = function() { - $scope.action.option = null; - $scope.action.error = null; - $scope.action.showing_confirmation = false; - $scope.action.confirmation_message = ''; - $scope.action.confirmation_details = []; - }; + // Returns true when the "check now" button for updating the power + // state should be shown. + $scope.canCheckPowerState = function() { + // This will get called very early and node can be empty. + // In that case just return false. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return false; + } + return $scope.node.power_state !== "unknown" && !$scope.checkingPower; + }; - // Perform the action. - $scope.actionGo = function() { - let extra = {}; - // Set deploy parameters if a deploy. - if ($scope.action.option.name === "deploy" && - angular.isString($scope.osSelection.osystem) && - angular.isString($scope.osSelection.release)) { - - // Set extra. UI side the release is structured os/release, but - // when it is sent over the websocket only the "release" is - // sent. - extra.osystem = $scope.osSelection.osystem; - var release = $scope.osSelection.release; - release = release.split("/"); - release = release[release.length - 1]; - extra.distro_series = release; - // hwe_kernel is optional so only include it if its specified - if (angular.isString($scope.osSelection.hwe_kernel) && - ($scope.osSelection.hwe_kernel.indexOf('hwe-') >= 0 || - $scope.osSelection.hwe_kernel.indexOf('ga-') >= 0)) { - extra.hwe_kernel = $scope.osSelection.hwe_kernel; - } - let installKVM = $scope.deployOptions.installKVM; - // KVM pod deployment required bionic. - if (installKVM) { - extra.osystem = "ubuntu"; - extra.distro_series = "bionic"; - } - extra.install_kvm = installKVM; - } else if ($scope.action.option.name === "commission") { - extra.enable_ssh = $scope.commissionOptions.enableSSH; - extra.skip_bmc_config = $scope.commissionOptions.skipBMCConfig; - extra.skip_networking = ( - $scope.commissionOptions.skipNetworking); - extra.skip_storage = $scope.commissionOptions.skipStorage; - extra.commissioning_scripts = []; - for (let i = 0; i < $scope.commissioningSelection.length; i++) { - extra.commissioning_scripts.push( - $scope.commissioningSelection[i].id); - } - if ($scope.commissionOptions.updateFirmware) { - extra.commissioning_scripts.push('update_firmware') - } - if ($scope.commissionOptions.configureHBA) { - extra.commissioning_scripts.push('configure_hba') - } - if (extra.commissioning_scripts.length === 0) { - // Tell the region not to run any custom commissioning - // scripts. - extra.commissioning_scripts.push('none'); - } - extra.testing_scripts = []; - for (let i = 0; i < $scope.testSelection.length; i++) { - extra.testing_scripts.push($scope.testSelection[i].id); - } - if (extra.testing_scripts.length === 0) { - // Tell the region not to run any tests. - extra.testing_scripts.push('none'); - } - } else if ($scope.action.option.name === "test") { - if ($scope.node.status_code === 6 && - !$scope.action.showing_confirmation) { - $scope.action.showing_confirmation = true; - $scope.action.confirmation_message = - $scope.type_name_title + " is in a deployed state."; - return; - } - // Set the test options. - extra.enable_ssh = $scope.commissionOptions.enableSSH; - extra.testing_scripts = []; - for (let i = 0; i < $scope.testSelection.length; i++) { - extra.testing_scripts.push($scope.testSelection[i].id); - } - if (extra.testing_scripts.length === 0) { - // Tell the region not to run any tests. - extra.testing_scripts.push('none'); - } - } else if ($scope.action.option.name === "release") { - // Set the release options. - extra.erase = $scope.releaseOptions.erase; - extra.secure_erase = $scope.releaseOptions.secureErase; - extra.quick_erase = $scope.releaseOptions.quickErase; - } else if ($scope.action.option.name === "delete" && - $scope.type_name === "controller" && - !$scope.action.showing_confirmation) { - for (let i = 0; i < $scope.vlans.length; i++) { - var vlan = $scope.vlans[i]; - if (vlan.primary_rack === $scope.node.system_id) { - $scope.action.confirmation_details.push( - $scope.node.fqdn + - " is the primary rack controller for " + - vlan.name); - } - if (vlan.secondary_rack === $scope.node.system_id) { - $scope.action.confirmation_details.push( - $scope.node.fqdn + - " is the secondary rack controller for " + - vlan.name); - } - } - if ($scope.action.confirmation_details.length > 0) { - $scope.action.confirmation_message += - $scope.type_name_title + " will be deleted." - $scope.action.showing_confirmation = true; - return; - } - } + // Check the power state of the node. + $scope.checkPowerState = function() { + $scope.checkingPower = true; + $scope.nodesManager.checkPowerState($scope.node).then(function() { + $scope.checkingPower = false; + }); + }; - $scope.nodesManager.performAction( - $scope.node, $scope.action.option.name, extra).then(function() { - // If the action was delete, then go back to listing. - if ($scope.action.option.name === "delete") { - $location.path("/machines"); - } - $scope.action.option = null; - $scope.action.error = null; - $scope.action.showing_confirmation = false; - $scope.action.confirmation_message = ''; - $scope.osSelection.$reset(); - $scope.commissionOptions.enableSSH = false; - $scope.commissionOptions.skipBMCConfig = false; - $scope.commissionOptions.skipNetworking = false; - $scope.commissionOptions.skipStorage = false; - $scope.commissionOptions.updateFirmware = false; - $scope.commissionOptions.configureHBA = false; - $scope.commissioningSelection = []; - $scope.testSelection = []; - }, function(error) { - $scope.action.error = error; - }); - }; + $scope.isUbuntuOS = function() { + // This will get called very early and node can be empty. + // In that case just return an empty string. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return false; + } - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; + if ($scope.node.osystem === "ubuntu") { + return true; + } + return false; + }; - // Return true if the authenticated user has `perm` on node. - $scope.hasPermission = function(perm) { - if (angular.isObject($scope.node) && - angular.isArray($scope.node.permissions)) { - return $scope.node.permissions.indexOf(perm) >= 0; - } - return false; - }; + $scope.isUbuntuCoreOS = function() { + // This will get called very early and node can be empty. + // In that case just return an empty string. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return false; + } - // Return true if their are usable architectures. - $scope.hasUsableArchitectures = function() { - return $scope.summary.architecture.options.length > 0; - }; + if ($scope.node.osystem === "ubuntu-core") { + return true; + } + return false; + }; - // Return the placeholder text for the architecture dropdown. - $scope.getArchitecturePlaceholder = function() { - if ($scope.hasUsableArchitectures()) { - return "Choose an architecture"; - } else { - return "-- No usable architectures --"; - } - }; + $scope.isCentOS = function() { + // This will get called very early and node can be empty. + // In that case just return an empty string. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return false; + } - // Return true if the saved architecture is invalid. - $scope.hasInvalidArchitecture = function() { - if (angular.isObject($scope.node)) { - return ( - !$scope.isDevice && ( - $scope.node.architecture === "" || - $scope.summary.architecture.options.indexOf( - $scope.node.architecture) === -1)); - } else { - return false; - } - }; + if ($scope.node.osystem === "centos" || $scope.node.osystem === "rhel") { + return true; + } + return false; + }; - // Return true if the current architecture selection is invalid. - $scope.invalidArchitecture = function() { - return ( - !$scope.isDevice && !$scope.isController && ( - $scope.summary.architecture.selected === "" || - $scope.summary.architecture.options.indexOf( - $scope.summary.architecture.selected) === -1)); - }; + $scope.isCustomOS = function() { + // This will get called very early and node can be empty. + // In that case just return an empty string. It will be + // called again to show the correct information. + if (!angular.isObject($scope.node)) { + return false; + } - // Return true if at least a rack controller is connected to the - // region controller. - $scope.isRackControllerConnected = function() { - // If power_types exist then a rack controller is connected. - return $scope.power_types.length > 0; - }; + if ($scope.node.osystem === "custom") { + return true; + } + return false; + }; - // Return true if the node is locked - $scope.isLocked = function() { - if ($scope.node === null) { - return false; - } + // Return true if there is an action error. + $scope.isActionError = function() { + return $scope.action.error !== null; + }; + + // Return True if in deploy action and the osinfo is missing. + $scope.isDeployError = function() { + // Never a deploy error when there is an action error. + if ($scope.isActionError()) { + return false; + } - return $scope.node.locked; - }; + var missing_osinfo = + angular.isUndefined($scope.osinfo.osystems) || + $scope.osinfo.osystems.length === 0; + if ( + angular.isObject($scope.action.option) && + $scope.action.option.name === "deploy" && + missing_osinfo + ) { + return true; + } + return false; + }; - // Return true when the edit buttons can be clicked. - $scope.canEdit = function() { - // Devices can be edited, if the user has the permission. - if ($scope.isDevice) { - return $scope.hasPermission('edit'); - } - // Other nodes require the rack to be connected and the - // machine to not be locked. - return ( - $scope.isRackControllerConnected() && - $scope.hasPermission('edit') && - !$scope.isLocked()); - }; + // Return True if deploy warning should be shown because of missing ssh keys. + $scope.isSSHKeyWarning = function() { + // Never a deploy error when there is an action error. + if ($scope.isActionError()) { + return false; + } + if ( + angular.isObject($scope.action.option) && + $scope.action.option.name === "deploy" && + UsersManager.getSSHKeyCount() === 0 + ) { + return true; + } + return false; + }; - // Called to edit the domain name. - $scope.editHeaderDomain = function() { - if ($scope.canEdit()) { - return; - } + // Called when the actionOption has changed. + $scope.action.optionChanged = function() { + // Clear the action error. + $scope.action.error = null; + $scope.action.showing_confirmation = false; + $scope.action.confirmation_message = ""; + $scope.action.confirmation_details = []; + }; + + // Cancel the action. + $scope.actionCancel = function() { + $scope.action.option = null; + $scope.action.error = null; + $scope.action.showing_confirmation = false; + $scope.action.confirmation_message = ""; + $scope.action.confirmation_details = []; + }; + + // Perform the action. + $scope.actionGo = function() { + let extra = {}; + // Set deploy parameters if a deploy. + if ( + $scope.action.option.name === "deploy" && + angular.isString($scope.osSelection.osystem) && + angular.isString($scope.osSelection.release) + ) { + // Set extra. UI side the release is structured os/release, but + // when it is sent over the websocket only the "release" is + // sent. + extra.osystem = $scope.osSelection.osystem; + var release = $scope.osSelection.release; + release = release.split("/"); + release = release[release.length - 1]; + extra.distro_series = release; + // hwe_kernel is optional so only include it if its specified + if ( + angular.isString($scope.osSelection.hwe_kernel) && + ($scope.osSelection.hwe_kernel.indexOf("hwe-") >= 0 || + $scope.osSelection.hwe_kernel.indexOf("ga-") >= 0) + ) { + extra.hwe_kernel = $scope.osSelection.hwe_kernel; + } + let installKVM = $scope.deployOptions.installKVM; + // KVM pod deployment required bionic. + if (installKVM) { + extra.osystem = "ubuntu"; + extra.distro_series = "bionic"; + } + extra.install_kvm = installKVM; + } else if ($scope.action.option.name === "commission") { + extra.enable_ssh = $scope.commissionOptions.enableSSH; + extra.skip_bmc_config = $scope.commissionOptions.skipBMCConfig; + extra.skip_networking = $scope.commissionOptions.skipNetworking; + extra.skip_storage = $scope.commissionOptions.skipStorage; + extra.commissioning_scripts = []; + for (let i = 0; i < $scope.commissioningSelection.length; i++) { + extra.commissioning_scripts.push($scope.commissioningSelection[i].id); + } + if ($scope.commissionOptions.updateFirmware) { + extra.commissioning_scripts.push("update_firmware"); + } + if ($scope.commissionOptions.configureHBA) { + extra.commissioning_scripts.push("configure_hba"); + } + if (extra.commissioning_scripts.length === 0) { + // Tell the region not to run any custom commissioning + // scripts. + extra.commissioning_scripts.push("none"); + } + extra.testing_scripts = []; + for (let i = 0; i < $scope.testSelection.length; i++) { + extra.testing_scripts.push($scope.testSelection[i].id); + } + if (extra.testing_scripts.length === 0) { + // Tell the region not to run any tests. + extra.testing_scripts.push("none"); + } + } else if ($scope.action.option.name === "test") { + if ( + $scope.node.status_code === 6 && + !$scope.action.showing_confirmation + ) { + $scope.action.showing_confirmation = true; + $scope.action.confirmation_message = + $scope.type_name_title + " is in a deployed state."; + return; + } + // Set the test options. + extra.enable_ssh = $scope.commissionOptions.enableSSH; + extra.testing_scripts = []; + for (let i = 0; i < $scope.testSelection.length; i++) { + extra.testing_scripts.push($scope.testSelection[i].id); + } + if (extra.testing_scripts.length === 0) { + // Tell the region not to run any tests. + extra.testing_scripts.push("none"); + } + } else if ($scope.action.option.name === "release") { + // Set the release options. + extra.erase = $scope.releaseOptions.erase; + extra.secure_erase = $scope.releaseOptions.secureErase; + extra.quick_erase = $scope.releaseOptions.quickErase; + } else if ( + $scope.action.option.name === "delete" && + $scope.type_name === "controller" && + !$scope.action.showing_confirmation + ) { + for (let i = 0; i < $scope.vlans.length; i++) { + var vlan = $scope.vlans[i]; + if (vlan.primary_rack === $scope.node.system_id) { + $scope.action.confirmation_details.push( + $scope.node.fqdn + + " is the primary rack controller for " + + vlan.name + ); + } + if (vlan.secondary_rack === $scope.node.system_id) { + $scope.action.confirmation_details.push( + $scope.node.fqdn + + " is the secondary rack controller for " + + vlan.name + ); + } + } + if ($scope.action.confirmation_details.length > 0) { + $scope.action.confirmation_message += + $scope.type_name_title + " will be deleted."; + $scope.action.showing_confirmation = true; + return; + } + } - // Do nothing if already editing because we don't want to reset - // the current value. - if ($scope.header.editing_domain) { - return; + $scope.nodesManager + .performAction($scope.node, $scope.action.option.name, extra) + .then( + function() { + // If the action was delete, then go back to listing. + if ($scope.action.option.name === "delete") { + $location.path("/machines"); + } + $scope.action.option = null; + $scope.action.error = null; + $scope.action.showing_confirmation = false; + $scope.action.confirmation_message = ""; + $scope.osSelection.$reset(); + $scope.commissionOptions.enableSSH = false; + $scope.commissionOptions.skipBMCConfig = false; + $scope.commissionOptions.skipNetworking = false; + $scope.commissionOptions.skipStorage = false; + $scope.commissionOptions.updateFirmware = false; + $scope.commissionOptions.configureHBA = false; + $scope.commissioningSelection = []; + $scope.testSelection = []; + }, + function(error) { + $scope.action.error = error; } - $scope.header.editing = false; - $scope.header.editing_domain = true; + ); + }; - // Set the value to the hostname, as hostname and domain are edited - // using different fields. - $scope.header.hostname.value = $scope.node.hostname; - }; + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Return true if the authenticated user has `perm` on node. + $scope.hasPermission = function(perm) { + if ( + angular.isObject($scope.node) && + angular.isArray($scope.node.permissions) + ) { + return $scope.node.permissions.indexOf(perm) >= 0; + } + return false; + }; - // Called to edit the node name. - $scope.editHeader = function() { - if (!$scope.canEdit()) { - return; - } + // Return true if their are usable architectures. + $scope.hasUsableArchitectures = function() { + return $scope.summary.architecture.options.length > 0; + }; + + // Return the placeholder text for the architecture dropdown. + $scope.getArchitecturePlaceholder = function() { + if ($scope.hasUsableArchitectures()) { + return "Choose an architecture"; + } else { + return "-- No usable architectures --"; + } + }; - // Do nothing if already editing because we don't want to reset - // the current value. - if ($scope.header.editing) { - return; - } - $scope.header.editing = true; - $scope.header.editing_domain = false; + // Return true if the saved architecture is invalid. + $scope.hasInvalidArchitecture = function() { + if (angular.isObject($scope.node)) { + return ( + !$scope.isDevice && + ($scope.node.architecture === "" || + $scope.summary.architecture.options.indexOf( + $scope.node.architecture + ) === -1) + ); + } else { + return false; + } + }; - // Set the value to the hostname, as hostname and domain are edited - // using different fields. - $scope.header.hostname.value = $scope.node.hostname; - }; + // Return true if the current architecture selection is invalid. + $scope.invalidArchitecture = function() { + return ( + !$scope.isDevice && + !$scope.isController && + ($scope.summary.architecture.selected === "" || + $scope.summary.architecture.options.indexOf( + $scope.summary.architecture.selected + ) === -1) + ); + }; + + // Return true if at least a rack controller is connected to the + // region controller. + $scope.isRackControllerConnected = function() { + // If power_types exist then a rack controller is connected. + return $scope.power_types.length > 0; + }; + + // Return true if the node is locked + $scope.isLocked = function() { + if ($scope.node === null) { + return false; + } - // Return true when the hostname or domain in the header is invalid. - $scope.editHeaderInvalid = function() { - // Not invalid unless editing. - if (!$scope.header.editing && !$scope.header.editing_domain) { - return false; - } + return $scope.node.locked; + }; - // The value cannot be blank. - var value = $scope.header.hostname.value; - if (value.length === 0) { - return true; - } - return !ValidationService.validateHostname(value); - }; + // Return true when the edit buttons can be clicked. + $scope.canEdit = function() { + // Devices can be edited, if the user has the permission. + if ($scope.isDevice) { + return $scope.hasPermission("edit"); + } + // Other nodes require the rack to be connected and the + // machine to not be locked. + return ( + $scope.isRackControllerConnected() && + $scope.hasPermission("edit") && + !$scope.isLocked() + ); + }; + + // Called to edit the domain name. + $scope.editHeaderDomain = function() { + if ($scope.canEdit()) { + return; + } - // Called to cancel editing of the node hostname and domain. - $scope.cancelEditHeader = function() { - $scope.header.editing = false; - $scope.header.editing_domain = false; - updateHeader(); - }; + // Do nothing if already editing because we don't want to reset + // the current value. + if ($scope.header.editing_domain) { + return; + } + $scope.header.editing = false; + $scope.header.editing_domain = true; - // Called to save editing of node hostname or domain. - $scope.saveEditHeader = function() { - // Does nothing if invalid. - if ($scope.editHeaderInvalid()) { - return; - } - $scope.header.editing = false; - $scope.header.editing_domain = false; + // Set the value to the hostname, as hostname and domain are edited + // using different fields. + $scope.header.hostname.value = $scope.node.hostname; + }; + + // Called to edit the node name. + $scope.editHeader = function() { + if (!$scope.canEdit()) { + return; + } - // Copy the node and make the changes. - var node = angular.copy($scope.node); - node.hostname = $scope.header.hostname.value; - node.domain = $scope.header.domain.selected; + // Do nothing if already editing because we don't want to reset + // the current value. + if ($scope.header.editing) { + return; + } + $scope.header.editing = true; + $scope.header.editing_domain = false; - // Update the node. - $scope.updateNode(node); - }; + // Set the value to the hostname, as hostname and domain are edited + // using different fields. + $scope.header.hostname.value = $scope.node.hostname; + }; + + // Return true when the hostname or domain in the header is invalid. + $scope.editHeaderInvalid = function() { + // Not invalid unless editing. + if (!$scope.header.editing && !$scope.header.editing_domain) { + return false; + } - // Called to enter edit mode in the summary section. - $scope.editSummary = function() { - if (!$scope.canEdit()) { - return; - } - $scope.summary.editing = true; - }; + // The value cannot be blank. + var value = $scope.header.hostname.value; + if (value.length === 0) { + return true; + } + return !ValidationService.validateHostname(value); + }; - // Called to cancel editing in the summary section. - $scope.cancelEditSummary = function() { - // Leave edit mode only if node has valid architecture. - if ($scope.isDevice || $scope.isController) { - $scope.summary.editing = false; - } else if (!$scope.hasInvalidArchitecture()) { - $scope.summary.editing = false; - } - }; + // Called to cancel editing of the node hostname and domain. + $scope.cancelEditHeader = function() { + $scope.header.editing = false; + $scope.header.editing_domain = false; + updateHeader(); + }; + + // Called to save editing of node hostname or domain. + $scope.saveEditHeader = function() { + // Does nothing if invalid. + if ($scope.editHeaderInvalid()) { + return; + } + $scope.header.editing = false; + $scope.header.editing_domain = false; - // Called to save the changes made in the summary section. - $scope.saveEditSummary = function() { - // Do nothing if invalidArchitecture. - if ($scope.invalidArchitecture()) { - return; - } + // Copy the node and make the changes. + var node = angular.copy($scope.node); + node.hostname = $scope.header.hostname.value; + node.domain = $scope.header.domain.selected; + + // Update the node. + $scope.updateNode(node); + }; + + // Called to enter edit mode in the summary section. + $scope.editSummary = function() { + if (!$scope.canEdit()) { + return; + } + $scope.summary.editing = true; + }; - $scope.summary.editing = false; + // Called to cancel editing in the summary section. + $scope.cancelEditSummary = function() { + // Leave edit mode only if node has valid architecture. + if ($scope.isDevice || $scope.isController) { + $scope.summary.editing = false; + } else if (!$scope.hasInvalidArchitecture()) { + $scope.summary.editing = false; + } + }; - // Copy the node and make the changes. - var node = angular.copy($scope.node); - node.zone = angular.copy($scope.summary.zone.selected); - node.pool = angular.copy($scope.summary.pool.selected); - node.description = angular.copy($scope.summary.description); - node.architecture = $scope.summary.architecture.selected; - if ($scope.summary.min_hwe_kernel.selected === null) { - node.min_hwe_kernel = ""; - } else { - node.min_hwe_kernel = $scope.summary.min_hwe_kernel.selected; - } - node.tags = []; - angular.forEach($scope.summary.tags, function(tag) { - node.tags.push(tag.text); - }); + // Called to save the changes made in the summary section. + $scope.saveEditSummary = function() { + // Do nothing if invalidArchitecture. + if ($scope.invalidArchitecture()) { + return; + } - // Update the node. - $scope.updateNode(node); - }; + $scope.summary.editing = false; - // Return true if the current power type selection is invalid. - $scope.invalidPowerType = function() { - return !angular.isObject($scope.power.type); - }; + // Copy the node and make the changes. + var node = angular.copy($scope.node); + node.zone = angular.copy($scope.summary.zone.selected); + node.pool = angular.copy($scope.summary.pool.selected); + node.description = angular.copy($scope.summary.description); + node.architecture = $scope.summary.architecture.selected; + if ($scope.summary.min_hwe_kernel.selected === null) { + node.min_hwe_kernel = ""; + } else { + node.min_hwe_kernel = $scope.summary.min_hwe_kernel.selected; + } + node.tags = []; + angular.forEach($scope.summary.tags, function(tag) { + node.tags.push(tag.text); + }); - // Called to enter edit mode in the power section. - $scope.editPower = function() { - if (!$scope.canEdit()) { - return; - } - $scope.power.editing = true; - }; + // Update the node. + $scope.updateNode(node); + }; + + // Return true if the current power type selection is invalid. + $scope.invalidPowerType = function() { + return !angular.isObject($scope.power.type); + }; + + // Called to enter edit mode in the power section. + $scope.editPower = function() { + if (!$scope.canEdit()) { + return; + } + $scope.power.editing = true; + }; - // Called to cancel editing in the power section. - $scope.cancelEditPower = function() { - // If the node is not a machine, only leave edit mode if node has - // valid power type. - if ($scope.node.node_type !== 0 || $scope.node.power_type !== "") { - $scope.power.editing = false; - } - updatePower(); - }; + // Called to cancel editing in the power section. + $scope.cancelEditPower = function() { + // If the node is not a machine, only leave edit mode if node has + // valid power type. + if ($scope.node.node_type !== 0 || $scope.node.power_type !== "") { + $scope.power.editing = false; + } + updatePower(); + }; - // Called to save the changes made in the power section. - $scope.saveEditPower = function() { - // Does nothing if invalid power type. - if ($scope.invalidPowerType()) { - return; - } - $scope.power.editing = false; + // Called to save the changes made in the power section. + $scope.saveEditPower = function() { + // Does nothing if invalid power type. + if ($scope.invalidPowerType()) { + return; + } + $scope.power.editing = false; - // Copy the node and make the changes. - var node = angular.copy($scope.node); - node.power_type = $scope.power.type.name; - node.power_parameters = angular.copy($scope.power.parameters); + // Copy the node and make the changes. + var node = angular.copy($scope.node); + node.power_type = $scope.power.type.name; + node.power_parameters = angular.copy($scope.power.parameters); + + // Update the node. + $scope.updateNode(node, true); + }; + + // Return true if the "load more" events button should be available. + $scope.allowShowMoreEvents = function() { + if (!angular.isObject($scope.node)) { + return false; + } + if (!angular.isArray($scope.node.events)) { + return false; + } + return ( + $scope.node.events.length > 0 && + $scope.node.events.length > $scope.events.limit && + $scope.events.limit < 50 + ); + }; + + // Show another 10 events. + $scope.showMoreEvents = function() { + $scope.events.limit += 10; + }; + + // Return the nice text for the given event. + $scope.getEventText = function(event) { + var text = event.type.description; + if (angular.isString(event.description) && event.description.length > 0) { + text += " - " + event.description; + } + return text; + }; - // Update the node. - $scope.updateNode(node, true); - }; + $scope.getPowerEventError = function() { + if ( + !angular.isObject($scope.node) || + !angular.isArray($scope.node.events) + ) { + return; + } - // Return true if the "load more" events button should be available. - $scope.allowShowMoreEvents = function() { - if (!angular.isObject($scope.node)) { - return false; - } - if (!angular.isArray($scope.node.events)) { - return false; - } - return ( - $scope.node.events.length > 0 && - $scope.node.events.length > $scope.events.limit && - $scope.events.limit < 50); - }; + var i; + for (i = 0; i < $scope.node.events.length; i++) { + var event = $scope.node.events[i]; + if ( + event.type.level === "warning" && + event.type.description === "Failed to query node's BMC" + ) { + // Latest power event is an error + return event; + } else if ( + event.type.level === "info" && + event.type.description === "Queried node's BMC" + ) { + // Latest power event is not an error + return; + } + } + // No power event found, thus no error + return; + }; + + $scope.hasPowerEventError = function() { + var event = $scope.getPowerEventError(); + return angular.isObject(event); + }; + + $scope.getPowerEventErrorText = function() { + var event = $scope.getPowerEventError(); + if (angular.isObject(event)) { + // Return text + return event.description; + } else { + return ""; + } + }; - // Show another 10 events. - $scope.showMoreEvents = function() { - $scope.events.limit += 10; - }; + // true if power error prevents the provided action + $scope.hasActionPowerError = function(actionName) { + if (!$scope.hasPowerError()) { + return false; // no error, no need to check state + } + // these states attempt to manipulate power + var powerChangingStates = ["commission", "deploy", "on", "off", "release"]; + if (actionName && powerChangingStates.indexOf(actionName) > -1) { + return true; + } + return false; + }; - // Return the nice text for the given event. - $scope.getEventText = function(event) { - var text = event.type.description; - if (angular.isString(event.description) && - event.description.length > 0) { - text += " - " + event.description; - } - return text; - }; + // Check to see if the power type has any missing system packages. + $scope.hasPowerError = function() { + if (angular.isObject($scope.power.type)) { + return $scope.power.type.missing_packages.length > 0; + } else { + return false; + } + }; - $scope.getPowerEventError = function() { - if (!angular.isObject($scope.node) || - !angular.isArray($scope.node.events)) { - return; + // Returns a formatted string of missing system packages. + $scope.getPowerErrors = function() { + var i; + var result = ""; + if (angular.isObject($scope.power.type)) { + var packages = $scope.power.type.missing_packages; + packages.sort(); + for (i = 0; i < packages.length; i++) { + result += packages[i]; + if (i + 2 < packages.length) { + result += ", "; + } else if (i + 1 < packages.length) { + result += " and "; } + } + result += packages.length > 1 ? " packages" : " package"; + } + return result; + }; - var i; - for (i = 0; i < $scope.node.events.length; i++) { - var event = $scope.node.events[i]; - if (event.type.level === "warning" && - event.type.description === "Failed to query node's BMC") { - // Latest power event is an error - return event; - } else if (event.type.level === "info" && - event.type.description === "Queried node's BMC") { - // Latest power event is not an error - return; - } - } - // No power event found, thus no error - return; - }; + // Return the class to apply to the service. + $scope.getServiceClass = function(service) { + if (!angular.isObject(service)) { + return "none"; + } else { + if (service.status === "running") { + return "success"; + } else if (service.status === "dead") { + return "error"; + } else if (service.status === "degraded") { + return "warning"; + } else { + return "none"; + } + } + }; - $scope.hasPowerEventError = function() { - var event = $scope.getPowerEventError(); - return angular.isObject(event); - }; + $scope.hasCustomCommissioningScripts = function() { + var i; + for (i = 0; i < $scope.scripts.length; i++) { + if ($scope.scripts[i].script_type === 0) { + return true; + } + } + return false; + }; - $scope.getPowerEventErrorText = function() { - var event = $scope.getPowerEventError(); - if (angular.isObject(event)) { - // Return text - return event.description; - } else { - return ""; - } - }; + // Called by the children controllers to let the parent know. + $scope.controllerLoaded = function(name, scope) { + $scope[name] = scope; + if (angular.isObject(scope.node)) { + scope.nodeLoaded(); + } + }; - // true if power error prevents the provided action - $scope.hasActionPowerError = function(actionName) { - if (!$scope.hasPowerError()) { - return false; // no error, no need to check state - } - // these states attempt to manipulate power - var powerChangingStates = [ - 'commission', - 'deploy', - 'on', - 'off', - 'release' - ]; - if (actionName && powerChangingStates.indexOf(actionName) > -1) { - return true; - } + // Only show a warning that tests have failed if there are failed tests + // and the node isn't currently commissioning or testing. + $scope.showFailedTestWarning = function() { + // Devices can't have failed tests and don't have status_code + // defined. + if ($scope.node.node_type === 1 || !$scope.node.status_code) { + return false; + } + switch ($scope.node.status_code) { + // NEW + case 0: + // COMMISSIONING + case 1: + // FAILED_COMMISSIONING + case 2: + // TESTING + case 21: + // FAILED_TESTING + case 22: return false; - }; + } + switch ($scope.node.testing_status) { + // Tests haven't been run + case -1: + // Tests have passed + case 2: + return false; + } + return true; + }; - // Check to see if the power type has any missing system packages. - $scope.hasPowerError = function() { - if (angular.isObject($scope.power.type)) { - return $scope.power.type.missing_packages.length > 0; - } else { - return false; - } - }; + // Get the subtext for the CPU card. Only nodes commissioned after + // MAAS 2.4 will have the CPU speed. + $scope.getCPUSubtext = function() { + var label = $scope.node.cpu_count + " cores"; + if (!$scope.node.cpu_speed || $scope.node.cpu_speed === 0) { + return label; + } else if ($scope.node.cpu_speed < 1000) { + return label + " @ " + $scope.node.cpu_speed + " Mhz"; + } else { + return label + " @ " + $scope.node.cpu_speed / 1000 + " Ghz"; + } + }; - // Returns a formatted string of missing system packages. - $scope.getPowerErrors = function() { - var i; - var result = ""; - if (angular.isObject($scope.power.type)) { - var packages = $scope.power.type.missing_packages; - packages.sort(); - for (i = 0; i < packages.length; i++) { - result += packages[i]; - if (i + 2 < packages.length) { - result += ", "; - } - else if (i + 1 < packages.length) { - result += " and "; - } - } - result += packages.length > 1 ? " packages" : " package"; - } - return result; - }; + $scope.getHardwareTestErrorText = function(error) { + if (error === "Unable to run destructive test while deployed!") { + return ( + "The selected hardware tests contain one or more destructive" + + " tests. Destructive tests cannot run on deployed machines." + ); + } else { + return error; + } + }; - // Return the class to apply to the service. - $scope.getServiceClass = function(service) { - if (!angular.isObject(service)) { - return "none"; - } else { - if (service.status === "running") { - return "success"; - } else if (service.status === "dead") { - return "error"; - } else if (service.status === "degraded") { - return "warning"; - } else { - return "none"; - } - } - }; + $scope.powerParametersValid = function(power_parameters) { + if (!angular.isObject(power_parameters)) { + return false; + } - $scope.hasCustomCommissioningScripts = function() { - var i; - for (i = 0; i < $scope.scripts.length; i++) { - if ($scope.scripts[i].script_type === 0) { - return true; - } - } - return false; - }; + // If no keys in obj + if (Object.keys(power_parameters).length === 0) { + return false; + } - // Called by the children controllers to let the parent know. - $scope.controllerLoaded = function(name, scope) { - $scope[name] = scope; - if (angular.isObject(scope.node)) { - scope.nodeLoaded(); - } - }; + // If keys but no values in obj + var hasParameters = false; - // Only show a warning that tests have failed if there are failed tests - // and the node isn't currently commissioning or testing. - $scope.showFailedTestWarning = function() { - // Devices can't have failed tests and don't have status_code - // defined. - if ($scope.node.node_type === 1 || !$scope.node.status_code) { - return false; - } - switch ($scope.node.status_code) { - // NEW - case 0: - // COMMISSIONING - case 1: - // FAILED_COMMISSIONING - case 2: - // TESTING - case 21: - // FAILED_TESTING - case 22: - return false; - } - switch ($scope.node.testing_status) { - // Tests haven't been run - case -1: - // Tests have passed - case 2: - return false; - } - return true; - }; + Object.keys(power_parameters).forEach(function(key) { + if (power_parameters[key] !== "") { + hasParameters = true; + } else { + hasParameters = false; + } + }); - // Get the subtext for the CPU card. Only nodes commissioned after - // MAAS 2.4 will have the CPU speed. - $scope.getCPUSubtext = function() { - var label = $scope.node.cpu_count + " cores"; - if (!$scope.node.cpu_speed || $scope.node.cpu_speed === 0) { - return label; - } else if ($scope.node.cpu_speed < 1000) { - return label + " @ " + $scope.node.cpu_speed + " Mhz"; - } else { - return label + " @ " + ($scope.node.cpu_speed / 1000) + " Ghz"; - } - }; + if (!hasParameters) { + return false; + } - // Reload osinfo when the page reloads - $scope.$on("$routeUpdate", function() { - GeneralManager.loadItems( - ["osinfo", "architectures", "min_hwe_kernels"]); - }); + return true; + }; - var page_managers; - if ($location.path().indexOf('/controller') !== -1) { - $scope.nodesManager = ControllersManager; - page_managers = [ControllersManager, ScriptsManager, VLANsManager]; - $scope.isController = true; - $scope.isDevice = false; - $scope.type_name = 'controller'; - $scope.type_name_title = 'Controller'; - $rootScope.page = 'controllers'; - } else if ($location.path().indexOf('/device') !== -1) { - $scope.nodesManager = DevicesManager; - page_managers = [DevicesManager]; - $scope.isController = false; - $scope.isDevice = true; - $scope.type_name = 'device'; - $scope.type_name_title = 'Device'; - $rootScope.page = 'devices'; + // Reload osinfo when the page reloads + $scope.$on("$routeUpdate", function() { + GeneralManager.loadItems(["osinfo", "architectures", "min_hwe_kernels"]); + }); + + var page_managers; + if ($location.path().indexOf("/controller") !== -1) { + $scope.nodesManager = ControllersManager; + page_managers = [ControllersManager, ScriptsManager, VLANsManager]; + $scope.isController = true; + $scope.isDevice = false; + $scope.type_name = "controller"; + $scope.type_name_title = "Controller"; + $rootScope.page = "controllers"; + } else if ($location.path().indexOf("/device") !== -1) { + $scope.nodesManager = DevicesManager; + page_managers = [DevicesManager]; + $scope.isController = false; + $scope.isDevice = true; + $scope.type_name = "device"; + $scope.type_name_title = "Device"; + $rootScope.page = "devices"; + } else { + $scope.nodesManager = MachinesManager; + page_managers = [MachinesManager, ScriptsManager]; + $scope.isController = false; + $scope.isDevice = false; + $scope.type_name = "machine"; + $scope.type_name_title = "Machine"; + $rootScope.page = "machines"; + } + + // Load all the required managers. + ManagerHelperService.loadManagers( + $scope, + [ + ZonesManager, + GeneralManager, + UsersManager, + TagsManager, + DomainsManager, + ServicesManager, + ResourcePoolsManager + ].concat(page_managers) + ).then(function() { + // Possibly redirected from another controller that already had + // this node set to active. Only call setActiveItem if not already + // the activeItem. + var activeNode = $scope.nodesManager.getActiveItem(); + if ( + angular.isObject(activeNode) && + activeNode.system_id === $routeParams.system_id + ) { + nodeLoaded(activeNode); } else { - $scope.nodesManager = MachinesManager; - page_managers = [MachinesManager, ScriptsManager]; - $scope.isController = false; - $scope.isDevice = false; - $scope.type_name = 'machine'; - $scope.type_name_title = 'Machine'; - $rootScope.page = 'machines'; - } - - // Load all the required managers. - ManagerHelperService.loadManagers($scope, [ - ZonesManager, - GeneralManager, - UsersManager, - TagsManager, - DomainsManager, - ServicesManager, - ResourcePoolsManager, - ].concat(page_managers)).then(function() { - // Possibly redirected from another controller that already had - // this node set to active. Only call setActiveItem if not already - // the activeItem. - var activeNode = $scope.nodesManager.getActiveItem(); - if (angular.isObject(activeNode) && - activeNode.system_id === $routeParams.system_id) { - nodeLoaded(activeNode); - } else { - $scope.nodesManager.setActiveItem( - $routeParams.system_id).then(function(node) { - nodeLoaded(node); - }, function(error) { - ErrorService.raiseError(error); - }); - activeNode = $scope.nodesManager.getActiveItem(); - } - if ($scope.isDevice) { - $scope.ip_assignment = activeNode.ip_assignment; + $scope.nodesManager.setActiveItem($routeParams.system_id).then( + function(node) { + nodeLoaded(node); + if (angular.isObject($scope.node.vlan)) { + if ( + localStorage.getItem( + `hideHighAvailabilityNotification-${$scope.node.vlan.id}` + ) + ) { + $scope.hideHighAvailabilityNotification = true; + } + } + }, + function(error) { + ErrorService.raiseError(error); } - }); + ); + activeNode = $scope.nodesManager.getActiveItem(); + } + if ($scope.isDevice) { + $scope.ip_assignment = activeNode.ip_assignment; + } + }); } export default NodeDetailsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details_networking.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details_networking.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details_networking.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details_networking.js 2019-06-01 02:18:13.000000000 +0000 @@ -7,2122 +7,2218 @@ // Filter that is specific to the NodeNetworkingController. Filters the // list of VLANs to be only those that are unused by the interface. export function filterByUnusedForInterface() { - return function(vlans, nic, originalInterfaces) { - var filtered = []; - if (!angular.isObject(nic) || - !angular.isObject(originalInterfaces)) { - return filtered; + return function(vlans, nic, originalInterfaces) { + var filtered = []; + if (!angular.isObject(nic) || !angular.isObject(originalInterfaces)) { + return filtered; + } + var usedVLANs = []; + angular.forEach(originalInterfaces, function(inter) { + if (inter.type === "vlan") { + var parent = inter.parents[0]; + if (parent === nic.id) { + usedVLANs.push(inter.vlan_id); } - var usedVLANs = []; - angular.forEach(originalInterfaces, function(inter) { - if (inter.type === "vlan") { - var parent = inter.parents[0]; - if (parent === nic.id) { - usedVLANs.push(inter.vlan_id); - } - } - }); - angular.forEach(vlans, function(vlan) { - var idx = usedVLANs.indexOf(vlan.id); - if (idx === -1) { - filtered.push(vlan); - } - }); - return filtered; - }; + } + }); + angular.forEach(vlans, function(vlan) { + var idx = usedVLANs.indexOf(vlan.id); + if (idx === -1) { + filtered.push(vlan); + } + }); + return filtered; + }; } // Filter that is specific to the NodeNetworkingController. Filters the // list of interfaces to not include the current parent interfaces being // bonded together. export function removeInterfaceParents() { - return function(interfaces, childInterface, skip) { - if (skip || !angular.isObject(childInterface) || - !angular.isArray(childInterface.parents)) { - return interfaces; - } - var filtered = []; - angular.forEach(interfaces, function(nic) { - var i, parent, found = false; - for (i = 0; i < childInterface.parents.length; i++) { - parent = childInterface.parents[i]; - if (parent.id === nic.id && parent.link_id === nic.link_id) { - found = true; - break; - } - } - if (!found) { - filtered.push(nic); - } - }); - return filtered; - }; + return function(interfaces, childInterface, skip) { + if ( + skip || + !angular.isObject(childInterface) || + !angular.isArray(childInterface.parents) + ) { + return interfaces; + } + var filtered = []; + angular.forEach(interfaces, function(nic) { + var i, + parent, + found = false; + for (i = 0; i < childInterface.parents.length; i++) { + parent = childInterface.parents[i]; + if (parent.id === nic.id && parent.link_id === nic.link_id) { + found = true; + break; + } + } + if (!found) { + filtered.push(nic); + } + }); + return filtered; + }; } // Filter that is specific to the NodeNetworkingController. Remove the default // VLAN if the interface is a VLAN interface. export function removeDefaultVLANIfVLAN() { - return function(vlans, interfaceType) { - if (!angular.isString(interfaceType)) { - return vlans; - } - var filtered = []; - angular.forEach(vlans, function(vlan) { - if (interfaceType !== "vlan") { - filtered.push(vlan); - } else if (vlan.vid !== 0) { - filtered.push(vlan); - } - }); - return filtered; - }; + return function(vlans, interfaceType) { + if (!angular.isString(interfaceType)) { + return vlans; + } + var filtered = []; + angular.forEach(vlans, function(vlan) { + if (interfaceType !== "vlan") { + filtered.push(vlan); + } else if (vlan.vid !== 0) { + filtered.push(vlan); + } + }); + return filtered; + }; } - // Filter that is specific to the NodeNetworkingController. Remove // VLANs that are no part of current fabric. export function filterVLANNotOnFabric() { - return function(items, VLANsInFabric) { - if (!angular.isArray(VLANsInFabric)) { - return items; - } - - return items.filter(function(item) { - var index = VLANsInFabric.indexOf(item.id); - return index !== -1; - }); + return function(items, VLANsInFabric) { + if (!angular.isArray(VLANsInFabric)) { + return items; } + + return items.filter(function(item) { + var index = VLANsInFabric.indexOf(item.id); + return index !== -1; + }); + }; } function isOnSameFabric(item, bondInterface) { - if (item.fabric && bondInterface.fabric) { - return item.fabric.name === bondInterface.fabric.name; - } + if (item.fabric && bondInterface.fabric) { + return item.fabric.name === bondInterface.fabric.name; + } - return false; + return false; } function isOnSameVLAN(item, bondInterface) { - if (item.vlan && bondInterface.vlan) { - return item.vlan.id === bondInterface.vlan.id; - } + if (item.vlan && bondInterface.vlan) { + return item.vlan.id === bondInterface.vlan.id; + } - return false; + return false; } - // Filter that is specific to the NodeNetworkingController. Remove // editInterface from list. export function filterEditInterface() { - return function (items, editInterface) { - if (!angular.isObject(editInterface)) { - return items; - } - - var results = items.filter(function (item) { - + return function(items, editInterface) { + if (!angular.isObject(editInterface)) { + return items; + } - return item.id !== editInterface.id - && isOnSameFabric(item, editInterface) - && isOnSameVLAN(item, editInterface); - }); + var results = items.filter(function(item) { + return ( + item.id !== editInterface.id && + isOnSameFabric(item, editInterface) && + isOnSameVLAN(item, editInterface) + ); + }); - return results; - }; + return results; + }; } - // Filter that is specific to the NodeNetworkingController. Remove the // selected interfaces export function filterSelectedInterfaces() { - return function(items, selectedInterfaces, newBondInterface) { - if (!angular.isArray(selectedInterfaces)) { - return items; - } + return function(items, selectedInterfaces, newBondInterface) { + if (!angular.isArray(selectedInterfaces)) { + return items; + } - if (!angular.isObject(newBondInterface)) { - return items; - } + if (!angular.isObject(newBondInterface)) { + return items; + } - return items.filter(function(item) { - var itemKey = item.id + "/" + item.link_id; + return items.filter(function(item) { + var itemKey = item.id + "/" + item.link_id; - return selectedInterfaces.indexOf(itemKey) === -1 - && item.fabric.name === newBondInterface.fabric.name - && item.vlan.id === newBondInterface.vlan.id; - }); - } + return ( + selectedInterfaces.indexOf(itemKey) === -1 && + item.fabric.name === newBondInterface.fabric.name && + item.vlan.id === newBondInterface.vlan.id + ); + }); + }; } - // Filter that is specific to the NodeNetworkingController. Only provide the // available modes for that interface type. export function filterLinkModes() { - return function(modes, nic) { - if (!angular.isObject(nic)) { - return modes; - } - var filtered = []; + return function(modes, nic) { + if (!angular.isObject(nic)) { + return modes; + } + var filtered = []; - // If this is not a $maasForm, make it work like one. - // We need to use getValue() to access attributes, because each - // type of maas-obj-form gets to define how values come out. - if (!angular.isFunction(nic.getValue)) { - nic.getValue = function(name) { - return this[name]; - }; - } + // If this is not a $maasForm, make it work like one. + // We need to use getValue() to access attributes, because each + // type of maas-obj-form gets to define how values come out. + if (!angular.isFunction(nic.getValue)) { + nic.getValue = function(name) { + return this[name]; + }; + } - if (!angular.isObject(nic.getValue('subnet'))) { - // No subnet is configure so the only allowed mode - // is 'link_up'. - angular.forEach(modes, function(mode) { - if (mode.mode === "link_up") { - filtered.push(mode); - } - }); - } else { - // Don't add LINK_UP if more than one link exists or - // if the interface is an alias. - var links = nic.getValue('links'); - var nicType = nic.getValue('type'); - var allowLinkUp = ( - (angular.isObject(links) && links.length > 1) || - (nicType === "alias")); - angular.forEach(modes, function(mode) { - if (allowLinkUp && mode.mode === "link_up") { - return; - } - // Can't run DHCP twice on one NIC. - if (nicType === "alias" && mode.mode === "dhcp") { - return; - } - filtered.push(mode); - }); + if (!angular.isObject(nic.getValue("subnet"))) { + // No subnet is configure so the only allowed mode + // is 'link_up'. + angular.forEach(modes, function(mode) { + if (mode.mode === "link_up") { + filtered.push(mode); + } + }); + } else { + // Don't add LINK_UP if more than one link exists or + // if the interface is an alias. + var links = nic.getValue("links"); + var nicType = nic.getValue("type"); + var allowLinkUp = + (angular.isObject(links) && links.length > 1) || nicType === "alias"; + angular.forEach(modes, function(mode) { + if (allowLinkUp && mode.mode === "link_up") { + return; + } + // Can't run DHCP twice on one NIC. + if (nicType === "alias" && mode.mode === "dhcp") { + return; } - return filtered; - }; + filtered.push(mode); + }); + } + return filtered; + }; } /* @ngInject */ export function NodeNetworkingController( - $scope, $filter, FabricsManager, VLANsManager, SubnetsManager, - ControllersManager, GeneralManager, UsersManager, - ManagerHelperService, ValidationService, JSONService, $log) { - - // Different interface types. - var INTERFACE_TYPE = { - PHYSICAL: "physical", - BOND: "bond", - BRIDGE: "bridge", - VLAN: "vlan", - ALIAS: "alias" - }; - var INTERFACE_TYPE_TEXTS = { - "physical": "Physical", - "bond": "Bond", - "bridge": "Bridge", - "vlan": "VLAN", - "alias": "Alias" - }; - - // Different link modes for an interface. - var LINK_MODE = { - AUTO: "auto", - STATIC: "static", - DHCP: "dhcp", - LINK_UP: "link_up" - }; - var LINK_MODE_TEXTS = { - "auto": "Auto assign", - "static": "Static assign", - "dhcp": "DHCP", - "link_up": "Unconfigured" - }; - - // Different selection modes. - var SELECTION_MODE = { - NONE: null, - SINGLE: "single", - MULTI: "multi", - DELETE: "delete", - ADD: "add", - CREATE_BOND: "create-bond", - CREATE_BRIDGE: "create-bridge", - CREATE_PHYSICAL: "create-physical", - EDIT: "edit" - }; + $scope, + $filter, + FabricsManager, + VLANsManager, + SubnetsManager, + ControllersManager, + GeneralManager, + UsersManager, + ManagerHelperService, + ValidationService, + JSONService, + $log +) { + // Different interface types. + var INTERFACE_TYPE = { + PHYSICAL: "physical", + BOND: "bond", + BRIDGE: "bridge", + VLAN: "vlan", + ALIAS: "alias" + }; + var INTERFACE_TYPE_TEXTS = { + physical: "Physical", + bond: "Bond", + bridge: "Bridge", + vlan: "VLAN", + alias: "Alias" + }; + + // Different link modes for an interface. + var LINK_MODE = { + AUTO: "auto", + STATIC: "static", + DHCP: "dhcp", + LINK_UP: "link_up" + }; + var LINK_MODE_TEXTS = { + auto: "Auto assign", + static: "Static assign", + dhcp: "DHCP", + link_up: "Unconfigured" + }; + + // Different selection modes. + var SELECTION_MODE = { + NONE: null, + SINGLE: "single", + MULTI: "multi", + DELETE: "delete", + ADD: "add", + CREATE_BOND: "create-bond", + CREATE_BRIDGE: "create-bridge", + CREATE_PHYSICAL: "create-physical", + EDIT: "edit" + }; + + var IP_ASSIGNMENT = { + DYNAMIC: "dynamic", + EXTERNAL: "external", + STATIC: "static" + }; + + // Device ip assignment options. + $scope.ipAssignments = [ + { + name: IP_ASSIGNMENT.EXTERNAL, + text: "External" + }, + { + name: IP_ASSIGNMENT.DYNAMIC, + text: "Dynamic" + }, + { + name: IP_ASSIGNMENT.STATIC, + text: "Static" + } + ]; - var IP_ASSIGNMENT = { - DYNAMIC: "dynamic", - EXTERNAL: "external", - STATIC: "static" - }; + // Set the initial values for this scope. + $scope.loaded = false; + $scope.nodeHasLoaded = false; + $scope.managersHaveLoaded = false; + $scope.tableInfo = { column: "name" }; + $scope.fabrics = FabricsManager.getItems(); + $scope.vlans = VLANsManager.getItems(); + $scope.subnets = SubnetsManager.getItems(); + $scope.interfaces = []; + $scope.interfaceLinksMap = {}; + $scope.interfaceErrorsByLinkId = {}; + $scope.originalInterfaces = {}; + $scope.selectedInterfaces = []; + $scope.selectedMode = null; + $scope.newInterface = {}; + $scope.newBondInterface = {}; + $scope.newBridgeInterface = {}; + $scope.editInterface = null; + $scope.bondOptions = GeneralManager.getData("bond_options"); + $scope.createBondError = null; + $scope.newInterfaceLinkMonitoring = null; + $scope.editInterfaceLinkMonitoring = null; + $scope.isSaving = false; + $scope.modes = [ + { + mode: LINK_MODE.AUTO, + text: LINK_MODE_TEXTS[LINK_MODE.AUTO] + }, + { + mode: LINK_MODE.STATIC, + text: LINK_MODE_TEXTS[LINK_MODE.STATIC] + }, + { + mode: LINK_MODE.DHCP, + text: LINK_MODE_TEXTS[LINK_MODE.DHCP] + }, + { + mode: LINK_MODE.LINK_UP, + text: LINK_MODE_TEXTS[LINK_MODE.LINK_UP] + } + ]; - // Device ip assignment options. - $scope.ipAssignments = [ - { - name: IP_ASSIGNMENT.EXTERNAL, - text: "External" - }, - { - name: IP_ASSIGNMENT.DYNAMIC, - text: "Dynamic" - }, - { - name: IP_ASSIGNMENT.STATIC, - text: "Static" - } - ]; + $scope.isBond = function(item) { + return item.type === "bond"; + }; + + // Sets loaded to true if both the node has been loaded at the + // other required managers for this scope have been loaded. + function updateLoaded() { + $scope.loaded = $scope.nodeHasLoaded && $scope.managersHaveLoaded; + if ($scope.loaded) { + updateInterfaces(); + } + } - // Set the initial values for this scope. - $scope.loaded = false; - $scope.nodeHasLoaded = false; - $scope.managersHaveLoaded = false; - $scope.tableInfo = { column: 'name' }; - $scope.fabrics = FabricsManager.getItems(); - $scope.vlans = VLANsManager.getItems(); - $scope.subnets = SubnetsManager.getItems(); - $scope.interfaces = []; - $scope.interfaceLinksMap = {}; - $scope.interfaceErrorsByLinkId = {}; + // Update the list of interfaces for the node. For each link on the + // interface, the interface is duplicated in the list to make render + // in a data-ng-repeat easier. + function updateInterfaces() { $scope.originalInterfaces = {}; - $scope.selectedInterfaces = []; - $scope.selectedMode = null; - $scope.newInterface = {}; - $scope.newBondInterface = {}; - $scope.newBridgeInterface = {}; - $scope.editInterface = null; - $scope.bondOptions = GeneralManager.getData("bond_options"); - $scope.createBondError = null; - $scope.newInterfaceLinkMonitoring = null; - $scope.editInterfaceLinkMonitoring = null; - $scope.isSaving = false; - $scope.modes = [ - { - mode: LINK_MODE.AUTO, - text: LINK_MODE_TEXTS[LINK_MODE.AUTO] - }, - { - mode: LINK_MODE.STATIC, - text: LINK_MODE_TEXTS[LINK_MODE.STATIC] - }, - { - mode: LINK_MODE.DHCP, - text: LINK_MODE_TEXTS[LINK_MODE.DHCP] - }, - { - mode: LINK_MODE.LINK_UP, - text: LINK_MODE_TEXTS[LINK_MODE.LINK_UP] - } - ]; - - $scope.isBond = function(item) { - return item.type === "bond"; - }; + angular.forEach($scope.node.interfaces, function(nic) { + $scope.originalInterfaces[nic.id] = nic; + }); - // Sets loaded to true if both the node has been loaded at the - // other required managers for this scope have been loaded. - function updateLoaded() { - $scope.loaded = $scope.nodeHasLoaded && $scope.managersHaveLoaded; - if ($scope.loaded) { - updateInterfaces(); + var interfaces = []; + // vlanTable contains data packaged for the 'Served VLANs' section, + // which is essentially Interface LEFT JOIN VLAN LEFT JOIN Subnet. + var vlanTable = []; + // Keep track of VLAN IDs we've processed. + var addedVlans = {}; + + angular.forEach($scope.node.interfaces, function(nic) { + // When a interface has a child that is a bond or bridge. + // Then that interface is not included in the interface list. + // Parent interface with a bond or bridge child can only have + // one child. + if (nic.children.length === 1) { + var child = $scope.originalInterfaces[nic.children[0]]; + if ( + child.type === INTERFACE_TYPE.BOND || + child.type === INTERFACE_TYPE.BRIDGE + ) { + // This parent now has a bond or bridge for a child. + // If this was the editInterface, then it needs to be + // unset. We only need to check the "id" (not + // the "link_id"), because if this interface did have + // aliases they have now been removed. + if ( + angular.isObject($scope.editInterface) && + $scope.editInterface.id === nic.id + ) { + $scope.editInterface = null; + $scope.selectedMode = SELECTION_MODE.NONE; + } + return; } - } + } - // Update the list of interfaces for the node. For each link on the - // interface, the interface is duplicated in the list to make render - // in a data-ng-repeat easier. - function updateInterfaces() { - $scope.originalInterfaces = {}; - angular.forEach($scope.node.interfaces, function(nic) { - $scope.originalInterfaces[nic.id] = nic; + // When the interface is a bond or a bridge, place the children + // as members for that interface. + if ( + nic.type === INTERFACE_TYPE.BOND || + nic.type === INTERFACE_TYPE.BRIDGE + ) { + nic.members = []; + angular.forEach(nic.parents, function(parent) { + nic.members.push(angular.copy($scope.originalInterfaces[parent])); }); + } - var interfaces = []; - // vlanTable contains data packaged for the 'Served VLANs' section, - // which is essentially Interface LEFT JOIN VLAN LEFT JOIN Subnet. - var vlanTable = []; - // Keep track of VLAN IDs we've processed. - var addedVlans = {}; - - angular.forEach($scope.node.interfaces, function(nic) { - // When a interface has a child that is a bond or bridge. - // Then that interface is not included in the interface list. - // Parent interface with a bond or bridge child can only have - // one child. - if (nic.children.length === 1) { - var child = $scope.originalInterfaces[nic.children[0]]; - if (child.type === INTERFACE_TYPE.BOND || - child.type === INTERFACE_TYPE.BRIDGE) { - // This parent now has a bond or bridge for a child. - // If this was the editInterface, then it needs to be - // unset. We only need to check the "id" (not - // the "link_id"), because if this interface did have - // aliases they have now been removed. - if (angular.isObject($scope.editInterface) && - $scope.editInterface.id === nic.id) { - $scope.editInterface = null; - $scope.selectedMode = SELECTION_MODE.NONE; - } - return; - } - } - - // When the interface is a bond or a bridge, place the children - // as members for that interface. - if (nic.type === INTERFACE_TYPE.BOND || - nic.type === INTERFACE_TYPE.BRIDGE) { - nic.members = []; - angular.forEach(nic.parents, function(parent) { - nic.members.push( - angular.copy($scope.originalInterfaces[parent])); - }); - } - - // Format the tags when they have not already been formatted. - if (angular.isArray(nic.tags) && - nic.tags.length > 0 && - !angular.isString(nic.tags[0].text)) { - nic.tags = formatTags(nic.tags); - } - - nic.vlan = VLANsManager.getItemFromList(nic.vlan_id); - if (angular.isObject(nic.vlan)) { - nic.fabric = FabricsManager.getItemFromList( - nic.vlan.fabric); - - // Build the vlanTable for controller detail page. - if ($scope.$parent.isController) { - // Skip duplicate VLANs (by id, they can share names). - if (!(Object.prototype.hasOwnProperty.call( - addedVlans, nic.vlan.id))) { - addedVlans[nic.vlan.id] = true; - var vlanRecord = { - "fabric": nic.fabric, - "vlan": nic.vlan, - "subnets": $filter('filter')( - $scope.subnets, { vlan: nic.vlan.id }, true), - "primary_rack": null, - "secondary_rack": null, - "sort_key": nic.fabric.name + "|" + - $scope.getVLANText(nic.vlan) - }; - if (nic.vlan.primary_rack) { - vlanRecord.primary_rack = - ControllersManager.getItemFromList( - nic.vlan.primary_rack); - } - if (nic.vlan.secondary_rack) { - vlanRecord.secondary_rack = - ControllersManager.getItemFromList( - nic.vlan.secondary_rack); - } - vlanTable.push(vlanRecord); - } - // Sort the table by (VLANText, fabric.name). - vlanTable.sort(function(a, b) { - return a.sort_key.localeCompare(b.sort_key); - }); - } - } + // Format the tags when they have not already been formatted. + if ( + angular.isArray(nic.tags) && + nic.tags.length > 0 && + !angular.isString(nic.tags[0].text) + ) { + nic.tags = formatTags(nic.tags); + } + + nic.vlan = VLANsManager.getItemFromList(nic.vlan_id); + if (angular.isObject(nic.vlan)) { + nic.fabric = FabricsManager.getItemFromList(nic.vlan.fabric); - // Update the interface based on its links or duplicate the - // interface if it has multiple links. - if (nic.links.length === 0) { - // No links on this interface. The interface is either - // disabled or has no links (which means the interface - // is in LINK_UP mode). - nic.link_id = -1; - nic.subnet = null; - nic.mode = LINK_MODE.LINK_UP; - nic.ip_address = ""; - interfaces.push(nic); - } else { - var idx = 0; - angular.forEach(nic.links, function(link) { - var nic_copy = angular.copy(nic); - nic_copy.link_id = link.id; - nic_copy.subnet = SubnetsManager.getItemFromList( - link.subnet_id); - nic_copy.mode = link.mode; - nic_copy.ip_address = link.ip_address; - if (angular.isUndefined(nic_copy.ip_address)) { - nic_copy.ip_address = ""; - } - // We don't want to deep copy the VLAN and fabric - // object so we set those back to the original. - nic_copy.vlan = nic.vlan; - nic_copy.fabric = nic.fabric; - if (idx > 0) { - // Each extra link is an alais on the original - // interface. - nic_copy.type = INTERFACE_TYPE.ALIAS; - nic_copy.name += ":" + idx; - } - idx++; - interfaces.push(nic_copy); - }); - } - }); + // Build the vlanTable for controller detail page. + if ($scope.$parent.isController) { + // Skip duplicate VLANs (by id, they can share names). + if (!Object.prototype.hasOwnProperty.call(addedVlans, nic.vlan.id)) { + addedVlans[nic.vlan.id] = true; + var vlanRecord = { + fabric: nic.fabric, + vlan: nic.vlan, + subnets: $filter("filter")( + $scope.subnets, + { vlan: nic.vlan.id }, + true + ), + primary_rack: null, + secondary_rack: null + }; - // Update the scopes interfaces. - $scope.interfaces = interfaces; - $scope.vlanTable = vlanTable; - - // Update the scope interface links mapping. - $scope.interfaceLinksMap = {}; - angular.forEach($scope.interfaces, function(nic) { - var linkMaps = $scope.interfaceLinksMap[nic.id]; - if (angular.isUndefined(linkMaps)) { - linkMaps = {}; - $scope.interfaceLinksMap[nic.id] = linkMaps; - } - linkMaps[nic.link_id] = nic; + if (angular.isObject(nic.fabric)) { + vlanRecord.sort_key = + nic.fabric.name + "|" + $scope.getVLANText(nic.vlan); + } + if (nic.vlan.primary_rack) { + vlanRecord.primary_rack = ControllersManager.getItemFromList( + nic.vlan.primary_rack + ); + } + if (nic.vlan.secondary_rack) { + vlanRecord.secondary_rack = ControllersManager.getItemFromList( + nic.vlan.secondary_rack + ); + } + vlanTable.push(vlanRecord); + } + // Sort the table by (VLANText, fabric.name). + vlanTable.sort(function(a, b) { + return a.sort_key.localeCompare(b.sort_key); + }); + } + } + + // Update the interface based on its links or duplicate the + // interface if it has multiple links. + if (nic.links.length === 0) { + // No links on this interface. The interface is either + // disabled or has no links (which means the interface + // is in LINK_UP mode). + nic.link_id = -1; + nic.subnet = null; + nic.mode = LINK_MODE.LINK_UP; + nic.ip_address = ""; + interfaces.push(nic); + } else { + var idx = 0; + angular.forEach(nic.links, function(link) { + var nic_copy = angular.copy(nic); + nic_copy.link_id = link.id; + nic_copy.subnet = SubnetsManager.getItemFromList(link.subnet_id); + nic_copy.mode = link.mode; + nic_copy.ip_address = link.ip_address; + if (angular.isUndefined(nic_copy.ip_address)) { + nic_copy.ip_address = ""; + } + // We don't want to deep copy the VLAN and fabric + // object so we set those back to the original. + nic_copy.vlan = nic.vlan; + nic_copy.fabric = nic.fabric; + if (idx > 0) { + // Each extra link is an alais on the original + // interface. + nic_copy.type = INTERFACE_TYPE.ALIAS; + nic_copy.name += ":" + idx; + } + idx++; + interfaces.push(nic_copy); }); + } + }); - // Clear the editInterface if it no longer exists in the - // interfaceLinksMap. - if (angular.isObject($scope.editInterface)) { - var links = $scope.interfaceLinksMap[$scope.editInterface.id]; - if (angular.isUndefined(links)) { - $scope.editInterface = null; - $scope.selectedMode = SELECTION_MODE.NONE; - } else { - var link = links[$scope.editInterface.link_id]; - if (angular.isUndefined(link)) { - $scope.editInterface = null; - $scope.selectedMode = SELECTION_MODE.NONE; - } - } - } + // Update the scopes interfaces. + $scope.interfaces = interfaces; + $scope.vlanTable = vlanTable; - // Update newInterface.parent if it has changed. - updateNewInterface(); - } + // Update the scope interface links mapping. + $scope.interfaceLinksMap = {}; + angular.forEach($scope.interfaces, function(nic) { + var linkMaps = $scope.interfaceLinksMap[nic.id]; + if (angular.isUndefined(linkMaps)) { + linkMaps = {}; + $scope.interfaceLinksMap[nic.id] = linkMaps; + } + linkMaps[nic.link_id] = nic; + }); - // Return the original link object for the given interface. - function mapNICToOriginalLink(nic_id, link_id) { - var originalInteface = $scope.originalInterfaces[nic_id]; - if (angular.isObject(originalInteface)) { - var i, link = null; - for (i = 0; i < originalInteface.links.length; i++) { - link = originalInteface.links[i]; - if (link.id === link_id) { - break; - } - } - return link; - } else { - return null; + // Clear the editInterface if it no longer exists in the + // interfaceLinksMap. + if (angular.isObject($scope.editInterface)) { + var links = $scope.interfaceLinksMap[$scope.editInterface.id]; + if (angular.isUndefined(links)) { + $scope.editInterface = null; + $scope.selectedMode = SELECTION_MODE.NONE; + } else { + var link = links[$scope.editInterface.link_id]; + if (angular.isUndefined(link)) { + $scope.editInterface = null; + $scope.selectedMode = SELECTION_MODE.NONE; } + } } - // Leave single selection mode. - function leaveSingleSelectionMode() { - if ($scope.selectedMode === SELECTION_MODE.SINGLE || - $scope.selectedMode === SELECTION_MODE.ADD || - $scope.selectedMode === SELECTION_MODE.DELETE) { - $scope.selectedMode = SELECTION_MODE.NONE; - } + // Update newInterface.parent if it has changed. + updateNewInterface(); + } + + // Return the original link object for the given interface. + function mapNICToOriginalLink(nic_id, link_id) { + var originalInteface = $scope.originalInterfaces[nic_id]; + if (angular.isObject(originalInteface)) { + var i, + link = null; + for (i = 0; i < originalInteface.links.length; i++) { + link = originalInteface.links[i]; + if (link.id === link_id) { + break; + } + } + return link; + } else { + return null; } + } - // Update the new interface since the interfaces list has - // been reloaded. - function updateNewInterface() { - if (angular.isObject($scope.newInterface.parent)) { - var parentId = $scope.newInterface.parent.id; - var linkId = $scope.newInterface.parent.link_id; - var links = $scope.interfaceLinksMap[parentId]; - if (angular.isObject(links)) { - var newParent = links[linkId]; - if (angular.isObject(newParent)) { - $scope.newInterface.parent = newParent; - - var iType = $scope.newInterface.type; - var isAlias = iType === INTERFACE_TYPE.ALIAS; - var isVLAN = iType === INTERFACE_TYPE.VLAN; - var canAddAlias = $scope.canAddAlias(newParent); - var canAddVLAN = $scope.canAddVLAN(newParent); - if (!canAddAlias && !canAddVLAN) { - // Cannot do any adding now. - $scope.newInterface = {}; - leaveSingleSelectionMode(); - } else { - if (isAlias && !canAddAlias && canAddVLAN) { - $scope.newInterface.type = "vlan"; - $scope.addTypeChanged(); - } else if (isVLAN && !canAddVLAN && canAddAlias) { - $scope.newInterface.type = "alias"; - $scope.addTypeChanged(); - } - } - return; - } - } + // Leave single selection mode. + function leaveSingleSelectionMode() { + if ( + $scope.selectedMode === SELECTION_MODE.SINGLE || + $scope.selectedMode === SELECTION_MODE.ADD || + $scope.selectedMode === SELECTION_MODE.DELETE + ) { + $scope.selectedMode = SELECTION_MODE.NONE; + } + } - // Parent no longer exists. Exit the single selection modes. + // Update the new interface since the interfaces list has + // been reloaded. + function updateNewInterface() { + if (angular.isObject($scope.newInterface.parent)) { + var parentId = $scope.newInterface.parent.id; + var linkId = $scope.newInterface.parent.link_id; + var links = $scope.interfaceLinksMap[parentId]; + if (angular.isObject(links)) { + var newParent = links[linkId]; + if (angular.isObject(newParent)) { + $scope.newInterface.parent = newParent; + + var iType = $scope.newInterface.type; + var isAlias = iType === INTERFACE_TYPE.ALIAS; + var isVLAN = iType === INTERFACE_TYPE.VLAN; + var canAddAlias = $scope.canAddAlias(newParent); + var canAddVLAN = $scope.canAddVLAN(newParent); + if (!canAddAlias && !canAddVLAN) { + // Cannot do any adding now. $scope.newInterface = {}; leaveSingleSelectionMode(); - } - } - - // Return the default VLAN for a fabric. - function getDefaultVLAN(fabric) { - return VLANsManager.getItemFromList(fabric.default_vlan_id); - } - - // Return list of unused VLANs for an interface. Also remove the - // ignoreVLANs from the returned list. - function getUnusedVLANs(nic, ignoreVLANs) { - var vlans = $filter('removeDefaultVLAN')($scope.vlans); - vlans = $filter('filterByFabric')(vlans, nic.fabric); - vlans = $filter('filterByUnusedForInterface')( - vlans, nic, $scope.originalInterfaces); - - // Remove the VLAN's that should be ignored when getting the unused - // VLANs. This is done to help the selection of the next default. - if (angular.isUndefined(ignoreVLANs)) { - ignoreVLANs = []; - } - angular.forEach(ignoreVLANs, function(vlan) { - var i; - for (i = 0; i < vlans.length; i++) { - if (vlans[i].id === vlan.id) { - vlans.splice(i, 1); - break; - } - } - }); - return vlans; - } - - // Return the currently selected interface objects. - function getSelectedInterfaces() { - var interfaces = []; - angular.forEach($scope.selectedInterfaces, function(key) { - var splitKey = key.split('/'); - var links = $scope.interfaceLinksMap[splitKey[0]]; - if (angular.isObject(links)) { - var nic = links[splitKey[1]]; - if (angular.isObject(nic)) { - interfaces.push(nic); - } - } - }); - return interfaces; - } - - // Get the next available name. - function getNextName(prefix) { - var idx = 0; - angular.forEach($scope.originalInterfaces, function(nic) { - if (nic.name === prefix + idx) { - idx++; - } - }); - return prefix + idx; + } else { + if (isAlias && !canAddAlias && canAddVLAN) { + $scope.newInterface.type = "vlan"; + $scope.addTypeChanged(); + } else if (isVLAN && !canAddVLAN && canAddAlias) { + $scope.newInterface.type = "alias"; + $scope.addTypeChanged(); + } + } + return; + } + } + + // Parent no longer exists. Exit the single selection modes. + $scope.newInterface = {}; + leaveSingleSelectionMode(); } + } - // Return the tags formatted for ngTagInput. - function formatTags(tags) { - var formatted = []; - angular.forEach(tags, function(tag) { - formatted.push({ text: tag }); - }); - return formatted; + // Return the default VLAN for a fabric. + function getDefaultVLAN(fabric) { + return VLANsManager.getItemFromList(fabric.default_vlan_id); + } + + // Return list of unused VLANs for an interface. Also remove the + // ignoreVLANs from the returned list. + function getUnusedVLANs(nic, ignoreVLANs) { + var vlans = $filter("removeDefaultVLAN")($scope.vlans); + vlans = $filter("filterByFabric")(vlans, nic.fabric); + vlans = $filter("filterByUnusedForInterface")( + vlans, + nic, + $scope.originalInterfaces + ); + + // Remove the VLAN's that should be ignored when getting the unused + // VLANs. This is done to help the selection of the next default. + if (angular.isUndefined(ignoreVLANs)) { + ignoreVLANs = []; } - - // Called by $parent when the node has been loaded. - $scope.nodeLoaded = function() { - $scope.$watch("node.interfaces", updateInterfaces); - // Watch subnets for the served VLANs section. - if ($scope.$parent.isController) { - $scope.$watch("subnets", updateInterfaces, true); - } - $scope.nodeHasLoaded = true; - updateLoaded(); - }; - - // Return true if only the name or mac address of an interface can - // be edited. - $scope.isLimitedEditingAllowed = function(nic) { - if (!$scope.canEdit()) { - // If the user is not the superuser, pretend it's not Ready. - return false; + angular.forEach(ignoreVLANs, function(vlan) { + var i; + for (i = 0; i < vlans.length; i++) { + if (vlans[i].id === vlan.id) { + vlans.splice(i, 1); + break; } - if ($scope.$parent.isController || $scope.$parent.isDevice) { - // Controllers and Devices are never in limited mode. - return false; - } - return ( - angular.isObject($scope.node) && - $scope.node.status === "Deployed" && - nic.type !== "vlan"); - }; - - // Return true if the networking information cannot be edited. - // (it can't be changed when the node is in any state other - // than Ready or Broken and the user is not a superuser) - $scope.isAllNetworkingDisabled = function() { - if (!$scope.canEdit() && !$scope.$parent.isDevice) { - // If the user is not a superuser and not looking at a - // device, disable the networking panel. - return true; - } - if ($scope.$parent.isController || $scope.$parent.isDevice) { - // Never disable the full networking panel when its a - // Controller or Device. - return false; - } - if (angular.isObject($scope.node) && - ["New", "Ready", "Allocated", "Broken"].indexOf( - $scope.node.status) === -1) { - // If a non-controller node is not ready allocated, or broken, - // disable networking panel. - return true; - } - // User must be a superuser and the node must be - // either ready or broken. Enable it. - return false; - }; + } + }); + return vlans; + } - // Return true if the interface is the boot interface or has a parent - // that is the boot interface. - $scope.isBootInterface = function(nic) { - if (!angular.isObject(nic)) { - return false; + // Return the currently selected interface objects. + function getSelectedInterfaces() { + var interfaces = []; + angular.forEach($scope.selectedInterfaces, function(key) { + var splitKey = key.split("/"); + var links = $scope.interfaceLinksMap[splitKey[0]]; + if (angular.isObject(links)) { + var nic = links[splitKey[1]]; + if (angular.isObject(nic)) { + interfaces.push(nic); } + } + }); + return interfaces; + } - if (nic.is_boot && nic.type !== INTERFACE_TYPE.ALIAS) { - return true; - } else if (nic.type === INTERFACE_TYPE.BOND || - nic.type === INTERFACE_TYPE.BRIDGE) { - var i; - for (i = 0; i < nic.members.length; i++) { - if (nic.members[i].is_boot) { - return true; - } - } - } - return false; - }; + // Get the next available name. + function getNextName(prefix) { + var idx = 0; + angular.forEach($scope.originalInterfaces, function(nic) { + if (nic.name === prefix + idx) { + idx++; + } + }); + return prefix + idx; + } - // Get the text for the type of the interface. - $scope.getInterfaceTypeText = function(nic) { - var text = INTERFACE_TYPE_TEXTS[nic.type]; - if (angular.isDefined(text)) { - return text; - } else { - return nic.type; - } - }; + // Return the tags formatted for ngTagInput. + function formatTags(tags) { + var formatted = []; + angular.forEach(tags, function(tag) { + formatted.push({ text: tag }); + }); + return formatted; + } - // Get the text for the link mode of the interface. - $scope.getLinkModeText = function(nic) { - var text = LINK_MODE_TEXTS[nic.mode]; - if (angular.isDefined(text)) { - return text; - } else { - return nic.mode; - } - }; + // Called by $parent when the node has been loaded. + $scope.nodeLoaded = function() { + $scope.$watch("node.interfaces", updateInterfaces); + // Watch subnets for the served VLANs section. + if ($scope.$parent.isController) { + $scope.$watch("subnets", updateInterfaces, true); + } + $scope.nodeHasLoaded = true; + updateLoaded(); + }; + + // Return true if only the name or mac address of an interface can + // be edited. + $scope.isLimitedEditingAllowed = function(nic) { + if (!$scope.canEdit()) { + // If the user is not the superuser, pretend it's not Ready. + return false; + } + if ($scope.$parent.isController || $scope.$parent.isDevice) { + // Controllers and Devices are never in limited mode. + return false; + } + return ( + angular.isObject($scope.node) && + $scope.node.status === "Deployed" && + nic.type !== "vlan" + ); + }; + + // Return true if the networking information cannot be edited. + // (it can't be changed when the node is in any state other + // than Ready or Broken and the user is not a superuser) + $scope.isAllNetworkingDisabled = function() { + if (!$scope.canEdit() && !$scope.$parent.isDevice) { + // If the user is not a superuser and not looking at a + // device, disable the networking panel. + return true; + } + if ($scope.$parent.isController || $scope.$parent.isDevice) { + // Never disable the full networking panel when its a + // Controller or Device. + return false; + } + if ( + angular.isObject($scope.node) && + ["New", "Ready", "Allocated", "Broken"].indexOf($scope.node.status) === -1 + ) { + // If a non-controller node is not ready allocated, or broken, + // disable networking panel. + return true; + } + // User must be a superuser and the node must be + // either ready or broken. Enable it. + return false; + }; - // Get the text to display in the VLAN dropdown. - $scope.getVLANText = function(vlan) { - if (!angular.isObject(vlan)) { - return ""; - } + // Return true if the interface is the boot interface or has a parent + // that is the boot interface. + $scope.isBootInterface = function(nic) { + if (!angular.isObject(nic)) { + return false; + } - if (vlan.vid === 0) { - return "untagged"; - } else if (angular.isString(vlan.name) && vlan.name.length > 0) { - return vlan.vid + " (" + vlan.name + ")"; - } else { - return vlan.vid; + if (nic.is_boot && nic.type !== INTERFACE_TYPE.ALIAS) { + return true; + } else if ( + nic.type === INTERFACE_TYPE.BOND || + nic.type === INTERFACE_TYPE.BRIDGE + ) { + var i; + for (i = 0; i < nic.members.length; i++) { + if (nic.members[i].is_boot) { + return true; } - }; + } + } + return false; + }; - // Get the text to display in the subnet dropdown. - $scope.getSubnetText = function(subnet) { - if (!angular.isObject(subnet)) { - return "Unconfigured"; - } else if (angular.isString(subnet.name) && - subnet.name.length > 0 && - subnet.cidr !== subnet.name) { - return subnet.cidr + " (" + subnet.name + ")"; - } else { - return subnet.cidr; - } - }; + // Get the text for the type of the interface. + $scope.getInterfaceTypeText = function(nic) { + var text = INTERFACE_TYPE_TEXTS[nic.type]; + if (angular.isDefined(text)) { + return text; + } else { + return nic.type; + } + }; - // Get the subnet from its ID. - $scope.getSubnet = function(subnetId) { - return SubnetsManager.getItemFromList(subnetId); - }; + // Get the text for the link mode of the interface. + $scope.getLinkModeText = function(nic) { + var text = LINK_MODE_TEXTS[nic.mode]; + if (angular.isDefined(text)) { + return text; + } else { + return nic.mode; + } + }; - // Show button for editing interfaces - $scope.showEditButton = function(item, interfaces) { - if (item.type !== "bond") { - return false; - } + // Get the text to display in the VLAN dropdown. + $scope.getVLANText = function(vlan) { + if (!angular.isObject(vlan)) { + return ""; + } - interfaces = $filter("filterEditInterface")( - interfaces, - $scope.editInterface - ); + if (vlan.vid === 0) { + return "untagged"; + } else if (angular.isString(vlan.name) && vlan.name.length > 0) { + return vlan.vid + " (" + vlan.name + ")"; + } else { + return vlan.vid; + } + }; - if (item.members.length <= 2 && !interfaces.length) { - return false; - } + // Get the text to display in the subnet dropdown. + $scope.getSubnetText = function(subnet) { + if (!angular.isObject(subnet)) { + return "Unconfigured"; + } else if ( + angular.isString(subnet.name) && + subnet.name.length > 0 && + subnet.cidr !== subnet.name + ) { + return subnet.cidr + " (" + subnet.name + ")"; + } else { + return subnet.cidr; + } + }; - return true; - }; + // Get the subnet from its ID. + $scope.getSubnet = function(subnetId) { + return SubnetsManager.getItemFromList(subnetId); + }; + + // Show button for editing interfaces + $scope.showEditButton = function(item, interfaces) { + if (item.type !== "bond") { + return false; + } - $scope.showCreateEditButton = function() { - var items = $filter("filterSelectedInterfaces")( - $scope.interfaces, - $scope.selectedInterfaces, - $scope.newBondInterface - ); + interfaces = $filter("filterEditInterface")( + interfaces, + $scope.editInterface + ); - if (items.length || $scope.selectedInterfaces.length > 2) { - return true; - } + if (item.members.length <= 2 && !interfaces.length) { + return false; + } - return false; - }; + return true; + }; - // Return True if the interface name that the user typed is invalid. - $scope.isInterfaceNameInvalid = function(nic) { - if (!angular.isObject(nic) || !nic.hasOwnProperty('name') || - nic.name.length === 0) { - return true; - } else if (angular.isArray($scope.node.interfaces)) { - var i; - for (i = 0; i < $scope.node.interfaces.length; i++) { - var otherNic = $scope.node.interfaces[i]; - if (otherNic.name === nic.name && otherNic.id !== nic.id) { - return true; - } - } - } - return false; - }; + $scope.showCreateEditButton = function() { + var items = $filter("filterSelectedInterfaces")( + $scope.interfaces, + $scope.selectedInterfaces, + $scope.newBondInterface + ); - // Return True if the link mode select should be disabled. - $scope.isLinkModeDisabled = function(nic) { - // This is only disabled when a subnet has not been selected. - if (angular.isFunction(nic.getValue)) { - return !angular.isObject(nic.getValue('subnet')); - } else { - return !angular.isObject(nic.subnet); - } - }; + if (items.length || $scope.selectedInterfaces.length > 2) { + return true; + } - // Return the interface errors. - $scope.getInterfaceError = function(nic) { - if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { - return $scope.interfaceErrorsByLinkId[nic.link_id]; - } - return null; - }; + return false; + }; - // Return True if the interface IP address that the user typed is - // invalid. - $scope.isIPAddressInvalid = function(nic) { - if (angular.isString(nic.ip_address) && nic.mode === 'static') { - return ( - !ValidationService.validateIP(nic.ip_address) || - !ValidationService.validateIPInNetwork( - nic.ip_address, nic.subnet.cidr)); - } else { - return false; + // Return True if the interface name that the user typed is invalid. + $scope.isInterfaceNameInvalid = function(nic) { + if ( + !angular.isObject(nic) || + !nic.hasOwnProperty("name") || + nic.name.length === 0 + ) { + return true; + } else if (angular.isArray($scope.node.interfaces)) { + var i; + for (i = 0; i < $scope.node.interfaces.length; i++) { + var otherNic = $scope.node.interfaces[i]; + if (otherNic.name === nic.name && otherNic.id !== nic.id) { + return true; } - }; + } + } + return false; + }; + // Return True if the link mode select should be disabled. + $scope.isLinkModeDisabled = function(nic) { + // This is only disabled when a subnet has not been selected. + if (angular.isFunction(nic.getValue)) { + return !angular.isObject(nic.getValue("subnet")); + } else { + return !angular.isObject(nic.subnet); + } + }; - // Return unique key for the interface. - $scope.getUniqueKey = function(nic) { - return nic.id + "/" + nic.link_id; - }; + // Return the interface errors. + $scope.getInterfaceError = function(nic) { + if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { + return $scope.interfaceErrorsByLinkId[nic.link_id]; + } + return null; + }; - // Toggle selection of the interface. - $scope.toggleInterfaceSelect = function(nic) { - var key = $scope.getUniqueKey(nic); - var idx = $scope.selectedInterfaces.indexOf(key); + // Return True if the interface IP address that the user typed is + // invalid. + $scope.isIPAddressInvalid = function(nic) { + if (angular.isString(nic.ip_address) && nic.mode === "static") { + return ( + !ValidationService.validateIP(nic.ip_address) || + !ValidationService.validateIPInNetwork(nic.ip_address, nic.subnet.cidr) + ); + } else { + return false; + } + }; - function removeSelectedInterface(index) { - $scope.selectedInterfaces.splice(index, 1); - } + // Return unique key for the interface. + $scope.getUniqueKey = function(nic) { + return nic.id + "/" + nic.link_id; + }; + + // Toggle selection of the interface. + $scope.toggleInterfaceSelect = function(nic) { + var key = $scope.getUniqueKey(nic); + var idx = $scope.selectedInterfaces.indexOf(key); - function interfaceIsSelected() { - return idx > -1; - } + function removeSelectedInterface(index) { + $scope.selectedInterfaces.splice(index, 1); + } - if (interfaceIsSelected()) { - removeSelectedInterface(idx); - } else { - $scope.selectedInterfaces.push(key); - } + function interfaceIsSelected() { + return idx > -1; + } - function removeCurrentItem(currentItem, items) { - return items.filter(function(item) { - var itemId = angular.isObject(item) ? item.id : item; - return itemId !== currentItem.id; - }); - } + if (interfaceIsSelected()) { + removeSelectedInterface(idx); + } else { + $scope.selectedInterfaces.push(key); + } - function getCurrentItem(currentItem, items) { - return items.filter(function(item) { - var itemId = angular.isObject(item) ? item.id : item; - return itemId === currentItem.id; - }); - } + function removeCurrentItem(currentItem, items) { + return items.filter(function(item) { + var itemId = angular.isObject(item) ? item.id : item; + return itemId !== currentItem.id; + }); + } - if ($scope.newBondInterface && $scope.newBondInterface.parents) { - var parents = $scope.newBondInterface.parents; - var filteredParents = removeCurrentItem(nic, parents); - - if (interfaceIsSelected()) { - $scope.newBondInterface.parents = filteredParents; - $scope.newBondInterface.primary = filteredParents[0]; - $scope.newBondInterface.mac_address = - filteredParents[0].mac_address; - if (!getCurrentItem(nic, $scope.interfaces)) { - $scope.interfaces.push(nic); - } - } else { - $scope.newBondInterface.parents.push(nic); - } - } + function getCurrentItem(currentItem, items) { + return items.filter(function(item) { + var itemId = angular.isObject(item) ? item.id : item; + return itemId === currentItem.id; + }); + } - function isMultipleSelectedInterfaces() { - return $scope.selectedInterfaces.length > 1; - } + if ($scope.newBondInterface && $scope.newBondInterface.parents) { + var parents = $scope.newBondInterface.parents; + var filteredParents = removeCurrentItem(nic, parents); + + if (interfaceIsSelected()) { + $scope.newBondInterface.parents = filteredParents; + $scope.newBondInterface.primary = filteredParents[0]; + $scope.newBondInterface.mac_address = filteredParents[0].mac_address; + if (!getCurrentItem(nic, $scope.interfaces)) { + $scope.interfaces.push(nic); + } + } else { + $scope.newBondInterface.parents.push(nic); + } + } - if (isMultipleSelectedInterfaces()) { - if ($scope.selectedMode !== SELECTION_MODE.BOND) { - if (!$scope.isShowingCreateBond()) { - $scope.selectedMode = SELECTION_MODE.MULTI; - } - } - } else if ($scope.selectedInterfaces.length === 1) { - if (!$scope.isShowingCreateBond()) { - $scope.selectedMode = SELECTION_MODE.SINGLE; - } - } else { - if (!$scope.isShowingCreateBond()) { - $scope.selectedMode = SELECTION_MODE.NONE; - } - } - }; + function isMultipleSelectedInterfaces() { + return $scope.selectedInterfaces.length > 1; + } - $scope.toggleEditInterfaceSelect = function (nic) { - var key = $scope.getUniqueKey(nic); - var keyIndex = $scope.selectedInterfaces.indexOf(key); - - if (keyIndex === -1) { - // Select item - $scope.selectedInterfaces.push(key); - - // Add to members - if (!$scope.editInterface.members.find(function (item) { - return item.id === nic.id; - })) { - $scope.editInterface.members.push(nic); - } + if (isMultipleSelectedInterfaces()) { + if ($scope.selectedMode !== SELECTION_MODE.BOND) { + if (!$scope.isShowingCreateBond()) { + $scope.selectedMode = SELECTION_MODE.MULTI; + } + } + } else if ($scope.selectedInterfaces.length === 1) { + if (!$scope.isShowingCreateBond()) { + $scope.selectedMode = SELECTION_MODE.SINGLE; + } + } else { + if (!$scope.isShowingCreateBond()) { + $scope.selectedMode = SELECTION_MODE.NONE; + } + } + }; - // Add to parents - if (!$scope.editInterface.parents.find(function (item) { - return item === nic.id; - })) { - $scope.editInterface.parents.push(nic.id); - } + $scope.toggleEditInterfaceSelect = function(nic) { + var key = $scope.getUniqueKey(nic); + var keyIndex = $scope.selectedInterfaces.indexOf(key); + + if (keyIndex === -1) { + // Select item + $scope.selectedInterfaces.push(key); + + // Add to members + if ( + !$scope.editInterface.members.find(function(item) { + return item.id === nic.id; + }) + ) { + $scope.editInterface.members.push(nic); + } + + // Add to parents + if ( + !$scope.editInterface.parents.find(function(item) { + return item === nic.id; + }) + ) { + $scope.editInterface.parents.push(nic.id); + } + + // Remove from unselected rows + var selectedInterface = $scope.interfaces.find(function(item) { + return item.id === nic.id; + }); + + var selectedIndex = $scope.interfaces.indexOf(selectedInterface); + + if (selectedIndex !== -1) { + $scope.interfaces.splice(selectedIndex, 1); + } + } else { + // Unselect item + $scope.selectedInterfaces.splice(keyIndex, 1); + + // Add to unselected rows + if ( + !$scope.interfaces.find(function(item) { + return item.id === nic.id; + }) + ) { + nic.fabric = $scope.editInterface.fabric; + nic.vlan = $scope.editInterface.vlan; + $scope.interfaces.push(nic); + } + + // Remove from members + var member = $scope.editInterface.members.find(function(item) { + return item.id === nic.id; + }); + + var memberIndex = $scope.editInterface.members.indexOf(member); + + if (memberIndex !== -1) { + $scope.editInterface.members.splice(memberIndex, 1); + } + + // Remove from parents + var parentIndex = $scope.editInterface.parents.indexOf(nic.id); + + if (parentIndex !== -1) { + $scope.editInterface.parents.splice(parentIndex, 1); + } + + // Reset primary and mac address + var primaryMember = $scope.editInterface.members[0]; + + if ($scope.editInterface.primary.id === nic.id) { + $scope.editInterface.primary = primaryMember; + } + + if ( + $scope.editInterface.mac_address === nic.mac_address && + primaryMember + ) { + $scope.editInterface.mac_address = primaryMember.mac_address; + } + } + }; - // Remove from unselected rows - var selectedInterface = $scope.interfaces.find(function (item) { - return item.id === nic.id; - }); + // Return true when the interface is selected. + $scope.isInterfaceSelected = function(nic) { + return $scope.selectedInterfaces.indexOf($scope.getUniqueKey(nic)) > -1; + }; + + // Returns true if the interface is not selected + $scope.cannotEditInterface = function(nic) { + if ($scope.selectedMode === SELECTION_MODE.NONE) { + return false; + } else if ( + $scope.selectedMode !== SELECTION_MODE.MULTI && + $scope.isInterfaceSelected(nic) + ) { + return false; + } else { + return true; + } + }; - var selectedIndex = $scope.interfaces.indexOf(selectedInterface); + // Return true if in editing mode for the interface. + $scope.isEditing = function(nic) { + if ($scope.selectedMode !== SELECTION_MODE.EDIT) { + return false; + } else { + return $scope.editInterface.id === nic.id; + } + }; - if (selectedIndex !== -1) { - $scope.interfaces.splice(selectedIndex, 1); - } - } else { - // Unselect item - $scope.selectedInterfaces.splice(keyIndex, 1); - - // Add to unselected rows - if (!$scope.interfaces.find(function (item) { - return item.id === nic.id; - })) { - nic.fabric = $scope.editInterface.fabric; - nic.vlan = $scope.editInterface.vlan; - $scope.interfaces.push(nic); - } + // Start editing this interface. + $scope.edit = function(nic) { + $scope.isShowingInterfaces = false; + $scope.selectedInterfaces = [$scope.getUniqueKey(nic)]; + $scope.selectedMode = SELECTION_MODE.EDIT; + if ($scope.$parent.isDevice) { + $scope.editInterface = { + id: nic.id, + name: nic.name, + mac_address: nic.mac_address, + tags: nic.tags.map(function(tag) { + return tag.text; + }), + subnet: nic.subnet, + ip_address: nic.ip_address, + ip_assignment: nic.ip_assignment, + link_id: nic.link_id, + type: nic.type, + bridge_fd: nic.params.bridge_fd, + bridge_stp: nic.params.bridge_stp, + bond_mode: nic.params.bond_mode, + xmitHashPolicy: nic.params.bond_xmit_hash_policy, + lacpRate: nic.params.bond_lacp_rate, + bond_downdelay: nic.params.bond_downdelay, + bond_updelay: nic.params.bond_updelay, + bond_miimon: nic.params.bond_miimon + }; + if (angular.isDefined(nic.subnet) && nic.subnet !== null) { + $scope.editInterface.defaultSubnet = nic.subnet; + } else { + $scope.editInterface.defaultSubnet = $scope.subnets[0]; + } + } else { + $scope.editInterface = { + id: nic.id, + name: nic.name, + mac_address: nic.mac_address, + tags: nic.tags.map(function(tag) { + return tag.text; + }), + fabric: nic.fabric, + vlan: nic.vlan, + subnet: nic.subnet, + mode: nic.mode, + ip_address: nic.ip_address, + link_id: nic.link_id, + type: nic.type, + bridge_fd: nic.params.bridge_fd, + bridge_stp: nic.params.bridge_stp, + bond_mode: nic.params.bond_mode, + xmitHashPolicy: nic.params.bond_xmit_hash_policy, + lacpRate: nic.params.bond_lacp_rate, + bond_downdelay: nic.params.bond_downdelay, + bond_updelay: nic.params.bond_updelay, + bond_miimon: nic.params.bond_miimon + }; + + $scope.editInterface.parents = nic.parents; + $scope.editInterface.members = nic.members; + if (nic.members && nic.members.length) { + $scope.editInterface.primary = nic.members[0]; + } else { + $scope.editInterface.primary = null; + } + + if (nic.members) { + nic.members.forEach(function(member) { + $scope.selectedInterfaces.push($scope.getUniqueKey(member)); + }); + } + } + }; - // Remove from members - var member = $scope.editInterface.members.find(function (item) { - return item.id === nic.id; - }); + // Called when the fabric is changed. + $scope.fabricChanged = function(nic) { + // Update the VLAN on the node to be the default VLAN for that + // fabric. The first VLAN for the fabric is the default. + if (nic.fabric !== null) { + nic.vlan = getDefaultVLAN(nic.fabric); + } else { + nic.vlan = null; + } + $scope.vlanChanged(nic); + }; - var memberIndex = $scope.editInterface.members.indexOf(member); + // Called when the fabric is changed in a maas-obj-form. + $scope.fabricChangedForm = function(key, value, form) { + var vlan; + if (value !== null) { + vlan = getDefaultVLAN(value); + } else { + vlan = null; + } + form.updateValue("vlan", vlan); + $scope.vlanChangedForm("vlan", vlan, form); + }; + + // Called when the VLAN is changed. + $scope.vlanChanged = function(nic) { + nic.subnet = null; + $scope.subnetChanged(nic); + }; + + // Called when the VLAN is changed on a maas-obj-form + $scope.vlanChangedForm = function(key, value, form) { + form.updateValue("subnet", null); + $scope.subnetChangedForm("subnet", null, form); + }; + + // Called when the subnet is changed. + $scope.subnetChanged = function(nic) { + if (!angular.isObject(nic.subnet)) { + // Set to 'Unconfigured' so the link mode should be set to + // 'link_up'. + nic.mode = LINK_MODE.LINK_UP; + } + if ($scope.$parent.isDevice) { + nic.ip_address = null; + } + $scope.modeChanged(nic); + }; - if (memberIndex !== -1) { - $scope.editInterface.members.splice(memberIndex, 1); - } + // Called when the subnet is changed. + $scope.subnetChangedForm = function(key, value, form) { + if (!angular.isObject(value)) { + // Set to 'Unconfigured' so the link mode should be set to + // 'link_up'. + form.updateValue("mode", LINK_MODE.LINK_UP); + } + const mode = form.getValue("mode"); + form.updateValue("ip_address", null); + $scope.modeChangedForm("mode", mode, form); + }; + + // Called when the mode is changed. + $scope.modeChanged = function(nic) { + // Clear the IP address when the mode is changed. + nic.ip_address = ""; + if (nic.mode === "static") { + var originalLink = mapNICToOriginalLink(nic.id, nic.link_id); + if ( + angular.isObject(originalLink) && + nic.subnet.id === originalLink.subnet_id + ) { + // Set the original IP address if same subnet. + nic.ip_address = originalLink.ip_address; + } else { + nic.ip_address = nic.subnet.statistics.first_address; + } + } + }; - // Remove from parents - var parentIndex = $scope.editInterface.parents.indexOf(nic.id); + // Update VLAN when fabric changed + $scope.updateVLAN = function(nic) { + var vlans = $filter("filterVLANNotOnFabric")( + $scope.vlans, + nic.fabric.vlan_ids + ); + nic.vlan = vlans[0]; + }; + + // Called when the mode is changed on a maas-obj-form. + $scope.modeChangedForm = function(key, value, form) { + // Clear the IP address when the mode is changed. + form.updateValue("ip_address", ""); + if (value === "static") { + var originalLink = mapNICToOriginalLink( + form.getValue("id"), + form.getValue("link_id") + ); + if ( + angular.isObject(originalLink) && + form.getValue("subnet").id === originalLink.subnet_id + ) { + // Set the original IP address if same subnet. + form.updateValue("ip_address", originalLink.ip_address); + } + } + }; - if (parentIndex !== -1) { - $scope.editInterface.parents.splice(parentIndex, 1); - } + // Called to cancel edit mode. + $scope.editCancel = function() { + $scope.isShowingInterfaces = false; + $scope.selectedInterfaces = []; + $scope.selectedMode = SELECTION_MODE.NONE; + $scope.editInterface = null; + updateInterfaces(); + }; - // Reset primary and mac address - var primaryMember = $scope.editInterface.members[0]; + // Preprocess things for updateInterfaceForm. + $scope.preProcessInterface = function(nic) { + var params = angular.copy(nic); + $scope.isSaving = true; + + delete params.id; + params.system_id = $scope.node.system_id; + params.interface_id = nic.id; + + // we need IDs not objects. + if (angular.isDefined(nic.fabric) && nic.fabric !== null) { + params.fabric = nic.fabric.id; + } else { + params.fabric = null; + } + if (angular.isDefined(nic.vlan) && nic.vlan !== null) { + params.vlan = nic.vlan.id; + } else { + params.vlan = null; + } + if (angular.isDefined(nic.subnet) && nic.subnet !== null) { + params.subnet = params.subnet.id; + } else { + delete params.subnet; + } + if (angular.isUndefined(nic.bridge_stp) && nic.bridge_stp === null) { + params.bridge_stp = null; + } + if (angular.isUndefined(nic.bridge_fd) && nic.bridge_fd === null) { + params.bridge_fd = null; + } + if (angular.isUndefined(nic.bond_mode) && nic.bond_mode === null) { + params.bond_mode = null; + } - if ($scope.editInterface.primary.id === nic.id) { - $scope.editInterface.primary = primaryMember; - } + if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { + params.link_id = nic.link_id; + delete $scope.interfaceErrorsByLinkId[nic.link_id]; + } else { + delete params.link_id; + } + if ( + (nic.mode === LINK_MODE.STATIC || + nic.ip_assignment !== IP_ASSIGNMENT.DYNAMIC) && + angular.isString(nic.ip_address) && + nic.ip_address.length > 0 + ) { + params.ip_address = nic.ip_address; + } else { + delete params.ip_address; + } + return params; + }; - if ($scope.editInterface.mac_address === nic.mac_address - && primaryMember) { - $scope.editInterface.mac_address = primaryMember.mac_address; - } - } - }; - - // Return true when the interface is selected. - $scope.isInterfaceSelected = function(nic) { - return $scope.selectedInterfaces.indexOf( - $scope.getUniqueKey(nic)) > -1; - }; - - // Returns true if the interface is not selected - $scope.cannotEditInterface = function(nic) { - if ($scope.selectedMode === SELECTION_MODE.NONE) { - return false; - } else if ( - $scope.selectedMode !== SELECTION_MODE.MULTI && - $scope.isInterfaceSelected(nic)) { - return false; - } else { - return true; - } - }; - - // Return true if in editing mode for the interface. - $scope.isEditing = function(nic) { - if ($scope.selectedMode !== SELECTION_MODE.EDIT) { - return false; - } else { - return $scope.editInterface.id === nic.id; - } - }; - - // Start editing this interface. - $scope.edit = function(nic) { - $scope.isShowingInterfaces = false; - $scope.selectedInterfaces = [$scope.getUniqueKey(nic)]; - $scope.selectedMode = SELECTION_MODE.EDIT; - if ($scope.$parent.isDevice) { - $scope.editInterface = { - id: nic.id, - name: nic.name, - mac_address: nic.mac_address, - tags: nic.tags.map(function(tag) { return tag.text; }), - subnet: nic.subnet, - ip_address: nic.ip_address, - ip_assignment: nic.ip_assignment, - link_id: nic.link_id, - type: nic.type, - bridge_fd: nic.params.bridge_fd, - bridge_stp: nic.params.bridge_stp, - bond_mode: nic.params.bond_mode, - xmitHashPolicy: nic.params.bond_xmit_hash_policy, - lacpRate: nic.params.bond_lacp_rate, - bond_downdelay: nic.params.bond_downdelay, - bond_updelay: nic.params.bond_updelay, - bond_miimon: nic.params.bond_miimon - }; - if (angular.isDefined(nic.subnet) && nic.subnet !== null) { - $scope.editInterface.defaultSubnet = nic.subnet; - } else { - $scope.editInterface.defaultSubnet = $scope.subnets[0]; - } - } else { - $scope.editInterface = { - id: nic.id, - name: nic.name, - mac_address: nic.mac_address, - tags: nic.tags.map(function(tag) { return tag.text; }), - fabric: nic.fabric, - vlan: nic.vlan, - subnet: nic.subnet, - mode: nic.mode, - ip_address: nic.ip_address, - link_id: nic.link_id, - type: nic.type, - bridge_fd: nic.params.bridge_fd, - bridge_stp: nic.params.bridge_stp, - bond_mode: nic.params.bond_mode, - xmitHashPolicy: nic.params.bond_xmit_hash_policy, - lacpRate: nic.params.bond_lacp_rate, - bond_downdelay: nic.params.bond_downdelay, - bond_updelay: nic.params.bond_updelay, - bond_miimon: nic.params.bond_miimon - }; - - $scope.editInterface.parents = nic.parents; - $scope.editInterface.members = nic.members; - if (nic.members && nic.members.length) { - $scope.editInterface.primary = nic.members[0]; - } else { - $scope.editInterface.primary = null; - } - - if (nic.members) { - nic.members.forEach(function(member) { - $scope.selectedInterfaces.push($scope.getUniqueKey(member)); - }); - } - } - }; - - // Called when the fabric is changed. - $scope.fabricChanged = function(nic) { - // Update the VLAN on the node to be the default VLAN for that - // fabric. The first VLAN for the fabric is the default. - if (nic.fabric !== null) { - nic.vlan = getDefaultVLAN(nic.fabric); - } else { - nic.vlan = null; - } - $scope.vlanChanged(nic); - }; - - // Called when the fabric is changed in a maas-obj-form. - $scope.fabricChangedForm = function(key, value, form) { - var vlan; - if (value !== null) { - vlan = getDefaultVLAN(value); - } else { - vlan = null; - } - form.updateValue('vlan', vlan); - $scope.vlanChangedForm('vlan', vlan, form); - }; - - // Called when the VLAN is changed. - $scope.vlanChanged = function(nic) { - nic.subnet = null; - $scope.subnetChanged(nic); - }; - - // Called when the VLAN is changed on a maas-obj-form - $scope.vlanChangedForm = function(key, value, form) { - form.updateValue('subnet', null); - $scope.subnetChangedForm('subnet', null, form); - }; - - // Called when the subnet is changed. - $scope.subnetChanged = function(nic) { - if (!angular.isObject(nic.subnet)) { - // Set to 'Unconfigured' so the link mode should be set to - // 'link_up'. - nic.mode = LINK_MODE.LINK_UP; - } - if ($scope.$parent.isDevice) { - nic.ip_address = null; - } - $scope.modeChanged(nic); - }; - - // Called when the subnet is changed. - $scope.subnetChangedForm = function(key, value, form) { - if (!angular.isObject(value)) { - // Set to 'Unconfigured' so the link mode should be set to - // 'link_up'. - form.updateValue('mode', LINK_MODE.LINK_UP); - } - const mode = form.getValue('mode'); - form.updateValue('ip_address', null); - $scope.modeChangedForm('mode', mode, form); - }; - - // Called when the mode is changed. - $scope.modeChanged = function(nic) { - // Clear the IP address when the mode is changed. - nic.ip_address = ""; - if (nic.mode === 'static') { - var originalLink = mapNICToOriginalLink(nic.id, nic.link_id); - if (angular.isObject(originalLink) && - nic.subnet.id === originalLink.subnet_id) { - // Set the original IP address if same subnet. - nic.ip_address = originalLink.ip_address; - } else { - nic.ip_address = nic.subnet.statistics.first_address; - } - } - }; - - // Update VLAN when fabric changed - $scope.updateVLAN = function(nic) { - var vlans = $filter("filterVLANNotOnFabric")( - $scope.vlans, - nic.fabric.vlan_ids - ); - nic.vlan = vlans[0]; - }; - - // Called when the mode is changed on a maas-obj-form. - $scope.modeChangedForm = function(key, value, form) { - // Clear the IP address when the mode is changed. - form.updateValue('ip_address', ""); - if (value === 'static') { - var originalLink = mapNICToOriginalLink( - form.getValue('id'), form.getValue('link_id')); - if (angular.isObject(originalLink) && - form.getValue('subnet').id === originalLink.subnet_id) { - // Set the original IP address if same subnet. - form.updateValue('ip_address', originalLink.ip_address); - } - } - }; + // Save the following interface on the node. + $scope.saveInterface = function(nic) { + var params; + if ($scope.$parent.isDevice) { + params = { + name: nic.name, + mac_address: nic.mac_address, + ip_assignment: nic.ip_assignment, + ip_address: nic.ip_address + }; + } else { + params = { + name: nic.name, + mac_address: nic.mac_address, + mode: nic.mode, + tags: nic.tags.map(function(tag) { + return tag.text; + }) + }; + } + if (angular.isDefined(nic.fabric) && nic.fabric !== null) { + params.fabric = nic.fabric.id; + } else { + params.fabric = null; + } + if (angular.isDefined(nic.vlan) && nic.vlan !== null) { + params.vlan = nic.vlan.id; + } else { + params.vlan = null; + } + if (angular.isDefined(nic.subnet) && nic.subnet !== null) { + params.subnet = nic.subnet.id; + } else { + params.subnet = null; + } + if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { + params.link_id = nic.link_id; + delete $scope.interfaceErrorsByLinkId[nic.link_id]; + } + if (angular.isString(nic.ip_address) && nic.ip_address.length > 0) { + params.ip_address = nic.ip_address; + } + return $scope.$parent.nodesManager + .updateInterface($scope.node, nic.id, params) + .then(null, function(error) { + // XXX blake_r: Just log the error in the console, but + // we need to expose this as a better message to the + // user. + $log.error(error); - // Called to cancel edit mode. - $scope.editCancel = function() { - $scope.isShowingInterfaces = false; - $scope.selectedInterfaces = []; - $scope.selectedMode = SELECTION_MODE.NONE; - $scope.editInterface = null; + // Update the interfaces so it is back to the way it + // was before the user changed it. updateInterfaces(); - }; + }); + }; - // Preprocess things for updateInterfaceForm. - $scope.preProcessInterface = function(nic) { - var params = angular.copy(nic); - $scope.isSaving = true; - - delete params.id; - params.system_id = $scope.node.system_id; - params.interface_id = nic.id; - - // we need IDs not objects. - if (angular.isDefined(nic.fabric) && nic.fabric !== null) { - params.fabric = nic.fabric.id; - } else { - params.fabric = null; - } - if (angular.isDefined(nic.vlan) && nic.vlan !== null) { - params.vlan = nic.vlan.id; - } else { - params.vlan = null; - } - if (angular.isDefined(nic.subnet) && nic.subnet !== null) { - params.subnet = params.subnet.id; - } else { - delete params.subnet; - } - if (angular.isUndefined(nic.bridge_stp) && nic.bridge_stp === null) { - params.bridge_stp = null; - } - if (angular.isUndefined(nic.bridge_fd) && nic.bridge_fd === null) { - params.bridge_fd = null; - } - if (angular.isUndefined(nic.bond_mode) && nic.bond_mode === null) { - params.bond_mode = null; - } - - if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { - params.link_id = nic.link_id; - delete $scope.interfaceErrorsByLinkId[nic.link_id]; - } else { - delete params.link_id; - } - if ((nic.mode === LINK_MODE.STATIC || - nic.ip_assignment !== IP_ASSIGNMENT.DYNAMIC) && - angular.isString(nic.ip_address) && - nic.ip_address.length > 0) { - params.ip_address = nic.ip_address; - } else { - delete params.ip_address; - } - return params; - }; - - // Save the following interface on the node. - $scope.saveInterface = function(nic) { - var params; - if ($scope.$parent.isDevice) { - params = { - "name": nic.name, - "mac_address": nic.mac_address, - "ip_assignment": nic.ip_assignment, - "ip_address": nic.ip_address - }; - } else { - params = { - "name": nic.name, - "mac_address": nic.mac_address, - "mode": nic.mode, - "tags": nic.tags.map( - function(tag) { return tag.text; }) - }; - } - if (angular.isDefined(nic.fabric) && nic.fabric !== null) { - params.fabric = nic.fabric.id; - } else { - params.fabric = null; - } - if (angular.isDefined(nic.vlan) && nic.vlan !== null) { - params.vlan = nic.vlan.id; - } else { - params.vlan = null; - } - if (angular.isDefined(nic.subnet) && nic.subnet !== null) { - params.subnet = nic.subnet.id; - } else { - params.subnet = null; - } - if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { - params.link_id = nic.link_id; - delete $scope.interfaceErrorsByLinkId[nic.link_id]; - } - if (angular.isString(nic.ip_address) && nic.ip_address.length > 0) { - params.ip_address = nic.ip_address; - } - return $scope.$parent.nodesManager.updateInterface( - $scope.node, nic.id, params).then(null, function(error) { - // XXX blake_r: Just log the error in the console, but - // we need to expose this as a better message to the - // user. - $log.error(error); - - // Update the interfaces so it is back to the way it - // was before the user changed it. - updateInterfaces(); - }); + // Save the following link on the node. + $scope.saveInterfaceLink = function(nic) { + var params = { + mode: nic.mode }; - - // Save the following link on the node. - $scope.saveInterfaceLink = function(nic) { - var params = { - "mode": nic.mode - }; - if ($scope.$parent.isDevice) { - params.ip_assignment = nic.ip_assignment; - } - if (angular.isObject(nic.subnet)) { - params.subnet = nic.subnet.id; - } + if ($scope.$parent.isDevice) { + params.ip_assignment = nic.ip_assignment; + } + if (angular.isObject(nic.subnet)) { + params.subnet = nic.subnet.id; + } + if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { + params.link_id = nic.link_id; + delete $scope.interfaceErrorsByLinkId[nic.link_id]; + } + if ( + nic.mode === LINK_MODE.STATIC && + angular.isString(nic.ip_address) && + nic.ip_address.length > 0 + ) { + params.ip_address = nic.ip_address; + } + return $scope.$parent.nodesManager + .linkSubnet($scope.node, nic.id, params) + .then(null, function(error) { + $log.info(error); if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { - params.link_id = nic.link_id; - delete $scope.interfaceErrorsByLinkId[nic.link_id]; - } - if (nic.mode === LINK_MODE.STATIC && - angular.isString(nic.ip_address) && - nic.ip_address.length > 0) { - params.ip_address = nic.ip_address; - } - return $scope.$parent.nodesManager.linkSubnet( - $scope.node, nic.id, params).then(null, function(error) { - $log.info(error); - if (angular.isDefined(nic.link_id) && nic.link_id >= 0) { - $scope.interfaceErrorsByLinkId[nic.link_id] = error; - } - // Update the interfaces so it is back to the way it - // was before the user changed it. - updateInterfaces(); - throw error; - }); - }; - - // Called to save the interface. - $scope.editSave = function(editInterface) { - $scope.isSaving = false; - $scope.editCancel(); - return editInterface; - }; - - // Return true if the interface delete confirm is being shown. - $scope.isShowingDeleteConfirm = function() { - return $scope.selectedMode === SELECTION_MODE.DELETE; - }; - - // Return true if the interface add interface is being shown. - $scope.isShowingAdd = function() { - return $scope.selectedMode === SELECTION_MODE.ADD; - }; - - // Return true if either an alias or VLAN can be added. - $scope.canAddAliasOrVLAN = function(nic) { - if ($scope.$parent.isController) { - return false; - } else if ($scope.isAllNetworkingDisabled()) { - return false; - } else { - return $scope.canAddAlias(nic) || $scope.canAddVLAN(nic); + $scope.interfaceErrorsByLinkId[nic.link_id] = error; } - }; - - // Return true if the alias can be added to interface. - $scope.canAddAlias = function(nic) { - if (!angular.isObject(nic)) { - return false; - } else if (nic.type === INTERFACE_TYPE.ALIAS) { - return false; - } else if (nic.links.length === 0 || - nic.links[0].mode === LINK_MODE.LINK_UP) { - return false; - } else { - return true; - } - }; - - // Return true if the VLAN can be added to interface. - $scope.canAddVLAN = function(nic) { - if (!angular.isObject(nic)) { - return false; - } else if (nic.type === INTERFACE_TYPE.ALIAS || - nic.type === INTERFACE_TYPE.VLAN) { - return false; - } - var unusedVLANs = getUnusedVLANs(nic); - return unusedVLANs.length > 0; - }; - - // Return true if another VLAN can be added to this already being - // added interface. - $scope.canAddAnotherVLAN = function(nic) { - if (!$scope.canAddVLAN(nic)) { - return false; - } - var unusedVLANs = getUnusedVLANs(nic); - return unusedVLANs.length > 1; - }; - - // Return the text to use for the remove link and message. - $scope.getRemoveTypeText = function(nic) { - if (nic.type === INTERFACE_TYPE.PHYSICAL) { - return "interface"; - } else if (nic.type === INTERFACE_TYPE.VLAN) { - return "VLAN"; - } else { - return nic.type; - } - }; - - // Return true if the interface can be removed. - $scope.canBeRemoved = function() { - return ( - !$scope.$parent.isController && - !$scope.isAllNetworkingDisabled()); - }; - - // Enter remove mode. - $scope.remove = function() { - $scope.selectedMode = SELECTION_MODE.DELETE; - }; - - // Quickly enter remove by selecting the node first. - $scope.quickRemove = function(nic) { - $scope.selectedInterfaces = [$scope.getUniqueKey(nic)]; - $scope.remove(); - }; - - // Cancel the current mode go back to sinle selection mode. - $scope.cancel = function() { - $scope.isShowingInterfaces = false; - $scope.newInterface = {}; - $scope.newBondInterface = {}; - $scope.newBridgeInterface = {}; - $scope.clearCreateBondError(); - if ($scope.selectedMode === SELECTION_MODE.CREATE_BOND) { - $scope.selectedMode = SELECTION_MODE.MULTI; - } else if ($scope.selectedMode === SELECTION_MODE.CREATE_PHYSICAL) { - $scope.selectedMode = SELECTION_MODE.NONE; - } else { - $scope.selectedMode = SELECTION_MODE.SINGLE; - } - }; + // Update the interfaces so it is back to the way it + // was before the user changed it. + updateInterfaces(); + throw error; + }); + }; - // Confirm the removal of interface. - $scope.confirmRemove = function(nic) { - $scope.selectedMode = SELECTION_MODE.NONE; - $scope.selectedInterfaces = []; - if (nic.type !== INTERFACE_TYPE.ALIAS) { - $scope.$parent.nodesManager.deleteInterface( - $scope.node, nic.id); - } else { - $scope.$parent.nodesManager.unlinkSubnet( - $scope.node, nic.id, nic.link_id); - } + // Called to save the interface. + $scope.editSave = function(editInterface) { + $scope.isSaving = false; + $scope.editCancel(); + return editInterface; + }; + + // Return true if the interface delete confirm is being shown. + $scope.isShowingDeleteConfirm = function() { + return $scope.selectedMode === SELECTION_MODE.DELETE; + }; + + // Return true if the interface add interface is being shown. + $scope.isShowingAdd = function() { + return $scope.selectedMode === SELECTION_MODE.ADD; + }; + + // Return true if either an alias or VLAN can be added. + $scope.canAddAliasOrVLAN = function(nic) { + if ($scope.$parent.isController) { + return false; + } else if ($scope.isAllNetworkingDisabled()) { + return false; + } else { + return $scope.canAddAlias(nic) || $scope.canAddVLAN(nic); + } + }; - // Remove the interface from available interfaces - var idx = $scope.interfaces.indexOf(nic); - if (idx > -1) { - $scope.interfaces.splice(idx, 1); - } - }; + // Return true if the alias can be added to interface. + $scope.canAddAlias = function(nic) { + if (!angular.isObject(nic)) { + return false; + } else if (nic.type === INTERFACE_TYPE.ALIAS) { + return false; + } else if ( + nic.links.length === 0 || + nic.links[0].mode === LINK_MODE.LINK_UP + ) { + return false; + } else { + return true; + } + }; - // Enter add mode. - $scope.add = function(type, nic) { - // When this is called right after another VLAN was just added, we - // remove its used VLAN from the available list. - var ignoreVLANs = []; - if (angular.isObject($scope.newInterface.vlan)) { - ignoreVLANs.push($scope.newInterface.vlan); - } + // Return true if the VLAN can be added to interface. + $scope.canAddVLAN = function(nic) { + if (!angular.isObject(nic)) { + return false; + } else if ( + nic.type === INTERFACE_TYPE.ALIAS || + nic.type === INTERFACE_TYPE.VLAN + ) { + return false; + } + var unusedVLANs = getUnusedVLANs(nic); + return unusedVLANs.length > 0; + }; + + // Return true if another VLAN can be added to this already being + // added interface. + $scope.canAddAnotherVLAN = function(nic) { + if (!$scope.canAddVLAN(nic)) { + return false; + } + var unusedVLANs = getUnusedVLANs(nic); + return unusedVLANs.length > 1; + }; + + // Return the text to use for the remove link and message. + $scope.getRemoveTypeText = function(nic) { + if (nic.type === INTERFACE_TYPE.PHYSICAL) { + return "interface"; + } else if (nic.type === INTERFACE_TYPE.VLAN) { + return "VLAN"; + } else { + return nic.type; + } + }; - // Get the default VLAN for the new interface. - var vlans = getUnusedVLANs(nic, ignoreVLANs); - var defaultVLAN = null; - if (vlans.length > 0) { - defaultVLAN = vlans[0]; - } - var defaultSubnet = null; - var defaultMode = LINK_MODE.LINK_UP; + // Return true if the interface can be removed. + $scope.canBeRemoved = function() { + return !$scope.$parent.isController && !$scope.isAllNetworkingDisabled(); + }; + + // Enter remove mode. + $scope.remove = function() { + $scope.selectedMode = SELECTION_MODE.DELETE; + }; + + // Quickly enter remove by selecting the node first. + $scope.quickRemove = function(nic) { + $scope.selectedInterfaces = [$scope.getUniqueKey(nic)]; + $scope.remove(); + }; + + // Cancel the current mode go back to sinle selection mode. + $scope.cancel = function() { + $scope.isShowingInterfaces = false; + $scope.newInterface = {}; + $scope.newBondInterface = {}; + $scope.newBridgeInterface = {}; + $scope.clearCreateBondError(); + if ($scope.selectedMode === SELECTION_MODE.CREATE_BOND) { + $scope.selectedMode = SELECTION_MODE.MULTI; + } else if ($scope.selectedMode === SELECTION_MODE.CREATE_PHYSICAL) { + $scope.selectedMode = SELECTION_MODE.NONE; + } else { + $scope.selectedMode = SELECTION_MODE.SINGLE; + } + }; - // Alias used defaults based from its parent. - if (type === INTERFACE_TYPE.ALIAS) { - defaultVLAN = nic.vlan; - defaultSubnet = $filter('filter')( - $scope.subnets, { vlan: defaultVLAN.id }, true)[0]; - defaultMode = LINK_MODE.AUTO; - } + // Confirm the removal of interface. + $scope.confirmRemove = function(nic) { + $scope.selectedMode = SELECTION_MODE.NONE; + $scope.selectedInterfaces = []; + if (nic.type !== INTERFACE_TYPE.ALIAS) { + $scope.$parent.nodesManager.deleteInterface($scope.node, nic.id); + } else { + $scope.$parent.nodesManager.unlinkSubnet( + $scope.node, + nic.id, + nic.link_id + ); + } - // Setup the new interface and enter add mode. - $scope.newInterface = { - type: type, - vlan: defaultVLAN, - subnet: defaultSubnet, - mode: defaultMode, - parent: nic, - tags: [] - }; - $scope.selectedMode = SELECTION_MODE.ADD; - }; + // Remove the interface from available interfaces + var idx = $scope.interfaces.indexOf(nic); + if (idx > -1) { + $scope.interfaces.splice(idx, 1); + } + }; - // Quickly enter add by selecting the node first. - $scope.quickAdd = function(nic) { - $scope.selectedInterfaces = [$scope.getUniqueKey(nic)]; - var type = 'alias'; - if (!$scope.canAddAlias(nic)) { - type = 'vlan'; - } - $scope.add(type, nic); - }; + // Enter add mode. + $scope.add = function(type, nic) { + // When this is called right after another VLAN was just added, we + // remove its used VLAN from the available list. + var ignoreVLANs = []; + if (angular.isObject($scope.newInterface.vlan)) { + ignoreVLANs.push($scope.newInterface.vlan); + } - // Return the name of the interface being added. - $scope.getAddName = function() { - if ($scope.newInterface.type === INTERFACE_TYPE.ALIAS) { - var aliasIdx = $scope.newInterface.parent.links.length; - return $scope.newInterface.parent.name + ":" + aliasIdx; - } else if ($scope.newInterface.type === INTERFACE_TYPE.VLAN) { - return ( - $scope.newInterface.parent.name + "." + - $scope.newInterface.vlan.vid); - } - }; + // Get the default VLAN for the new interface. + var vlans = getUnusedVLANs(nic, ignoreVLANs); + var defaultVLAN = null; + if (vlans.length > 0) { + defaultVLAN = vlans[0]; + } + var defaultSubnet = null; + var defaultMode = LINK_MODE.LINK_UP; - // Called when the type of interface is changed. - $scope.addTypeChanged = function() { - if ($scope.newInterface.type === INTERFACE_TYPE.ALIAS) { - $scope.newInterface.vlan = $scope.newInterface.parent.vlan; - $scope.newInterface.subnet = $filter('filter')( - $scope.subnets, - { vlan: $scope.newInterface.vlan.id }, true)[0]; - $scope.newInterface.mode = LINK_MODE.AUTO; - } else if ($scope.newInterface.type === INTERFACE_TYPE.VLAN) { - var vlans = getUnusedVLANs($scope.newInterface.parent); - $scope.newInterface.vlan = null; - if (vlans.length > 0) { - $scope.newInterface.vlan = vlans[0]; - } - $scope.newInterface.subnet = null; - $scope.newInterface.mode = LINK_MODE.LINK_UP; - } - }; + // Alias used defaults based from its parent. + if (type === INTERFACE_TYPE.ALIAS) { + defaultVLAN = nic.vlan; + defaultSubnet = $filter("filter")( + $scope.subnets, + { vlan: defaultVLAN.id }, + true + )[0]; + defaultMode = LINK_MODE.AUTO; + } - // Perform the add action over the websocket. - $scope.addInterface = function(type) { - var nic; - if ($scope.$parent.isDevice) { - nic = { - id: $scope.newInterface.parent.id, - tags: $scope.newInterface.tags.map( - function(tag) { return tag.text; }), - ip_assignment: $scope.newInterface.ip_assignment, - subnet: $scope.newInterface.subnet, - ip_address: $scope.newInterface.ip_address - }; - $scope.saveInterfaceLink(nic); - } else if ($scope.newInterface.type === INTERFACE_TYPE.ALIAS) { - // Add a link to the current interface. - nic = { - id: $scope.newInterface.parent.id, - mode: $scope.newInterface.mode, - subnet: $scope.newInterface.subnet, - ip_address: $scope.newInterface.ip_address - }; - $scope.saveInterfaceLink(nic); - } else if ($scope.newInterface.type === INTERFACE_TYPE.VLAN) { - var params = { - parent: $scope.newInterface.parent.id, - vlan: $scope.newInterface.vlan.id, - mode: $scope.newInterface.mode, - tags: $scope.newInterface.tags.map( - function(tag) { return tag.text; }) - }; - if (angular.isObject($scope.newInterface.subnet)) { - params.subnet = $scope.newInterface.subnet.id; - params.ip_address = $scope.newInterface.ip_address; - } - $scope.$parent.nodesManager.createVLANInterface( - $scope.node, params).then(null, function(error) { - // Should do something better but for now just log - // the error. - $log.error(error); - }); - } + // Setup the new interface and enter add mode. + $scope.newInterface = { + type: type, + vlan: defaultVLAN, + subnet: defaultSubnet, + mode: defaultMode, + parent: nic, + tags: [] + }; + $scope.selectedMode = SELECTION_MODE.ADD; + }; + + // Quickly enter add by selecting the node first. + $scope.quickAdd = function(nic) { + $scope.selectedInterfaces = [$scope.getUniqueKey(nic)]; + var type = "alias"; + if (!$scope.canAddAlias(nic)) { + type = "vlan"; + } + $scope.add(type, nic); + }; - // Add again based on the clicked option. - if (angular.isString(type)) { - $scope.add(type, $scope.newInterface.parent); - } else { - $scope.selectedMode = SELECTION_MODE.NONE; - $scope.selectedInterfaces = []; - $scope.newInterface = {}; - } - }; + // Return the name of the interface being added. + $scope.getAddName = function() { + if ($scope.newInterface.type === INTERFACE_TYPE.ALIAS) { + var aliasIdx = $scope.newInterface.parent.links.length; + return $scope.newInterface.parent.name + ":" + aliasIdx; + } else if ($scope.newInterface.type === INTERFACE_TYPE.VLAN) { + return ( + $scope.newInterface.parent.name + "." + $scope.newInterface.vlan.vid + ); + } + }; - // Return true if the networking information cannot be edited - // or if this interface should be disabled in the list. Only - // returns true when in create bond mode. - $scope.isDisabled = function() { - if ($scope.isAllNetworkingDisabled()) { - return true; - } else { - return ( - $scope.selectedMode !== SELECTION_MODE.NONE && - $scope.selectedMode !== SELECTION_MODE.SINGLE && - $scope.selectedMode !== SELECTION_MODE.MULTI); - } - }; + // Called when the type of interface is changed. + $scope.addTypeChanged = function() { + if ($scope.newInterface.type === INTERFACE_TYPE.ALIAS) { + $scope.newInterface.vlan = $scope.newInterface.parent.vlan; + $scope.newInterface.subnet = $filter("filter")( + $scope.subnets, + { vlan: $scope.newInterface.vlan.id }, + true + )[0]; + $scope.newInterface.mode = LINK_MODE.AUTO; + } else if ($scope.newInterface.type === INTERFACE_TYPE.VLAN) { + var vlans = getUnusedVLANs($scope.newInterface.parent); + $scope.newInterface.vlan = null; + if (vlans.length > 0) { + $scope.newInterface.vlan = vlans[0]; + } + $scope.newInterface.subnet = null; + $scope.newInterface.mode = LINK_MODE.LINK_UP; + } + }; - // Return true when a bond can be created based on the current - // selection. Only can be done if no aliases are selected and all - // selected interfaces are on the same VLAN. - $scope.canCreateBond = function() { - if ($scope.selectedMode !== SELECTION_MODE.MULTI) { - return false; - } - var interfaces = getSelectedInterfaces(); - var i, vlan; - for (i = 0; i < interfaces.length; i++) { - var nic = interfaces[i]; - if (nic.type === INTERFACE_TYPE.ALIAS || - nic.type === INTERFACE_TYPE.BOND) { - return false; - } else if (!angular.isObject(vlan)) { - vlan = nic.vlan; - } else if (vlan !== nic.vlan) { - return false; - } - } - return true; - }; + // Perform the add action over the websocket. + $scope.addInterface = function(type) { + var nic; + if ($scope.$parent.isDevice) { + nic = { + id: $scope.newInterface.parent.id, + tags: $scope.newInterface.tags.map(function(tag) { + return tag.text; + }), + ip_assignment: $scope.newInterface.ip_assignment, + subnet: $scope.newInterface.subnet, + ip_address: $scope.newInterface.ip_address + }; + $scope.saveInterfaceLink(nic); + } else if ($scope.newInterface.type === INTERFACE_TYPE.ALIAS) { + // Add a link to the current interface. + nic = { + id: $scope.newInterface.parent.id, + mode: $scope.newInterface.mode, + subnet: $scope.newInterface.subnet, + ip_address: $scope.newInterface.ip_address + }; + $scope.saveInterfaceLink(nic); + } else if ($scope.newInterface.type === INTERFACE_TYPE.VLAN) { + var params = { + parent: $scope.newInterface.parent.id, + vlan: $scope.newInterface.vlan.id, + mode: $scope.newInterface.mode, + tags: $scope.newInterface.tags.map(function(tag) { + return tag.text; + }) + }; + if (angular.isObject($scope.newInterface.subnet)) { + params.subnet = $scope.newInterface.subnet.id; + params.ip_address = $scope.newInterface.ip_address; + } + $scope.$parent.nodesManager + .createVLANInterface($scope.node, params) + .then(null, function(error) { + // Should do something better but for now just log + // the error. + $log.error(error); + }); + } - // Return true when the create bond view is being shown. - $scope.isShowingCreateBond = function() { - return $scope.selectedMode === SELECTION_MODE.CREATE_BOND; - }; + // Add again based on the clicked option. + if (angular.isString(type)) { + $scope.add(type, $scope.newInterface.parent); + } else { + $scope.selectedMode = SELECTION_MODE.NONE; + $scope.selectedInterfaces = []; + $scope.newInterface = {}; + } + }; - // Show the create bond view. - $scope.showCreateBond = function() { - $scope.clearCreateBondError(); - if ($scope.selectedMode === SELECTION_MODE.MULTI && - $scope.canCreateBond()) { - $scope.selectedMode = SELECTION_MODE.CREATE_BOND; - - var parents = getSelectedInterfaces(); - var primary = parents[0]; - var mac_address = ''; - var fabric = ''; - var vlan = {}; - var subnet = ''; - if (primary && primary.mac_address) { - mac_address = primary.mac_address; - } - if (primary && primary.fabric) { - fabric = primary.fabric; - } - if (primary && primary.vlan) { - vlan = primary.vlan; - } - if (primary && primary.subnet) { - subnet = primary.subnet; - } - $scope.newBondInterface = { - name: getNextName("bond"), - tags: [], - parents: parents, - primary: primary, - mac_address: mac_address, - fabric: fabric, - vlan: vlan, - subnet: subnet, - bond_mode: "active-backup", - lacpRate: "fast", - xmitHashPolicy: "layer2", - bond_updelay: 0, - bond_downdelay: 0, - bond_miimon: 100 - }; - } - }; + // Return true if the networking information cannot be edited + // or if this interface should be disabled in the list. Only + // returns true when in create bond mode. + $scope.isDisabled = function() { + if ($scope.isAllNetworkingDisabled()) { + return true; + } else { + return ( + $scope.selectedMode !== SELECTION_MODE.NONE && + $scope.selectedMode !== SELECTION_MODE.SINGLE && + $scope.selectedMode !== SELECTION_MODE.MULTI + ); + } + }; - // Return true if the interface has a parent that is a boot interface. - $scope.hasBootInterface = function(nic) { - if (!angular.isArray(nic.parents)) { - return false; - } - var i; - for (i = 0; i < nic.parents.length; i++) { - if (nic.parents[i].is_boot) { - return true; - } - } + // Return true when a bond can be created based on the current + // selection. Only can be done if no aliases are selected and all + // selected interfaces are on the same VLAN. + $scope.canCreateBond = function() { + if ($scope.selectedMode !== SELECTION_MODE.MULTI) { + return false; + } + var interfaces = getSelectedInterfaces(); + var i, vlan; + for (i = 0; i < interfaces.length; i++) { + var nic = interfaces[i]; + if ( + nic.type === INTERFACE_TYPE.ALIAS || + nic.type === INTERFACE_TYPE.BOND + ) { return false; - }; - - // Return the MAC address that should be shown as a placeholder. This - // this is the MAC address of the primary interface. - $scope.getInterfacePlaceholderMACAddress = function(nic) { - if (!angular.isObject(nic.primary)) { - return ""; - } else { - return nic.primary.mac_address; - } - }; - - // Return true if the user has inputed a value in the MAC address field - // but it is invalid. - $scope.isMACAddressInvalid = function(mac_address, invalidEmpty) { - if (angular.isUndefined(invalidEmpty)) { - invalidEmpty = false; - } - if (!angular.isString(mac_address) || mac_address === "") { - return invalidEmpty; - } - return !ValidationService.validateMAC(mac_address); - }; - - // Return true when the LACR rate selection should be shown. - $scope.showLACPRate = function() { - if ($scope.editInterface) { - return $scope.editInterface.bond_mode === "802.3ad"; - } else { - return $scope.newBondInterface.bond_mode === "802.3ad"; - } - }; - - // Return true when hash policy is not fully 802.3ad compliant. - $scope.modeAndPolicyCompliant = function() { - if ($scope.editInterface) { - return ( - $scope.editInterface.bond_mode === "802.3ad" && - ($scope.editInterface.xmitHashPolicy === "layer3+4" || - $scope.editInterface.xmitHashPolicy === "encap3+4")); - } else { - return ( - $scope.newBondInterface.bond_mode === "802.3ad" && - ($scope.newBondInterface.xmitHashPolicy === "layer3+4" || - $scope.newBondInterface.xmitHashPolicy === "encap3+4")); - } - }; - - // Return true when the XMIT hash policy should be shown. - $scope.showXMITHashPolicy = function() { - if ($scope.editInterface) { - return ( - $scope.editInterface.bond_mode === "balance-xor" || - $scope.editInterface.bond_mode === "802.3ad" || - $scope.editInterface.bond_mode === "balance-tlb"); - } else { - return ( - $scope.newBondInterface.bond_mode === "balance-xor" || - $scope.newBondInterface.bond_mode === "802.3ad" || - $scope.newBondInterface.bond_mode === "balance-tlb"); - } - }; - - // Return true if cannot add the bond. - $scope.cannotAddBond = function() { - return ( - $scope.isInterfaceNameInvalid($scope.newBondInterface) || - $scope.isMACAddressInvalid( - $scope.newBondInterface.mac_address)); - }; + } else if (!angular.isObject(vlan)) { + vlan = nic.vlan; + } else if (vlan !== nic.vlan) { + return false; + } + } + return true; + }; - // Return true if cannot edit the bond. - $scope.cannotEditBond = function(nic) { - return $scope.isInterfaceNameInvalid(nic) - && $scope.isIPAddressInvalid(nic) - && $scope.isMACAddressInvalid(nic.mac_address, true); - }; + // Return true when the create bond view is being shown. + $scope.isShowingCreateBond = function() { + return $scope.selectedMode === SELECTION_MODE.CREATE_BOND; + }; + + // Show the create bond view. + $scope.showCreateBond = function() { + $scope.clearCreateBondError(); + if ( + $scope.selectedMode === SELECTION_MODE.MULTI && + $scope.canCreateBond() + ) { + $scope.selectedMode = SELECTION_MODE.CREATE_BOND; + + var parents = getSelectedInterfaces(); + var primary = parents[0]; + var mac_address = ""; + var fabric = ""; + var vlan = {}; + var subnet = ""; + if (primary && primary.mac_address) { + mac_address = primary.mac_address; + } + if (primary && primary.fabric) { + fabric = primary.fabric; + } + if (primary && primary.vlan) { + vlan = primary.vlan; + } + if (primary && primary.subnet) { + subnet = primary.subnet; + } + $scope.newBondInterface = { + name: getNextName("bond"), + tags: [], + parents: parents, + primary: primary, + mac_address: mac_address, + fabric: fabric, + vlan: vlan, + subnet: subnet, + bond_mode: "active-backup", + lacpRate: "fast", + xmitHashPolicy: "layer2", + bond_updelay: 0, + bond_downdelay: 0, + bond_miimon: 100 + }; + } + }; - // Actually add the bond. - $scope.addBond = function() { - if ($scope.cannotAddBond()) { - return; - } + // Return true if the interface has a parent that is a boot interface. + $scope.hasBootInterface = function(nic) { + if (!angular.isArray(nic.parents)) { + return false; + } + var i; + for (i = 0; i < nic.parents.length; i++) { + if (nic.parents[i].is_boot) { + return true; + } + } + return false; + }; - $scope.isSaving = true; + // Return the MAC address that should be shown as a placeholder. This + // this is the MAC address of the primary interface. + $scope.getInterfacePlaceholderMACAddress = function(nic) { + if (!angular.isObject(nic.primary)) { + return ""; + } else { + return nic.primary.mac_address; + } + }; - var parents = $scope.newBondInterface.parents.map( - function(nic) { return nic.id; }); - var mac_address = $scope.newBondInterface.mac_address; - if (mac_address === "") { - mac_address = $scope.newBondInterface.primary.mac_address; - } - var vlan_id, vlan = $scope.newBondInterface.vlan; - if (angular.isObject(vlan)) { - vlan_id = vlan.id; - } else if (angular.isObject($scope.newBondInterface.primary.vlan)) { - vlan = $scope.newBondInterface.primary.vlan; - vlan_id = vlan.id; - } else { - vlan_id = null; - } - var subnet_id, subnet = $scope.newBondInterface.subnet; - if (angular.isObject(subnet)) { - subnet_id = subnet.id; - } else { - subnet_id = null; - } - var params = { - name: $scope.newBondInterface.name, - mac_address: mac_address, - tags: $scope.newBondInterface.tags.map( - function(tag) { return tag.text; }), - parents: parents, - bond_mode: $scope.newBondInterface.bond_mode, - bond_lacp_rate: $scope.newBondInterface.lacpRate, - bond_xmit_hash_policy: $scope.newBondInterface.xmitHashPolicy, - vlan: vlan_id, - subnet: subnet_id, - mode: $scope.newBondInterface.mode, - ip_address: $scope.newBondInterface.ip_address, - bond_miimon: $scope.newBondInterface.bond_miimon, - bond_updelay: $scope.newBondInterface.bond_updelay, - bond_downdelay: $scope.newBondInterface.bond_downdelay - }; - $scope.$parent.nodesManager - .createBondInterface($scope.node, params) - .then(function() { - // Remove the parent interfaces so that they don't show up - // in the listing unti the new bond appears. - var parents = $scope.newBondInterface.parents; - angular.forEach(parents, function(parent) { - var idx = $scope.interfaces.indexOf(parent); - if (idx > -1) { - $scope.interfaces.splice(idx, 1); - } - }); - $scope.isSaving = false; - }) - .catch(function(error) { - var parsedError = angular.fromJson(error); - $scope.createBondError - = parsedError[Object.keys(parsedError)[0]][0]; - $scope.isSaving = false; - }); - - - // Clear the bond interface and reset the mode. - $scope.newBondInterface = {}; - $scope.selectedInterfaces = []; - $scope.selectedMode = SELECTION_MODE.NONE; - }; + // Return true if the user has inputed a value in the MAC address field + // but it is invalid. + $scope.isMACAddressInvalid = function(mac_address, invalidEmpty) { + if (angular.isUndefined(invalidEmpty)) { + invalidEmpty = false; + } + if (!angular.isString(mac_address) || mac_address === "") { + return invalidEmpty; + } + return !ValidationService.validateMAC(mac_address); + }; - $scope.clearCreateBondError = function() { - $scope.createBondError = null; - }; + // Return true when the LACR rate selection should be shown. + $scope.showLACPRate = function() { + if ($scope.editInterface) { + return $scope.editInterface.bond_mode === "802.3ad"; + } else { + return $scope.newBondInterface.bond_mode === "802.3ad"; + } + }; - // Return true when a bridge can be created based on the current - // selection. Only can be done if no aliases are selected and only - // one interface is selected. - $scope.canCreateBridge = function() { - if ($scope.selectedMode !== SELECTION_MODE.SINGLE) { - return false; - } - var nic = getSelectedInterfaces()[0]; - if (nic.type === INTERFACE_TYPE.ALIAS || - nic.type === INTERFACE_TYPE.BRIDGE) { - return false; - } - return true; - }; + // Return true when hash policy is not fully 802.3ad compliant. + $scope.modeAndPolicyCompliant = function() { + if ($scope.editInterface) { + return ( + $scope.editInterface.bond_mode === "802.3ad" && + ($scope.editInterface.xmitHashPolicy === "layer3+4" || + $scope.editInterface.xmitHashPolicy === "encap3+4") + ); + } else { + return ( + $scope.newBondInterface.bond_mode === "802.3ad" && + ($scope.newBondInterface.xmitHashPolicy === "layer3+4" || + $scope.newBondInterface.xmitHashPolicy === "encap3+4") + ); + } + }; - // Return true when the create bridge view is being shown. - $scope.isShowingCreateBridge = function() { - return $scope.selectedMode === SELECTION_MODE.CREATE_BRIDGE; - }; + // Return true when the XMIT hash policy should be shown. + $scope.showXMITHashPolicy = function() { + if ($scope.editInterface) { + return ( + $scope.editInterface.bond_mode === "balance-xor" || + $scope.editInterface.bond_mode === "802.3ad" || + $scope.editInterface.bond_mode === "balance-tlb" + ); + } else { + return ( + $scope.newBondInterface.bond_mode === "balance-xor" || + $scope.newBondInterface.bond_mode === "802.3ad" || + $scope.newBondInterface.bond_mode === "balance-tlb" + ); + } + }; - // Return true when the edit bridge view is being shown. - $scope.isShowingEdit = function() { - return $scope.selectedMode === SELECTION_MODE.EDIT; - }; + // Return true if cannot add the bond. + $scope.cannotAddBond = function() { + return ( + $scope.isInterfaceNameInvalid($scope.newBondInterface) || + $scope.isMACAddressInvalid($scope.newBondInterface.mac_address) + ); + }; + + // Return true if cannot edit the bond. + $scope.cannotEditBond = function(nic) { + return ( + $scope.isInterfaceNameInvalid(nic) && + $scope.isIPAddressInvalid(nic) && + $scope.isMACAddressInvalid(nic.mac_address, true) + ); + }; + + // Actually add the bond. + $scope.addBond = function() { + if ($scope.cannotAddBond()) { + return; + } - // Toogle interfaces in edit table - $scope.toggleInterfaces = function() { - $scope.isShowingInterfaces = !$scope.isShowingInterfaces; - }; + $scope.isSaving = true; - // Checks if row is correct type and id - $scope.isCorrectInterfaceType = function(bondInterface, parents) { - var parentIds = parents.map(function(parent) { - return parent.id; + var parents = $scope.newBondInterface.parents.map(function(nic) { + return nic.id; + }); + var mac_address = $scope.newBondInterface.mac_address; + if (mac_address === "") { + mac_address = $scope.newBondInterface.primary.mac_address; + } + var vlan_id, + vlan = $scope.newBondInterface.vlan; + if (angular.isObject(vlan)) { + vlan_id = vlan.id; + } else if (angular.isObject($scope.newBondInterface.primary.vlan)) { + vlan = $scope.newBondInterface.primary.vlan; + vlan_id = vlan.id; + } else { + vlan_id = null; + } + var subnet_id, + subnet = $scope.newBondInterface.subnet; + if (angular.isObject(subnet)) { + subnet_id = subnet.id; + } else { + subnet_id = null; + } + var params = { + name: $scope.newBondInterface.name, + mac_address: mac_address, + tags: $scope.newBondInterface.tags.map(function(tag) { + return tag.text; + }), + parents: parents, + bond_mode: $scope.newBondInterface.bond_mode, + bond_lacp_rate: $scope.newBondInterface.lacpRate, + bond_xmit_hash_policy: $scope.newBondInterface.xmitHashPolicy, + vlan: vlan_id, + subnet: subnet_id, + mode: $scope.newBondInterface.mode, + ip_address: $scope.newBondInterface.ip_address, + bond_miimon: $scope.newBondInterface.bond_miimon, + bond_updelay: $scope.newBondInterface.bond_updelay, + bond_downdelay: $scope.newBondInterface.bond_downdelay + }; + $scope.$parent.nodesManager + .createBondInterface($scope.node, params) + .then(function() { + // Remove the parent interfaces so that they don't show up + // in the listing unti the new bond appears. + var parents = $scope.newBondInterface.parents; + angular.forEach(parents, function(parent) { + var idx = $scope.interfaces.indexOf(parent); + if (idx > -1) { + $scope.interfaces.splice(idx, 1); + } }); + $scope.isSaving = false; + }) + .catch(function(error) { + var parsedError = angular.fromJson(error); + $scope.createBondError = parsedError[Object.keys(parsedError)[0]][0]; + $scope.isSaving = false; + }); - var parentType = parents[0].type; - var parentFabric = parents[0].fabric; - - if (bondInterface.type === parentType - && bondInterface.fabric === parentFabric - && !parentIds.includes(bondInterface.id)) { - return true; - } + // Clear the bond interface and reset the mode. + $scope.newBondInterface = {}; + $scope.selectedInterfaces = []; + $scope.selectedMode = SELECTION_MODE.NONE; + }; - return false; - }; + $scope.clearCreateBondError = function() { + $scope.createBondError = null; + }; - // Show the create bridge view. - $scope.showCreateBridge = function() { - if ($scope.selectedMode === SELECTION_MODE.SINGLE && - $scope.canCreateBridge()) { - $scope.selectedMode = SELECTION_MODE.CREATE_BRIDGE; - - var parents = getSelectedInterfaces(); - var primary = parents[0]; - var mac_address = ''; - var fabric = ''; - var vlan = {}; - if (primary && primary.mac_address) { - mac_address = primary.mac_address; - } - if (primary && primary.fabric) { - fabric = primary.fabric; - } - if (primary && primary.vlan) { - vlan = primary.vlan; - } - $scope.newBridgeInterface = { - name: getNextName("br"), - tags: [], - parents: parents, - primary: primary, - mac_address: mac_address, - fabric: fabric, - vlan: vlan, - bridge_stp: false, - bridge_fd: 15 - }; - } - }; + // Return true when a bridge can be created based on the current + // selection. Only can be done if no aliases are selected and only + // one interface is selected. + $scope.canCreateBridge = function() { + if ($scope.selectedMode !== SELECTION_MODE.SINGLE) { + return false; + } + var nic = getSelectedInterfaces()[0]; + if ( + nic.type === INTERFACE_TYPE.ALIAS || + nic.type === INTERFACE_TYPE.BRIDGE + ) { + return false; + } + return true; + }; - // Return true if cannot add the bridge. - $scope.cannotAddBridge = function() { - return ( - $scope.isInterfaceNameInvalid($scope.newBridgeInterface) || - $scope.isMACAddressInvalid( - $scope.newBridgeInterface.mac_address)); - }; + // Return true when the create bridge view is being shown. + $scope.isShowingCreateBridge = function() { + return $scope.selectedMode === SELECTION_MODE.CREATE_BRIDGE; + }; + + // Return true when the edit bridge view is being shown. + $scope.isShowingEdit = function() { + return $scope.selectedMode === SELECTION_MODE.EDIT; + }; + + // Toogle interfaces in edit table + $scope.toggleInterfaces = function() { + $scope.isShowingInterfaces = !$scope.isShowingInterfaces; + }; + + // Checks if row is correct type and id + $scope.isCorrectInterfaceType = function(bondInterface, parents) { + var parentIds = parents.map(function(parent) { + return parent.id; + }); - // Actually add the bridge. - $scope.addBridge = function() { - if ($scope.cannotAddBridge()) { - return; - } + var parentType = parents[0].type; + var parentFabric = parents[0].fabric; - var parents = [$scope.newBridgeInterface.primary.id]; - var mac_address = $scope.newBridgeInterface.mac_address; - if (mac_address === "") { - mac_address = $scope.newBridgeInterface.primary.mac_address; - } + if ( + bondInterface.type === parentType && + bondInterface.fabric === parentFabric && + !parentIds.includes(bondInterface.id) + ) { + return true; + } - var vlan_id, vlan = $scope.newBridgeInterface.vlan; - if (angular.isObject(vlan)) { - vlan_id = vlan.id; - } else if (angular.isObject( - $scope.newBridgeInterface.primary.vlan)) { - vlan = $scope.newBridgeInterface.primary.vlan; - vlan_id = vlan.id; - } else { - vlan_id = null; - } - var subnet_id, subnet = $scope.newBridgeInterface.subnet; - if (angular.isObject(subnet)) { - subnet_id = subnet.id; - } else { - subnet_id = null; - } + return false; + }; - var params = { - name: $scope.newBridgeInterface.name, - mac_address: mac_address, - tags: $scope.newBridgeInterface.tags.map( - function(tag) { return tag.text; }), - parents: parents, - bridge_stp: $scope.newBridgeInterface.bridge_stp, - bridge_fd: $scope.newBridgeInterface.bridge_fd, - vlan: vlan_id, - subnet: subnet_id, - mode: $scope.newBridgeInterface.mode, - ip_address: $scope.newBridgeInterface.ip_address - }; - $scope.$parent.nodesManager.createBridgeInterface( - $scope.node, params).then(null, function(error) { - // Should do something better but for now just log - // the error. - $log.error(error); - }); + // Show the create bridge view. + $scope.showCreateBridge = function() { + if ( + $scope.selectedMode === SELECTION_MODE.SINGLE && + $scope.canCreateBridge() + ) { + $scope.selectedMode = SELECTION_MODE.CREATE_BRIDGE; + + var parents = getSelectedInterfaces(); + var primary = parents[0]; + var mac_address = ""; + var fabric = ""; + var vlan = {}; + if (primary && primary.mac_address) { + mac_address = primary.mac_address; + } + if (primary && primary.fabric) { + fabric = primary.fabric; + } + if (primary && primary.vlan) { + vlan = primary.vlan; + } + $scope.newBridgeInterface = { + name: getNextName("br"), + tags: [], + parents: parents, + primary: primary, + mac_address: mac_address, + fabric: fabric, + vlan: vlan, + bridge_stp: false, + bridge_fd: 15 + }; + } + }; - // Remove the parent interface so that they don't show up - // in the listing unti the new bond appears. - var idx = $scope.interfaces.indexOf( - $scope.newBridgeInterface.primary); - if (idx > -1) { - $scope.interfaces.splice(idx, 1); - } + // Return true if cannot add the bridge. + $scope.cannotAddBridge = function() { + return ( + $scope.isInterfaceNameInvalid($scope.newBridgeInterface) || + $scope.isMACAddressInvalid($scope.newBridgeInterface.mac_address) + ); + }; + + // Actually add the bridge. + $scope.addBridge = function() { + if ($scope.cannotAddBridge()) { + return; + } - // Clear the bridge interface and reset the mode. - $scope.newBridgeInterface = {}; - $scope.selectedInterfaces = []; - $scope.selectedMode = SELECTION_MODE.NONE; - }; + var parents = [$scope.newBridgeInterface.primary.id]; + var mac_address = $scope.newBridgeInterface.mac_address; + if (mac_address === "") { + mac_address = $scope.newBridgeInterface.primary.mac_address; + } - // Return true when the create physical interface view is being shown. - $scope.isShowingCreatePhysical = function() { - return $scope.selectedMode === SELECTION_MODE.CREATE_PHYSICAL; - }; + var vlan_id, + vlan = $scope.newBridgeInterface.vlan; + if (angular.isObject(vlan)) { + vlan_id = vlan.id; + } else if (angular.isObject($scope.newBridgeInterface.primary.vlan)) { + vlan = $scope.newBridgeInterface.primary.vlan; + vlan_id = vlan.id; + } else { + vlan_id = null; + } + var subnet_id, + subnet = $scope.newBridgeInterface.subnet; + if (angular.isObject(subnet)) { + subnet_id = subnet.id; + } else { + subnet_id = null; + } - // Show the create interface view. - $scope.showCreatePhysical = function() { - if ($scope.selectedMode === SELECTION_MODE.NONE) { - $scope.selectedMode = SELECTION_MODE.CREATE_PHYSICAL; - if ($scope.$parent.isDevice) { - $scope.newInterface = { - name: getNextName("eth"), - mac_address: "", - macError: false, - tags: [], - errorMsg: null, - subnet: null, - ip_assignment: IP_ASSIGNMENT.DYNAMIC - }; - } else { - $scope.newInterface = { - name: getNextName("eth"), - mac_address: "", - macError: false, - tags: [], - errorMsg: null, - fabric: $scope.fabrics[0], - vlan: getDefaultVLAN($scope.fabrics[0]), - subnet: null, - mode: LINK_MODE.LINK_UP - }; - } - } - }; + var params = { + name: $scope.newBridgeInterface.name, + mac_address: mac_address, + tags: $scope.newBridgeInterface.tags.map(function(tag) { + return tag.text; + }), + parents: parents, + bridge_stp: $scope.newBridgeInterface.bridge_stp, + bridge_fd: $scope.newBridgeInterface.bridge_fd, + vlan: vlan_id, + subnet: subnet_id, + mode: $scope.newBridgeInterface.mode, + ip_address: $scope.newBridgeInterface.ip_address + }; + $scope.$parent.nodesManager + .createBridgeInterface($scope.node, params) + .then(null, function(error) { + // Should do something better but for now just log + // the error. + $log.error(error); + }); + + // Remove the parent interface so that they don't show up + // in the listing unti the new bond appears. + var idx = $scope.interfaces.indexOf($scope.newBridgeInterface.primary); + if (idx > -1) { + $scope.interfaces.splice(idx, 1); + } - // Return true if cannot add the interface. - $scope.cannotAddPhysicalInterface = function() { - return ( - $scope.isInterfaceNameInvalid($scope.newInterface) || - $scope.isMACAddressInvalid( - $scope.newInterface.mac_address, true)); - }; + // Clear the bridge interface and reset the mode. + $scope.newBridgeInterface = {}; + $scope.selectedInterfaces = []; + $scope.selectedMode = SELECTION_MODE.NONE; + }; - // Actually add the new physical interface. - $scope.addPhysicalInterface = function() { - if ($scope.cannotAddPhysicalInterface()) { - return; - } + // Return true when the create physical interface view is being shown. + $scope.isShowingCreatePhysical = function() { + return $scope.selectedMode === SELECTION_MODE.CREATE_PHYSICAL; + }; + + // Show the create interface view. + $scope.showCreatePhysical = function() { + if ($scope.selectedMode === SELECTION_MODE.NONE) { + $scope.selectedMode = SELECTION_MODE.CREATE_PHYSICAL; + if ($scope.$parent.isDevice) { + $scope.newInterface = { + name: getNextName("eth"), + mac_address: "", + macError: false, + tags: [], + errorMsg: null, + subnet: null, + ip_assignment: IP_ASSIGNMENT.DYNAMIC + }; + } else { + $scope.newInterface = { + name: getNextName("eth"), + mac_address: "", + macError: false, + tags: [], + errorMsg: null, + fabric: $scope.fabrics[0], + vlan: getDefaultVLAN($scope.fabrics[0]), + subnet: null, + mode: LINK_MODE.LINK_UP + }; + } + } + }; - var params; - if ($scope.$parent.isDevice) { - params = { - name: $scope.newInterface.name, - mac_address: $scope.newInterface.mac_address, - tags: $scope.newInterface.tags.map( - function(tag) { return tag.text; }), - ip_assignment: $scope.newInterface.ip_assignment, - ip_address: $scope.newInterface.ip_address - }; - } else { - params = { - name: $scope.newInterface.name, - tags: $scope.newInterface.tags.map( - function(tag) { return tag.text; }), - mac_address: $scope.newInterface.mac_address, - vlan: $scope.newInterface.vlan.id, - mode: $scope.newInterface.mode, - ip_address: $scope.newInterface.ip_address - }; - } - if (angular.isObject($scope.newInterface.subnet)) { - params.subnet = $scope.newInterface.subnet.id; - } - $scope.newInterface.macError = false; - $scope.newInterface.errorMsg = null; - $scope.$parent.nodesManager.createPhysicalInterface( - $scope.node, params).then(function() { - // Clear the interface and reset the mode. - $scope.newInterface = {}; - $scope.selectedMode = SELECTION_MODE.NONE; - }, - function(errorStr) { - const error = JSONService.tryParse(errorStr); - if (!angular.isObject(error)) { - // Was not a JSON error. This is wrong here as it - // should be, so just log to the console. - $log.error(errorStr); - } else { - const macError = error.mac_address; - if (angular.isArray(macError)) { - $scope.newInterface.macError = true; - $scope.newInterface.errorMsg = macError[0]; - } - } - }); - }; + // Return true if cannot add the interface. + $scope.cannotAddPhysicalInterface = function() { + return ( + $scope.isInterfaceNameInvalid($scope.newInterface) || + $scope.isMACAddressInvalid($scope.newInterface.mac_address, true) + ); + }; + + // Actually add the new physical interface. + $scope.addPhysicalInterface = function() { + if ($scope.cannotAddPhysicalInterface()) { + return; + } - // Load all the required managers. NodesManager and GeneralManager - // are loaded by the parent controller "NodeDetailsController". - ManagerHelperService.loadManagers($scope, [ - FabricsManager, - VLANsManager, - SubnetsManager, - UsersManager, - ControllersManager - ]).then(function() { - // GeneralManager is loaded by the parent scope however - // bond_options may not have been loaded. If it hasn't been - // loaded, load it. - if (!GeneralManager.isDataLoaded('bond_options')) { - GeneralManager.loadItems(['bond_options']); - } - $scope.managersHaveLoaded = true; - updateLoaded(); - }); + var params; + if ($scope.$parent.isDevice) { + params = { + name: $scope.newInterface.name, + mac_address: $scope.newInterface.mac_address, + tags: $scope.newInterface.tags.map(function(tag) { + return tag.text; + }), + ip_assignment: $scope.newInterface.ip_assignment, + ip_address: $scope.newInterface.ip_address + }; + } else { + params = { + name: $scope.newInterface.name, + tags: $scope.newInterface.tags.map(function(tag) { + return tag.text; + }), + mac_address: $scope.newInterface.mac_address, + vlan: $scope.newInterface.vlan.id, + mode: $scope.newInterface.mode, + ip_address: $scope.newInterface.ip_address + }; + } + if (angular.isObject($scope.newInterface.subnet)) { + params.subnet = $scope.newInterface.subnet.id; + } + $scope.newInterface.macError = false; + $scope.newInterface.errorMsg = null; + $scope.$parent.nodesManager + .createPhysicalInterface($scope.node, params) + .then( + function() { + // Clear the interface and reset the mode. + $scope.newInterface = {}; + $scope.selectedMode = SELECTION_MODE.NONE; + }, + function(errorStr) { + const error = JSONService.tryParse(errorStr); + if (!angular.isObject(error)) { + // Was not a JSON error. This is wrong here as it + // should be, so just log to the console. + $log.error(errorStr); + } else { + const macError = error.mac_address; + if (angular.isArray(macError)) { + $scope.newInterface.macError = true; + $scope.newInterface.errorMsg = macError[0]; + } + } + } + ); + }; + + // Load all the required managers. NodesManager and GeneralManager + // are loaded by the parent controller "NodeDetailsController". + ManagerHelperService.loadManagers($scope, [ + FabricsManager, + VLANsManager, + SubnetsManager, + UsersManager, + ControllersManager + ]).then(function() { + // GeneralManager is loaded by the parent scope however + // bond_options may not have been loaded. If it hasn't been + // loaded, load it. + if (!GeneralManager.isDataLoaded("bond_options")) { + GeneralManager.loadItems(["bond_options"]); + } + $scope.managersHaveLoaded = true; + updateLoaded(); + }); - // Tell $parent that the networkingController has been loaded. - $scope.$parent.controllerLoaded('networkingController', $scope); + // Tell $parent that the networkingController has been loaded. + $scope.$parent.controllerLoaded("networkingController", $scope); } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details_storage_filesystems.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details_storage_filesystems.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details_storage_filesystems.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details_storage_filesystems.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,103 +6,109 @@ */ class SpecialFilesystem { - constructor(fstype = null) { - this.fstype = fstype; - this.mountPoint = ""; - this.mountOptions = ""; + constructor(fstype = null) { + this.fstype = fstype; + this.mountPoint = ""; + this.mountOptions = ""; + } + + isValid() { + return this.mountPoint.startsWith("/"); + } + + describe() { + if (!this.fstype) { + return; } - - isValid() { - return this.mountPoint.startsWith("/"); + var parts = [this.fstype]; + // Add the mount point if specified and valid. + if (this.mountPoint.startsWith("/")) { + parts.push("at " + this.mountPoint); } - - describe() { - if (!this.fstype) { - return; - } - var parts = [this.fstype]; - // Add the mount point if specified and valid. - if (this.mountPoint.startsWith('/')) { - parts.push("at " + this.mountPoint); + // Filesystem-specific bits. + switch (this.fstype) { + case "tmpfs": + // Extract size=n parameter from mount options. Other + // options could be added in the future. + var size = this.mountOptions.match(/\bsize=(\d+)(%?)/); + if (size !== null) { + if (size[2] === "%") { + parts.push("limited to " + size[1] + "% of memory"); + } else { + parts.push("limited to " + size[1] + " bytes"); + } } - // Filesystem-specific bits. - switch (this.fstype) { - case "tmpfs": - // Extract size=n parameter from mount options. Other - // options could be added in the future. - var size = this.mountOptions.match(/\bsize=(\d+)(%?)/); - if (size !== null) { - if (size[2] === "%") { - parts.push("limited to " + size[1] + "% of memory"); - } - else { - parts.push("limited to " + size[1] + " bytes"); - } - } - break; - case "ramfs": - // This filesystem does not recognise any options. Consider - // warning about lack of a size limit. - break; - } - return parts.join(" "); + break; + case "ramfs": + // This filesystem does not recognise any options. Consider + // warning about lack of a size limit. + break; } + return parts.join(" "); + } } /* @ngInject */ export function NodeFilesystemsController($scope) { - // Which drop-down is currently selected, e.g. "special". - $scope.dropdown = null; + // Which drop-down is currently selected, e.g. "special". + $scope.dropdown = null; - // Select the "special" drop-down. - $scope.addSpecialFilesystem = function() { - $scope.dropdown = "special"; - }; - - // Deselect the "special" drop-down. - $scope.addSpecialFilesystemFinished = function() { - if ($scope.dropdown === "special") { - $scope.dropdown = null; - } - }; + // Select the "special" drop-down. + $scope.addSpecialFilesystem = function() { + $scope.dropdown = "special"; + }; + + // Deselect the "special" drop-down. + $scope.addSpecialFilesystemFinished = function() { + if ($scope.dropdown === "special") { + $scope.dropdown = null; + } + }; } /* @ngInject */ export function NodeAddSpecialFilesystemController($scope, MachinesManager) { - $scope.machineManager = MachinesManager; - $scope.specialFilesystemTypes = ['tmpfs', 'ramfs']; - $scope.newFilesystem = { - 'system_id': $scope.node.system_id, - }; - $scope.filesystem = new SpecialFilesystem(); - $scope.description = null; // Updated by watch. - - const watches = { - fstype: 'fstype', - mountPoint: 'mount_point', - mountOptions: 'mount_options' - }; - - for (let k in watches) { - $scope.$watch(function() { - return $scope.newFilesystem.$maasForm.getValue(watches[k]); - }, function(result) { - $scope.filesystem[k] = result; - }); - } + $scope.machineManager = MachinesManager; + $scope.specialFilesystemTypes = ["tmpfs", "ramfs"]; + $scope.newFilesystem = { + system_id: $scope.node.system_id + }; + $scope.filesystem = new SpecialFilesystem(); + $scope.description = null; // Updated by watch. + + const watches = { + fstype: "fstype", + mountPoint: "mount_point", + mountOptions: "mount_options" + }; + + for (let k in watches) { + $scope.$watch( + function() { + return $scope.newFilesystem.$maasForm.getValue(watches[k]); + }, + function(result) { + $scope.filesystem[k] = result; + } + ); + } + + $scope.$watch( + "filesystem", + function() { + if ($scope.filesystem) { + $scope.description = $scope.filesystem.describe(); + } + }, + true + ); + + $scope.canMount = function() { + return $scope.filesystem.isValid(); + }; - $scope.$watch('filesystem', function() { - if ($scope.filesystem) { - $scope.description = $scope.filesystem.describe(); - } - }, true); - - $scope.canMount = function() { - return $scope.filesystem.isValid(); - }; - - $scope.cancel = function() { - $scope.filesystem = new SpecialFilesystem(); - $scope.addSpecialFilesystemFinished(); - }; + $scope.cancel = function() { + $scope.filesystem = new SpecialFilesystem(); + $scope.addSpecialFilesystemFinished(); + }; } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details_storage.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details_storage.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_details_storage.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_details_storage.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,2065 +4,2446 @@ * MAAS Node Storage Controller */ - // Filter that is specific to the NodeStorageController. Remove the available // disks from the list if being used in the availableNew. export function removeAvailableByNew() { - return function(disks, availableNew) { - if (!angular.isObject(availableNew) || ( - !angular.isObject(availableNew.device) && - !angular.isArray(availableNew.devices))) { - return disks; - } - - var filtered = []; - var single = true; - if (angular.isArray(availableNew.devices)) { - single = false; - } - angular.forEach(disks, function(disk) { - if (single) { - if (disk !== availableNew.device) { - filtered.push(disk); - } - } else { - var i, found = false; - for (i = 0; i < availableNew.devices.length; i++) { - if (disk === availableNew.devices[i]) { - found = true; - break; - } - } - if (!found) { - filtered.push(disk); - } - } - }); - return filtered; - }; + return function(disks, availableNew) { + if ( + !angular.isObject(availableNew) || + (!angular.isObject(availableNew.device) && + !angular.isArray(availableNew.devices)) + ) { + return disks; + } + + var filtered = []; + var single = true; + if (angular.isArray(availableNew.devices)) { + single = false; + } + angular.forEach(disks, function(disk) { + if (single) { + if (disk !== availableNew.device) { + filtered.push(disk); + } + } else { + var i, + found = false; + for (i = 0; i < availableNew.devices.length; i++) { + if (disk === availableNew.devices[i]) { + found = true; + break; + } + } + if (!found) { + filtered.push(disk); + } + } + }); + return filtered; + }; +} + +export function datastoresOnly() { + return function(filesystems) { + return filesystems.filter(filesystem => { + return filesystem.parent_type == "vmfs6"; + }); + }; } /* @ngInject */ export function NodeStorageController( - $scope, MachinesManager, ConverterService, UsersManager) { - // From models/partitiontable.py - must be kept in sync. - var INITIAL_PARTITION_OFFSET = 4 * 1024 * 1024; - var END_OF_PARTITION_TABLE_SPACE = 1024 * 1024; - var PARTITION_TABLE_EXTRA_SPACE = INITIAL_PARTITION_OFFSET + - END_OF_PARTITION_TABLE_SPACE; - var PREP_PARTITION_SIZE = 8 * 1024 * 1024; - - // From models/partition.py - must be kept in sync. - var PARTITION_ALIGNMENT_SIZE = 4 * 1024 * 1024; - var MIN_PARTITION_SIZE = PARTITION_ALIGNMENT_SIZE; - - // Different selection modes. - var SELECTION_MODE = { - NONE: null, - SINGLE: "single", - MUTLI: "multi", - UNMOUNT: "unmount", - UNFORMAT: "unformat", - EDIT: "edit", - DELETE: "delete", - FORMAT_AND_MOUNT: "format-mount", - PARTITION: "partition", - BCACHE: "bcache", - RAID: "raid", - VOLUME_GROUP: "volume-group", - LOGICAL_VOLUME: "logical-volume" - }; + $scope, + MachinesManager, + ConverterService, + UsersManager, + $log, + $timeout, + $filter +) { + // From models/partitiontable.py - must be kept in sync. + var INITIAL_PARTITION_OFFSET = 4 * 1024 * 1024; + var END_OF_PARTITION_TABLE_SPACE = 1024 * 1024; + var PARTITION_TABLE_EXTRA_SPACE = + INITIAL_PARTITION_OFFSET + END_OF_PARTITION_TABLE_SPACE; + var PREP_PARTITION_SIZE = 8 * 1024 * 1024; + + // From models/partition.py - must be kept in sync. + var PARTITION_ALIGNMENT_SIZE = 4 * 1024 * 1024; + var MIN_PARTITION_SIZE = PARTITION_ALIGNMENT_SIZE; + + // Different selection modes. + var SELECTION_MODE = { + NONE: null, + SINGLE: "single", + MUTLI: "multi", + UNMOUNT: "unmount", + UNFORMAT: "unformat", + EDIT: "edit", + DELETE: "delete", + FORMAT_AND_MOUNT: "format-mount", + PARTITION: "partition", + BCACHE: "bcache", + RAID: "raid", + VOLUME_GROUP: "volume-group", + LOGICAL_VOLUME: "logical-volume" + }; + + // Different available raid modes. + var RAID_MODES = [ + { + level: "raid-0", + title: "RAID 0", + min_disks: 2, + allows_spares: false, + calculateSize: function(minSize, numDisks) { + return minSize * numDisks; + } + }, + { + level: "raid-1", + title: "RAID 1", + min_disks: 2, + allows_spares: true, + calculateSize: function(minSize) { + return minSize; + } + }, + { + level: "raid-5", + title: "RAID 5", + min_disks: 3, + allows_spares: true, + calculateSize: function(minSize, numDisks) { + return minSize * (numDisks - 1); + } + }, + { + level: "raid-6", + title: "RAID 6", + min_disks: 4, + allows_spares: true, + calculateSize: function(minSize, numDisks) { + return minSize * (numDisks - 2); + } + }, + { + level: "raid-10", + title: "RAID 10", + min_disks: 3, + allows_spares: true, + calculateSize: function(minSize, numDisks) { + return (minSize * numDisks) / 2; + } + } + ]; + + var datastoreOnly = $filter("datastoresOnly"); + var formatBytes = $filter("formatBytes"); - // Different available raid modes. - var RAID_MODES = [ + $scope.tableInfo = { column: "name" }; + $scope.has_disks = false; + $scope.filesystems = []; + $scope.filesystemsMap = {}; + $scope.filesystemMode = SELECTION_MODE.NONE; + $scope.filesystemAllSelected = false; + $scope.cachesets = []; + $scope.cachesetsMap = {}; + $scope.cachesetsMode = SELECTION_MODE.NONE; + $scope.cachesetsAllSelected = false; + $scope.available = []; + $scope.availableMap = {}; + $scope.availableMode = SELECTION_MODE.NONE; + $scope.availableAllSelected = false; + $scope.availableNew = {}; + $scope.newPartition = {}; + $scope.nodeManager = MachinesManager; + $scope.used = []; + $scope.showMembers = []; + $scope.createNewDatastore = false; + $scope.addToExistingDatastore = false; + $scope.datastores = { + new: {}, + old: {} + }; + $scope.selectedAvailableDatastores = []; + $scope.creatingDatastore = false; + $scope.updatingDatastore = false; + $scope.updatingOSFamily = false; + $scope.updatingStorageLayout = false; + $scope.confirmStorageLayout = false; + $scope.newLayout = ""; + $scope.addToDatastoreValid = false; + + // XXX: Steve Rydz 09/08/2019 + // Hardcoded for now in current cycle as no mapping exists + $scope.osFamilies = [ + { + id: "linux", + name: "Linux", + layouts: [ { - level: "raid-0", - title: "RAID 0", - min_disks: 2, - allows_spares: false, - calculateSize: function(minSize, numDisks) { - return minSize * numDisks; - } + id: "flat", + name: "Flat" }, { - level: "raid-1", - title: "RAID 1", - min_disks: 2, - allows_spares: true, - calculateSize: function(minSize, numDisks) { - return minSize; - } + id: "lvm", + name: "LVM" }, { - level: "raid-5", - title: "RAID 5", - min_disks: 3, - allows_spares: true, - calculateSize: function(minSize, numDisks) { - return minSize * (numDisks - 1); - } + id: "bcache", + name: "bcache" }, { - level: "raid-6", - title: "RAID 6", - min_disks: 4, - allows_spares: true, - calculateSize: function(minSize, numDisks) { - return minSize * (numDisks - 2); - } + id: "vmfs6", + name: "VMFS6 (VMware ESXI)" }, { - level: "raid-10", - title: "RAID 10", - min_disks: 3, - allows_spares: true, - calculateSize: function(minSize, numDisks) { - return minSize * numDisks / 2; - } - } - ]; - - $scope.tableInfo = { column: 'name' }; - $scope.has_disks = false; - $scope.filesystems = []; - $scope.filesystemsMap = {}; - $scope.filesystemMode = SELECTION_MODE.NONE; - $scope.filesystemAllSelected = false; - $scope.cachesets = []; - $scope.cachesetsMap = {}; - $scope.cachesetsMode = SELECTION_MODE.NONE; - $scope.cachesetsAllSelected = false; - $scope.available = []; - $scope.availableMap = {}; - $scope.availableMode = SELECTION_MODE.NONE; - $scope.availableAllSelected = false; - $scope.availableNew = {}; - $scope.newPartition = {}; - $scope.nodeManager = MachinesManager; - $scope.used = []; - $scope.showMembers = []; - - // Return True if the filesystem is mounted. - function isMountedFilesystem(filesystem) { - return angular.isObject(filesystem) && - angular.isString(filesystem.mount_point) && - filesystem.mount_point !== ""; - } - - // Return True if the item has a filesystem and it's mounted. - function hasMountedFilesystem(item) { - return angular.isObject(item) && - isMountedFilesystem(item.filesystem); - } - - // Returns the fstype if the item has a filesystem and its unmounted. - function hasFormattedUnmountedFilesystem(item) { - if (angular.isObject(item.filesystem) && - angular.isString(item.filesystem.fstype) && - item.filesystem.fstype !== '' && - (angular.isString(item.filesystem.mount_point) === false || - item.filesystem.mount_point === '')) { - return item.filesystem.fstype; - } else { - return null; + id: "blank", + name: "No storage (blank) layout" } + ] } + ]; - // Return True if the item is in use. - function isInUse(item) { - if (item.type === "cache-set") { - return true; - } else if (angular.isObject(item.filesystem)) { - if (item.filesystem.is_format_fstype && - angular.isString(item.filesystem.mount_point) && - item.filesystem.mount_point !== "") { - return true; - } else if (!item.filesystem.is_format_fstype) { - return true; - } - return false; - } - return item.available_size < MIN_PARTITION_SIZE; - } + $scope.osFamily = $scope.osFamilies[0]; + $scope.storageLayout = $scope.osFamily.layouts.find(layout => { + return layout.id === $scope.node.detected_storage_layout; + }); + + $scope.openStorageLayoutConfirm = function(selectedLayout) { + $scope.osFamily.layouts.forEach(layout => { + if (layout.id === selectedLayout) { + $scope.newLayout = layout; + } + }); + $scope.confirmStorageLayout = true; + }; + + $scope.closeStorageLayoutConfirm = function() { + $scope.confirmStorageLayout = false; + }; + + $scope.updateStorageLayout = function(storageLayout) { + storageLayout = $scope.storageLayout = $scope.newLayout; + + var params = { + system_id: $scope.node.system_id, + storage_layout: storageLayout.id + }; + + $scope.updatingStorageLayout = true; + + MachinesManager.applyStorageLayout(params) + .then(function() { + $timeout(function() { + $scope.updatingStorageLayout = false; + }, 0); + }) + .catch(function(error) { + $log.error(error); + $timeout(function() { + $scope.updatingStorageLayout = false; + }, 0); + }); + + $scope.closeStorageLayoutConfirm(); + }; + + $scope.openNewDatastorePanel = function() { + $scope.createNewDatastore = true; + var selectedDisks = $scope.getSelectedAvailable(); + $scope.datastores.new = { + id: selectedDisks[0].id, + name: "", + mountpoint: selectedDisks[0].mount_point, + filesystem: "VMFS6", + size: selectedDisks[0].size_human + }; + }; + + $scope.closeNewDatastorePanel = function() { + $scope.createNewDatastore = false; + $scope.datastores.new = {}; + }; + + $scope.openAddToExistingDatastorePanel = function() { + $scope.addToExistingDatastore = true; + $scope.selectedAvailableDatastores = $scope.getSelectedAvailable(); + $scope.datastores.old = datastoreOnly($scope.node.disks)[0]; + }; + + $scope.closeAddToExistingDatastorePanel = function() { + $scope.addToExistingDatastore = false; + $scope.datastores.new = {}; + }; + + $scope.canPerformActionOnDatastoreSet = function() { + var editing = $scope.addToExistingDatastore || $scope.createNewDatastore; + var selected = $scope.selectedAvailableDatastores.length > 0; + var vmfs6 = $scope.storageLayout && $scope.storageLayout.id === "vmfs6"; + return !editing && selected && vmfs6; + }; + + $scope.canAddToDatastore = function() { + var datastores = $scope.node.disks.filter(function(disk) { + return disk.parent_type === "vmfs6"; + }); - // Return the tags formatted for ngTagInput. - function getTags(disk) { - var tags = []; - angular.forEach(disk.tags, function(tag) { - tags.push({ text: tag }); - }); - return tags; + if ($scope.canPerformActionOnDatastoreSet() && datastores.length) { + return true; } - // Return a unique key that will never change. - function getUniqueKey(disk) { - if (disk.type === "cache-set") { - return "cache-set-" + disk.cache_set_id; - } else { - var key = disk.type + "-" + disk.block_id; - if (angular.isNumber(disk.partition_id)) { - key += "-" + disk.partition_id; - } - return key; - } - } - - // Update the list of filesystems. Only filesystems with a mount point - // set go here. If no mount point is set, it goes in available. - function updateFilesystems() { - // Create the new list of filesystems. - var filesystems = []; - angular.forEach($scope.node.disks, function(disk) { - if (hasMountedFilesystem(disk)) { - var data = { - "type": "filesystem", - "name": disk.name, - "size_human": disk.size_human, - "fstype": disk.filesystem.fstype, - "mount_point": disk.filesystem.mount_point, - "mount_options": disk.filesystem.mount_options, - "block_id": disk.id, - "partition_id": null, - "filesystem_id": disk.filesystem.id, - "original_type": disk.type, - "original": disk - }; - if (disk.type === "virtual") { - disk.parent_type = disk.parent.type; - } - filesystems.push(data); - } - angular.forEach(disk.partitions, function(partition) { - if (hasMountedFilesystem(partition)) { - filesystems.push({ - "type": "filesystem", - "name": partition.name, - "size_human": partition.size_human, - "fstype": partition.filesystem.fstype, - "mount_point": partition.filesystem.mount_point, - "mount_options": - partition.filesystem.mount_options, - "block_id": disk.id, - "partition_id": partition.id, - "filesystem_id": partition.filesystem.id, - "original_type": "partition", - "original": partition - }); - } - }); - }); + return false; + }; - // Add special filesystems to the filesystem list. A special - // filesystem cannot exist unless mounted, so we don't need - // to check. - angular.forEach( - $scope.node.special_filesystems, - function(filesystem) { - filesystems.push({ - "type": "filesystem", - "name": "\u2014", - "size_human": "\u2014", - "fstype": filesystem.fstype, - "mount_point": filesystem.mount_point, - "mount_options": filesystem.mount_options, - "block_id": null, - "partition_id": null, - "original_type": "special" - }); - }); - - // Update the selected filesystems with the currently selected - // filesystems. - angular.forEach(filesystems, function(filesystem) { - var key = getUniqueKey(filesystem); - var oldFilesystem = $scope.filesystemsMap[key]; - if (angular.isObject(oldFilesystem)) { - filesystem.$selected = oldFilesystem.$selected; - } else { - filesystem.$selected = false; - } - }); + $scope.createDatastore = function() { + $scope.createNewDatastore = true; - // Update the filesystems and filesystemsMap on the scope. - $scope.filesystems = filesystems; - $scope.filesystemsMap = {}; - angular.forEach(filesystems, function(filesystem) { - $scope.filesystemsMap[getUniqueKey(filesystem)] = filesystem; - }); - - // Update the selection mode. - $scope.updateFilesystemSelection(false); + var selectedAvailable = $scope.getSelectedAvailable(); + var blockDeviceIDs = []; + var partitionIDs = []; + + selectedAvailable.forEach(function(item) { + if (item.type === "partition") { + partitionIDs.push(item.partition_id); + } else { + blockDeviceIDs.push(item.block_id); + } + }); + + var params = { + system_id: $scope.node.system_id, + block_devices: blockDeviceIDs, + partitions: partitionIDs, + name: $scope.datastores.new.name + }; + + $scope.creatingDatastore = true; + + MachinesManager.createDatastore(params) + .then(function() { + $timeout(function() { + $scope.creatingDatastore = false; + }, 0); + $scope.closeNewDatastorePanel(); + $scope.selectedAvailableDatastores = []; + }) + .catch(function(error) { + $log.error(error); + $timeout(function() { + $scope.creatingDatastore = false; + }, 0); + }); + }; + + $scope.checkAddToDatastoreValid = function() { + var selectedAvailable = $scope.getSelectedAvailable(); + var valid = true; + if (selectedAvailable.length < 1) { + valid = false; } + selectedAvailable.forEach(function(item) { + if (item.has_partitions) { + valid = false; + } + }); + $scope.addToDatastoreValid = valid; + }; + + $scope.addToDatastore = function() { + var selectedAvailable = $scope.getSelectedAvailable(); + var blockDeviceIDs = []; + var partitionIDs = []; + + selectedAvailable.forEach(function(item) { + if (item.type === "partition") { + partitionIDs.push(item.partition_id); + } else { + blockDeviceIDs.push(item.block_id); + } + }); + + var params = { + system_id: $scope.node.system_id, + add_block_devices: blockDeviceIDs, + add_partitions: partitionIDs, + name: $scope.datastores.old.name, + vmfs_datastore_id: $scope.datastores.old.id + }; + + $scope.updatingDatastore = true; + + MachinesManager.updateDatastore(params) + .then(function() { + $timeout(function() { + $scope.updatingDatastore = false; + }, 0); + $scope.closeAddToExistingDatastorePanel(); + $scope.selectedAvailableDatastores = []; + }) + .catch(function(error) { + $log.error(error); + $timeout(function() { + $scope.updatingDatastore = false; + }, 0); + }); + }; + + $scope.storageLayoutIsReadOnly = function(layouts) { + return layouts.length <= 1; + }; + + $scope.storageLayoutIsDisabled = function(layouts) { + return !layouts.length; + }; + + $scope.hasStorageLayout = function(storageLayout) { + return storageLayout ? true : false; + }; + + // Return True if the filesystem is mounted. + function isMountedFilesystem(filesystem) { + return ( + angular.isObject(filesystem) && + angular.isString(filesystem.mount_point) && + filesystem.mount_point !== "" + ); + } + + // Return True if the item has a filesystem and it's mounted. + function hasMountedFilesystem(item) { + return angular.isObject(item) && isMountedFilesystem(item.filesystem); + } + + // Returns the fstype if the item has a filesystem and its unmounted. + function hasFormattedUnmountedFilesystem(item) { + if ( + angular.isObject(item.filesystem) && + angular.isString(item.filesystem.fstype) && + item.filesystem.fstype !== "" && + (angular.isString(item.filesystem.mount_point) === false || + item.filesystem.mount_point === "") + ) { + return item.filesystem.fstype; + } else { + return null; + } + } - // Update the list of cache sets. - function updateCacheSets() { - // Create the new list of cache sets. - var cachesets = []; - angular.forEach($scope.node.disks, function(disk) { - if (disk.type === "cache-set") { - cachesets.push({ - "type": "cache-set", - "name": disk.name, - "size_human": disk.size_human, - "cache_set_id": disk.id, - "used_by": disk.used_for - }); - } - }); - - // Update the selected cache sets with the currently selected - // cache sets. - angular.forEach(cachesets, function(cacheset) { - var key = getUniqueKey(cacheset); - var oldCacheSet = $scope.cachesetsMap[key]; - if (angular.isObject(oldCacheSet)) { - cacheset.$selected = oldCacheSet.$selected; - } else { - cacheset.$selected = false; - } - }); - - // Update the cachesets and cachesetsMap on the scope. - $scope.cachesets = cachesets; - $scope.cachesetsMap = {}; - angular.forEach(cachesets, function(cacheset) { - $scope.cachesetsMap[getUniqueKey(cacheset)] = cacheset; - }); - - // Update the selection mode. - $scope.updateCacheSetsSelection(false); + // Return True if the item is in use. + function isInUse(item) { + if (item.type === "cache-set") { + return true; + } else if (angular.isObject(item.filesystem)) { + if ( + item.filesystem.is_format_fstype && + angular.isString(item.filesystem.mount_point) && + item.filesystem.mount_point !== "" + ) { + return true; + } else if (!item.filesystem.is_format_fstype) { + return true; + } + return false; } + return item.available_size < MIN_PARTITION_SIZE; + } - // Update list of all available disks. - function updateAvailable() { - var available = []; - angular.forEach($scope.node.disks, function(disk) { - if (!isInUse(disk)) { - var has_partitions = false; - if (angular.isArray(disk.partitions) && - disk.partitions.length > 0) { - has_partitions = true; - } - var data = { - "name": disk.name, - "size_human": disk.size_human, - "available_size_human": disk.available_size_human, - "used_size_human": disk.used_size_human, - "type": disk.type, - "model": disk.model, - "serial": disk.serial, - "tags": getTags(disk), - "fstype": hasFormattedUnmountedFilesystem(disk), - "mount_point": null, - "mount_options": null, - "block_id": disk.id, - "partition_id": null, - "has_partitions": has_partitions, - "is_boot": disk.is_boot, - "original": disk, - "test_status": disk.test_status, - "firmware_version": disk.firmware_version - }; - if (disk.type === "virtual") { - data.parent_type = disk.parent.type; - } - available.push(data); - } - angular.forEach(disk.partitions, function(partition) { - if (!isInUse(partition)) { - available.push({ - "name": partition.name, - "size_human": partition.size_human, - "available_size_human": ( - partition.available_size_human), - "used_size_human": partition.used_size_human, - "type": "partition", - "model": "", - "serial": "", - "tags": [], - "fstype": - hasFormattedUnmountedFilesystem(partition), - "mount_point": null, - "mount_options": null, - "block_id": disk.id, - "partition_id": partition.id, - "has_partitions": false, - "is_boot": false, - "original": partition - }); - } - }); - }); + // Return the tags formatted for ngTagInput. + function getTags(disk) { + var tags = []; + angular.forEach(disk.tags, function(tag) { + tags.push({ text: tag }); + }); + return tags; + } + + // Return a unique key that will never change. + function getUniqueKey(disk) { + if (disk.type === "cache-set") { + return "cache-set-" + disk.cache_set_id; + } else { + var key = disk.type + "-" + disk.block_id; + if (angular.isNumber(disk.partition_id)) { + key += "-" + disk.partition_id; + } + return key; + } + } - // Update the selected available disks with the currently selected - // available disks. Also copy the $options so they are not lost - // for the current action. - angular.forEach(available, function(disk) { - var key = getUniqueKey(disk); - var oldDisk = $scope.availableMap[key]; - if (angular.isObject(oldDisk)) { - disk.$selected = oldDisk.$selected; - disk.$options = oldDisk.$options; - } else { - disk.$selected = false; - disk.$options = {}; - } - }); + // Update the list of filesystems. Only filesystems with a mount point + // set go here. If no mount point is set, it goes in available. + function updateFilesystems() { + // Create the new list of filesystems. + var filesystems = []; + angular.forEach($scope.node.disks, function(disk) { + if (hasMountedFilesystem(disk)) { + var data = { + type: "filesystem", + name: disk.name, + size_human: disk.size_human, + fstype: disk.filesystem.fstype, + mount_point: disk.filesystem.mount_point, + mount_options: disk.filesystem.mount_options, + block_id: disk.id, + partition_id: null, + filesystem_id: disk.filesystem.id, + original_type: disk.type, + original: disk + }; + if (disk.type === "virtual") { + disk.parent_type = disk.parent.type; + } + filesystems.push(data); + } + angular.forEach(disk.partitions, function(partition) { + if (hasMountedFilesystem(partition)) { + filesystems.push({ + type: "filesystem", + name: partition.name, + size_human: partition.size_human, + fstype: partition.filesystem.fstype, + mount_point: partition.filesystem.mount_point, + mount_options: partition.filesystem.mount_options, + block_id: disk.id, + partition_id: partition.id, + filesystem_id: partition.filesystem.id, + original_type: "partition", + original: partition + }); + } + }); + }); + + // Add special filesystems to the filesystem list. A special + // filesystem cannot exist unless mounted, so we don't need + // to check. + angular.forEach($scope.node.special_filesystems, function(filesystem) { + filesystems.push({ + type: "filesystem", + name: "\u2014", + size_human: "\u2014", + fstype: filesystem.fstype, + mount_point: filesystem.mount_point, + mount_options: filesystem.mount_options, + block_id: null, + partition_id: null, + original_type: "special" + }); + }); + + // Update the selected filesystems with the currently selected + // filesystems. + angular.forEach(filesystems, function(filesystem) { + var key = getUniqueKey(filesystem); + var oldFilesystem = $scope.filesystemsMap[key]; + if (angular.isObject(oldFilesystem)) { + filesystem.$selected = oldFilesystem.$selected; + } else { + filesystem.$selected = false; + } + }); - // Update available and availableMap on the scope. - $scope.available = available; - $scope.availableMap = {}; - angular.forEach(available, function(disk) { - $scope.availableMap[getUniqueKey(disk)] = disk; - }); + // Update the filesystems and filesystemsMap on the scope. + $scope.filesystems = filesystems; + $scope.filesystemsMap = {}; + angular.forEach(filesystems, function(filesystem) { + $scope.filesystemsMap[getUniqueKey(filesystem)] = filesystem; + }); + + // Update the selection mode. + $scope.updateFilesystemSelection(false); + } + + // Update the list of cache sets. + function updateCacheSets() { + // Create the new list of cache sets. + var cachesets = []; + angular.forEach($scope.node.disks, function(disk) { + if (disk.type === "cache-set") { + cachesets.push({ + type: "cache-set", + name: disk.name, + size_human: disk.size_human, + cache_set_id: disk.id, + used_by: disk.used_for + }); + } + }); + + // Update the selected cache sets with the currently selected + // cache sets. + angular.forEach(cachesets, function(cacheset) { + var key = getUniqueKey(cacheset); + var oldCacheSet = $scope.cachesetsMap[key]; + if (angular.isObject(oldCacheSet)) { + cacheset.$selected = oldCacheSet.$selected; + } else { + cacheset.$selected = false; + } + }); - // Update device or devices on the availableNew object to be - // there new objects. - if (angular.isObject($scope.availableNew)) { - // Update device. - if (angular.isObject($scope.availableNew.device)) { - var key = getUniqueKey($scope.availableNew.device); - $scope.availableNew.device = $scope.availableMap[key]; - // Update devices. - } else if (angular.isArray($scope.availableNew.devices)) { - var newDevices = []; - angular.forEach( - $scope.availableNew.devices, function(device) { - var key = getUniqueKey(device); - var newDevice = $scope.availableMap[key]; - if (angular.isObject(newDevice)) { - newDevices.push(newDevice); - } - }); - $scope.availableNew.devices = newDevices; - } + // Update the cachesets and cachesetsMap on the scope. + $scope.cachesets = cachesets; + $scope.cachesetsMap = {}; + angular.forEach(cachesets, function(cacheset) { + $scope.cachesetsMap[getUniqueKey(cacheset)] = cacheset; + }); + + // Update the selection mode. + $scope.updateCacheSetsSelection(false); + } + + // Update list of all available disks. + function updateAvailable() { + var available = []; + angular.forEach($scope.node.disks, function(disk) { + if (!isInUse(disk)) { + var has_partitions = false; + if (angular.isArray(disk.partitions) && disk.partitions.length > 0) { + has_partitions = true; + } + var data = { + name: disk.name, + size_human: disk.size_human, + size: disk.size, + available_size_human: disk.available_size_human, + used_size_human: disk.used_size_human, + type: disk.type, + model: disk.model, + serial: disk.serial, + tags: getTags(disk), + fstype: hasFormattedUnmountedFilesystem(disk), + mount_point: null, + mount_options: null, + block_id: disk.id, + partition_id: null, + has_partitions: has_partitions, + is_boot: disk.is_boot, + original: disk, + test_status: disk.test_status, + firmware_version: disk.firmware_version + }; + if (disk.type === "virtual") { + data.parent_type = disk.parent.type; } + available.push(data); + } + angular.forEach(disk.partitions, function(partition) { + if (!isInUse(partition)) { + available.push({ + name: partition.name, + size_human: partition.size_human, + size: partition.size, + available_size_human: partition.available_size_human, + used_size_human: partition.used_size_human, + type: "partition", + model: "", + serial: "", + tags: [], + fstype: hasFormattedUnmountedFilesystem(partition), + mount_point: null, + mount_options: null, + block_id: disk.id, + partition_id: partition.id, + has_partitions: false, + is_boot: false, + original: partition + }); + } + }); + }); + + // Update the selected available disks with the currently selected + // available disks. Also copy the $options so they are not lost + // for the current action. + angular.forEach(available, function(disk) { + var key = getUniqueKey(disk); + var oldDisk = $scope.availableMap[key]; + if (angular.isObject(oldDisk)) { + disk.$selected = oldDisk.$selected; + disk.$options = oldDisk.$options; + } else { + disk.$selected = false; + disk.$options = {}; + } + }); - // Update the selection mode. - $scope.updateAvailableSelection(false); - } - - // Update list of all used disks. - function updateUsed() { - var used = []; - angular.forEach($scope.node.disks, function(disk) { - if (isInUse(disk) && disk.type !== "cache-set") { - var has_partitions = false; - if (angular.isArray(disk.partitions) && - disk.partitions.length > 0) { - has_partitions = true; - } - var data = { - "name": disk.name, - "type": disk.type, - "model": disk.model, - "serial": disk.serial, - "tags": getTags(disk), - "used_for": disk.used_for, - "is_boot": disk.is_boot, - "has_partitions": has_partitions, - "test_status": disk.test_status, - "firmware_version": disk.firmware_version - }; - if (disk.type === "virtual") { - data.parent_type = disk.parent.type; - } - used.push(data); - } - angular.forEach(disk.partitions, function(partition) { - if (isInUse(partition) && partition.type !== "cache-set") { - used.push({ - "name": partition.name, - "type": "partition", - "model": "", - "serial": "", - "tags": [], - "used_for": partition.used_for, - "is_boot": false - }); - } - }); + // Update available and availableMap on the scope. + $scope.available = available; + $scope.availableMap = {}; + angular.forEach(available, function(disk) { + $scope.availableMap[getUniqueKey(disk)] = disk; + }); + + // Update device or devices on the availableNew object to be + // there new objects. + if (angular.isObject($scope.availableNew)) { + // Update device. + if (angular.isObject($scope.availableNew.device)) { + var key = getUniqueKey($scope.availableNew.device); + $scope.availableNew.device = $scope.availableMap[key]; + // Update devices. + } else if (angular.isArray($scope.availableNew.devices)) { + var newDevices = []; + angular.forEach($scope.availableNew.devices, function(device) { + var key = getUniqueKey(device); + var newDevice = $scope.availableMap[key]; + if (angular.isObject(newDevice)) { + newDevices.push(newDevice); + } }); - $scope.used = used; + $scope.availableNew.devices = newDevices; + } } - // Updates the filesystem, available, and used list. - function updateDisks() { - if (angular.isArray($scope.node.disks)) { - $scope.has_disks = $scope.node.disks.length > 0; - updateFilesystems(); - updateCacheSets(); - updateAvailable(); - updateUsed(); - } else { - $scope.has_disks = false; - $scope.filesystems = []; - $scope.filesystemsMap = {}; - $scope.filesystemMode = SELECTION_MODE.NONE; - $scope.filesystemAllSelected = false; - $scope.cachesets = []; - $scope.cachesetsMap = {}; - $scope.cachesetsMode = SELECTION_MODE.NONE; - $scope.cachesetsAllSelected = false; - $scope.available = []; - $scope.availableMap = {}; - $scope.availableMode = SELECTION_MODE.NONE; - $scope.availableAllSelected = false; - $scope.availableNew = {}; - $scope.used = []; + // Update the selection mode. + $scope.updateAvailableSelection(false); + } + + // Update list of all used disks. + function updateUsed() { + var used = []; + angular.forEach($scope.node.disks, function(disk) { + if (isInUse(disk) && disk.type !== "cache-set") { + var has_partitions = false; + if (angular.isArray(disk.partitions) && disk.partitions.length > 0) { + has_partitions = true; + } + var data = { + name: disk.name, + type: disk.type, + model: disk.model, + serial: disk.serial, + size_human: disk.size_human, + tags: getTags(disk), + used_for: disk.used_for, + is_boot: disk.is_boot, + has_partitions: has_partitions, + test_status: disk.test_status, + firmware_version: disk.firmware_version + }; + if (disk.type === "virtual") { + data.parent_type = disk.parent.type; } + used.push(data); + } + angular.forEach(disk.partitions, function(partition) { + if (isInUse(partition) && partition.type !== "cache-set") { + used.push({ + name: partition.name, + type: "partition", + model: "", + serial: "", + size_human: partition.size_human, + tags: [], + used_for: partition.used_for, + is_boot: false + }); + } + }); + }); + $scope.used = used; + } + + // Updates the filesystem, available, and used list. + function updateDisks() { + if (angular.isArray($scope.node.disks)) { + $scope.has_disks = $scope.node.disks.length > 0; + updateFilesystems(); + updateCacheSets(); + updateAvailable(); + updateUsed(); + } else { + $scope.has_disks = false; + $scope.filesystems = []; + $scope.filesystemsMap = {}; + $scope.filesystemMode = SELECTION_MODE.NONE; + $scope.filesystemAllSelected = false; + $scope.cachesets = []; + $scope.cachesetsMap = {}; + $scope.cachesetsMode = SELECTION_MODE.NONE; + $scope.cachesetsAllSelected = false; + $scope.available = []; + $scope.availableMap = {}; + $scope.availableMode = SELECTION_MODE.NONE; + $scope.availableAllSelected = false; + $scope.availableNew = {}; + $scope.used = []; } + } - // Deselect all items in the array. - function deselectAll(items) { - angular.forEach(items, function(item) { - item.$selected = false; - }); - } - - // Capitalize the first letter of the string. - function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); + // Deselect all items in the array. + function deselectAll(items) { + angular.forEach(items, function(item) { + item.$selected = false; + }); + } + + // Capitalize the first letter of the string. + function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); + } + + // Return true if the string is a number. + function isNumber(string) { + var pattern = /^-?\d+\.?\d*$/; + return pattern.test(string); + } + + // Extract the index from the name based on prefix. + function getIndexFromName(prefix, name) { + var pattern = new RegExp("^" + prefix + "([0-9]+)$"); + var match = pattern.exec(name); + if (angular.isArray(match) && match.length === 2) { + return parseInt(match[1], 10); } + } - // Return true if the string is a number. - function isNumber(string) { - var pattern = /^-?\d+\.?\d*$/; - return pattern.test(string); + // Get the next device name based on prefix. + function getNextName(prefix) { + var idx = -1; + angular.forEach($scope.node.disks, function(disk) { + var dIdx = getIndexFromName(prefix, disk.name); + if (angular.isNumber(dIdx)) { + idx = Math.max(idx, dIdx); + } + angular.forEach(disk.partitions, function(partition) { + dIdx = getIndexFromName(prefix, partition.name); + if (angular.isNumber(dIdx)) { + idx = Math.max(idx, dIdx); + } + }); + }); + return prefix + (idx + 1); + } + + // Return true if another disk exists with name. + function isNameAlreadyInUse(name, exclude_disk) { + if (!angular.isArray($scope.node.disks)) { + return false; } - // Extract the index from the name based on prefix. - function getIndexFromName(prefix, name) { - var pattern = new RegExp("^" + prefix + "([0-9]+)$"); - var match = pattern.exec(name); - if (angular.isArray(match) && match.length === 2) { - return parseInt(match[1], 10); + var i, j; + for (i = 0; i < $scope.node.disks.length; i++) { + var disk = $scope.node.disks[i]; + if (disk.name === name) { + if ( + !angular.isObject(exclude_disk) || + exclude_disk.type === "partition" || + exclude_disk.block_id !== disk.id + ) { + return true; + } + } + if (angular.isArray(disk.partitions)) { + for (j = 0; j < disk.partitions.length; j++) { + var partition = disk.partitions[j]; + if (partition.name === name) { + if ( + !angular.isObject(exclude_disk) || + exclude_disk.type !== "partition" || + exclude_disk.partition_id !== partition.id + ) { + return true; + } + } } + } } + return false; + } - // Get the next device name based on prefix. - function getNextName(prefix) { - var idx = -1; - angular.forEach($scope.node.disks, function(disk) { - var dIdx = getIndexFromName(prefix, disk.name); - if (angular.isNumber(dIdx)) { - idx = Math.max(idx, dIdx); - } - angular.forEach(disk.partitions, function(partition) { - dIdx = getIndexFromName(prefix, partition.name); - if (angular.isNumber(dIdx)) { - idx = Math.max(idx, dIdx); - } - }); - }); - return prefix + (idx + 1); + // Return true if the disk is a logical volume. + function isLogicalVolume(disk) { + return disk.type === "virtual" && disk.parent_type === "lvm-vg"; + } + + // Called by $parent when the node has been loaded. + $scope.nodeLoaded = function() { + $scope.$watch("node.disks", updateDisks); + }; + + // Return true if the item can be a boot disk. + $scope.isBootDiskDisabled = function(item, section) { + // Only superusers can change the boot disk. + if (!$scope.canEdit()) { + return true; } - // Return true if another disk exists with name. - function isNameAlreadyInUse(name, exclude_disk) { - if (!angular.isArray($scope.node.disks)) { - return false; - } - - var i, j; - for (i = 0; i < $scope.node.disks.length; i++) { - var disk = $scope.node.disks[i]; - if (disk.name === name) { - if (!angular.isObject(exclude_disk) || - exclude_disk.type === "partition" || - exclude_disk.block_id !== disk.id) { - return true; - } - } - if (angular.isArray(disk.partitions)) { - for (j = 0; j < disk.partitions.length; j++) { - var partition = disk.partitions[j]; - if (partition.name === name) { - if (!angular.isObject(exclude_disk) || - exclude_disk.type !== "partition" || - exclude_disk.partition_id !== partition.id) { - return true; - } - } - } - } - } - return false; + // Not ready or allocated so the boot disk cannot be changed. + if ( + angular.isObject($scope.node) && + ["Ready", "Allocated"].indexOf($scope.node.status) === -1 + ) { + return true; } - // Return true if the disk is a logical volume. - function isLogicalVolume(disk) { - return disk.type === "virtual" && disk.parent_type === "lvm-vg"; + // Only physical disks can be the boot disk. + if (item.type !== "physical") { + return true; } - // Called by $parent when the node has been loaded. - $scope.nodeLoaded = function() { - $scope.$watch("node.disks", updateDisks); - }; - - // Return true if the item can be a boot disk. - $scope.isBootDiskDisabled = function(item, section) { - // Only superusers can change the boot disk. - if (!$scope.canEdit()) { - return true; - } - - // Not ready or allocated so the boot disk cannot be changed. - if (angular.isObject($scope.node) && - ["Ready", "Allocated"].indexOf( - $scope.node.status) === -1) { - return true; - } - - // Only physical disks can be the boot disk. - if (item.type !== "physical") { - return true; - } - - // If the disk is in the used section and does not have any - // partitions then it cannot be a boot disk. Boot disk either - // require that it be unused or that some partitions exists - // on the disk. This is because the boot disk has to have a - // partition table header. - if (section === "used") { - return !item.has_partitions; - } - return false; - }; - - // Called to change the disk to a boot disk. - $scope.setAsBootDisk = function(item) { - // Do nothing if already the boot disk. - if (item.is_boot) { - return; - } - // Do nothing if disabled. - if ($scope.isBootDiskDisabled(item)) { - return; - } - - MachinesManager.setBootDisk($scope.node, item.block_id); - }; - - // Return array of selected filesystems. - $scope.getSelectedFilesystems = function() { - var filesystems = []; - angular.forEach($scope.filesystems, function(filesystem) { - if (filesystem.$selected) { - filesystems.push(filesystem); - } - }); - return filesystems; - }; - - // Update the currect mode for the filesystem section and the all - // selected value. - $scope.updateFilesystemSelection = function(force) { - if (angular.isUndefined(force)) { - force = false; - } - var filesystems = $scope.getSelectedFilesystems(); - if (filesystems.length === 0) { - $scope.filesystemMode = SELECTION_MODE.NONE; - } else if (filesystems.length === 1 && force) { - $scope.filesystemMode = SELECTION_MODE.SINGLE; - } else if (force) { - $scope.filesystemMode = SELECTION_MODE.MUTLI; - } - - if ($scope.filesystems.length === 0) { - $scope.filesystemAllSelected = false; - } else if (filesystems.length === $scope.filesystems.length) { - $scope.filesystemAllSelected = true; - } else { - $scope.filesystemAllSelected = false; - } - }; - - // Toggle the selection of the filesystem. - $scope.toggleFilesystemSelect = function(filesystem) { - filesystem.$selected = !filesystem.$selected; - $scope.updateFilesystemSelection(true); - }; - - // Toggle the selection of all filesystems. - $scope.toggleFilesystemAllSelect = function() { - angular.forEach($scope.filesystems, function(filesystem) { - if ($scope.filesystemAllSelected) { - filesystem.$selected = false; - } else { - filesystem.$selected = true; - } - }); - $scope.updateFilesystemSelection(true); - }; - - // Return true if checkboxes in the filesystem section should be - // disabled. - $scope.isFilesystemsDisabled = function() { - return (( - $scope.filesystemMode !== SELECTION_MODE.NONE && - $scope.filesystemMode !== SELECTION_MODE.SINGLE && - $scope.filesystemMode !== SELECTION_MODE.MUTLI) || - $scope.isAllStorageDisabled()); - }; - - // Cancel the current filesystem operation. - $scope.filesystemCancel = function() { - deselectAll($scope.filesystems); - $scope.updateFilesystemSelection(true); - }; + // If the disk is in the used section and does not have any + // partitions then it cannot be a boot disk. Boot disk either + // require that it be unused or that some partitions exists + // on the disk. This is because the boot disk has to have a + // partition table header. + if (section === "used") { + return !item.has_partitions; + } + return false; + }; - // Enter unmount mode. - $scope.filesystemUnmount = function() { - $scope.filesystemMode = SELECTION_MODE.UNMOUNT; - }; + // Called to change the disk to a boot disk. + $scope.setAsBootDisk = function(item) { + // Do nothing if already the boot disk. + if (item.is_boot) { + return; + } + // Do nothing if disabled. + if ($scope.isBootDiskDisabled(item)) { + return; + } - // Quickly enter unmount by selecting the filesystem first. - $scope.quickFilesystemUnmount = function(filesystem) { - deselectAll($scope.filesystems); - filesystem.$selected = true; - $scope.updateFilesystemSelection(true); - $scope.filesystemUnmount(); - }; + MachinesManager.setBootDisk($scope.node, item.block_id); + }; - // Confirm the unmount action for filesystem. - $scope.filesystemConfirmUnmount = function(filesystem) { - MachinesManager.updateFilesystem( - $scope.node, - filesystem.block_id, filesystem.partition_id, - filesystem.fstype, null, null); - - var idx = $scope.filesystems.indexOf(filesystem); - $scope.filesystems.splice(idx, 1); - $scope.updateFilesystemSelection(); - }; + // Return array of selected filesystems. + $scope.getSelectedFilesystems = function() { + var filesystems = []; + angular.forEach($scope.filesystems, function(filesystem) { + if (filesystem.$selected) { + filesystems.push(filesystem); + } + }); + return filesystems; + }; + + // Update the currect mode for the filesystem section and the all + // selected value. + $scope.updateFilesystemSelection = function(force) { + if (angular.isUndefined(force)) { + force = false; + } + var filesystems = $scope.getSelectedFilesystems(); + if (filesystems.length === 0) { + $scope.filesystemMode = SELECTION_MODE.NONE; + } else if (filesystems.length === 1 && force) { + $scope.filesystemMode = SELECTION_MODE.SINGLE; + } else if (force) { + $scope.filesystemMode = SELECTION_MODE.MUTLI; + } - // Enter delete mode. - $scope.filesystemDelete = function() { - $scope.filesystemMode = SELECTION_MODE.DELETE; - }; + if ($scope.filesystems.length === 0) { + $scope.filesystemAllSelected = false; + } else if (filesystems.length === $scope.filesystems.length) { + $scope.filesystemAllSelected = true; + } else { + $scope.filesystemAllSelected = false; + } + }; - // Quickly enter delete by selecting the filesystem first. - $scope.quickFilesystemDelete = function(filesystem) { - deselectAll($scope.filesystems); + // Toggle the selection of the filesystem. + $scope.toggleFilesystemSelect = function(filesystem) { + filesystem.$selected = !filesystem.$selected; + $scope.updateFilesystemSelection(true); + }; + + // Toggle the selection of all filesystems. + $scope.toggleFilesystemAllSelect = function() { + angular.forEach($scope.filesystems, function(filesystem) { + if ($scope.filesystemAllSelected) { + filesystem.$selected = false; + } else { filesystem.$selected = true; - $scope.updateFilesystemSelection(true); - $scope.filesystemDelete(); - }; - - // Confirm the delete action for filesystem. - $scope.filesystemConfirmDelete = function(filesystem) { - if (filesystem.original_type === "special") { - // Delete the special filesystem. - MachinesManager.unmountSpecialFilesystem( - $scope.node, filesystem.mount_point); - } else if (filesystem.original_type === "partition") { - // Delete the partition. - MachinesManager.deletePartition( - $scope.node, filesystem.original.id); - } else { - // Delete the disk. - MachinesManager.deleteFilesystem( - $scope.node, filesystem.block_id, filesystem.partition_id, - filesystem.filesystem_id); - } - - var idx = $scope.filesystems.indexOf(filesystem); - $scope.filesystems.splice(idx, 1); - $scope.updateFilesystemSelection(); - }; - - // Return true if the disk has an unmouted filesystem. - $scope.hasUnmountedFilesystem = function(disk) { - if (angular.isString(disk.fstype) && disk.fstype !== "") { - if (!angular.isString(disk.mount_point) || - disk.mount_point === "") { - return true; - } - } - return false; - }; - - // Return true if the free space label should be shown. - $scope.showFreeSpace = function(disk) { - if (disk.type === "lvm-vg") { - return true; - } else if (disk.type === "physical" || disk.type === "virtual") { - return disk.has_partitions; - } else { - return false; - } - }; - - // Return the device type for the disk. - $scope.getDeviceType = function(disk) { - if (angular.isUndefined(disk)) { - return ""; - } - - if (disk.type === "virtual") { - if (disk.parent_type === "lvm-vg") { - return "Logical volume"; - } else if (disk.parent_type.indexOf("raid-") === 0) { - return "RAID " + disk.parent_type.split("-")[1]; - } else { - return capitalizeFirstLetter(disk.parent_type); - } - } else if (disk.type === "lvm-vg") { - return "Volume group"; - } else { - return capitalizeFirstLetter(disk.type); - } - }; - - // Return array of selected available disks. - $scope.getSelectedAvailable = function() { - var available = []; - angular.forEach($scope.available, function(disk) { - if (disk.$selected) { - available.push(disk); - } - }); - return available; - }; - - // Update the current mode for the available section and the all - // selected value. - $scope.updateAvailableSelection = function(force) { - if (angular.isUndefined(force)) { - force = false; - } - var available = $scope.getSelectedAvailable(); - if (available.length === 0) { - $scope.availableMode = SELECTION_MODE.NONE; - } else if (available.length === 1 && force) { - $scope.availableMode = SELECTION_MODE.SINGLE; - } else if (force) { - $scope.availableMode = SELECTION_MODE.MUTLI; - } - - if ($scope.available.length === 0) { - $scope.availableAllSelected = false; - } else if (available.length === $scope.available.length) { - $scope.availableAllSelected = true; - } else { - $scope.availableAllSelected = false; - } - }; - - // Toggle the selection of the available disk. - $scope.toggleAvailableSelect = function(disk) { - disk.$selected = !disk.$selected; - $scope.updateAvailableSelection(true); - }; - - // Toggle the selection of all available disks. - $scope.toggleAvailableAllSelect = function() { - angular.forEach($scope.available, function(disk) { - if (!$scope.availableAllSelected) { - disk.$selected = true; - } else { - disk.$selected = false; - } - }); - $scope.updateAvailableSelection(true); - }; - - // Return true if checkboxes in the avaiable section should be - // disabled. - $scope.isAvailableDisabled = function() { - return (( - $scope.availableMode !== SELECTION_MODE.NONE && - $scope.availableMode !== SELECTION_MODE.SINGLE && - $scope.availableMode !== SELECTION_MODE.MUTLI) || - $scope.isAllStorageDisabled()); - }; - - // Return true if the disk can be formatted and mounted. - $scope.canFormatAndMount = function(disk) { - if ($scope.isAllStorageDisabled()) { - return false; - } else if (disk.type === "lvm-vg" || disk.has_partitions) { - return false; - } else if (disk.type === "physical" && disk.original.is_boot) { - return false; - } else { - return true; - } - }; - - // Return the text for the partition button. - $scope.getPartitionButtonText = function(disk) { - if (disk.has_partitions) { - return "Add partition"; - } else { - return "Partition"; - } - }; - - $scope.availablePartitionSpace = function(disk) { - var space_to_reserve = 0; - if (!angular.isString(disk.original.partition_table_type) - || disk.original.partition_table_type === "") { - // Disk has no partition table, so reserve space for it. - space_to_reserve = PARTITION_TABLE_EXTRA_SPACE; - // ppc64el node requires that space be saved for the prep - // partition. - if ($scope.node.architecture.indexOf("ppc64el") === 0) { - space_to_reserve += PREP_PARTITION_SIZE; - } - } - return ConverterService.roundByBlockSize( - disk.original.available_size - space_to_reserve, - PARTITION_ALIGNMENT_SIZE); - }; - - // Return true if a partition can be added to disk. - $scope.canAddPartition = function(disk) { - if (!$scope.canEdit() || $scope.isAllStorageDisabled()) { - return false; - } else if (disk.type === "partition" || disk.type === "lvm-vg") { - return false; - } else if (disk.type === "virtual" && - (disk.parent_type === "lvm-vg" || - disk.parent_type === "bcache")) { - return false; - } else if (angular.isString(disk.fstype) && disk.fstype !== "") { - return false; - } - // If we can fit a minimum partition, we're golden. - return ($scope.availablePartitionSpace(disk) - - MIN_PARTITION_SIZE) >= 0; - }; - - // Return true if the name is invalid. - $scope.isNameInvalid = function(disk) { - if (disk.name === "") { - return false; - } else if (isNameAlreadyInUse(disk.name, disk)) { - return true; - } else { - return false; - } - }; - - // Prevent logical volumes from changing the volume group prefix. - $scope.nameHasChanged = function(disk) { - if (isLogicalVolume(disk)) { - var parentName = disk.original.name.split("-")[0] + "-"; - var startsWith = disk.name.indexOf(parentName); - if (startsWith !== 0) { - disk.name = parentName; - } - } - }; + } + }); + $scope.updateFilesystemSelection(true); + }; + + // Return true if checkboxes in the filesystem section should be + // disabled. + $scope.isFilesystemsDisabled = function() { + return ( + ($scope.filesystemMode !== SELECTION_MODE.NONE && + $scope.filesystemMode !== SELECTION_MODE.SINGLE && + $scope.filesystemMode !== SELECTION_MODE.MUTLI) || + $scope.isAllStorageDisabled() + ); + }; + + // Cancel the current filesystem operation. + $scope.filesystemCancel = function() { + deselectAll($scope.filesystems); + $scope.updateFilesystemSelection(true); + }; + + // Enter unmount mode. + $scope.filesystemUnmount = function() { + $scope.filesystemMode = SELECTION_MODE.UNMOUNT; + }; + + // Quickly enter unmount by selecting the filesystem first. + $scope.quickFilesystemUnmount = function(filesystem) { + deselectAll($scope.filesystems); + filesystem.$selected = true; + $scope.updateFilesystemSelection(true); + $scope.filesystemUnmount(); + }; + + // Confirm the unmount action for filesystem. + $scope.filesystemConfirmUnmount = function(filesystem) { + MachinesManager.updateFilesystem( + $scope.node, + filesystem.block_id, + filesystem.partition_id, + filesystem.fstype, + null, + null + ); + + var idx = $scope.filesystems.indexOf(filesystem); + $scope.filesystems.splice(idx, 1); + $scope.updateFilesystemSelection(); + }; + + // Enter delete mode. + $scope.filesystemDelete = function() { + $scope.filesystemMode = SELECTION_MODE.DELETE; + }; + + // Quickly enter delete by selecting the filesystem first. + $scope.quickFilesystemDelete = function(filesystem) { + deselectAll($scope.filesystems); + filesystem.$selected = true; + $scope.updateFilesystemSelection(true); + $scope.filesystemDelete(); + }; + + // Confirm the delete action for filesystem. + $scope.filesystemConfirmDelete = function(filesystem) { + if (filesystem.original_type === "special") { + // Delete the special filesystem. + MachinesManager.unmountSpecialFilesystem( + $scope.node, + filesystem.mount_point + ); + } else if (filesystem.original_type === "partition") { + // Delete the partition. + MachinesManager.deletePartition($scope.node, filesystem.original.id); + } else if (filesystem.parent_type === "vmfs6") { + // Delete the datastore. + MachinesManager.deleteDisk($scope.node, filesystem.id); + } else { + // Delete the disk. + MachinesManager.deleteFilesystem( + $scope.node, + filesystem.block_id, + filesystem.partition_id, + filesystem.filesystem_id + ); + } - // Cancel the current available operation. - $scope.availableCancel = function(disk) { - $scope.updateAvailableSelection(true); - $scope.availableNew = {}; - }; + var idx = $scope.filesystems.indexOf(filesystem); + $scope.filesystems.splice(idx, 1); + $scope.updateFilesystemSelection(); + }; + + // Return true if the disk has an unmouted filesystem. + $scope.hasUnmountedFilesystem = function(disk) { + if (angular.isString(disk.fstype) && disk.fstype !== "") { + if (!angular.isString(disk.mount_point) || disk.mount_point === "") { + return true; + } + } + return false; + }; - // Return true if the filesystem can be mounted at a directory. - $scope.usesMountPoint = function(fstype) { - return angular.isString(fstype) && fstype !== "swap"; - }; + // Return true if the free space label should be shown. + $scope.showFreeSpace = function(disk) { + if (disk.type === "lvm-vg") { + return true; + } else if (disk.type === "physical" || disk.type === "virtual") { + return disk.has_partitions; + } else { + return false; + } + }; - // Return true if the filesystem uses storage (partition or - // block device). - $scope.usesStorage = function(fstype) { - return angular.isString(fstype) && - fstype !== "tmpfs" && fstype !== "ramfs"; - }; + // Return the device type for the disk. + $scope.getDeviceType = function(disk) { + if (angular.isUndefined(disk)) { + return ""; + } - // Return true if the mount point is invalid. - $scope.isMountPointInvalid = function(mountPoint) { - if (angular.isUndefined(mountPoint) || mountPoint === "") { - return false; - } else if (mountPoint === "none") { - // XXX: Hack to allow "swap" filesystems to be mounted. - // This should be allowed only when fstype is 'swap' but - // doing that would require more refactoring (or more - // hacks) that I have time for right now. - return false; - } else if (mountPoint[0] !== "/") { - return true; - } else { - return false; - } - }; + if (disk.type === "virtual") { + if (disk.parent_type === "lvm-vg") { + return "Logical volume"; + } else if (disk.parent_type.indexOf("raid-") === 0) { + return "RAID " + disk.parent_type.split("-")[1]; + } else { + return capitalizeFirstLetter(disk.parent_type); + } + } else if (disk.type === "lvm-vg") { + return "Volume group"; + } else { + return capitalizeFirstLetter(disk.type); + } + }; - // Return true if the disk can be deleted. - $scope.canDelete = function(disk) { - if (!$scope.canEdit() || $scope.isAllStorageDisabled()) { - return false; - } else if (disk.type === "lvm-vg") { - return disk.original.used_size === 0; - } else { - return !disk.has_partitions; - } - }; + // Return the device type for the disk in lower case. + $scope.getDeviceTypeLower = function(disk) { + const type = $scope.getDeviceType(disk); + return type.toLowerCase(); + }; + + // Return array of selected available disks. + $scope.getSelectedAvailable = function() { + var available = []; + angular.forEach($scope.available, function(disk) { + if (disk.$selected) { + available.push(disk); + } + }); + return available; + }; + + // Update the current mode for the available section and the all + // selected value. + $scope.updateAvailableSelection = function(force) { + if (angular.isUndefined(force)) { + force = false; + } + var available = $scope.getSelectedAvailable(); + if (available.length === 0) { + $scope.availableMode = SELECTION_MODE.NONE; + } else if (available.length === 1 && force) { + $scope.availableMode = SELECTION_MODE.SINGLE; + } else if (force) { + $scope.availableMode = SELECTION_MODE.MUTLI; + } - // Enter delete mode. - $scope.availableDelete = function() { - $scope.availableMode = SELECTION_MODE.DELETE; - }; + if ($scope.available.length === 0) { + $scope.availableAllSelected = false; + } else if (available.length === $scope.available.length) { + $scope.availableAllSelected = true; + } else { + $scope.availableAllSelected = false; + } + }; - // Quickly enter delete mode. - $scope.availableQuickDelete = function(disk) { - deselectAll($scope.available); + // Toggle the selection of the available disk. + $scope.toggleAvailableSelect = function(disk) { + disk.$selected = !disk.$selected; + $scope.updateAvailableSelection(true); + $scope.selectedAvailableDatastores = $scope.getSelectedAvailable(); + $scope.checkAddToDatastoreValid(); + }; + + // Toggle the selection of all available disks. + $scope.toggleAvailableAllSelect = function() { + angular.forEach($scope.available, function(disk) { + if (!$scope.availableAllSelected) { disk.$selected = true; - $scope.updateAvailableSelection(true); - $scope.availableDelete(); - }; + } else { + disk.$selected = false; + } + }); + $scope.updateAvailableSelection(true); + }; + + // Return true if checkboxes in the avaiable section should be + // disabled. + $scope.isAvailableDisabled = function() { + return ( + ($scope.availableMode !== SELECTION_MODE.NONE && + $scope.availableMode !== SELECTION_MODE.SINGLE && + $scope.availableMode !== SELECTION_MODE.MUTLI) || + $scope.isAllStorageDisabled() + ); + }; + + // Return true if the disk can be formatted and mounted. + $scope.canFormatAndMount = function(disk) { + if ($scope.isAllStorageDisabled()) { + return false; + } else if (disk.type === "lvm-vg" || disk.has_partitions) { + return false; + } else if (disk.type === "physical" && disk.original.is_boot) { + return false; + } else { + return true; + } + }; - // Return true if it can be edited. - $scope.canEdit = function(disk) { - if (!$scope.$parent.canEdit() || $scope.isAllStorageDisabled()) { - return false; - } else { - return true; - } - }; + // Return the text for the partition button. + $scope.getPartitionButtonText = function(disk) { + if (disk.has_partitions) { + return "Add partition"; + } else { + return "Partition"; + } + }; - // Enter Edit mode, disable certain fields based on disk type - $scope.availableEdit = function(disk) { - $scope.availableMode = SELECTION_MODE.EDIT; - - if (disk.type === "lvm-vg") { - disk.$options = { - editingTags: false, - editingFilesystem: false - }; - } else if (disk.type === "partition") { - disk.$options = { - editingTags: false, - editingFilesystem: true, - fstype: disk.fstype - }; - } else { - disk.$options = { - editingFilesystem: true, - editingTags: true, - tags: angular.copy(disk.tags), - fstype: disk.fstype - }; - if (!$scope.canFormatAndMount(disk)) { - disk.$options.editingFilesystem = false; - } - } - }; + $scope.availablePartitionSpace = function(disk) { + var space_to_reserve = 0; + if ( + !angular.isString(disk.original.partition_table_type) || + disk.original.partition_table_type === "" + ) { + // Disk has no partition table, so reserve space for it. + space_to_reserve = PARTITION_TABLE_EXTRA_SPACE; + // ppc64el node requires that space be saved for the prep + // partition. + if ($scope.node.architecture.indexOf("ppc64el") === 0) { + space_to_reserve += PREP_PARTITION_SIZE; + } + } + return ConverterService.roundByBlockSize( + disk.original.available_size - space_to_reserve, + PARTITION_ALIGNMENT_SIZE + ); + }; + + // Return true if a partition can be added to disk. + $scope.canAddPartition = function(disk) { + if (!$scope.canEdit() || $scope.isAllStorageDisabled()) { + return false; + } else if (disk.type === "partition" || disk.type === "lvm-vg") { + return false; + } else if ( + disk.type === "virtual" && + (disk.parent_type === "lvm-vg" || disk.parent_type === "bcache") + ) { + return false; + } else if (angular.isString(disk.fstype) && disk.fstype !== "") { + return false; + } + // If we can fit a minimum partition, we're golden. + return $scope.availablePartitionSpace(disk) - MIN_PARTITION_SIZE >= 0; + }; + + // Return true if the name is invalid. + $scope.isNameInvalid = function(disk) { + if (disk.name === "") { + return false; + } else if (isNameAlreadyInUse(disk.name, disk)) { + return true; + } else { + return false; + } + }; - // Quickly enter Edit mode - $scope.availableQuickEdit = function(disk) { - deselectAll($scope.available); - disk.$selected = true; - $scope.updateAvailableSelection(true); - $scope.availableEdit(disk); - }; + // Prevent logical volumes from changing the volume group prefix. + $scope.nameHasChanged = function(disk) { + if (isLogicalVolume(disk)) { + var parentName = disk.original.name.split("-")[0] + "-"; + var startsWith = disk.name.indexOf(parentName); + if (startsWith !== 0) { + disk.name = parentName; + } + } + }; - // Save the disk which is in Edit mode - $scope.availableConfirmEdit = function(disk) { - var params = { - name: disk.name - }; + // Cancel the current available operation. + $scope.availableCancel = function() { + $scope.updateAvailableSelection(true); + $scope.availableNew = {}; + }; - // Do nothing if not valid. - if ($scope.isNameInvalid(disk) || - $scope.isMountPointInvalid(disk.$options.mountPoint)) { - return; - } + // Return true if the filesystem can be mounted at a directory. + $scope.usesMountPoint = function(fstype) { + return angular.isString(fstype) && fstype !== "swap"; + }; + + // Return true if the filesystem uses storage (partition or + // block device). + $scope.usesStorage = function(fstype) { + return angular.isString(fstype) && fstype !== "tmpfs" && fstype !== "ramfs"; + }; + + // Return true if the mount point is invalid. + $scope.isMountPointInvalid = function(mountPoint) { + if (angular.isUndefined(mountPoint) || mountPoint === "") { + return false; + } else if (mountPoint === "none") { + // XXX: Hack to allow "swap" filesystems to be mounted. + // This should be allowed only when fstype is 'swap' but + // doing that would require more refactoring (or more + // hacks) that I have time for right now. + return false; + } else if (mountPoint[0] !== "/") { + return true; + } else { + return false; + } + }; - // Reset the name if its blank. - if (disk.name === "") { - disk.name = disk.original.name; - } + // Return true if the disk can be deleted. + $scope.canDelete = function(disk) { + if (!$scope.canEdit() || $scope.isAllStorageDisabled()) { + return false; + } else if (disk.type === "lvm-vg") { + return disk.original.used_size === 0; + } else { + return !disk.has_partitions; + } + }; - // Ensure logical volume has parent prefix in its name. - if (isLogicalVolume(disk)) { - var parentName = disk.original.name.split("-")[0] + "-"; - params.name = disk.name.slice(parentName.length); - } + // Return true if the filesystem can be deleted. + $scope.canDeleteFilesystem = function(filesystem) { + if (filesystem.original_type === "special") { + return true; + } + return $scope.canEdit(); + }; - // Set filesystem options so formatting and mounting is performed. - if (angular.isDefined(disk.$options.fstype)) { - params.fstype = disk.$options.fstype; - params.mount_point = disk.$options.mountPoint || ''; - params.mount_options = disk.$options.mountOptions || ''; - } + // Enter delete mode. + $scope.availableDelete = function() { + $scope.availableMode = SELECTION_MODE.DELETE; + }; + + // Quickly enter delete mode. + $scope.availableQuickDelete = function(disk) { + deselectAll($scope.available); + disk.$selected = true; + $scope.updateAvailableSelection(true); + $scope.availableDelete(); + }; + + // Return true if it can be edited. + $scope.canEdit = function(item) { + if ($scope.isAllStorageDisabled()) { + return false; + } + if (item && item.type === "partition") { + return true; + } + if (!$scope.$parent.canEdit()) { + return false; + } + return true; + }; - // Update the tags for the disk. - if (angular.isArray(disk.$options.tags)) { - params.tags = disk.$options.tags.map( - function(tag) { return tag.text; }); - } + // Enter Edit mode, disable certain fields based on disk type + $scope.availableEdit = function(disk) { + $scope.availableMode = SELECTION_MODE.EDIT; + + if (disk.type === "lvm-vg") { + disk.$options = { + editingTags: false, + editingFilesystem: false + }; + } else if (disk.type === "partition") { + disk.$options = { + editingTags: false, + editingFilesystem: true, + fstype: disk.fstype + }; + } else { + disk.$options = { + editingFilesystem: true, + editingTags: true, + tags: angular.copy(disk.tags), + fstype: disk.fstype + }; + if (!$scope.canFormatAndMount(disk)) { + disk.$options.editingFilesystem = false; + } + } + }; - // Save the options. - if (disk.type === "partition") { - MachinesManager.updateFilesystem( - $scope.node, disk.block_id, disk.partition_id, - params.fstype, params.mount_point, - params.mount_options, params.tags); - } else { - MachinesManager.updateDisk( - $scope.node, disk.block_id, params); - } + // Quickly enter Edit mode + $scope.availableQuickEdit = function(disk) { + deselectAll($scope.available); + disk.$selected = true; + $scope.updateAvailableSelection(true); + $scope.availableEdit(disk); + }; + + // Save the disk which is in Edit mode + $scope.availableConfirmEdit = function(disk) { + var params = { + name: disk.name + }; + + // Do nothing if not valid. + if ( + $scope.isNameInvalid(disk) || + $scope.isMountPointInvalid(disk.$options.mountPoint) + ) { + return; + } - // Set the options on the object so no flicker occurs while waiting - // for the new object to be received. - disk.fstype = disk.$options.fstype; - disk.mount_point = disk.$options.mountPoint; - disk.mount_options = disk.$options.mountOptions; - disk.tags = disk.$options.tags; - disk.$options = {}; + // Reset the name if its blank. + if (disk.name === "") { + disk.name = disk.original.name; + } - // If the mount_point is set then we need to transition this to - // the filesystem section. - if (angular.isString(disk.mount_point) && disk.mount_point !== "") { - $scope.filesystems.push({ - "name": disk.name, - "size_human": disk.size_human, - "fstype": disk.fstype, - "mount_point": disk.mount_point, - "mount_options": disk.mount_options, - "block_id": disk.block_id, - "partition_id": disk.partition_id - }); - - // Remove the selected disk from available. - var idx = $scope.available.indexOf(disk); - $scope.available.splice(idx, 1); - } + // Ensure logical volume has parent prefix in its name. + if (isLogicalVolume(disk)) { + var parentName = disk.original.name.split("-")[0] + "-"; + params.name = disk.name.slice(parentName.length); + } - // Deselect the disk after saving - disk.$selected = false; + // Set filesystem options so formatting and mounting is performed. + if (angular.isDefined(disk.$options.fstype)) { + params.fstype = disk.$options.fstype; + params.mount_point = disk.$options.mountPoint || ""; + params.mount_options = disk.$options.mountOptions || ""; + } - // Update the current selections. - $scope.updateAvailableSelection(true); - }; + // Update the tags for the disk. + if (angular.isArray(disk.$options.tags)) { + params.tags = disk.$options.tags.map(function(tag) { + return tag.text; + }); + } - // Return the text for remove confirmation message. - $scope.getRemoveTypeText = function(disk) { - if (disk.type === "filesystem") { - if (angular.isObject(disk.original)) { - disk = disk.original; - } else { - return "special filesystem"; - } - } + // Save the options. + if (disk.type === "partition") { + MachinesManager.updateFilesystem( + $scope.node, + disk.block_id, + disk.partition_id, + params.fstype, + params.mount_point, + params.mount_options, + params.tags + ); + } else { + MachinesManager.updateDisk($scope.node, disk.block_id, params); + } - if (disk.type === "physical") { - return "physical disk"; - } else if (disk.type === "partition") { - return "partition"; - } else if (disk.type === "lvm-vg") { - return "volume group"; - } else if (disk.type === "virtual") { - if (disk.parent_type === "lvm-vg") { - return "logical volume"; - } else if (disk.parent_type.indexOf("raid-") === 0) { - return "RAID " + disk.parent_type.split("-")[1] + " disk"; - } else { - return disk.parent_type + " disk"; - } - } - }; + // Set the options on the object so no flicker occurs while waiting + // for the new object to be received. + disk.fstype = disk.$options.fstype; + disk.mount_point = disk.$options.mountPoint; + disk.mount_options = disk.$options.mountOptions; + disk.tags = disk.$options.tags; + disk.$options = {}; + + // If the mount_point is set then we need to transition this to + // the filesystem section. + if (angular.isString(disk.mount_point) && disk.mount_point !== "") { + $scope.filesystems.push({ + name: disk.name, + size_human: disk.size_human, + fstype: disk.fstype, + mount_point: disk.mount_point, + mount_options: disk.mount_options, + block_id: disk.block_id, + partition_id: disk.partition_id + }); + + // Remove the selected disk from available. + var idx = $scope.available.indexOf(disk); + $scope.available.splice(idx, 1); + } - // Delete the disk, partition, or volume group. - $scope.availableConfirmDelete = function(disk) { - if (disk.type === "lvm-vg") { - // Delete the volume group. - MachinesManager.deleteVolumeGroup( - $scope.node, disk.block_id); - } else if (disk.type === "partition") { - // Delete the partition. - MachinesManager.deletePartition( - $scope.node, disk.partition_id); - } else { - // Delete the disk. - MachinesManager.deleteDisk( - $scope.node, disk.block_id); - } + // Deselect the disk after saving + disk.$selected = false; - // Remove the selected disk from available. - var idx = $scope.available.indexOf(disk); - $scope.available.splice(idx, 1); - $scope.updateAvailableSelection(true); - }; + // Update the current selections. + $scope.updateAvailableSelection(true); + }; + + // Return the text for remove confirmation message. + $scope.getRemoveTypeText = function(disk) { + if (disk.type === "filesystem") { + if (angular.isObject(disk.original)) { + disk = disk.original; + } else { + return "special filesystem"; + } + } - // Enter partition mode. - $scope.availablePartition = function(disk) { - $scope.availableMode = SELECTION_MODE.PARTITION; - // Set starting size to the maximum available space. - var size_and_units = disk.available_size_human.split(" "); - disk.$options = { - size: size_and_units[0], - sizeUnits: size_and_units[1], - fstype: null, - mountPoint: "", - mountOptions: "" - }; - }; + if (disk.type === "physical") { + return "physical disk"; + } else if (disk.type === "partition") { + return "partition"; + } else if (disk.type === "lvm-vg") { + return "volume group"; + } else if (disk.type === "virtual") { + if (disk.parent_type === "lvm-vg") { + return "logical volume"; + } else if (disk.parent_type.indexOf("raid-") === 0) { + return "RAID " + disk.parent_type.split("-")[1] + " disk"; + } else { + return disk.parent_type + " disk"; + } + } + }; - // Quickly enter partition mode. - $scope.availableQuickPartition = function(disk) { - deselectAll($scope.available); - disk.$selected = true; - $scope.updateAvailableSelection(true); - $scope.availablePartition(disk); - }; + // Delete the disk, partition, or volume group. + $scope.availableConfirmDelete = function(disk) { + if (disk.type === "lvm-vg") { + // Delete the volume group. + MachinesManager.deleteVolumeGroup($scope.node, disk.block_id); + } else if (disk.type === "partition") { + // Delete the partition. + MachinesManager.deletePartition($scope.node, disk.partition_id); + } else { + // Delete the disk. + MachinesManager.deleteDisk($scope.node, disk.block_id); + } - // Get the new name of the partition. - $scope.getAddPartitionName = function(disk) { - var length, partitions = disk.original.partitions; - if (angular.isArray(partitions)) { - length = partitions.length; - } else { - length = 0; - } - if (disk.original.partition_table_type === "mbr" && - length > 2) { - return disk.name + "-part" + (length + 2); - } else if ($scope.node.architecture.indexOf("ppc64el") === 0 && - disk.original.is_boot) { - // Boot disk on ppc64el machines skip the first partition as - // its reserved for the prep partition. - return disk.name + "-part" + (length + 2); - } else { - return disk.name + "-part" + (length + 1); - } - }; + // Remove the selected disk from available. + var idx = $scope.available.indexOf(disk); + $scope.available.splice(idx, 1); + $scope.updateAvailableSelection(true); + }; + + // Enter partition mode. + $scope.availablePartition = function(disk) { + $scope.availableMode = SELECTION_MODE.PARTITION; + // Set starting size to the maximum available space. + var size_and_units = disk.available_size_human.split(" "); + disk.$options = { + size: size_and_units[0], + sizeUnits: size_and_units[1], + fstype: null, + mountPoint: "", + mountOptions: "" + }; + }; + + // Quickly enter partition mode. + $scope.availableQuickPartition = function(disk) { + deselectAll($scope.available); + disk.$selected = true; + $scope.updateAvailableSelection(true); + $scope.availablePartition(disk); + }; + + // Get the new name of the partition. + $scope.getAddPartitionName = function(disk) { + var length, + partitions = disk.original.partitions; + if (angular.isArray(partitions)) { + length = partitions.length; + } else { + length = 0; + } + if (disk.original.partition_table_type === "mbr" && length > 2) { + return disk.name + "-part" + (length + 2); + } else if ( + $scope.node.architecture.indexOf("ppc64el") === 0 && + disk.original.is_boot + ) { + // Boot disk on ppc64el machines skip the first partition as + // its reserved for the prep partition. + return disk.name + "-part" + (length + 2); + } else { + return disk.name + "-part" + (length + 1); + } + }; - // Return true if the size is invalid. - $scope.isAddPartitionSizeInvalid = function(disk) { - let size = disk.$options.size; - // blr 2018-07-23: special cased as isAddLogicalVolumeSizeInvalid - // calls this but has not yet migrated to maas-obj-form. - if ($scope.newPartition.$maasForm && - $scope.newPartition.$maasForm.getValue('size')) { - size = $scope.newPartition.$maasForm.getValue('size'); - } - if (size === "" || !isNumber(size)) { - return true; + // Return true if the size is invalid. + $scope.isAddPartitionSizeInvalid = function(disk) { + let size = disk.$options.size; + // blr 2018-07-23: special cased as isAddLogicalVolumeSizeInvalid + // calls this but has not yet migrated to maas-obj-form. + if ( + $scope.newPartition.$maasForm && + $scope.newPartition.$maasForm.getValue("size") + ) { + size = $scope.newPartition.$maasForm.getValue("size"); + } + if (size === "" || !isNumber(size)) { + return true; + } else { + var bytes = ConverterService.unitsToBytes(size, disk.$options.sizeUnits); + if (bytes < MIN_PARTITION_SIZE) { + return true; + } else if (bytes > disk.original.available_size) { + // Round the size down to the lowest tolerance for that + // to see if it now fits. + var rounded = ConverterService.roundUnits( + size, + disk.$options.sizeUnits + ); + if (rounded > disk.original.available_size) { + return true; } else { - var bytes = ConverterService.unitsToBytes( - size, disk.$options.sizeUnits); - if (bytes < MIN_PARTITION_SIZE) { - return true; - } else if (bytes > disk.original.available_size) { - // Round the size down to the lowest tolerance for that - // to see if it now fits. - var rounded = ConverterService.roundUnits( - size, disk.$options.sizeUnits); - if (rounded > disk.original.available_size) { - return true; - } else { - return false; - } - } else { - return false; - } - } - }; - - // Confirm the partition creation. - $scope.availableConfirmPartition = function(disk) { - const form = $scope.newPartition.$maasForm; - const size = form.getValue('size'); - const mountPoint = form.getValue('mount_point'); - const mountOptions = form.getValue('mount_options'); - - // Do nothing if not valid. - if ($scope.isAddPartitionSizeInvalid(disk) || - $scope.isMountPointInvalid(mountPoint)) { - return; - } - - // Get the bytes to create the partition. - var bytes = ConverterService.unitsToBytes( - size, disk.$options.sizeUnits); - - // Accepting prefilled defaults means use whole disk (lp:1509535). - var size_and_units = disk.original.available_size_human.split(" "); - if (size === size_and_units[0] && - disk.$options.sizeUnits === size_and_units[1]) { - bytes = disk.original.available_size; - } - - var removeDisk = false; - var available_space = $scope.availablePartitionSpace(disk); - if (bytes >= available_space) { - // Clamp to available space. - bytes = available_space; - // Remove the disk if partition uses all the remaining space. - removeDisk = true; - } - - // Set params - var params = {}; - if (angular.isString(disk.$options.fstype) && - disk.$options.fstype !== "") { - params.fstype = disk.$options.fstype; - if (mountPoint !== "") { - params.mount_point = mountPoint; - params.mount_options = mountOptions; - } + return false; } + } else { + return false; + } + } + }; - // Set values on maas-obj-form object - $scope.newPartition.system_id = $scope.node.system_id; - $scope.newPartition.block_id = disk.block_id; - $scope.newPartition.partition_size = bytes; - $scope.newPartition.params = params; - - // Remove the disk if needed. - if (removeDisk) { - var idx = $scope.available.indexOf(disk); - $scope.available.splice(idx, 1); - } - $scope.updateAvailableSelection(true); - }; + // Confirm the partition creation. + $scope.availableConfirmPartition = function(disk) { + const form = $scope.newPartition.$maasForm; + const size = form.getValue("size"); + const mountPoint = form.getValue("mount_point"); + const mountOptions = form.getValue("mount_options"); + + // Do nothing if not valid. + if ( + $scope.isAddPartitionSizeInvalid(disk) || + $scope.isMountPointInvalid(mountPoint) + ) { + return; + } - // Return array of selected cache sets. - $scope.getSelectedCacheSets = function() { - var cachesets = []; - angular.forEach($scope.cachesets, function(cacheset) { - if (cacheset.$selected) { - cachesets.push(cacheset); - } - }); - return cachesets; - }; + // Get the bytes to create the partition. + var bytes = ConverterService.unitsToBytes(size, disk.$options.sizeUnits); - // Update the currect mode for the cache sets section and the all - // selected value. - $scope.updateCacheSetsSelection = function(force) { - if (angular.isUndefined(force)) { - force = false; - } - var cachesets = $scope.getSelectedCacheSets(); - if (cachesets.length === 0) { - $scope.cachesetsMode = SELECTION_MODE.NONE; - } else if (cachesets.length === 1 && force) { - $scope.cachesetsMode = SELECTION_MODE.SINGLE; - } else if (force) { - $scope.cachesetsMode = SELECTION_MODE.MUTLI; - } - - if ($scope.cachesets.length === 0) { - $scope.cachesetsAllSelected = false; - } else if (cachesets.length === $scope.cachesets.length) { - $scope.cachesetsAllSelected = true; - } else { - $scope.cachesetsAllSelected = false; - } - }; - - // Toggle the selection of the filesystem. - $scope.toggleCacheSetSelect = function(cacheset) { - cacheset.$selected = !cacheset.$selected; - $scope.updateCacheSetsSelection(true); - }; + // Accepting prefilled defaults means use whole disk (lp:1509535). + var size_and_units = disk.original.available_size_human.split(" "); + if ( + size === size_and_units[0] && + disk.$options.sizeUnits === size_and_units[1] + ) { + bytes = disk.original.available_size; + } - // Toggle the selection of all filesystems. - $scope.toggleCacheSetAllSelect = function() { - angular.forEach($scope.cachesets, function(cacheset) { - if ($scope.cachesetsAllSelected) { - cacheset.$selected = false; - } else { - cacheset.$selected = true; - } - }); - $scope.updateCacheSetsSelection(true); - }; + var removeDisk = false; + var available_space = $scope.availablePartitionSpace(disk); + if (bytes >= available_space) { + // Clamp to available space. + bytes = available_space; + // Remove the disk if partition uses all the remaining space. + removeDisk = true; + } - // Return true if checkboxes in the cache sets section should be - // disabled. - $scope.isCacheSetsDisabled = function() { - return (( - $scope.isAllStorageDisabled() && - !$scope.canEdit()) || ( - $scope.cachesetsMode !== SELECTION_MODE.NONE && - $scope.cachesetsMode !== SELECTION_MODE.SINGLE && - $scope.cachesetsMode !== SELECTION_MODE.MUTLI)); - }; + // Set params + var params = {}; + if (angular.isString(disk.$options.fstype) && disk.$options.fstype !== "") { + params.fstype = disk.$options.fstype; + if (mountPoint !== "") { + params.mount_point = mountPoint; + params.mount_options = mountOptions; + } + } - // Cancel the current cache set operation. - $scope.cacheSetCancel = function() { - deselectAll($scope.cachesets); - $scope.updateCacheSetsSelection(true); - }; + // Set values on maas-obj-form object + $scope.newPartition.system_id = $scope.node.system_id; + $scope.newPartition.block_id = disk.block_id; + $scope.newPartition.partition_size = bytes; + $scope.newPartition.params = params; + + // Remove the disk if needed. + if (removeDisk) { + var idx = $scope.available.indexOf(disk); + $scope.available.splice(idx, 1); + } + $scope.updateAvailableSelection(true); + }; - // Can delete the cache set. - $scope.canDeleteCacheSet = function(cacheset) { - return (cacheset.used_by === "" && - !$scope.isAllStorageDisabled() && - $scope.canEdit()); - }; + // Return array of selected cache sets. + $scope.getSelectedCacheSets = function() { + var cachesets = []; + angular.forEach($scope.cachesets, function(cacheset) { + if (cacheset.$selected) { + cachesets.push(cacheset); + } + }); + return cachesets; + }; + + // Update the currect mode for the cache sets section and the all + // selected value. + $scope.updateCacheSetsSelection = function(force) { + if (angular.isUndefined(force)) { + force = false; + } + var cachesets = $scope.getSelectedCacheSets(); + if (cachesets.length === 0) { + $scope.cachesetsMode = SELECTION_MODE.NONE; + } else if (cachesets.length === 1 && force) { + $scope.cachesetsMode = SELECTION_MODE.SINGLE; + } else if (force) { + $scope.cachesetsMode = SELECTION_MODE.MUTLI; + } - // Enter delete mode. - $scope.cacheSetDelete = function() { - $scope.cachesetsMode = SELECTION_MODE.DELETE; - }; + if ($scope.cachesets.length === 0) { + $scope.cachesetsAllSelected = false; + } else if (cachesets.length === $scope.cachesets.length) { + $scope.cachesetsAllSelected = true; + } else { + $scope.cachesetsAllSelected = false; + } + }; - // Quickly enter delete by selecting the cache set first. - $scope.quickCacheSetDelete = function(cacheset) { - deselectAll($scope.cachesets); + // Toggle the selection of the filesystem. + $scope.toggleCacheSetSelect = function(cacheset) { + cacheset.$selected = !cacheset.$selected; + $scope.updateCacheSetsSelection(true); + }; + + // Toggle the selection of all filesystems. + $scope.toggleCacheSetAllSelect = function() { + angular.forEach($scope.cachesets, function(cacheset) { + if ($scope.cachesetsAllSelected) { + cacheset.$selected = false; + } else { cacheset.$selected = true; - $scope.updateCacheSetsSelection(true); - $scope.cacheSetDelete(); - }; - - // Confirm the delete action for cache set. - $scope.cacheSetConfirmDelete = function(cacheset) { - MachinesManager.deleteCacheSet( - $scope.node, cacheset.cache_set_id); - - var idx = $scope.cachesets.indexOf(cacheset); - $scope.cachesets.splice(idx, 1); - $scope.updateCacheSetsSelection(); - }; - - // Return true if a cache set can be created. - $scope.canCreateCacheSet = function() { - if ($scope.isAvailableDisabled() || !$scope.canEdit()) { - return false; - } - - var selected = $scope.getSelectedAvailable(); - if (selected.length === 1) { - return ( - !selected[0].has_partitions && - !$scope.hasUnmountedFilesystem(selected[0]) && - selected[0].type !== "lvm-vg"); - } - return false; - }; - - // Called to create a cache set. - $scope.createCacheSet = function() { - if (!$scope.canCreateCacheSet()) { - return; - } - - // Create cache set. - var disk = $scope.getSelectedAvailable()[0]; - MachinesManager.createCacheSet( - $scope.node, disk.block_id, disk.partition_id); - - // Remove from available. - var idx = $scope.available.indexOf(disk); - $scope.available.splice(idx, 1); - }; - - // Return the reason a bcache device cannot be created. - $scope.getCannotCreateBcacheMsg = function() { - if ($scope.cachesets.length === 0) { - return "Create at least one cache set to create bcache"; - } else { - var selected = $scope.getSelectedAvailable(); - if (selected.length === 1) { - if ($scope.hasUnmountedFilesystem(selected[0])) { - return ( - "Device is formatted; unformat the " + - "device to create bcache"); - } else if (selected[0].type === "lvm-vg") { - return ( - "Cannot use a logical volume as a backing " + - "device for bcache."); - } else if (selected[0].has_partitions) { - return ( - "Device has already been partitioned; create a " + - "new partition to use as the bcache backing " + - "device"); - } else { - return null; - } - } else { - return "Select only one available device to create bcache"; - } - } - }; - - // Return true if a bcache can be created. - $scope.canCreateBcache = function() { - if ($scope.isAvailableDisabled() || !$scope.canEdit()) { - return false; - } - - var msg = $scope.getCannotCreateBcacheMsg(); - if (msg === null) { - return true; - } else { - return false; - } - }; + } + }); + $scope.updateCacheSetsSelection(true); + }; + + // Return true if checkboxes in the cache sets section should be + // disabled. + $scope.isCacheSetsDisabled = function() { + return ( + ($scope.isAllStorageDisabled() && !$scope.canEdit()) || + ($scope.cachesetsMode !== SELECTION_MODE.NONE && + $scope.cachesetsMode !== SELECTION_MODE.SINGLE && + $scope.cachesetsMode !== SELECTION_MODE.MUTLI) + ); + }; + + // Cancel the current cache set operation. + $scope.cacheSetCancel = function() { + deselectAll($scope.cachesets); + $scope.updateCacheSetsSelection(true); + }; + + // Can delete the cache set. + $scope.canDeleteCacheSet = function(cacheset) { + return ( + cacheset.used_by === "" && + !$scope.isAllStorageDisabled() && + $scope.canEdit() + ); + }; + + // Enter delete mode. + $scope.cacheSetDelete = function() { + $scope.cachesetsMode = SELECTION_MODE.DELETE; + }; + + // Quickly enter delete by selecting the cache set first. + $scope.quickCacheSetDelete = function(cacheset) { + deselectAll($scope.cachesets); + cacheset.$selected = true; + $scope.updateCacheSetsSelection(true); + $scope.cacheSetDelete(); + }; + + // Confirm the delete action for cache set. + $scope.cacheSetConfirmDelete = function(cacheset) { + MachinesManager.deleteCacheSet($scope.node, cacheset.cache_set_id); + + var idx = $scope.cachesets.indexOf(cacheset); + $scope.cachesets.splice(idx, 1); + $scope.updateCacheSetsSelection(); + }; + + // Return true if a cache set can be created. + $scope.canCreateCacheSet = function() { + if ($scope.isAvailableDisabled() || !$scope.canEdit()) { + return false; + } - // Enter bcache mode. - $scope.createBcache = function() { - if (!$scope.canCreateBcache()) { - return; - } - $scope.availableMode = SELECTION_MODE.BCACHE; - $scope.availableNew = { - name: getNextName("bcache"), - device: $scope.getSelectedAvailable()[0], - cacheset: $scope.cachesets[0], - cacheMode: "writeback", - fstype: null, - mountPoint: "", - mountOptions: "", - tags: [] - }; - }; + var selected = $scope.getSelectedAvailable(); + if (selected.length === 1) { + return ( + !selected[0].has_partitions && + !$scope.hasUnmountedFilesystem(selected[0]) && + selected[0].type !== "lvm-vg" + ); + } + return false; + }; - // Clear mount point when the fstype is changed. - $scope.fstypeChanged = function(options) { - if (options.fstype === null) { - options.mountPoint = ""; - options.mountOptions = ""; - } - else { - // Update the mount point to "none" if "swap" is - // selected, and vice-versa. - if ($scope.usesMountPoint(options.fstype)) { - if (options.mountPoint === "none") { - options.mountPoint = ""; - } - } - else { - options.mountPoint = "none"; - } - } - }; + // Called to create a cache set. + $scope.createCacheSet = function() { + if (!$scope.canCreateCacheSet()) { + return; + } - // Return true when the name of the new disk is invalid. - $scope.isNewDiskNameInvalid = function() { - if (!angular.isObject($scope.node) || - !angular.isArray($scope.node.disks)) { - return true; - } + // Create cache set. + var disk = $scope.getSelectedAvailable()[0]; + MachinesManager.createCacheSet( + $scope.node, + disk.block_id, + disk.partition_id + ); + + // Remove from available. + var idx = $scope.available.indexOf(disk); + $scope.available.splice(idx, 1); + }; + + // Return the reason a bcache device cannot be created. + $scope.getCannotCreateBcacheMsg = function() { + if ($scope.cachesets.length === 0) { + return "Create at least one cache set to create bcache"; + } else { + var selected = $scope.getSelectedAvailable(); + if (selected.length === 1) { + if ($scope.hasUnmountedFilesystem(selected[0])) { + return ( + "Device is formatted; unformat the " + "device to create bcache" + ); + } else if (selected[0].type === "lvm-vg") { + return ( + "Cannot use a logical volume as a backing " + "device for bcache." + ); + } else if (selected[0].has_partitions) { + return ( + "Device has already been partitioned; create a " + + "new partition to use as the bcache backing " + + "device" + ); + } else if (selected[0].parent_type === "bcache") { + return "Device is already bcache"; + } else { + return null; + } + } else { + return "Select only one available device to create bcache"; + } + } + }; - if ($scope.availableNew.name === "") { - return true; - } else { - var i, j; - for (i = 0; i < $scope.node.disks.length; i++) { - var disk = $scope.node.disks[i]; - if ($scope.availableNew.name === disk.name) { - return true; - } - if (angular.isArray(disk.partitions)) { - for (j = 0; j < disk.partitions.length; j++) { - var partition = disk.partitions[j]; - if ($scope.availableNew.name === partition.name) { - return true; - } - } - } - } - } - return false; - }; + // Return true if a bcache can be created. + $scope.canCreateBcache = function() { + if ($scope.isAvailableDisabled() || !$scope.canEdit()) { + return false; + } - // Return true if bcache can be saved. - $scope.createBcacheCanSave = function() { - return ( - !$scope.isNewDiskNameInvalid() && - !$scope.isMountPointInvalid($scope.availableNew.mountPoint)); - }; + var selectedBache = $scope.selectedAvailableDatastores.filter(function(ds) { + return ds.parent_type === "bcache"; + }); - // Confirm and create the bcache device. - $scope.availableConfirmCreateBcache = function() { - if (!$scope.createBcacheCanSave()) { - return; - } + if (selectedBache.length) { + return false; + } - // Create the bcache. - var params = { - name: $scope.availableNew.name, - cache_set: $scope.availableNew.cacheset.cache_set_id, - cache_mode: $scope.availableNew.cacheMode - }; - if ($scope.availableNew.device.type === "partition") { - params.partition_id = $scope.availableNew.device.partition_id; - } else { - params.block_id = $scope.availableNew.device.block_id; - } - if (angular.isString($scope.availableNew.fstype) && - $scope.availableNew.fstype !== "") { - params.fstype = $scope.availableNew.fstype; - // XXX: Inconsistent tests of mountPoint/mount_point; in - // places it's compared to "" (like here), in others - // it's tested with angular.isDefined(), others with - // angular.isString(), others angular.isString() === - // false. This is *begging* for bugs. - if ($scope.availableNew.mountPoint !== "") { - params.mount_point = $scope.availableNew.mountPoint; - params.mount_options = $scope.availableNew.mountOptions; - } - } - if (angular.isArray($scope.availableNew.tags) - && $scope.availableNew.tags.length > 0) { - params.tags = $scope.availableNew.tags.map( - function(tag) { return tag.text; }); - } - MachinesManager.createBcache($scope.node, params); - - // Remove device from available. - var idx = $scope.available.indexOf($scope.availableNew.device); - $scope.available.splice(idx, 1); - $scope.availableNew = {}; + var msg = $scope.getCannotCreateBcacheMsg(); + if (msg === null) { + return true; + } else { + return false; + } + }; - // Update the selection. - $scope.updateAvailableSelection(true); - }; + // Enter bcache mode. + $scope.createBcache = function() { + if (!$scope.canCreateBcache()) { + return; + } + $scope.availableMode = SELECTION_MODE.BCACHE; + $scope.availableNew = { + name: getNextName("bcache"), + device: $scope.getSelectedAvailable()[0], + cacheset: $scope.cachesets[0], + cacheMode: "writeback", + fstype: null, + mountPoint: "", + mountOptions: "", + tags: [] + }; + }; + + // Clear mount point when the fstype is changed. + $scope.fstypeChanged = function(options) { + if (options.fstype === null) { + options.mountPoint = ""; + options.mountOptions = ""; + } else { + // Update the mount point to "none" if "swap" is + // selected, and vice-versa. + if ($scope.usesMountPoint(options.fstype)) { + if (options.mountPoint === "none") { + options.mountPoint = ""; + } + } else { + options.mountPoint = "none"; + } + } + }; - // Return true if a RAID can be created. - $scope.canCreateRAID = function() { - if ($scope.isAvailableDisabled() || !$scope.canEdit()) { - return false; - } + // Return true when the name of the new disk is invalid. + $scope.isNewDiskNameInvalid = function(newDiskName) { + if (!angular.isObject($scope.node) || !angular.isArray($scope.node.disks)) { + return true; + } - var selected = $scope.getSelectedAvailable(); - if (selected.length > 1) { - var i; - for (i = 0; i < selected.length; i++) { - if ($scope.hasUnmountedFilesystem(selected[i])) { - return false; - } else if (selected[i].type === "lvm-vg") { - return false; - } + if (newDiskName === "") { + return true; + } else { + var i, j; + for (i = 0; i < $scope.node.disks.length; i++) { + var disk = $scope.node.disks[i]; + if (newDiskName === disk.name) { + return true; + } + if (angular.isArray(disk.partitions)) { + for (j = 0; j < disk.partitions.length; j++) { + var partition = disk.partitions[j]; + if (newDiskName === partition.name) { + return true; } - return true; + } } - return false; - }; + } + } + return false; + }; - // Called to create a RAID. - $scope.createRAID = function() { - if (!$scope.canCreateRAID()) { - return; - } - $scope.availableMode = SELECTION_MODE.RAID; - $scope.availableNew = { - name: getNextName("md"), - devices: $scope.getSelectedAvailable(), - mode: null, - spares: [], - fstype: null, - mountPoint: "", - mountOptions: "", - tags: [] - }; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[0]; - }; + // Return true if bcache can be saved. + $scope.createBcacheCanSave = function() { + return ( + !$scope.isNewDiskNameInvalid($scope.availableNew.name) && + !$scope.isMountPointInvalid($scope.availableNew.mountPoint) + ); + }; + + // Confirm and create the bcache device. + $scope.availableConfirmCreateBcache = function() { + if (!$scope.createBcacheCanSave()) { + return; + } - // Get the available RAID modes. - $scope.getAvailableRAIDModes = function() { - if (!angular.isObject($scope.availableNew) || - !angular.isArray($scope.availableNew.devices)) { - return []; - } + // Create the bcache. + var params = { + name: $scope.availableNew.name, + cache_set: $scope.availableNew.cacheset.cache_set_id, + cache_mode: $scope.availableNew.cacheMode + }; + if ($scope.availableNew.device.type === "partition") { + params.partition_id = $scope.availableNew.device.partition_id; + } else { + params.block_id = $scope.availableNew.device.block_id; + } + if ( + angular.isString($scope.availableNew.fstype) && + $scope.availableNew.fstype !== "" + ) { + params.fstype = $scope.availableNew.fstype; + // XXX: Inconsistent tests of mountPoint/mount_point; in + // places it's compared to "" (like here), in others + // it's tested with angular.isDefined(), others with + // angular.isString(), others angular.isString() === + // false. This is *begging* for bugs. + if ($scope.availableNew.mountPoint !== "") { + params.mount_point = $scope.availableNew.mountPoint; + params.mount_options = $scope.availableNew.mountOptions; + } + } + if ( + angular.isArray($scope.availableNew.tags) && + $scope.availableNew.tags.length > 0 + ) { + params.tags = $scope.availableNew.tags.map(function(tag) { + return tag.text; + }); + } + MachinesManager.createBcache($scope.node, params); - var modes = []; - angular.forEach(RAID_MODES, function(mode) { - if ($scope.availableNew.devices.length >= mode.min_disks) { - modes.push(mode); - } - }); - return modes; - }; + // Remove device from available. + var idx = $scope.available.indexOf($scope.availableNew.device); + $scope.available.splice(idx, 1); + $scope.availableNew = {}; - // Return the total number of available spares for the current mode. - $scope.getTotalNumberOfAvailableSpares = function() { - var mode = $scope.availableNew.mode; - if (angular.isUndefined(mode) || !mode.allows_spares) { - return 0; - } else { - var diff = $scope.availableNew.devices.length - mode.min_disks; - if (diff < 0) { - diff = 0; - } - return diff; - } - }; + // Update the selection. + $scope.updateAvailableSelection(true); + }; + + // Return true if a RAID can be created. + $scope.canCreateRAID = function() { + if ($scope.isAvailableDisabled() || !$scope.canEdit()) { + return false; + } - // Return the number of remaining spares that can be selected. - $scope.getNumberOfRemainingSpares = function() { - var allowed = $scope.getTotalNumberOfAvailableSpares(); - if (allowed <= 0) { - return 0; - } else { - return allowed - $scope.availableNew.spares.length; + var selected = $scope.getSelectedAvailable(); + if (selected.length > 1) { + var i; + for (i = 0; i < selected.length; i++) { + if ($scope.hasUnmountedFilesystem(selected[i])) { + return false; + } else if (selected[i].type === "lvm-vg") { + return false; } - }; + } + return true; + } + return false; + }; - // Return true if the spares column should be shown. - $scope.showSparesColumn = function() { - return $scope.getTotalNumberOfAvailableSpares() > 0; - }; + // Called to create a RAID. + $scope.createRAID = function() { + if (!$scope.canCreateRAID()) { + return; + } + $scope.availableMode = SELECTION_MODE.RAID; + $scope.availableNew = { + name: getNextName("md"), + devices: $scope.getSelectedAvailable(), + mode: null, + spares: [], + fstype: null, + mountPoint: "", + mountOptions: "", + tags: [] + }; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[0]; + }; + + // Get the available RAID modes. + $scope.getAvailableRAIDModes = function() { + if ( + !angular.isObject($scope.availableNew) || + !angular.isArray($scope.availableNew.devices) + ) { + return []; + } - // Called when the RAID mode is changed to reset the selected spares. - $scope.RAIDModeChanged = function() { - $scope.availableNew.spares = []; - }; + var modes = []; + angular.forEach(RAID_MODES, function(mode) { + if ($scope.availableNew.devices.length >= mode.min_disks) { + modes.push(mode); + } + }); + return modes; + }; + + // Return the total number of available spares for the current mode. + $scope.getTotalNumberOfAvailableSpares = function() { + var mode = $scope.availableNew.mode; + if (angular.isUndefined(mode) || !mode.allows_spares) { + return 0; + } else { + var diff = $scope.availableNew.devices.length - mode.min_disks; + if (diff < 0) { + diff = 0; + } + return diff; + } + }; - // Return true if the disk is an active RAID member. - $scope.isActiveRAIDMember = function(disk) { - if (!angular.isArray($scope.availableNew.spares)) { - return true; - } else { - var idx = $scope.availableNew.spares.indexOf( - getUniqueKey(disk)); - return idx === -1; - } - }; + // Return the number of remaining spares that can be selected. + $scope.getNumberOfRemainingSpares = function() { + var allowed = $scope.getTotalNumberOfAvailableSpares(); + if (allowed <= 0) { + return 0; + } else { + return allowed - $scope.availableNew.spares.length; + } + }; - // Return true if the disk is a spare RAID member. - $scope.isSpareRAIDMember = function(disk) { - return !$scope.isActiveRAIDMember(disk); - }; + // Return true if the spares column should be shown. + $scope.showSparesColumn = function() { + return $scope.getTotalNumberOfAvailableSpares() > 0; + }; + + // Called when the RAID mode is changed to reset the selected spares. + $scope.RAIDModeChanged = function() { + $scope.availableNew.spares = []; + }; + + // Return true if the disk is an active RAID member. + $scope.isActiveRAIDMember = function(disk) { + if (!angular.isArray($scope.availableNew.spares)) { + return true; + } else { + var idx = $scope.availableNew.spares.indexOf(getUniqueKey(disk)); + return idx === -1; + } + }; - // Set the disk as an active RAID member. - $scope.setAsActiveRAIDMember = function(disk) { - var idx = $scope.availableNew.spares.indexOf(getUniqueKey(disk)); - if (idx > -1) { - $scope.availableNew.spares.splice(idx, 1); - } - }; + // Return true if the disk is a spare RAID member. + $scope.isSpareRAIDMember = function(disk) { + return !$scope.isActiveRAIDMember(disk); + }; + + // Set the disk as an active RAID member. + $scope.setAsActiveRAIDMember = function(disk) { + var idx = $scope.availableNew.spares.indexOf(getUniqueKey(disk)); + if (idx > -1) { + $scope.availableNew.spares.splice(idx, 1); + } + }; - // Set the disk as a spare RAID member. - $scope.setAsSpareRAIDMember = function(disk) { - var key = getUniqueKey(disk); - var idx = $scope.availableNew.spares.indexOf(key); - if (idx === -1) { - $scope.availableNew.spares.push(key); - } - }; + // Set the disk as a spare RAID member. + $scope.setAsSpareRAIDMember = function(disk) { + var key = getUniqueKey(disk); + var idx = $scope.availableNew.spares.indexOf(key); + if (idx === -1) { + $scope.availableNew.spares.push(key); + } + }; - // Return the size of the new RAID device. - $scope.getNewRAIDSize = function() { - if (angular.isUndefined($scope.availableNew.mode)) { - return ""; - } - var calculateSize = $scope.availableNew.mode.calculateSize; - if (!angular.isFunction(calculateSize)) { - return ""; - } + // Return the size of the new RAID device. + $scope.getNewRAIDSize = function() { + if (angular.isUndefined($scope.availableNew.mode)) { + return ""; + } + var calculateSize = $scope.availableNew.mode.calculateSize; + if (!angular.isFunction(calculateSize)) { + return ""; + } - // Get the number of disks and the minimum disk size in the RAID. - var numDisks = ( - $scope.availableNew.devices.length - - $scope.availableNew.spares.length); - var minSize = Number.MAX_VALUE; - angular.forEach($scope.availableNew.devices, function(device) { - // Get the size of the device. For a block device it will be - // at available_size and for a partition it will be at size. - var deviceSize = ( - device.original.available_size || device.original.size); - minSize = Math.min(minSize, deviceSize); - }); + // Get the number of disks and the minimum disk size in the RAID. + var numDisks = + $scope.availableNew.devices.length - $scope.availableNew.spares.length; + var minSize = Number.MAX_VALUE; + angular.forEach($scope.availableNew.devices, function(device) { + // Get the size of the device. For a block device it will be + // at available_size and for a partition it will be at size. + var deviceSize = device.original.available_size || device.original.size; + minSize = Math.min(minSize, deviceSize); + }); + + // Calculate the new size. + var size = calculateSize(minSize, numDisks); + return ConverterService.bytesToUnits(size).string; + }; + + // Return true if RAID can be saved. + $scope.createRAIDCanSave = function() { + return ( + !$scope.isNewDiskNameInvalid($scope.availableNew.name) && + !$scope.isMountPointInvalid($scope.availableNew.mountPoint) + ); + }; + + // Confirm and create the RAID device. + $scope.availableConfirmCreateRAID = function() { + if (!$scope.createRAIDCanSave()) { + return; + } - // Calculate the new size. - var size = calculateSize(minSize, numDisks); - return ConverterService.bytesToUnits(size).string; - }; + // Create the RAID. + var params = { + name: $scope.availableNew.name, + level: $scope.availableNew.mode.level, + block_devices: [], + partitions: [], + spare_devices: [], + spare_partitions: [] + }; + angular.forEach($scope.availableNew.devices, function(device) { + if ($scope.isActiveRAIDMember(device)) { + if (device.type === "partition") { + params.partitions.push(device.partition_id); + } else { + params.block_devices.push(device.block_id); + } + } else { + if (device.type === "partition") { + params.spare_partitions.push(device.partition_id); + } else { + params.spare_devices.push(device.block_id); + } + } + }); + if ( + angular.isString($scope.availableNew.fstype) && + $scope.availableNew.fstype !== "" + ) { + params.fstype = $scope.availableNew.fstype; + if ($scope.availableNew.mountPoint !== "") { + params.mount_point = $scope.availableNew.mountPoint; + params.mount_options = $scope.availableNew.mountOptions; + } + } + if ( + angular.isArray($scope.availableNew.tags) && + $scope.availableNew.tags.length > 0 + ) { + params.tags = $scope.availableNew.tags.map(function(tag) { + return tag.text; + }); + } + MachinesManager.createRAID($scope.node, params); - // Return true if RAID can be saved. - $scope.createRAIDCanSave = function() { - return ( - !$scope.isNewDiskNameInvalid() && - !$scope.isMountPointInvalid($scope.availableNew.mountPoint)); - }; + // Remove devices from available. + angular.forEach($scope.availableNew.devices, function() { + var idx = $scope.available.indexOf($scope.availableNew.device); + $scope.available.splice(idx, 1); + }); + $scope.availableNew = {}; - // Confirm and create the RAID device. - $scope.availableConfirmCreateRAID = function() { - if (!$scope.createRAIDCanSave()) { - return; - } + // Update the selection. + $scope.updateAvailableSelection(true); + }; + + // Return true if a volume group can be created. + $scope.canCreateVolumeGroup = function() { + if ($scope.isAvailableDisabled() || !$scope.canEdit()) { + return false; + } - // Create the RAID. - var params = { - name: $scope.availableNew.name, - level: $scope.availableNew.mode.level, - block_devices: [], - partitions: [], - spare_devices: [], - spare_partitions: [] - }; - angular.forEach($scope.availableNew.devices, function(device) { - if ($scope.isActiveRAIDMember(device)) { - if (device.type === "partition") { - params.partitions.push(device.partition_id); - } else { - params.block_devices.push(device.block_id); - } - } else { - if (device.type === "partition") { - params.spare_partitions.push(device.partition_id); - } else { - params.spare_devices.push(device.block_id); - } - } - }); - if (angular.isString($scope.availableNew.fstype) && - $scope.availableNew.fstype !== "") { - params.fstype = $scope.availableNew.fstype; - if ($scope.availableNew.mountPoint !== "") { - params.mount_point = $scope.availableNew.mountPoint; - params.mount_options = $scope.availableNew.mountOptions; - } - } - if (angular.isArray($scope.availableNew.tags) - && $scope.availableNew.tags.length > 0) { - params.tags = $scope.availableNew.tags.map( - function(tag) { return tag.text; }); + var selected = $scope.getSelectedAvailable(); + if (selected.length > 0) { + var i; + for (i = 0; i < selected.length; i++) { + if (selected[i].has_partitions) { + return false; + } else if ($scope.hasUnmountedFilesystem(selected[i])) { + return false; + } else if (selected[i].type === "lvm-vg") { + return false; } - MachinesManager.createRAID($scope.node, params); - - // Remove devices from available. - angular.forEach($scope.availableNew.devices, function(device) { - var idx = $scope.available.indexOf($scope.availableNew.device); - $scope.available.splice(idx, 1); - }); - $scope.availableNew = {}; + } + return true; + } + return false; + }; - // Update the selection. - $scope.updateAvailableSelection(true); - }; + // Called to create a volume group. + $scope.createVolumeGroup = function() { + if (!$scope.canCreateVolumeGroup()) { + return; + } + $scope.availableMode = SELECTION_MODE.VOLUME_GROUP; + $scope.availableNew = { + name: getNextName("vg"), + devices: $scope.getSelectedAvailable() + }; + }; + + // Return the size of the new volume group. + $scope.getNewVolumeGroupSize = function() { + var total = 0; + angular.forEach($scope.availableNew.devices, function(device) { + // Add available_size or size if available_size is not set. + total += device.original.available_size || device.original.size; + }); + return ConverterService.bytesToUnits(total).string; + }; + + // Return true if volume group can be saved. + $scope.createVolumeGroupCanSave = function() { + return !$scope.isNewDiskNameInvalid($scope.availableNew.name); + }; + + // Confirm and create the volume group device. + $scope.availableConfirmCreateVolumeGroup = function() { + if (!$scope.createVolumeGroupCanSave()) { + return; + } - // Return true if a volume group can be created. - $scope.canCreateVolumeGroup = function() { - if ($scope.isAvailableDisabled() || !$scope.canEdit()) { - return false; - } - - var selected = $scope.getSelectedAvailable(); - if (selected.length > 0) { - var i; - for (i = 0; i < selected.length; i++) { - if (selected[i].has_partitions) { - return false; - } else if ($scope.hasUnmountedFilesystem(selected[i])) { - return false; - } else if (selected[i].type === "lvm-vg") { - return false; - } - } - return true; - } - return false; - }; + // Create the RAID. + var params = { + name: $scope.availableNew.name, + block_devices: [], + partitions: [] + }; + angular.forEach($scope.availableNew.devices, function(device) { + if (device.type === "partition") { + params.partitions.push(device.partition_id); + } else { + params.block_devices.push(device.block_id); + } + }); + MachinesManager.createVolumeGroup($scope.node, params); + + // Remove devices from available. + angular.forEach($scope.availableNew.devices, function() { + var idx = $scope.available.indexOf($scope.availableNew.device); + $scope.available.splice(idx, 1); + }); + $scope.availableNew = {}; - // Called to create a volume group. - $scope.createVolumeGroup = function() { - if (!$scope.canCreateVolumeGroup()) { - return; - } - $scope.availableMode = SELECTION_MODE.VOLUME_GROUP; - $scope.availableNew = { - name: getNextName("vg"), - devices: $scope.getSelectedAvailable() - }; - }; + // Update the selection. + $scope.updateAvailableSelection(true); + }; + + // Return true if a logical volume can be added to disk. + $scope.canAddLogicalVolume = function(disk) { + if (disk.type !== "lvm-vg") { + return false; + } else if (disk.original.available_size < MIN_PARTITION_SIZE) { + return false; + } else { + return true; + } + }; - // Return the size of the new volume group. - $scope.getNewVolumeGroupSize = function() { - var total = 0; - angular.forEach($scope.availableNew.devices, function(device) { - // Add available_size or size if available_size is not set. - total += ( - device.original.available_size || device.original.size); - }); - return ConverterService.bytesToUnits(total).string; - }; + // Enter logical volume mode. + $scope.availableLogicalVolume = function(disk) { + $scope.availableMode = SELECTION_MODE.LOGICAL_VOLUME; + disk.$selected = true; + // Set starting size to the maximum available space. + var size_and_units = disk.available_size_human.split(" "); + var namePrefix = disk.name + "-lv"; + disk.$options = { + name: getNextName(namePrefix), + size: size_and_units[0], + sizeUnits: size_and_units[1], + fstype: null, + tags: [] + }; + }; + + // Return true if the name of the logical volume is invalid. + $scope.isLogicalVolumeNameInvalid = function(disk) { + if (!angular.isString(disk.$options.name)) { + return false; + } + var startsWith = disk.$options.name.indexOf(disk.name + "-"); + return ( + startsWith !== 0 || + disk.$options.name.length <= disk.name.length + 1 || + isNameAlreadyInUse(disk.$options.name) + ); + }; + + // Don't allow the name of the logical volume to remove the volume + // group name. + $scope.newLogicalVolumeNameChanged = function(disk) { + if (!angular.isString(disk.$options.name)) { + return; + } + var startsWith = disk.$options.name.indexOf(disk.name + "-"); + if (startsWith !== 0) { + disk.$options.name = disk.name + "-"; + } + }; - // Return true if volume group can be saved. - $scope.createVolumeGroupCanSave = function() { - return !$scope.isNewDiskNameInvalid(); - }; + // Return true if the logical volume size is invalid. + $scope.isAddLogicalVolumeSizeInvalid = function(disk) { + // Uses the same logic as the partition size checked. + return $scope.isAddPartitionSizeInvalid(disk); + }; + + // Confirm the logical volume creation. + $scope.availableConfirmLogicalVolume = function(disk) { + // Do nothing if not valid. + if ( + $scope.isLogicalVolumeNameInvalid(disk) || + $scope.isAddLogicalVolumeSizeInvalid(disk) || + $scope.isMountPointInvalid(disk.$options.mountPoint) + ) { + return; + } - // Confirm and create the volume group device. - $scope.availableConfirmCreateVolumeGroup = function() { - if (!$scope.createVolumeGroupCanSave()) { - return; - } + // Get the bytes to create the partition. + var bytes = ConverterService.unitsToBytes( + disk.$options.size, + disk.$options.sizeUnits + ); + + // Accepting prefilled defaults means use whole disk (lp:1509535). + var size_and_units = disk.original.available_size_human.split(" "); + if ( + disk.$options.size === size_and_units[0] && + disk.$options.sizeUnits === size_and_units[1] + ) { + bytes = disk.original.available_size; + } - // Create the RAID. - var params = { - name: $scope.availableNew.name, - block_devices: [], - partitions: [] - }; - angular.forEach($scope.availableNew.devices, function(device) { - if (device.type === "partition") { - params.partitions.push(device.partition_id); - } else { - params.block_devices.push(device.block_id); - } - }); - MachinesManager.createVolumeGroup($scope.node, params); + // Clamp to available space. + if (bytes > disk.original.available_size) { + bytes = disk.original.available_size; + } - // Remove devices from available. - angular.forEach($scope.availableNew.devices, function(device) { - var idx = $scope.available.indexOf($scope.availableNew.device); - $scope.available.splice(idx, 1); - }); - $scope.availableNew = {}; + // Remove the disk if it is going to use all the remaining space. + var removeDisk = false; + if (bytes === disk.original.available_size) { + removeDisk = true; + } - // Update the selection. - $scope.updateAvailableSelection(true); - }; + // Remove the volume group name from the name. + var name = disk.$options.name.slice(disk.name.length + 1); - // Return true if a logical volume can be added to disk. - $scope.canAddLogicalVolume = function(disk) { - if (disk.type !== "lvm-vg") { - return false; - } else if (disk.original.available_size < MIN_PARTITION_SIZE) { - return false; - } else { - return true; - } - }; + // Create the logical volume. + var params = {}; + if (angular.isString(disk.$options.fstype) && disk.$options.fstype !== "") { + params.fstype = disk.$options.fstype; + if (disk.$options.mountPoint !== "") { + params.mount_point = disk.$options.mountPoint; + params.mount_options = disk.$options.mountOptions; + } + } + if (angular.isArray(disk.$options.tags) && disk.$options.tags.length > 0) { + params.tags = disk.$options.tags.map(function(tag) { + return tag.text; + }); + } + MachinesManager.createLogicalVolume( + $scope.node, + disk.block_id, + name, + bytes, + params + ); + + // Remove the disk if needed. + if (removeDisk) { + var idx = $scope.available.indexOf(disk); + $scope.available.splice(idx, 1); + } + $scope.updateAvailableSelection(true); + }; - // Enter logical volume mode. - $scope.availableLogicalVolume = function(disk) { - $scope.availableMode = SELECTION_MODE.LOGICAL_VOLUME; - disk.$selected = true; - // Set starting size to the maximum available space. - var size_and_units = disk.available_size_human.split(" "); - var namePrefix = disk.name + "-lv"; - disk.$options = { - name: getNextName(namePrefix), - size: size_and_units[0], - sizeUnits: size_and_units[1], - fstype: null, - tags: [] - }; - }; + // Returns true if storage cannot be edited. + // (it can't be changed when the node is in any state other + // than Ready or Allocated) + $scope.isAllStorageDisabled = function() { + var authUser = UsersManager.getAuthUser(); + if ( + !angular.isObject(authUser) || + !angular.isObject($scope.node) || + (!authUser.is_superuser && authUser.username !== $scope.node.owner) + ) { + return true; + } else if ( + angular.isObject($scope.node) && + ["Ready", "Allocated"].indexOf($scope.node.status) === -1 + ) { + // If the node is not ready or allocated, disable storage panel. + return true; + } else { + // The node must be either ready or broken. Enable it. + return false; + } + }; - // Return true if the name of the logical volume is invalid. - $scope.isLogicalVolumeNameInvalid = function(disk) { - if (!angular.isString(disk.$options.name)) { - return false; - } - var startsWith = disk.$options.name.indexOf(disk.name + "-"); - return ( - startsWith !== 0 || - disk.$options.name.length <= disk.name.length + 1 || - isNameAlreadyInUse(disk.$options.name)); - }; + // Returns true if there are storage layout errors + $scope.hasStorageLayoutIssues = function() { + if ( + angular.isObject($scope.node) && + angular.isArray($scope.node.storage_layout_issues) + ) { + return $scope.node.storage_layout_issues.length > 0; + } + return false; + }; - // Don't allow the name of the logical volume to remove the volume - // group name. - $scope.newLogicalVolumeNameChanged = function(disk) { - if (!angular.isString(disk.$options.name)) { - return; - } - var startsWith = disk.$options.name.indexOf(disk.name + "-"); - if (startsWith !== 0) { - disk.$options.name = disk.name + "-"; - } - }; + // Returns warning text based on number of datastores + $scope.getRemoveDatastoreWarningText = function(disks) { + var datastores = disks.filter(function(disk) { + return disk.parent_type === "vmfs6"; + }); + var warningText = "Are you sure you want to remove this datastore?"; - // Return true if the logical volume size is invalid. - $scope.isAddLogicalVolumeSizeInvalid = function(disk) { - // Uses the same logic as the partition size checked. - return $scope.isAddPartitionSizeInvalid(disk); - }; + if (datastores.length === 1) { + warningText += " ESXi requires at least one VMFS datastore to deploy."; + } - // Confirm the logical volume creation. - $scope.availableConfirmLogicalVolume = function(disk) { - // Do nothing if not valid. - if ($scope.isLogicalVolumeNameInvalid(disk) || - $scope.isAddLogicalVolumeSizeInvalid(disk) || - $scope.isMountPointInvalid(disk.$options.mountPoint)) { - return; - } - - // Get the bytes to create the partition. - var bytes = ConverterService.unitsToBytes( - disk.$options.size, disk.$options.sizeUnits); - - // Accepting prefilled defaults means use whole disk (lp:1509535). - var size_and_units = disk.original.available_size_human.split(" "); - if (disk.$options.size === size_and_units[0] && - disk.$options.sizeUnits === size_and_units[1]) { - bytes = disk.original.available_size; - } - - // Clamp to available space. - if (bytes > disk.original.available_size) { - bytes = disk.original.available_size; - } - - // Remove the disk if it is going to use all the remaining space. - var removeDisk = false; - if (bytes === disk.original.available_size) { - removeDisk = true; - } - - // Remove the volume group name from the name. - var name = disk.$options.name.slice(disk.name.length + 1); - - // Create the logical volume. - var params = {}; - if (angular.isString(disk.$options.fstype) && - disk.$options.fstype !== "") { - params.fstype = disk.$options.fstype; - if (disk.$options.mountPoint !== "") { - params.mount_point = disk.$options.mountPoint; - params.mount_options = disk.$options.mountOptions; - } - } - if (angular.isArray(disk.$options.tags) - && disk.$options.tags.length > 0) { - params.tags = disk.$options.tags.map( - function(tag) { return tag.text; }); - } - MachinesManager.createLogicalVolume( - $scope.node, disk.block_id, name, bytes, params); - - // Remove the disk if needed. - if (removeDisk) { - var idx = $scope.available.indexOf(disk); - $scope.available.splice(idx, 1); - } - $scope.updateAvailableSelection(true); - }; + return warningText; + }; - // Returns true if storage cannot be edited. - // (it can't be changed when the node is in any state other - // than Ready or Allocated) - $scope.isAllStorageDisabled = function() { - var authUser = UsersManager.getAuthUser(); - if (!angular.isObject(authUser) || !angular.isObject($scope.node) || - (!authUser.is_superuser && - authUser.username !== $scope.node.owner)) { - return true; - } else if (angular.isObject($scope.node) && - ["Ready", "Allocated"].indexOf( - $scope.node.status) === -1) { - // If the node is not ready or allocated, disable storage panel. - return true; - } else { - // The node must be either ready or broken. Enable it. - return false; - } - }; + $scope.getTotalDiskSize = function(disks) { + var totalSize = 0; - // Returns true if there are storage layout errors - $scope.hasStorageLayoutIssues = function() { - if (angular.isObject($scope.node) && - angular.isArray($scope.node.storage_layout_issues)) { - return $scope.node.storage_layout_issues.length > 0; - } - return false; - }; + angular.forEach(disks, function(disk) { + totalSize = totalSize + disk.size; + }); + + return totalSize; + }; + + $scope.getFormattedTotalDiskSize = function(disks) { + var totalDiskSize = $scope.getTotalDiskSize(disks); + return formatBytes(totalDiskSize); + }; - // Tell $parent that the storageController has been loaded. - $scope.$parent.controllerLoaded('storageController', $scope); + // Tell $parent that the storageController has been loaded. + $scope.$parent.controllerLoaded("storageController", $scope); } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_events.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_events.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_events.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_events.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,94 +5,105 @@ */ /* @ngInject */ -function NodeEventsController($scope, $rootScope, - $routeParams, $location, MachinesManager, ControllersManager, - EventsManagerFactory, ManagerHelperService, ErrorService) { - - // Events manager that is loaded once the node is loaded. - var eventsManager = null; - - // Set the title and page. - $rootScope.title = "Loading..."; - - // Initial values. - $scope.loaded = false; - $scope.node = null; - $scope.events = []; - $scope.eventsLoaded = false; - $scope.days = 1; - - // Called once the node is loaded. - function nodeLoaded(node) { - $scope.node = node; - $scope.loaded = true; - - // Get the events manager and load it. - eventsManager = EventsManagerFactory.getManager(node.id); - $scope.events = eventsManager.getItems(); - $scope.days = eventsManager.getMaximumDays(); - eventsManager.loadItems().then(function() { - $scope.eventsLoaded = true; - }); - - // Update the title when the fqdn of the node changes. - $scope.$watch("node.fqdn", function() { - $rootScope.title = $scope.node.fqdn + " - events"; - }); +function NodeEventsController( + $scope, + $rootScope, + $routeParams, + $location, + MachinesManager, + ControllersManager, + EventsManagerFactory, + ManagerHelperService, + ErrorService +) { + // Events manager that is loaded once the node is loaded. + var eventsManager = null; + + // Set the title and page. + $rootScope.title = "Loading..."; + + // Initial values. + $scope.loaded = false; + $scope.node = null; + $scope.events = []; + $scope.eventsLoaded = false; + $scope.days = 1; + + // Called once the node is loaded. + function nodeLoaded(node) { + $scope.node = node; + $scope.loaded = true; + + // Get the events manager and load it. + eventsManager = EventsManagerFactory.getManager(node.id); + $scope.events = eventsManager.getItems(); + $scope.days = eventsManager.getMaximumDays(); + eventsManager.loadItems().then(function() { + $scope.eventsLoaded = true; + }); + + // Update the title when the fqdn of the node changes. + $scope.$watch("node.fqdn", function() { + $rootScope.title = $scope.node.fqdn + " - events"; + }); + } + + // Return the nice text for the given event. + $scope.getEventText = function(event) { + var text = event.type.description; + if (angular.isString(event.description) && event.description.length > 0) { + text += " - " + event.description; } + return text; + }; - // Return the nice text for the given event. - $scope.getEventText = function(event) { - var text = event.type.description; - if (angular.isString(event.description) && - event.description.length > 0) { - text += " - " + event.description; - } - return text; - }; - - // Called to load more events. - $scope.loadMore = function() { - $scope.days += 1; - eventsManager.loadMaximumDays($scope.days); - }; - - if ($location.path().indexOf('/controller') !== -1) { - $scope.nodesManager = ControllersManager; - $scope.type_name = 'controller'; - $rootScope.page = 'controllers'; - } else { - $scope.nodesManager = MachinesManager; - $scope.type_name = 'machine'; - $rootScope.page = 'machines'; + // Called to load more events. + $scope.loadMore = function() { + $scope.days += 1; + eventsManager.loadMaximumDays($scope.days); + }; + + if ($location.path().indexOf("/controller") !== -1) { + $scope.nodesManager = ControllersManager; + $scope.type_name = "controller"; + $rootScope.page = "controllers"; + } else { + $scope.nodesManager = MachinesManager; + $scope.type_name = "machine"; + $rootScope.page = "machines"; + } + // Load nodes manager. + ManagerHelperService.loadManager($scope, $scope.nodesManager).then( + function() { + // If redirected from the NodeDetailsController then the node + // will already be active. No need to set it active again. + var activeNode = $scope.nodesManager.getActiveItem(); + if ( + angular.isObject(activeNode) && + activeNode.system_id === $routeParams.system_id + ) { + nodeLoaded(activeNode); + } else { + $scope.nodesManager.setActiveItem($routeParams.system_id).then( + function(node) { + nodeLoaded(node); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } } - // Load nodes manager. - ManagerHelperService.loadManager( - $scope, $scope.nodesManager).then(function() { - // If redirected from the NodeDetailsController then the node - // will already be active. No need to set it active again. - var activeNode = $scope.nodesManager.getActiveItem(); - if (angular.isObject(activeNode) && - activeNode.system_id === $routeParams.system_id) { - nodeLoaded(activeNode); - } else { - $scope.nodesManager.setActiveItem( - $routeParams.system_id).then(function(node) { - nodeLoaded(node); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); - - // Destroy the events manager when the scope is destroyed. This is so - // the client will not recieve any more notifications about events - // for this node. - $scope.$on("$destroy", function() { - if (angular.isObject(eventsManager)) { - eventsManager.destroy(); - } - }); + ); + + // Destroy the events manager when the scope is destroyed. This is so + // the client will not recieve any more notifications about events + // for this node. + $scope.$on("$destroy", function() { + if (angular.isObject(eventsManager)) { + eventsManager.destroy(); + } + }); } export default NodeEventsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_result.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_result.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_result.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_result.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,100 +6,106 @@ /* @ngInject */ function NodeResultController( - $scope, $rootScope, $routeParams, $location, MachinesManager, - ControllersManager, NodeResultsManagerFactory, - ManagerHelperService, ErrorService) { - // Set the title and page. - $rootScope.title = "Loading..."; - - // Initial values. - $scope.loaded = false; - $scope.resultLoaded = false; - $scope.node = null; - $scope.output = 'combined'; - $scope.result = null; - - $scope.get_result_data = function(output) { - $scope.output = output; - $scope.data = "Loading..."; - var nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - nodeResultsManager.get_result_data( - $scope.result.id, $scope.output).then( - function(data) { - if (data === '') { - $scope.data = "Empty file."; - } else { - $scope.data = data; - } - }); - }; - - // Called once the node is loaded. - function nodeLoaded(node) { - $scope.node = node; - $scope.loaded = true; - - // Get the NodeResultsManager and load it. - var nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - var requestedResult = parseInt($routeParams.id, 10); - nodeResultsManager.getItem(requestedResult).then(function(result) { - $scope.result = result; - $scope.get_result_data($scope.output); - $scope.resultLoaded = true; - $rootScope.title = $scope.node.fqdn + " - " + - $scope.result.name; - }); - } - - // Update the title when the fqdn of the node changes. - $scope.$watch("node.fqdn", function() { - if (angular.isObject($scope.node) && - angular.isObject($scope.result)) { - $rootScope.title = $scope.node.fqdn + " - " + - $scope.result.name; + $scope, + $rootScope, + $routeParams, + $location, + MachinesManager, + ControllersManager, + NodeResultsManagerFactory, + ManagerHelperService, + ErrorService +) { + // Set the title and page. + $rootScope.title = "Loading..."; + + // Initial values. + $scope.loaded = false; + $scope.resultLoaded = false; + $scope.node = null; + $scope.output = "combined"; + $scope.result = null; + + $scope.get_result_data = function(output) { + $scope.output = output; + $scope.data = "Loading..."; + var nodeResultsManager = NodeResultsManagerFactory.getManager($scope.node); + nodeResultsManager + .get_result_data($scope.result.id, $scope.output) + .then(function(data) { + if (data === "") { + $scope.data = "Empty file."; + } else { + $scope.data = data; } + }); + }; + + // Called once the node is loaded. + function nodeLoaded(node) { + $scope.node = node; + $scope.loaded = true; + + // Get the NodeResultsManager and load it. + var nodeResultsManager = NodeResultsManagerFactory.getManager($scope.node); + var requestedResult = parseInt($routeParams.id, 10); + nodeResultsManager.getItem(requestedResult).then(function(result) { + $scope.result = result; + $scope.get_result_data($scope.output); + $scope.resultLoaded = true; + $rootScope.title = $scope.node.fqdn + " - " + $scope.result.name; }); + } - if ($location.path().indexOf("/controller") !== -1) { - $scope.nodesManager = ControllersManager; - $scope.type_name = 'controller'; - $rootScope.page = 'controllers'; - } else { - $scope.nodesManager = MachinesManager; - $scope.type_name = 'machine'; - $rootScope.page = 'machines'; + // Update the title when the fqdn of the node changes. + $scope.$watch("node.fqdn", function() { + if (angular.isObject($scope.node) && angular.isObject($scope.result)) { + $rootScope.title = $scope.node.fqdn + " - " + $scope.result.name; } - // Load nodes manager. - ManagerHelperService.loadManager( - $scope, $scope.nodesManager).then(function() { - // If redirected from the NodeDetailsController then the node - // will already be active. No need to set it active again. - var activeNode = $scope.nodesManager.getActiveItem(); - if (angular.isObject(activeNode) && - activeNode.system_id === $routeParams.system_id) { - nodeLoaded(activeNode); - } else { - $scope.nodesManager.setActiveItem( - $routeParams.system_id).then(function(node) { - nodeLoaded(node); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); - - // Destroy the NodeResultsManager when the scope is destroyed. This is - // so the client will not recieve any more notifications about results - // from this node. - $scope.$on("$destroy", function() { - var nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - if (angular.isObject(nodeResultsManager)) { - nodeResultsManager.destroy(); - } - }); + }); + + if ($location.path().indexOf("/controller") !== -1) { + $scope.nodesManager = ControllersManager; + $scope.type_name = "controller"; + $rootScope.page = "controllers"; + } else { + $scope.nodesManager = MachinesManager; + $scope.type_name = "machine"; + $rootScope.page = "machines"; + } + // Load nodes manager. + ManagerHelperService.loadManager($scope, $scope.nodesManager).then( + function() { + // If redirected from the NodeDetailsController then the node + // will already be active. No need to set it active again. + var activeNode = $scope.nodesManager.getActiveItem(); + if ( + angular.isObject(activeNode) && + activeNode.system_id === $routeParams.system_id + ) { + nodeLoaded(activeNode); + } else { + $scope.nodesManager.setActiveItem($routeParams.system_id).then( + function(node) { + nodeLoaded(node); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } + } + ); + + // Destroy the NodeResultsManager when the scope is destroyed. This is + // so the client will not recieve any more notifications about results + // from this node. + $scope.$on("$destroy", function() { + var nodeResultsManager = NodeResultsManagerFactory.getManager($scope.node); + if (angular.isObject(nodeResultsManager)) { + nodeResultsManager.destroy(); + } + }); } export default NodeResultController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_results.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_results.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/node_results.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/node_results.js 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -import NodeResultController from "./node_result"; +import { ScriptStatus } from "../enum"; /* Copyright 2017-2018 Canonical Ltd. This software is licensed under the * GNU Affero General Public License version 3 (see the file LICENSE). @@ -7,222 +7,268 @@ */ /* @ngInject */ -function NodeResultsController($scope, $routeParams, $location, MachinesManager, - ControllersManager, NodeResultsManagerFactory, - ManagerHelperService, ErrorService) { - - // NodeResultsManager that is loaded once the node is loaded. - $scope.nodeResultsManager = null; - // References to manager data used in scope. - $scope.commissioning_results = null; - $scope.testing_results = null; - $scope.installation_results = null; - $scope.results = null; - - // List of logs available. - $scope.logs = { - option: null, - availableOptions: [] - }; - // Log content being displayed. - $scope.logOutput = 'Loading...'; - - // Initial values. - $scope.loaded = false; - $scope.resultsLoaded = false; - $scope.node = null; - - function updateLogs() { - var i; - var option; - var had_installation = $scope.logs.availableOptions.length === 3; - $scope.logs.availableOptions.length = 0; - // XXX ltrager 2017-12-01 - Only show the current installation log - // if the machine is deploying, deployed, or failed deploying. The - // logs page needs to be redesigned to show previous installation - // results. - if ($scope.installation_results && - $scope.installation_results.length > 0 && ( - $scope.node.status_code === 6 || - $scope.node.status_code === 9 || - $scope.node.status_code === 11)) { - // If installation fails Curtin uploads a tar file of logs, the - // UI needs to display the text log file, not the tar. - for (i = 0; i < $scope.installation_results.length; i++) { - if ($scope.installation_results[i].name === - "/tmp/install.log") { - $scope.logs.availableOptions.push({ - 'title': 'Installation output', - 'id': $scope.installation_results[i].id - }); - break; - } - } - } - $scope.logs.availableOptions.push({ - 'title': 'Machine output (YAML)', - 'id': 'summary_yaml' - }); - $scope.logs.availableOptions.push({ - 'title': 'Machine output (XML)', - 'id': 'summary_xml' - }); - if (!had_installation && - $scope.logs.availableOptions.length === 3) { - // A new installation log has appeared, show it. - $scope.logs.option = $scope.logs.availableOptions[0]; - } else if (!$scope.selectedLog || ( - had_installation && $scope.logs.length === 2)) { - // No longer in a deployed state. - $scope.logs.option = $scope.logs.availableOptions[0]; +function NodeResultsController( + $scope, + $routeParams, + $location, + MachinesManager, + ControllersManager, + NodeResultsManagerFactory, + ManagerHelperService, + ErrorService +) { + // NodeResultsManager that is loaded once the node is loaded. + $scope.nodeResultsManager = null; + // References to manager data used in scope. + $scope.commissioning_results = null; + $scope.testing_results = null; + $scope.installation_results = null; + $scope.results = null; + + // List of logs available. + $scope.logs = { + option: null, + availableOptions: [] + }; + // Log content being displayed. + $scope.logOutput = "Loading..."; + + // Initial values. + $scope.loaded = false; + $scope.resultsLoaded = false; + $scope.node = null; + + function updateLogs() { + var i; + var had_installation = $scope.logs.availableOptions.length === 3; + $scope.logs.availableOptions.length = 0; + // XXX ltrager 2017-12-01 - Only show the current installation log + // if the machine is deploying, deployed, or failed deploying. The + // logs page needs to be redesigned to show previous installation + // results. + if ( + $scope.installation_results && + $scope.installation_results.length > 0 && + ($scope.node.status_code === 6 || + $scope.node.status_code === 9 || + $scope.node.status_code === 11) + ) { + // If installation fails Curtin uploads a tar file of logs, the + // UI needs to display the text log file, not the tar. + for (i = 0; i < $scope.installation_results.length; i++) { + if ($scope.installation_results[i].name === "/tmp/install.log") { + $scope.logs.availableOptions.push({ + title: "Installation output", + id: $scope.installation_results[i].id + }); + break; } + } } + $scope.logs.availableOptions.push({ + title: "Machine output (YAML)", + id: "summary_yaml" + }); + $scope.logs.availableOptions.push({ + title: "Machine output (XML)", + id: "summary_xml" + }); + if (!had_installation && $scope.logs.availableOptions.length === 3) { + // A new installation log has appeared, show it. + $scope.logs.option = $scope.logs.availableOptions[0]; + } else if ( + !$scope.selectedLog || + (had_installation && $scope.logs.length === 2) + ) { + // No longer in a deployed state. + $scope.logs.option = $scope.logs.availableOptions[0]; + } + } - // Called once the node has loaded. - function nodeLoaded(node) { - $scope.node = node; - $scope.loaded = true; - // Get the NodeResultsManager and load it. - $scope.nodeResultsManager = NodeResultsManagerFactory.getManager( - node, $scope.section.area); - $scope.nodeResultsManager.loadItems().then(function() { - $scope.commissioning_results = - $scope.nodeResultsManager.commissioning_results; - $scope.testing_results = - $scope.nodeResultsManager.testing_results; - $scope.installation_results = - $scope.nodeResultsManager.installation_results; - $scope.results = $scope.nodeResultsManager.results; - // Only load and monitor logs when on the logs page. - if ($scope.section.area === "logs") { - updateLogs(); - $scope.$watch("installation_results", updateLogs, true); - $scope.$watch( - "installation_results", $scope.updateLogOutput, true); - } - $scope.resultsLoaded = true; - }); + // Called once the node has loaded. + function nodeLoaded(node) { + $scope.node = node; + $scope.loaded = true; + // Get the NodeResultsManager and load it. + $scope.nodeResultsManager = NodeResultsManagerFactory.getManager( + node, + $scope.section.area + ); + $scope.nodeResultsManager.loadItems().then(function() { + $scope.commissioning_results = + $scope.nodeResultsManager.commissioning_results; + $scope.testing_results = $scope.nodeResultsManager.testing_results; + $scope.installation_results = + $scope.nodeResultsManager.installation_results; + $scope.results = $scope.nodeResultsManager.results; + // Only load and monitor logs when on the logs page. + if ($scope.section.area === "logs") { + updateLogs(); + $scope.$watch("installation_results", updateLogs, true); + $scope.$watch("installation_results", $scope.updateLogOutput, true); + } + $scope.resultsLoaded = true; + }); + } + + if ($location.path().indexOf("/controller") !== -1) { + $scope.nodesManager = ControllersManager; + } else { + $scope.nodesManager = MachinesManager; + } + // Load nodes manager. + ManagerHelperService.loadManager($scope, $scope.nodesManager).then( + function() { + // If redirected from the NodeDetailsController then the node + // will already be active. No need to set it active again. + var activeNode = $scope.nodesManager.getActiveItem(); + if ( + angular.isObject(activeNode) && + activeNode.system_id === $routeParams.system_id + ) { + nodeLoaded(activeNode); + } else { + $scope.nodesManager.setActiveItem($routeParams.system_id).then( + function(node) { + nodeLoaded(node); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } } + ); - if ($location.path().indexOf('/controller') !== -1) { - $scope.nodesManager = ControllersManager; + $scope.updateLogOutput = function() { + $scope.logOutput = "Loading..."; + if (!$scope.node) { + return; + } else if ($scope.logs.option.id === "summary_xml") { + $scope.nodesManager.getSummaryXML($scope.node).then(function(output) { + $scope.logOutput = output; + }); + } else if ($scope.logs.option.id === "summary_yaml") { + $scope.nodesManager.getSummaryYAML($scope.node).then(function(output) { + $scope.logOutput = output; + }); } else { - $scope.nodesManager = MachinesManager; + var result = null; + var i; + // Find the installation result to be displayed. + for (i = 0; i < $scope.installation_results.length; i++) { + if ($scope.installation_results[i].id === $scope.logs.option.id) { + result = $scope.installation_results[i]; + break; + } + } + switch (result.status) { + case 0: + $scope.logOutput = "System is booting..."; + break; + case 1: + $scope.logOutput = "Installation has begun!"; + break; + case 2: + $scope.nodeResultsManager + .get_result_data(result.id, "combined") + .then(function(output) { + if (output === "") { + $scope.logOutput = + "Installation has succeeded but " + "no output was given."; + } else { + $scope.logOutput = output; + } + }); + break; + case 3: + $scope.nodeResultsManager + .get_result_data(result.id, "combined") + .then(function(output) { + if (output === "") { + $scope.logOutput = + "Installation has failed and no " + "output was given."; + } else { + $scope.logOutput = output; + } + }); + break; + case 4: + $scope.logOutput = "Installation failed after 40 minutes."; + break; + case 5: + $scope.logOutput = "Installation was aborted."; + break; + default: + $scope.logOutput = "BUG: Unknown log status " + result.status; + break; + } + } + }; + + $scope.loadHistory = function(result) { + result.showing_results = false; + // History has already been loaded, no need to request it. + if (angular.isArray(result.history_list)) { + result.showing_history = true; + return; } - // Load nodes manager. - ManagerHelperService.loadManager( - $scope, $scope.nodesManager).then(function() { - // If redirected from the NodeDetailsController then the node - // will already be active. No need to set it active again. - var activeNode = $scope.nodesManager.getActiveItem(); - if (angular.isObject(activeNode) && - activeNode.system_id === $routeParams.system_id) { - nodeLoaded(activeNode); - } else { - $scope.nodesManager.setActiveItem( - $routeParams.system_id).then(function(node) { - nodeLoaded(node); - }, function(error) { - ErrorService.raiseError(error); - }); - } + result.loading_history = true; + $scope.nodeResultsManager.get_history(result.id).then(function(history) { + result.history_list = history; + result.loading_history = false; + result.showing_history = true; + }); + }; + + $scope.hasSuppressedTests = () => { + return $scope.results.some(type => { + const entries = Object.entries(type.results); + return entries.some(entry => entry[1].some(result => result.suppressed)); + }); + }; + + $scope.isSuppressible = result => + result.status === ScriptStatus.FAILED || + result.status === ScriptStatus.FAILED_INSTALLING || + result.status === ScriptStatus.TIMEDOUT; + + $scope.getSuppressedCount = () => { + const suppressibleTests = $scope.results.reduce((acc, type) => { + const entries = Object.entries(type.results); + entries.forEach(entry => { + entry[1].forEach(result => { + if ($scope.isSuppressible(result)) { + acc.push(result); + } }); + }); + return acc; + }, []); + const suppressedTests = suppressibleTests.filter(test => test.suppressed); - $scope.updateLogOutput = function() { - $scope.logOutput = "Loading..."; - if (!$scope.node) { - return; - } else if ($scope.logs.option.id === 'summary_xml') { - $scope.nodesManager.getSummaryXML($scope.node).then( - function(output) { - $scope.logOutput = output; - }); - } else if ($scope.logs.option.id === 'summary_yaml') { - $scope.nodesManager.getSummaryYAML($scope.node).then( - function(output) { - $scope.logOutput = output; - }); - } else { - var result = null; - var i; - // Find the installation result to be displayed. - for (i = 0; i < $scope.installation_results.length; i++) { - if ($scope.installation_results[i].id === - $scope.logs.option.id) { - result = $scope.installation_results[i]; - break; - } - } - switch (result.status) { - case 0: - $scope.logOutput = "System is booting..."; - break; - case 1: - $scope.logOutput = "Installation has begun!"; - break; - case 2: - $scope.nodeResultsManager.get_result_data( - result.id, 'combined').then(function(output) { - if (output === '') { - $scope.logOutput = - "Installation has succeeded but " + - "no output was given."; - } else { - $scope.logOutput = output; - } - }); - break; - case 3: - $scope.nodeResultsManager.get_result_data( - result.id, 'combined').then(function(output) { - if (output === '') { - $scope.logOutput = - "Installation has failed and no " + - "output was given."; - } else { - $scope.logOutput = output; - } - }); - break; - case 4: - $scope.logOutput = - "Installation failed after 40 minutes."; - break; - case 5: - $scope.logOutput = "Installation was aborted."; - break; - default: - $scope.logOutput = "BUG: Unknown log status " + - result.status; - break; - } - } - }; + if (suppressibleTests.length === suppressedTests.length) { + return "All"; + } + return suppressedTests.length; + }; - $scope.loadHistory = function(result) { - result.showing_results = false; - // History has already been loaded, no need to request it. - if (angular.isArray(result.history_list)) { - result.showing_history = true; - return; - } - result.loading_history = true; - $scope.nodeResultsManager.get_history(result.id).then( - function(history) { - result.history_list = history; - result.loading_history = false; - result.showing_history = true; - }); - }; + $scope.toggleSuppressed = result => { + if (result.suppressed) { + $scope.nodesManager.unsuppressTests($scope.node, [result]); + } else { + $scope.nodesManager.suppressTests($scope.node, [result]); + } + }; - // Destroy the NodeResultsManager when the scope is destroyed. This is - // so the client will not recieve any more notifications about results - // from this node. - $scope.$on("$destroy", function() { - if (angular.isObject($scope.nodeResultsManager)) { - $scope.nodeResultsManager.destroy(); - } - }); + // Destroy the NodeResultsManager when the scope is destroyed. This is + // so the client will not recieve any more notifications about results + // from this node. + $scope.$on("$destroy", function() { + if (angular.isObject($scope.nodeResultsManager)) { + $scope.nodeResultsManager.destroy(); + } + }); } export default NodeResultsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/nodes_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/nodes_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/nodes_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/nodes_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,1067 +6,1254 @@ /* @ngInject */ function NodesListController( - $q, $scope, $interval, $rootScope, $routeParams, $location, $window, - MachinesManager, DevicesManager, ControllersManager, GeneralManager, - ManagerHelperService, SearchService, ZonesManager, UsersManager, - ServicesManager, ScriptsManager, SwitchesManager, - ResourcePoolsManager, VLANsManager, TagsManager) { - - // Mapping of device.ip_assignment to viewable text. - var DEVICE_IP_ASSIGNMENT = { - external: "External", - dynamic: "Dynamic", - "static": "Static" - }; - - // Set title and page. - $rootScope.title = "Machines"; - $rootScope.page = "machines"; - - // Set initial values. - $scope.MAAS_config = $window.MAAS_config; - $scope.machines = MachinesManager.getItems(); - $scope.zones = ZonesManager.getItems(); - $scope.pools = ResourcePoolsManager.getItems(); - $scope.devices = DevicesManager.getItems(); - $scope.controllers = ControllersManager.getItems(); - $scope.switches = SwitchesManager.getItems(); - $scope.showswitches = $routeParams.switches === 'on'; - $scope.currentpage = "machines"; - $scope.osinfo = {}; - $scope.scripts = ScriptsManager.getItems(); - $scope.vlans = VLANsManager.getItems(); - $scope.loading = true; - $scope.tags = []; - - // Called for autocomplete when the user is typing a tag name. - $scope.tagsAutocomplete = function(query) { - return TagsManager.autocomplete(query); - }; - - $scope.tabs = {}; - $scope.pluralize = function(tab) { - var singulars = { - 'machines': 'machine', - 'switches': 'switch', - 'devices': 'device', - 'controllers': 'controller', - }; - var verb = singulars[tab]; - if ($scope.tabs[tab].selectedItems.length > 1) { - verb = tab; - } - return verb; - }; - // Machines tab. - $scope.tabs.machines = {}; - $scope.tabs.machines.pagetitle = "Machines"; - $scope.tabs.machines.currentpage = "machines"; - $scope.tabs.machines.manager = MachinesManager; - $scope.tabs.machines.previous_search = ""; - $scope.tabs.machines.search = ""; - $scope.tabs.machines.searchValid = true; - $scope.tabs.machines.selectedItems = MachinesManager.getSelectedItems(); - $scope.tabs.machines.metadata = MachinesManager.getMetadata(); - $scope.tabs.machines.filters = SearchService.getEmptyFilter(); - $scope.tabs.machines.actionOption = null; - $scope.tabs.machines.takeActionOptions = []; - $scope.tabs.machines.actionErrorCount = 0; - $scope.tabs.machines.actionProgress = { - total: 0, - completed: 0, - errors: {}, - showing_confirmation: false, - confirmation_message: '', - confirmation_details: [], - affected_nodes: 0 - }; - $scope.tabs.machines.osSelection = { - osystem: null, - release: null, - hwe_kernel: null - }; - $scope.tabs.machines.zoneSelection = null; - $scope.tabs.machines.poolSelection = null; - $scope.tabs.machines.poolAction = 'select-pool'; - $scope.tabs.machines.newPool = {}; - $scope.tabs.machines.commissionOptions = { - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }; - $scope.tabs.machines.deployOptions = { - installKVM: false - }; - $scope.tabs.machines.releaseOptions = {}; - $scope.tabs.machines.commissioningSelection = []; - $scope.tabs.machines.testSelection = []; - - // Pools tab. - $scope.tabs.pools = {}; - // The Pools tab is actually a sub tab of Machines. - $scope.tabs.pools.pagetitle = "Machines"; - $scope.tabs.pools.currentpage = "pools"; - $scope.tabs.pools.manager = ResourcePoolsManager; + $q, + $scope, + $interval, + $rootScope, + $routeParams, + $location, + $window, + $log, + MachinesManager, + DevicesManager, + ControllersManager, + GeneralManager, + ManagerHelperService, + SearchService, + ZonesManager, + UsersManager, + ServicesManager, + ScriptsManager, + SwitchesManager, + ResourcePoolsManager, + VLANsManager, + TagsManager, + NotificationsManager +) { + // Mapping of device.ip_assignment to viewable text. + var DEVICE_IP_ASSIGNMENT = { + external: "External", + dynamic: "Dynamic", + static: "Static" + }; + + // Set title and page. + $rootScope.title = "Machines"; + $rootScope.page = "machines"; + + // Set initial values. + $scope.MAAS_config = $window.MAAS_config; + $scope.machines = MachinesManager.getItems(); + $scope.zones = ZonesManager.getItems(); + $scope.pools = ResourcePoolsManager.getItems(); + $scope.devices = DevicesManager.getItems(); + $scope.controllers = ControllersManager.getItems(); + $scope.switches = SwitchesManager.getItems(); + $scope.showswitches = $routeParams.switches === "on"; + $scope.currentpage = "machines"; + $scope.osinfo = {}; + $scope.scripts = ScriptsManager.getItems(); + $scope.vlans = VLANsManager.getItems(); + $scope.loading = true; + $scope.tags = []; + $scope.failedActionSentence = "Action cannot be performed."; + + // Called for autocomplete when the user is typing a tag name. + $scope.tagsAutocomplete = function(query) { + return TagsManager.autocomplete(query); + }; + + $scope.tabs = {}; + $scope.pluralize = function(tab) { + var singulars = { + machines: "machine", + switches: "switch", + devices: "device", + controllers: "controller" + }; + var verb = singulars[tab]; + if ($scope.tabs[tab].selectedItems.length > 1) { + verb = tab; + } + return verb; + }; + // Machines tab. + $scope.tabs.machines = {}; + $scope.tabs.machines.pagetitle = "Machines"; + $scope.tabs.machines.currentpage = "machines"; + $scope.tabs.machines.manager = MachinesManager; + $scope.tabs.machines.previous_search = ""; + $scope.tabs.machines.search = ""; + $scope.tabs.machines.searchValid = true; + $scope.tabs.machines.selectedItems = MachinesManager.getSelectedItems(); + $scope.tabs.machines.metadata = MachinesManager.getMetadata(); + $scope.tabs.machines.filters = SearchService.getEmptyFilter(); + $scope.tabs.machines.actionOption = null; + $scope.tabs.machines.takeActionOptions = []; + $scope.tabs.machines.actionErrorCount = 0; + $scope.tabs.machines.actionProgress = { + total: 0, + completed: 0, + errors: {}, + showing_confirmation: false, + confirmation_message: "", + confirmation_details: [], + affected_nodes: 0 + }; + $scope.tabs.machines.osSelection = { + osystem: null, + release: null, + hwe_kernel: null + }; + $scope.tabs.machines.zoneSelection = null; + $scope.tabs.machines.poolSelection = null; + $scope.tabs.machines.poolAction = "select-pool"; + $scope.tabs.machines.newPool = {}; + $scope.tabs.machines.commissionOptions = { + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }; + $scope.tabs.machines.deployOptions = { + installKVM: false + }; + $scope.tabs.machines.releaseOptions = {}; + $scope.tabs.machines.commissioningSelection = []; + $scope.tabs.machines.testSelection = []; + $scope.tabs.machines.failedTests = []; + $scope.tabs.machines.loadingFailedTests = false; + $scope.tabs.machines.suppressFailedTestsChecked = false; + + // Pools tab. + $scope.tabs.pools = {}; + // The Pools tab is actually a sub tab of Machines. + $scope.tabs.pools.pagetitle = "Machines"; + $scope.tabs.pools.currentpage = "pools"; + $scope.tabs.pools.manager = ResourcePoolsManager; + $scope.tabs.pools.actionOption = false; + $scope.tabs.pools.newPool = { name: null, description: null }; + $scope.tabs.pools.addPool = function() { + $scope.tabs.pools.actionOption = true; + }; + $scope.tabs.pools.cancelAddPool = function() { $scope.tabs.pools.actionOption = false; - $scope.tabs.pools.newPool = { name: null, description: null }; - $scope.tabs.pools.addPool = function() { - $scope.tabs.pools.actionOption = true; - }; - $scope.tabs.pools.cancelAddPool = function() { - $scope.tabs.pools.actionOption = false; - $scope.tabs.pools.newPool = {}; - }; - $scope.tabs.pools.activeTarget = null; - $scope.tabs.pools.activeTargetAction = null; - $scope.tabs.pools.actionErrorMessage = null; - $scope.tabs.pools.initiatePoolAction = function(pool, action) { - let tab = $scope.tabs.pools; - // reset state in case of switching between deletes - tab.cancelPoolAction(); - tab.activeTargetAction = action; - tab.activeTarget = pool; - tab.editingPool = pool; // used by maas-obj-form for editing - }; - $scope.tabs.pools.cancelPoolAction = function() { - let tab = $scope.tabs.pools; - tab.activeTargetAction = null; - tab.activeTarget = null; - tab.actionErrorMessage = null; - }; - $scope.tabs.pools.isPoolAction = function(pool, action) { - let tab = $scope.tabs.pools; - return ( - action === undefined || tab.activeTargetAction === action) && - tab.activeTarget !== null && - tab.activeTarget.id === pool.id; - }; - $scope.tabs.pools.actionConfirmEditPool = function() { - $scope.tabs.pools.cancelPoolAction(); - }; - $scope.tabs.pools.actionConfirmDeletePool = function() { - let tab = $scope.tabs.pools; - tab.manager.deleteItem(tab.activeTarget).then( - tab.cancelPoolAction, - function(error) { - $scope.tabs.pools.actionErrorMessage = error; - }); - }; - $scope.tabs.pools.goToPoolMachines = function(pool) { - $scope.clearSearch('machines'); - $scope.toggleFilter('pool', pool.name, 'machines'); - $scope.toggleTab('machines'); - // update the location URL otherwise to match the tab - $location.path('/machines'); - }; - $scope.tabs.pools.isDefaultPool = function(pool) { - return pool.id === 0; - }; - - $scope.nodesManager = MachinesManager; - - // Device tab. - $scope.tabs.devices = {}; - $scope.tabs.devices.pagetitle = "Devices"; - $scope.tabs.devices.currentpage = "devices"; - $scope.tabs.devices.manager = DevicesManager; - $scope.tabs.devices.previous_search = ""; - $scope.tabs.devices.search = ""; - $scope.tabs.devices.searchValid = true; - $scope.tabs.devices.selectedItems = DevicesManager.getSelectedItems(); - $scope.tabs.devices.filtered_items = []; - $scope.tabs.devices.predicate = 'fqdn'; - $scope.tabs.devices.allViewableChecked = false; - $scope.tabs.devices.metadata = DevicesManager.getMetadata(); - $scope.tabs.devices.filters = SearchService.getEmptyFilter(); - $scope.tabs.devices.column = 'fqdn'; - $scope.tabs.devices.actionOption = null; - $scope.tabs.devices.takeActionOptions = []; - $scope.tabs.devices.actionErrorCount = 0; - $scope.tabs.devices.actionProgress = { - total: 0, - completed: 0, - errors: {}, - showing_confirmation: false, - confirmation_message: '', - confirmation_details: [], - affected_nodes: 0 - }; - $scope.tabs.devices.zoneSelection = null; - $scope.tabs.devices.poolSelection = null; - $scope.tabs.devices.poolAction = 'select-pool'; - $scope.tabs.devices.newPool = {}; - - // Controller tab. - $scope.tabs.controllers = {}; - $scope.tabs.controllers.pagetitle = "Controllers"; - $scope.tabs.controllers.currentpage = "controllers"; - $scope.tabs.controllers.manager = ControllersManager; - $scope.tabs.controllers.previous_search = ""; - $scope.tabs.controllers.search = ""; - $scope.tabs.controllers.searchValid = true; - $scope.tabs.controllers.selectedItems = - ControllersManager.getSelectedItems(); - $scope.tabs.controllers.filtered_items = []; - $scope.tabs.controllers.predicate = 'fqdn'; - $scope.tabs.controllers.allViewableChecked = false; - $scope.tabs.controllers.metadata = ControllersManager.getMetadata(); - $scope.tabs.controllers.filters = SearchService.getEmptyFilter(); - $scope.tabs.controllers.column = 'fqdn'; - $scope.tabs.controllers.actionOption = null; - // Rack controllers contain all options - $scope.tabs.controllers.takeActionOptions = []; - $scope.tabs.controllers.actionErrorCount = 0; - $scope.tabs.controllers.actionProgress = { - total: 0, - completed: 0, - errors: {}, - showing_confirmation: false, - confirmation_message: '', - confirmation_details: [], - affected_nodes: 0 - }; - $scope.tabs.controllers.zoneSelection = null; - $scope.tabs.controllers.poolSelection = null; - $scope.tabs.controllers.poolAction = 'select-pool'; - $scope.tabs.controllers.newPool = {}; - $scope.tabs.controllers.syncStatuses = {}; - $scope.tabs.controllers.addController = false; - $scope.tabs.controllers.registerUrl = MAAS_config.register_url; - $scope.tabs.controllers.registerSecret = MAAS_config.register_secret; - - // Switch tab. - $scope.tabs.switches = {}; - $scope.tabs.switches.pagetitle = "Switches"; - $scope.tabs.switches.currentpage = "switches"; - $scope.tabs.switches.manager = SwitchesManager; - $scope.tabs.switches.previous_search = ""; - $scope.tabs.switches.search = ""; - $scope.tabs.switches.searchValid = true; - $scope.tabs.switches.selectedItems = SwitchesManager.getSelectedItems(); - $scope.tabs.switches.predicate = 'fqdn'; - $scope.tabs.switches.allViewableChecked = false; - $scope.tabs.switches.metadata = SwitchesManager.getMetadata(); - $scope.tabs.switches.filters = SearchService.getEmptyFilter(); - $scope.tabs.switches.column = 'fqdn'; - $scope.tabs.switches.actionOption = null; - $scope.tabs.switches.takeActionOptions = []; - $scope.tabs.switches.actionErrorCount = 0; - $scope.tabs.switches.actionProgress = { - total: 0, - completed: 0, - errors: {}, - showing_confirmation: false, - confirmation_message: '', - confirmation_details: [], - affected_nodes: 0 - }; - $scope.tabs.switches.osSelection = { - osystem: null, - release: null, - hwe_kernel: null - }; - $scope.tabs.switches.zoneSelection = null; - $scope.tabs.switches.poolSelection = null; - $scope.tabs.switches.poolAction = 'select-pool'; - $scope.tabs.switches.newPool = {}; - $scope.tabs.switches.commissioningSelection = []; - $scope.tabs.switches.commissionOptions = { - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }; - $scope.tabs.switches.deployOptions = { - installKVM: false - }; - $scope.tabs.switches.releaseOptions = {}; - + $scope.tabs.pools.newPool = {}; + }; + $scope.tabs.pools.activeTarget = null; + $scope.tabs.pools.activeTargetAction = null; + $scope.tabs.pools.actionErrorMessage = null; + $scope.tabs.pools.initiatePoolAction = function(pool, action) { + let tab = $scope.tabs.pools; + // reset state in case of switching between deletes + tab.cancelPoolAction(); + tab.activeTargetAction = action; + tab.activeTarget = pool; + tab.editingPool = pool; // used by maas-obj-form for editing + }; + $scope.tabs.pools.cancelPoolAction = function() { + let tab = $scope.tabs.pools; + tab.activeTargetAction = null; + tab.activeTarget = null; + tab.actionErrorMessage = null; + }; + $scope.tabs.pools.isPoolAction = function(pool, action) { + let tab = $scope.tabs.pools; + return ( + (angular.isUndefined(action) || tab.activeTargetAction === action) && + tab.activeTarget !== null && + tab.activeTarget.id === pool.id + ); + }; + $scope.tabs.pools.actionConfirmEditPool = function() { + $scope.tabs.pools.cancelPoolAction(); + }; + $scope.tabs.pools.actionConfirmDeletePool = function() { + let tab = $scope.tabs.pools; + tab.manager + .deleteItem(tab.activeTarget) + .then(tab.cancelPoolAction, function(error) { + $scope.tabs.pools.actionErrorMessage = error; + }); + }; + $scope.tabs.pools.goToPoolMachines = function(pool) { + $scope.clearSearch("machines"); + $scope.toggleFilter("pool", pool.name, "machines"); + $scope.toggleTab("machines"); + // update the location URL otherwise to match the tab + $location.path("/machines"); + }; + $scope.tabs.pools.isDefaultPool = function(pool) { + return pool.id === 0; + }; + + $scope.nodesManager = MachinesManager; + + // Device tab. + $scope.tabs.devices = {}; + $scope.tabs.devices.pagetitle = "Devices"; + $scope.tabs.devices.currentpage = "devices"; + $scope.tabs.devices.manager = DevicesManager; + $scope.tabs.devices.previous_search = ""; + $scope.tabs.devices.search = ""; + $scope.tabs.devices.searchValid = true; + $scope.tabs.devices.selectedItems = DevicesManager.getSelectedItems(); + $scope.tabs.devices.filtered_items = []; + $scope.tabs.devices.predicate = "fqdn"; + $scope.tabs.devices.allViewableChecked = false; + $scope.tabs.devices.metadata = DevicesManager.getMetadata(); + $scope.tabs.devices.filters = SearchService.getEmptyFilter(); + $scope.tabs.devices.column = "fqdn"; + $scope.tabs.devices.actionOption = null; + $scope.tabs.devices.takeActionOptions = []; + $scope.tabs.devices.actionErrorCount = 0; + $scope.tabs.devices.actionProgress = { + total: 0, + completed: 0, + errors: {}, + showing_confirmation: false, + confirmation_message: "", + confirmation_details: [], + affected_nodes: 0 + }; + $scope.tabs.devices.zoneSelection = null; + $scope.tabs.devices.poolSelection = null; + $scope.tabs.devices.poolAction = "select-pool"; + $scope.tabs.devices.newPool = {}; + + // Controller tab. + $scope.tabs.controllers = {}; + $scope.tabs.controllers.pagetitle = "Controllers"; + $scope.tabs.controllers.currentpage = "controllers"; + $scope.tabs.controllers.manager = ControllersManager; + $scope.tabs.controllers.previous_search = ""; + $scope.tabs.controllers.search = ""; + $scope.tabs.controllers.searchValid = true; + $scope.tabs.controllers.selectedItems = ControllersManager.getSelectedItems(); + $scope.tabs.controllers.filtered_items = []; + $scope.tabs.controllers.predicate = "fqdn"; + $scope.tabs.controllers.allViewableChecked = false; + $scope.tabs.controllers.metadata = ControllersManager.getMetadata(); + $scope.tabs.controllers.filters = SearchService.getEmptyFilter(); + $scope.tabs.controllers.column = "fqdn"; + $scope.tabs.controllers.actionOption = null; + // Rack controllers contain all options + $scope.tabs.controllers.takeActionOptions = []; + $scope.tabs.controllers.actionErrorCount = 0; + $scope.tabs.controllers.actionProgress = { + total: 0, + completed: 0, + errors: {}, + showing_confirmation: false, + confirmation_message: "", + confirmation_details: [], + affected_nodes: 0 + }; + $scope.tabs.controllers.zoneSelection = null; + $scope.tabs.controllers.poolSelection = null; + $scope.tabs.controllers.poolAction = "select-pool"; + $scope.tabs.controllers.newPool = {}; + $scope.tabs.controllers.syncStatuses = {}; + $scope.tabs.controllers.addController = false; + $scope.tabs.controllers.registerUrl = $window.MAAS_config.register_url; + $scope.tabs.controllers.registerSecret = $window.MAAS_config.register_secret; + + // Switch tab. + $scope.tabs.switches = {}; + $scope.tabs.switches.pagetitle = "Switches"; + $scope.tabs.switches.currentpage = "switches"; + $scope.tabs.switches.manager = SwitchesManager; + $scope.tabs.switches.previous_search = ""; + $scope.tabs.switches.search = ""; + $scope.tabs.switches.searchValid = true; + $scope.tabs.switches.selectedItems = SwitchesManager.getSelectedItems(); + $scope.tabs.switches.predicate = "fqdn"; + $scope.tabs.switches.allViewableChecked = false; + $scope.tabs.switches.metadata = SwitchesManager.getMetadata(); + $scope.tabs.switches.filters = SearchService.getEmptyFilter(); + $scope.tabs.switches.column = "fqdn"; + $scope.tabs.switches.actionOption = null; + $scope.tabs.switches.takeActionOptions = []; + $scope.tabs.switches.actionErrorCount = 0; + $scope.tabs.switches.actionProgress = { + total: 0, + completed: 0, + errors: {}, + showing_confirmation: false, + confirmation_message: "", + confirmation_details: [], + affected_nodes: 0 + }; + $scope.tabs.switches.osSelection = { + osystem: null, + release: null, + hwe_kernel: null + }; + $scope.tabs.switches.zoneSelection = null; + $scope.tabs.switches.poolSelection = null; + $scope.tabs.switches.poolAction = "select-pool"; + $scope.tabs.switches.newPool = {}; + $scope.tabs.switches.commissioningSelection = []; + $scope.tabs.switches.commissionOptions = { + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }; + $scope.tabs.switches.deployOptions = { + installKVM: false + }; + $scope.tabs.switches.releaseOptions = {}; + + // Options for add hardware dropdown. + $scope.addHardwareOption = null; + $scope.addHardwareOptions = [ + { + name: "machine", + title: "Machine" + }, + { + name: "chassis", + title: "Chassis" + } + ]; - // Options for add hardware dropdown. + // This will hold the AddHardwareController once it is initialized. + // The controller will set this variable as it's always a child of + // this scope. + $scope.addHardwareScope = null; + + // This will hold the AddDeviceController once it is initialized. + // The controller will set this variable as it's always a child of + // this scope. + $scope.addDeviceScope = null; + + // When the addHardwareScope is hidden it will emit this event. We + // clear the call to action button, so it can be used again. + $scope.$on("addHardwareHidden", function() { $scope.addHardwareOption = null; - $scope.addHardwareOptions = [ - { - name: "machine", - title: "Machine" - }, - { - name: "chassis", - title: "Chassis" - } - ]; + }); - // This will hold the AddHardwareController once it is initialized. - // The controller will set this variable as it's always a child of - // this scope. - $scope.addHardwareScope = null; - - // This will hold the AddDeviceController once it is initialized. - // The controller will set this variable as it's always a child of - // this scope. - $scope.addDeviceScope = null; - - // When the addHardwareScope is hidden it will emit this event. We - // clear the call to action button, so it can be used again. - $scope.$on("addHardwareHidden", function() { - $scope.addHardwareOption = null; - }); - - // Return true if the tab is in viewing selected mode. - function isViewingSelected(tab) { - var search = $scope.tabs[tab].search.toLowerCase(); - return search === "in:(selected)" || search === "in:selected"; - } - - // Sets the search bar to only show selected. - function enterViewSelected(tab) { - $scope.tabs[tab].previous_search = $scope.tabs[tab].search; - $scope.tabs[tab].search = "in:(Selected)"; - } - - // Clear search bar from viewing selected. - function leaveViewSelected(tab) { - if (isViewingSelected(tab)) { - $scope.tabs[tab].search = $scope.tabs[tab].previous_search; - $scope.updateFilters(tab); - } + // Return true if the tab is in viewing selected mode. + function isViewingSelected(tab) { + var search = $scope.tabs[tab].search.toLowerCase(); + return search === "in:(selected)" || search === "in:selected"; + } + + // Sets the search bar to only show selected. + function enterViewSelected(tab) { + $scope.tabs[tab].previous_search = $scope.tabs[tab].search; + $scope.tabs[tab].search = "in:(Selected)"; + } + + // Clear search bar from viewing selected. + function leaveViewSelected(tab) { + $scope.tabs.machines.suppressFailedTestsChecked = false; + if (isViewingSelected(tab)) { + $scope.tabs[tab].search = $scope.tabs[tab].previous_search; + $scope.updateFilters(tab); } + } - // Called to update `allViewableChecked`. - function updateAllViewableChecked(tab) { - // Not checked when the filtered nodes are empty. - if ($scope.tabs[tab].filtered_items.length === 0) { - $scope.tabs[tab].allViewableChecked = false; - return; - } - - // Loop through all filtered nodes and see if all are checked. - var i; - for (i = 0; i < $scope.tabs[tab].filtered_items.length; i++) { - if (!$scope.tabs[tab].filtered_items[i].$selected) { - $scope.tabs[tab].allViewableChecked = false; - return; - } - } - $scope.tabs[tab].allViewableChecked = true; + // Called to update `allViewableChecked`. + function updateAllViewableChecked(tab) { + // Not checked when the filtered nodes are empty. + if ($scope.tabs[tab].filtered_items.length === 0) { + $scope.tabs[tab].allViewableChecked = false; + return; } - function clearAction(tab) { - resetActionProgress(tab); - leaveViewSelected(tab); - $scope.tabs[tab].actionOption = null; - $scope.tabs[tab].zoneSelection = null; - $scope.tabs[tab].poolSelection = null; - $scope.tabs[tab].poolAction = 'select-pool'; - $scope.tabs[tab].newPool = {}; - if (tab === "machines" || tab === "switches") { - // Possible for this to be called before the osSelect - // direction is initialized. In that case it has not - // created the $reset function on the model object. - if (angular.isFunction( - $scope.tabs[tab].osSelection.$reset)) { - $scope.tabs[tab].osSelection.$reset(); - } - $scope.tabs[tab].commissionOptions.enableSSH = false; - $scope.tabs[tab].commissionOptions.skipBMCConfig = false; - $scope.tabs[tab].commissionOptions.skipNetworking = false; - $scope.tabs[tab].commissionOptions.skipStorage = false; - $scope.tabs[tab].commissionOptions.updateFirmware = false; - $scope.tabs[tab].commissionOptions.configureHBA = false; - $scope.tabs[tab].deployOptions.installKVM = false; - } - $scope.tabs[tab].commissioningSelection = []; - $scope.tabs[tab].testSelection = []; + // Loop through all filtered nodes and see if all are checked. + var i; + for (i = 0; i < $scope.tabs[tab].filtered_items.length; i++) { + if (!$scope.tabs[tab].filtered_items[i].$selected) { + $scope.tabs[tab].allViewableChecked = false; + return; + } } + $scope.tabs[tab].allViewableChecked = true; + } - // Clear the action if required. - function shouldClearAction(tab) { - if ($scope.tabs[tab].selectedItems.length === 0) { - clearAction(tab); - } - if ($scope.tabs[tab].actionOption && !isViewingSelected(tab)) { - $scope.tabs[tab].actionOption = null; - } + function clearAction(tab) { + resetActionProgress(tab); + leaveViewSelected(tab); + $scope.tabs[tab].actionOption = null; + $scope.tabs[tab].zoneSelection = null; + $scope.tabs[tab].poolSelection = null; + $scope.tabs[tab].poolAction = "select-pool"; + $scope.tabs[tab].newPool = {}; + if (tab === "machines" || tab === "switches") { + // Possible for this to be called before the osSelect + // direction is initialized. In that case it has not + // created the $reset function on the model object. + if (angular.isFunction($scope.tabs[tab].osSelection.$reset)) { + $scope.tabs[tab].osSelection.$reset(); + } + $scope.tabs[tab].commissionOptions.enableSSH = false; + $scope.tabs[tab].commissionOptions.skipBMCConfig = false; + $scope.tabs[tab].commissionOptions.skipNetworking = false; + $scope.tabs[tab].commissionOptions.skipStorage = false; + $scope.tabs[tab].commissionOptions.updateFirmware = false; + $scope.tabs[tab].commissionOptions.configureHBA = false; + $scope.tabs[tab].deployOptions.installKVM = false; } - - // Called when the filtered_items are updated. Checks if the - // filtered_items are empty and if the search still matches the - // previous search. This will reset the search when no nodes match - // the current filter. - function removeEmptyFilter(tab) { - if ($scope.tabs[tab].filtered_items.length === 0 && - $scope.tabs[tab].search !== "" && - $scope.tabs[tab].search === $scope.tabs[tab].previous_search) { - $scope.tabs[tab].search = ""; - $scope.updateFilters(tab); - } + $scope.tabs[tab].commissioningSelection = []; + $scope.tabs[tab].testSelection = []; + } + + // Clear the action if required. + function shouldClearAction(tab) { + if ($scope.tabs[tab].selectedItems.length === 0) { + clearAction(tab); } - - // Update the number of selected items which have an error based on the - // current selected action. - function updateActionErrorCount(tab) { - var i; - $scope.tabs[tab].actionErrorCount = 0; - for (i = 0; i < $scope.tabs[tab].selectedItems.length; i++) { - var supported = $scope.supportsAction( - $scope.tabs[tab].selectedItems[i], tab); - if (!supported) { - $scope.tabs[tab].actionErrorCount += 1; - } - $scope.tabs[tab].selectedItems[i].action_failed = false; - } + if ($scope.tabs[tab].actionOption && !isViewingSelected(tab)) { + $scope.tabs[tab].actionOption = null; } + } - // Reset actionProgress on tab to zero. - function resetActionProgress(tab) { - var progress = $scope.tabs[tab].actionProgress; - progress.completed = progress.total = 0; - progress.errors = {}; - progress.showing_confirmation = false; - progress.confirmation_message = ''; - progress.confirmation_details = []; - progress.affected_nodes = 0; - } - - // Add error to action progress and group error messages by nodes. - function addErrorToActionProgress(tab, error, node) { - var progress = $scope.tabs[tab].actionProgress; - progress.completed += 1; - var nodes = progress.errors[error]; - if (angular.isUndefined(nodes)) { - progress.errors[error] = [node]; - } else { - nodes.push(node); - } + // Called when the filtered_items are updated. Checks if the + // filtered_items are empty and if the search still matches the + // previous search. This will reset the search when no nodes match + // the current filter. + function removeEmptyFilter(tab) { + if ( + $scope.tabs[tab].filtered_items.length === 0 && + $scope.tabs[tab].search !== "" && + $scope.tabs[tab].search === $scope.tabs[tab].previous_search + ) { + $scope.tabs[tab].search = ""; + $scope.updateFilters(tab); } + } - // After an action has been performed check if we can leave all nodes - // selected or if an error occured and we should only show the failed - // nodes. - function updateSelectedItems(tab) { - if (!$scope.hasActionsFailed(tab)) { - if (!$scope.hasActionsInProgress(tab)) { - clearAction(tab); - enterViewSelected(tab); - } - return; - } - angular.forEach($scope.tabs[tab].manager.getItems(), - function(node) { - if (node.action_failed === false) { - $scope.tabs[tab].manager.unselectItem(node.system_id); - } - }); + // Update the number of selected items which have an error based on the + // current selected action. + function updateActionErrorCount(tab) { + var i; + $scope.tabs[tab].actionErrorCount = 0; + for (i = 0; i < $scope.tabs[tab].selectedItems.length; i++) { + var supported = $scope.supportsAction( + $scope.tabs[tab].selectedItems[i], + tab + ); + if (!supported) { + $scope.tabs[tab].actionErrorCount += 1; + } + $scope.tabs[tab].selectedItems[i].action_failed = false; } + $scope.updateFailedActionSentence(tab); + } - // Toggles between the current tab. - $scope.toggleTab = function(tab) { - $rootScope.title = $scope.tabs[tab].pagetitle; - $rootScope.page = tab; - $scope.currentpage = tab; - - switch (tab) { - case 'machines': - $scope.osinfo = GeneralManager.getData('osinfo'); - $scope.tabs.machines.takeActionOptions = GeneralManager.getData( - 'machine_actions'); - break; - case 'devices': - $scope.tabs.devices.takeActionOptions = GeneralManager.getData( - 'device_actions'); - break; - case 'controllers': - $scope.tabs.controllers.takeActionOptions = - GeneralManager.getData('rack_controller_actions'); - break; - case 'switches': - // XXX: Which actions should there be? - $scope.tabs.switches.takeActionOptions = GeneralManager.getData( - "machine_actions"); - break; - } - }; - - // Clear the search bar. - $scope.clearSearch = function(tab) { - $scope.tabs[tab].search = ""; - $scope.updateFilters(tab); - }; - - // Mark a node as selected or unselected. - $scope.toggleChecked = function(node, tab) { - if (tab !== 'machines' && tab !== 'switches') { - if ($scope.tabs[tab].manager.isSelected(node.system_id)) { - $scope.tabs[tab].manager.unselectItem(node.system_id); - } else { - $scope.tabs[tab].manager.selectItem(node.system_id); - } - updateAllViewableChecked(tab); - } + // Reset actionProgress on tab to zero. + function resetActionProgress(tab) { + var progress = $scope.tabs[tab].actionProgress; + progress.completed = progress.total = 0; + progress.errors = {}; + progress.showing_confirmation = false; + progress.confirmation_message = ""; + progress.confirmation_details = []; + progress.affected_nodes = 0; + } + + // Add error to action progress and group error messages by nodes. + function addErrorToActionProgress(tab, error, node) { + var progress = $scope.tabs[tab].actionProgress; + progress.completed += 1; + var nodes = progress.errors[error]; + if (angular.isUndefined(nodes)) { + progress.errors[error] = [node]; + } else { + nodes.push(node); + } + } - updateActionErrorCount(tab); - shouldClearAction(tab); - }; + // After an action has been performed check if we can leave all nodes + // selected or if an error occured and we should only show the failed + // nodes. + function updateSelectedItems(tab) { + if (!$scope.hasActionsFailed(tab)) { + if (!$scope.hasActionsInProgress(tab)) { + clearAction(tab); + enterViewSelected(tab); + } + return; + } + angular.forEach($scope.tabs[tab].manager.getItems(), function(node) { + if (node.action_failed === false) { + $scope.tabs[tab].manager.unselectItem(node.system_id); + } + }); + } - // Select all viewable nodes or deselect all viewable nodes. - $scope.toggleCheckAll = function(tab) { - if (tab !== 'machines' && tab !== 'switches') { - if ($scope.tabs[tab].allViewableChecked) { - angular.forEach( - $scope.tabs[tab].filtered_items, function(node) { - $scope.tabs[tab].manager.unselectItem( - node.system_id); - }); - } else { - angular.forEach( - $scope.tabs[tab].filtered_items, function(node) { - $scope.tabs[tab].manager.selectItem( - node.system_id); - }); - } - updateAllViewableChecked(tab); - } - updateActionErrorCount(tab); - shouldClearAction(tab); - }; + // Toggles between the current tab. + $scope.toggleTab = function(tab) { + $rootScope.title = $scope.tabs[tab].pagetitle; + $rootScope.page = tab; + $scope.currentpage = tab; + + switch (tab) { + case "machines": + $scope.osinfo = GeneralManager.getData("osinfo"); + $scope.tabs.machines.takeActionOptions = GeneralManager.getData( + "machine_actions" + ); + break; + case "devices": + $scope.tabs.devices.takeActionOptions = GeneralManager.getData( + "device_actions" + ); + break; + case "controllers": + $scope.tabs.controllers.takeActionOptions = GeneralManager.getData( + "rack_controller_actions" + ); + break; + case "switches": + // XXX: Which actions should there be? + $scope.tabs.switches.takeActionOptions = GeneralManager.getData( + "machine_actions" + ); + break; + } + }; - $scope.updateAvailableActions = function(tab) { - var selectedNodes = $scope.tabs[tab].selectedItems; - var actionOptions = $scope.tabs[tab].takeActionOptions; - - actionOptions.forEach(function(action) { - var count = 0; - selectedNodes.forEach(function(node) { - if (node.actions.indexOf(action.name) > -1) { - count += 1; - } - action.available = count; - }); - }); + // Clear the search bar. + $scope.clearSearch = function(tab) { + $scope.tabs[tab].search = ""; + $scope.updateFilters(tab); + }; + + // Mark a node as selected or unselected. + $scope.toggleChecked = function(node, tab) { + if (tab !== "machines" && tab !== "switches") { + if ($scope.tabs[tab].manager.isSelected(node.system_id)) { + $scope.tabs[tab].manager.unselectItem(node.system_id); + } else { + $scope.tabs[tab].manager.selectItem(node.system_id); + } + updateAllViewableChecked(tab); } - $scope.unselectImpossibleNodes = function(tab) { - var selectedNodes = $scope.tabs[tab].selectedItems; - var actionOption = $scope.tabs[tab].actionOption; - var nodesToUnselect = []; - - selectedNodes.forEach(function(node) { - if (node.actions.indexOf(actionOption.name) === -1) { - nodesToUnselect.push(node.system_id); - } + updateActionErrorCount(tab); + shouldClearAction(tab); + }; + + // Select all viewable nodes or deselect all viewable nodes. + $scope.toggleCheckAll = function(tab) { + if (tab !== "machines" && tab !== "switches") { + if ($scope.tabs[tab].allViewableChecked) { + angular.forEach($scope.tabs[tab].filtered_items, function(node) { + $scope.tabs[tab].manager.unselectItem(node.system_id); }); - - // Extra forEach loop to prevent iterating over a mutating array - nodesToUnselect.forEach(function(nodeId) { - $scope.tabs[tab].manager.unselectItem(nodeId); - }) + } else { + angular.forEach($scope.tabs[tab].filtered_items, function(node) { + $scope.tabs[tab].manager.selectItem(node.system_id); + }); + } + updateAllViewableChecked(tab); } - - $scope.onNodeListingChanged = function(nodes, tab) { - if (nodes.length === 0 && - $scope.tabs[tab].search !== "" && - $scope.tabs[tab].search === $scope.tabs[tab].previous_search) { - $scope.tabs[tab].search = ""; - $scope.updateFilters(tab); + updateActionErrorCount(tab); + shouldClearAction(tab); + }; + + $scope.updateAvailableActions = function(tab) { + var selectedNodes = $scope.tabs[tab].selectedItems; + var actionOptions = $scope.tabs[tab].takeActionOptions; + + actionOptions.forEach(function(action) { + var count = 0; + selectedNodes.forEach(function(node) { + if (node.actions.indexOf(action.name) > -1) { + count += 1; } - }; - - // When the filtered nodes change update if all check buttons - // should be checked or not. - $scope.$watchCollection("tabs.devices.filtered_items", function() { - updateAllViewableChecked("devices"); - removeEmptyFilter("devices"); - }); - $scope.$watchCollection("tabs.controllers.filtered_items", function() { - updateAllViewableChecked("controllers"); - removeEmptyFilter("controllers"); + action.available = count; + }); }); + }; - // Shows the current selection. - $scope.showSelected = function(tab) { - enterViewSelected(tab); - $scope.updateFilters(tab); - }; + $scope.unselectImpossibleNodes = tab => { + const { actionOption, manager, selectedItems } = $scope.tabs[tab]; - // Adds or removes a filter to the search. - $scope.toggleFilter = function(type, value, tab) { - // Don't allow a filter to be changed when an action is - // in progress. - if (angular.isObject($scope.tabs[tab].actionOption)) { - return; - } - $scope.tabs[tab].filters = SearchService.toggleFilter( - $scope.tabs[tab].filters, type, value, true); - $scope.tabs[tab].search = SearchService.filtersToString( - $scope.tabs[tab].filters); - }; + const nodesToUnselect = selectedItems.reduce((acc, node) => { + if (!node.actions.includes(actionOption.name)) { + acc.push(node); + } + return acc; + }, []); - // Return True if the filter is active. - $scope.isFilterActive = function(type, value, tab) { - return SearchService.isFilterActive( - $scope.tabs[tab].filters, type, value, true); - }; - - // Update the filters object when the search bar is updated. - $scope.updateFilters = function(tab) { - var filters = SearchService.getCurrentFilters( - $scope.tabs[tab].search); - if (filters === null) { - $scope.tabs[tab].filters = SearchService.getEmptyFilter(); - $scope.tabs[tab].searchValid = false; - } else { - $scope.tabs[tab].filters = filters; - $scope.tabs[tab].searchValid = true; - } - shouldClearAction(tab); - }; - - // Sorts the table by predicate. - $scope.sortTable = function(predicate, tab) { - $scope.tabs[tab].predicate = predicate; - $scope.tabs[tab].reverse = !$scope.tabs[tab].reverse; - }; + nodesToUnselect.forEach(node => { + manager.unselectItem(node.system_id); + }); - // Sets the viewable column or sorts. - $scope.selectColumnOrSort = function(predicate, tab) { - if ($scope.tabs[tab].column !== predicate) { - $scope.tabs[tab].column = predicate; - } else { - $scope.sortTable(predicate, tab); - } - }; + // 07/05/2019 Caleb: Force refresh of filtered machines. + // Remove when machines table rewritten with one-way binding. + $scope.tabs[tab].search = "in:(selected)"; + }; + + $scope.onNodeListingChanged = function(nodes, tab) { + if ( + nodes.length === 0 && + $scope.tabs[tab].search !== "" && + $scope.tabs[tab].search === $scope.tabs[tab].previous_search + ) { + $scope.tabs[tab].search = ""; + $scope.updateFilters(tab); + } + }; - // Return True if the node supports the action. - $scope.supportsAction = function(node, tab) { - if (!$scope.tabs[tab].actionOption) { - return true; - } - return node.actions.indexOf( - $scope.tabs[tab].actionOption.name) >= 0; - }; + // When the filtered nodes change update if all check buttons + // should be checked or not. + $scope.$watchCollection("tabs.devices.filtered_items", function() { + updateAllViewableChecked("devices"); + removeEmptyFilter("devices"); + }); + $scope.$watchCollection("tabs.controllers.filtered_items", function() { + updateAllViewableChecked("controllers"); + removeEmptyFilter("controllers"); + }); + + // Shows the current selection. + $scope.showSelected = function(tab) { + enterViewSelected(tab); + $scope.updateFilters(tab); + }; + + // Adds or removes a filter to the search. + $scope.toggleFilter = function(type, value, tab) { + // Don't allow a filter to be changed when an action is + // in progress. + if (angular.isObject($scope.tabs[tab].actionOption)) { + return; + } + $scope.tabs[tab].filters = SearchService.toggleFilter( + $scope.tabs[tab].filters, + type, + value, + true + ); + $scope.tabs[tab].search = SearchService.filtersToString( + $scope.tabs[tab].filters + ); + }; + + // Return True if the filter is active. + $scope.isFilterActive = function(type, value, tab) { + return SearchService.isFilterActive( + $scope.tabs[tab].filters, + type, + value, + true + ); + }; + + // Update the filters object when the search bar is updated. + $scope.updateFilters = function(tab) { + var filters = SearchService.getCurrentFilters($scope.tabs[tab].search); + if (filters === null) { + $scope.tabs[tab].filters = SearchService.getEmptyFilter(); + $scope.tabs[tab].searchValid = false; + } else { + $scope.tabs[tab].filters = filters; + $scope.tabs[tab].searchValid = true; + } + shouldClearAction(tab); + }; - // Called when the action option gets changed. - $scope.actionOptionSelected = function(tab) { - updateActionErrorCount(tab); - enterViewSelected(tab); + // Sorts the table by predicate. + $scope.sortTable = function(predicate, tab) { + $scope.tabs[tab].predicate = predicate; + $scope.tabs[tab].reverse = !$scope.tabs[tab].reverse; + }; + + // Sets the viewable column or sorts. + $scope.selectColumnOrSort = function(predicate, tab) { + if ($scope.tabs[tab].column !== predicate) { + $scope.tabs[tab].column = predicate; + } else { + $scope.sortTable(predicate, tab); + } + }; - // Hide the add hardware/device section. - if (tab === 'machines') { - if (angular.isObject($scope.addHardwareScope)) { - $scope.addHardwareScope.hide(); - } - } else if (tab === 'devices') { - if (angular.isObject($scope.addDeviceScope)) { - $scope.addDeviceScope.hide(); - } - } - }; + // Return True if the node supports the action. + $scope.supportsAction = function(node, tab) { + if (!$scope.tabs[tab].actionOption) { + return true; + } + return node.actions.indexOf($scope.tabs[tab].actionOption.name) >= 0; + }; - // Return True if there is an action error. - $scope.isActionError = function(tab) { - if (angular.isObject($scope.tabs[tab].actionOption) && - $scope.tabs[tab].actionOption.name === "deploy" && - $scope.tabs[tab].actionErrorCount === 0 && - ($scope.osinfo.osystems.length === 0 || - UsersManager.getSSHKeyCount() === 0)) { - return true; + $scope.getFailedTests = tabName => { + const tab = $scope.tabs[tabName]; + const nodes = tab.selectedItems; + tab.failedTests = []; + tab.loadingFailedTests = true; + MachinesManager.getLatestFailedTests(nodes).then( + tests => { + tab.failedTests = tests; + tab.loadingFailedTests = false; + }, + error => { + const authUser = UsersManager.getAuthUser(); + if (angular.isObject(authUser)) { + NotificationsManager.createItem({ + message: `Unable to load tests: ${error}`, + category: "error", + user: authUser.id + }); + } else { + $log.error(error); } - return $scope.tabs[tab].actionErrorCount !== 0; - }; + } + ); + }; + + $scope.getFailedTestCount = tabName => { + const tab = $scope.tabs[tabName]; + const nodes = tab.selectedItems; + const tests = tab.failedTests; + return nodes.reduce((acc, node) => { + if (tests[node.system_id]) { + acc += tests[node.system_id].length; + } + return acc; + }, 0); + }; + + // Called when the action option gets changed. + $scope.actionOptionSelected = function(tab) { + updateActionErrorCount(tab); + enterViewSelected(tab); + + // Hide the add hardware/device section. + if (tab === "machines") { + if (angular.isObject($scope.addHardwareScope)) { + $scope.addHardwareScope.hide(); + } + } else if (tab === "devices") { + if (angular.isObject($scope.addDeviceScope)) { + $scope.addDeviceScope.hide(); + } + } - // Return True if unable to deploy because of missing images. - $scope.isDeployError = function(tab) { - if ($scope.tabs[tab].actionErrorCount !== 0) { - return false; - } - if (angular.isObject($scope.tabs[tab].actionOption) && - $scope.tabs[tab].actionOption.name === "deploy" && - $scope.osinfo.osystems.length === 0) { - return true; - } - return false; - }; + if ( + $scope.tabs[tab].actionOption && + $scope.tabs[tab].actionOption.name === "override-failed-testing" + ) { + $scope.getFailedTests(tab); + } + }; - // Return True if unable to deploy because of missing ssh keys. - $scope.isSSHKeyError = function(tab) { - if ($scope.tabs[tab].actionErrorCount !== 0) { - return false; - } - if (angular.isObject($scope.tabs[tab].actionOption) && - $scope.tabs[tab].actionOption.name === "deploy" && - UsersManager.getSSHKeyCount() === 0) { - return true; - } - return false; - }; + // Return True if there is an action error. + $scope.isActionError = function(tab) { + if ( + angular.isObject($scope.tabs[tab].actionOption) && + $scope.tabs[tab].actionOption.name === "deploy" && + $scope.tabs[tab].actionErrorCount === 0 && + $scope.osinfo.osystems.length === 0 + ) { + return true; + } + return $scope.tabs[tab].actionErrorCount !== 0; + }; - // Called when the current action is cancelled. - $scope.actionCancel = function(tab) { - resetActionProgress(tab); - leaveViewSelected(tab); - $scope.tabs[tab].actionOption = null; - }; + // Return True if unable to deploy because of missing images. + $scope.isDeployError = function(tab) { + if ($scope.tabs[tab].actionErrorCount !== 0) { + return false; + } + if ( + angular.isObject($scope.tabs[tab].actionOption) && + $scope.tabs[tab].actionOption.name === "deploy" && + $scope.osinfo.osystems.length === 0 + ) { + return true; + } + return false; + }; - // Perform the action on all nodes. - $scope.actionGo = function(tabName) { - var tab = $scope.tabs[tabName]; - var extra = {}; - var deferred = $q.defer(); - // Actions can use preAction is to execute before the action - // is exectued on all the nodes. We initialize it with a - // promise so that later we can always treat it as a - // promise, no matter if something is to be executed or not. - var preAction = deferred.promise; - deferred.resolve(); - var i, j; - - // Set deploy parameters if a deploy or set zone action. - if (tab.actionOption.name === "deploy" && - angular.isString(tab.osSelection.osystem) && - angular.isString(tab.osSelection.release)) { - - // Set extra. UI side the release is structured os/release, but - // when it is sent over the websocket only the "release" is - // sent. - extra.osystem = tab.osSelection.osystem; - var release = tab.osSelection.release; - release = release.split("/"); - release = release[release.length - 1]; - extra.distro_series = release; - // hwe_kernel is optional so only include it if its specified - if (angular.isString(tab.osSelection.hwe_kernel) && - (tab.osSelection.hwe_kernel.indexOf('hwe-') >= 0 || - tab.osSelection.hwe_kernel.indexOf('ga-') >= 0)) { - extra.hwe_kernel = tab.osSelection.hwe_kernel; - } - let installKVM = tab.deployOptions.installKVM; - // KVM pod deployment requires bionic. - if (installKVM) { - extra.osystem = "ubuntu"; - extra.distro_series = "bionic"; - } - extra.install_kvm = installKVM; - } else if (tab.actionOption.name === "set-zone" && - angular.isNumber(tab.zoneSelection.id)) { - // Set the zone parameter. - extra.zone_id = tab.zoneSelection.id; - } else if (tab.actionOption.name === "set-pool") { - if ((tab.poolAction === 'create-pool') && - (tab.newPool.name !== undefined)) { - // Create the pool and set the action options with - // the new pool id. - preAction = ResourcePoolsManager.createItem( - { name: tab.newPool.name }).then(function(newPool) { - extra.pool_id = newPool.id - }); - } else if (angular.isNumber(tab.poolSelection.id)) { - // Set the pool parameter. - extra.pool_id = tab.poolSelection.id; - } - } else if (tab.actionOption.name === "commission") { - // Set the commission options. - extra.enable_ssh = tab.commissionOptions.enableSSH; - extra.skip_bmc_config = tab.commissionOptions.skipBMCConfig; - extra.skip_networking = tab.commissionOptions.skipNetworking; - extra.skip_storage = tab.commissionOptions.skipStorage; - extra.commissioning_scripts = []; - for (i = 0; i < tab.commissioningSelection.length; i++) { - extra.commissioning_scripts.push( - tab.commissioningSelection[i].id); - } - if (tab.commissionOptions.updateFirmware) { - extra.commissioning_scripts.push('update_firmware') - } - if (tab.commissionOptions.configureHBA) { - extra.commissioning_scripts.push('configure_hba') - } - if (extra.commissioning_scripts.length === 0) { - // Tell the region not to run any custom commissioning - // scripts. - extra.commissioning_scripts.push('none'); - } - extra.testing_scripts = []; - for (i = 0; i < tab.testSelection.length; i++) { - extra.testing_scripts.push( - tab.testSelection[i].id); - } - if (extra.testing_scripts.length === 0) { - // Tell the region not to run any tests. - extra.testing_scripts.push('none'); - } - } else if (tab.actionOption.name === "test") { - if (!tab.actionProgress.showing_confirmation) { - var progress = tab.actionProgress; - for (i = 0; i < tab.selectedItems.length; i++) { - if (tab.selectedItems[i].status_code === 6) { - progress.affected_nodes++; - } - } - if (progress.affected_nodes != 0) { - progress.confirmation_message = - progress.affected_nodes + " of " - + tab.selectedItems.length + " " + $scope.page - + " are in a deployed state."; - progress.showing_confirmation = true; - return; - } - } - // Set the test options. - extra.enable_ssh = tab.commissionOptions.enableSSH; - extra.testing_scripts = []; - for (i = 0; i < tab.testSelection.length; i++) { - extra.testing_scripts.push( - tab.testSelection[i].id); - } - if (extra.testing_scripts.length === 0) { - // Tell the region not to run any tests. - extra.testing_scripts.push('none'); - } - } else if (tab.actionOption.name === "release") { - // Set the release options. - extra.erase = tab.releaseOptions.erase; - extra.secure_erase = tab.releaseOptions.secureErase; - extra.quick_erase = tab.releaseOptions.quickErase; - } else if (tab.actionOption.name === "delete" && - tabName === "controllers" && - !tab.actionProgress.showing_confirmation) { - for (i = 0; i < tab.selectedItems.length; i++) { - var controller = tab.selectedItems[i]; - for (j = 0; j < $scope.vlans.length; j++) { - var vlan = $scope.vlans[j]; - if (vlan.primary_rack === controller.system_id) { - tab.actionProgress.confirmation_details.push( - controller.fqdn + - " is the primary rack controller for " + - vlan.name); - tab.actionProgress.affected_nodes++; - } - if (vlan.secondary_rack === controller.system_id) { - tab.actionProgress.confirmation_details.push( - controller.fqdn + - " is the secondary rack controller for " + - vlan.name); - tab.actionProgress.affected_nodes++; - } - } - } - if (tab.actionProgress.affected_nodes != 0) { - if (tab.actionProgress.affected_nodes === 1) { - tab.actionProgress.confirmation_message = - "1 controller will be deleted."; - } else { - tab.actionProgress.confirmation_message = - tab.actionProgress.affected_nodes + - " controllers will be deleted."; - } - tab.actionProgress.showing_confirmation = true; - return; - } - } else if (tab.actionOption.name === "tag") { - extra.tags = $scope.tags.map(function(tag) { - return tag.text; - }); + // Return True if deploy warning should be shown because of missing ssh keys. + $scope.isSSHKeyWarning = function(tab) { + if ($scope.tabs[tab].actionErrorCount !== 0) { + return false; + } + if ( + angular.isObject($scope.tabs[tab].actionOption) && + $scope.tabs[tab].actionOption.name === "deploy" && + UsersManager.getSSHKeyCount() === 0 + ) { + return true; + } + return false; + }; - $scope.tags = []; + // Called when the current action is cancelled. + $scope.actionCancel = function(tab) { + resetActionProgress(tab); + leaveViewSelected(tab); + $scope.tabs[tab].actionOption = null; + $scope.tabs[tab].suppressFailedTestsChecked = false; + }; + + // Perform the action on all nodes. + $scope.actionGo = function(tabName) { + var tab = $scope.tabs[tabName]; + var extra = {}; + var deferred = $q.defer(); + // Actions can use preAction is to execute before the action + // is exectued on all the nodes. We initialize it with a + // promise so that later we can always treat it as a + // promise, no matter if something is to be executed or not. + var preAction = deferred.promise; + deferred.resolve(); + var i, j; + + // Set deploy parameters if a deploy or set zone action. + if ( + tab.actionOption.name === "deploy" && + angular.isString(tab.osSelection.osystem) && + angular.isString(tab.osSelection.release) + ) { + // Set extra. UI side the release is structured os/release, but + // when it is sent over the websocket only the "release" is + // sent. + extra.osystem = tab.osSelection.osystem; + var release = tab.osSelection.release; + release = release.split("/"); + release = release[release.length - 1]; + extra.distro_series = release; + // hwe_kernel is optional so only include it if its specified + if ( + angular.isString(tab.osSelection.hwe_kernel) && + (tab.osSelection.hwe_kernel.indexOf("hwe-") >= 0 || + tab.osSelection.hwe_kernel.indexOf("ga-") >= 0) + ) { + extra.hwe_kernel = tab.osSelection.hwe_kernel; + } + let installKVM = tab.deployOptions.installKVM; + // KVM pod deployment requires bionic. + if (installKVM) { + extra.osystem = "ubuntu"; + extra.distro_series = "bionic"; + } + extra.install_kvm = installKVM; + } else if ( + tab.actionOption.name === "set-zone" && + angular.isNumber(tab.zoneSelection.id) + ) { + // Set the zone parameter. + extra.zone_id = tab.zoneSelection.id; + } else if (tab.actionOption.name === "set-pool") { + if ( + tab.poolAction === "create-pool" && + angular.isDefined(tab.newPool.name) + ) { + // Create the pool and set the action options with + // the new pool id. + preAction = ResourcePoolsManager.createItem({ + name: tab.newPool.name + }).then(function(newPool) { + extra.pool_id = newPool.id; + }); + } else if (angular.isNumber(tab.poolSelection.id)) { + // Set the pool parameter. + extra.pool_id = tab.poolSelection.id; + } + } else if (tab.actionOption.name === "commission") { + // Set the commission options. + extra.enable_ssh = tab.commissionOptions.enableSSH; + extra.skip_bmc_config = tab.commissionOptions.skipBMCConfig; + extra.skip_networking = tab.commissionOptions.skipNetworking; + extra.skip_storage = tab.commissionOptions.skipStorage; + extra.commissioning_scripts = []; + for (i = 0; i < tab.commissioningSelection.length; i++) { + extra.commissioning_scripts.push(tab.commissioningSelection[i].id); + } + if (tab.commissionOptions.updateFirmware) { + extra.commissioning_scripts.push("update_firmware"); + } + if (tab.commissionOptions.configureHBA) { + extra.commissioning_scripts.push("configure_hba"); + } + if (extra.commissioning_scripts.length === 0) { + // Tell the region not to run any custom commissioning + // scripts. + extra.commissioning_scripts.push("none"); + } + extra.testing_scripts = []; + for (i = 0; i < tab.testSelection.length; i++) { + extra.testing_scripts.push(tab.testSelection[i].id); + } + if (extra.testing_scripts.length === 0) { + // Tell the region not to run any tests. + extra.testing_scripts.push("none"); + } + } else if (tab.actionOption.name === "test") { + if (!tab.actionProgress.showing_confirmation) { + var progress = tab.actionProgress; + for (i = 0; i < tab.selectedItems.length; i++) { + if (tab.selectedItems[i].status_code === 6) { + progress.affected_nodes++; + } + } + if (progress.affected_nodes != 0) { + progress.confirmation_message = + progress.affected_nodes + + " of " + + tab.selectedItems.length + + " " + + $scope.page + + " are in a deployed state."; + progress.showing_confirmation = true; + return; + } + } + // Set the test options. + extra.enable_ssh = tab.commissionOptions.enableSSH; + extra.testing_scripts = []; + for (i = 0; i < tab.testSelection.length; i++) { + extra.testing_scripts.push(tab.testSelection[i].id); + } + if (extra.testing_scripts.length === 0) { + // Tell the region not to run any tests. + extra.testing_scripts.push("none"); + } + } else if (tab.actionOption.name === "release") { + // Set the release options. + extra.erase = tab.releaseOptions.erase; + extra.secure_erase = tab.releaseOptions.secureErase; + extra.quick_erase = tab.releaseOptions.quickErase; + } else if ( + tab.actionOption.name === "delete" && + tabName === "controllers" && + !tab.actionProgress.showing_confirmation + ) { + for (i = 0; i < tab.selectedItems.length; i++) { + var controller = tab.selectedItems[i]; + for (j = 0; j < $scope.vlans.length; j++) { + var vlan = $scope.vlans[j]; + if (vlan.primary_rack === controller.system_id) { + tab.actionProgress.confirmation_details.push( + controller.fqdn + + " is the primary rack controller for " + + vlan.name + ); + tab.actionProgress.affected_nodes++; + } + if (vlan.secondary_rack === controller.system_id) { + tab.actionProgress.confirmation_details.push( + controller.fqdn + + " is the secondary rack controller for " + + vlan.name + ); + tab.actionProgress.affected_nodes++; + } + } + } + if (tab.actionProgress.affected_nodes != 0) { + if (tab.actionProgress.affected_nodes === 1) { + tab.actionProgress.confirmation_message = + "1 controller will be deleted."; + } else { + tab.actionProgress.confirmation_message = + tab.actionProgress.affected_nodes + " controllers will be deleted."; } - - preAction.then( - function() { - // Setup actionProgress. - resetActionProgress(tabName); - tab.actionProgress.total = tab.selectedItems.length; - // Perform the action on all selected items. - angular.forEach(tab.selectedItems, function(node) { - tab.manager.performAction( - node, tab.actionOption.name, - extra).then(function() { - tab.actionProgress.completed += 1; - node.action_failed = false; - updateSelectedItems(tabName); - }, function(error) { - addErrorToActionProgress(tabName, error, node); - node.action_failed = true; - updateSelectedItems(tabName); - }); - }); - }, - function(error) { - addErrorToActionProgress(tabName, error); - }); - }; - - // Returns true when actions are being performed. - $scope.hasActionsInProgress = function(tab) { - var progress = $scope.tabs[tab].actionProgress; - return progress.total > 0 && progress.completed !== progress.total; - }; - - // Returns true if any of the actions have failed. - $scope.hasActionsFailed = function(tab) { - return Object.keys( - $scope.tabs[tab].actionProgress.errors).length > 0; - }; - - // Called to when the addHardwareOption has changed. - $scope.addHardwareOptionChanged = function() { - if ($scope.addHardwareOption) { - $scope.addHardwareScope.show( - $scope.addHardwareOption.name); + tab.actionProgress.showing_confirmation = true; + return; + } + } else if (tab.actionOption.name === "tag") { + extra.tags = $scope.tags.map(function(tag) { + return tag.text; + }); + + $scope.tags = []; + } else if ( + tab.actionOption.name === "override-failed-testing" && + tab.suppressFailedTestsChecked + ) { + const nodes = tab.selectedItems; + const tests = tab.failedTests; + nodes.forEach(node => { + if (tests[node.system_id]) { + tab.manager.suppressTests(node, tests[node.system_id]); } - }; - - // Called when the add device button is pressed. - $scope.addDevice = function() { - $scope.addDeviceScope.show(); - }; - - // Called when the cancel add device button is pressed. - $scope.cancelAddDevice = function() { - $scope.addDeviceScope.cancel(); - }; - - // Get the display text for device ip assignment type. - $scope.getDeviceIPAssignment = function(ipAssignment) { - return DEVICE_IP_ASSIGNMENT[ipAssignment]; - }; - - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Return true if the user can create a resource pool. - $scope.canAddMachine = function() { - return UsersManager.hasGlobalPermission('machine_create'); - }; - - // Return true if the user can create a resource pool. - $scope.canCreateResourcePool = function() { - return UsersManager.hasGlobalPermission('resource_pool_create'); - }; + }); + $scope.tabs.machines.suppressFailedTestsChecked = false; + } - // Return true if the actions column should be shown. - $scope.showResourcePoolActions = function() { - for (var i = 0; i < $scope.pools.length; i++) { - if ($scope.pools[i].permissions && - $scope.pools[i].permissions.length > 0) { - return true; - } - } - return false; - }; + preAction.then( + function() { + // Setup actionProgress. + resetActionProgress(tabName); + tab.actionProgress.total = tab.selectedItems.length; + // Perform the action on all selected items. + angular.forEach(tab.selectedItems, function(node) { + tab.manager + .performAction(node, tab.actionOption.name, extra) + .then( + function() { + tab.actionProgress.completed += 1; + node.action_failed = false; + }, + function(error) { + addErrorToActionProgress(tabName, error, node); + node.action_failed = true; + } + ) + .finally(function() { + updateSelectedItems(tabName); + }); + }); + }, + function(error) { + addErrorToActionProgress(tabName, error); + } + ); + }; + + // Returns true when actions are being performed. + $scope.hasActionsInProgress = function(tab) { + var progress = $scope.tabs[tab].actionProgress; + return progress.total > 0 && progress.completed !== progress.total; + }; + + // Returns true if any of the actions have failed. + $scope.hasActionsFailed = function(tab) { + return Object.keys($scope.tabs[tab].actionProgress.errors).length > 0; + }; + + // Called to when the addHardwareOption has changed. + $scope.addHardwareOptionChanged = function() { + if ($scope.addHardwareOption) { + $scope.addHardwareScope.show($scope.addHardwareOption.name); + } + }; - // Return true if user can edit resource pool. - $scope.canEditResourcePool = function(pool) { - if (pool.permissions && pool.permissions.indexOf('edit') !== -1) { - return true; - } - return false; - }; + // Called when the add device button is pressed. + $scope.addDevice = function() { + $scope.addDeviceScope.show(); + }; + + // Called when the cancel add device button is pressed. + $scope.cancelAddDevice = function() { + $scope.addDeviceScope.cancel(); + }; + + // Get the display text for device ip assignment type. + $scope.getDeviceIPAssignment = function(ipAssignment) { + return DEVICE_IP_ASSIGNMENT[ipAssignment]; + }; + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Return true if the user can create a resource pool. + $scope.canAddMachine = function() { + return UsersManager.hasGlobalPermission("machine_create"); + }; + + // Return true if the user can create a resource pool. + $scope.canCreateResourcePool = function() { + return UsersManager.hasGlobalPermission("resource_pool_create"); + }; + + // Return true if the actions column should be shown. + $scope.showResourcePoolActions = function() { + for (var i = 0; i < $scope.pools.length; i++) { + if ( + $scope.pools[i].permissions && + $scope.pools[i].permissions.length > 0 + ) { + return true; + } + } + return false; + }; - // Return true if user can delete resource pool. - $scope.canDeleteResourcePool = function() { - return UsersManager.hasGlobalPermission('resource_pool_delete'); - }; + // Return true if user can edit resource pool. + $scope.canEditResourcePool = function(pool) { + if (pool.permissions && pool.permissions.indexOf("edit") !== -1) { + return true; + } + return false; + }; - // Return true if custom commissioning scripts exist. - $scope.hasCustomCommissioningScripts = function() { - var i; - for (i = 0; i < $scope.scripts.length; i++) { - if ($scope.scripts[i].script_type === 0) { - return true; - } - } - return false; - }; + // Return true if user can delete resource pool. + $scope.canDeleteResourcePool = function() { + return UsersManager.hasGlobalPermission("resource_pool_delete"); + }; + + // Return true if custom commissioning scripts exist. + $scope.hasCustomCommissioningScripts = function() { + var i; + for (i = 0; i < $scope.scripts.length; i++) { + if ($scope.scripts[i].script_type === 0) { + return true; + } + } + return false; + }; - // Reload osinfo when the page reloads - $scope.$on("$routeUpdate", function() { - GeneralManager.loadItems(["osinfo"]); - }); + $scope.updateFailedActionSentence = tab => { + const { actionOption, actionErrorCount } = $scope.tabs[tab]; - // Switch to the specified tab, if specified. - angular.forEach( - ["machines", "pools", "devices", "controllers", "switches"], - function(node_type) { - if ($location.path().indexOf("/" + node_type) !== -1) { - $scope.toggleTab(node_type); - } - }); + // e.g. "5 machines" or "1 controller" + const nodeString = + actionErrorCount > 1 + ? `${actionErrorCount} ${tab}` + : `${actionErrorCount} ${tab.slice(0, -1)}`; + let sentence = `Action cannot be performed on ${nodeString}.`; + + if (actionOption && actionOption.name) { + switch (actionOption.name) { + case "exit-rescue-mode": + sentence = `${nodeString} cannot exit rescue mode.`; + break; + case "lock": + sentence = `${nodeString} cannot be locked.`; + break; + case "override-failed-testing": + sentence = `Cannot override failed tests on ${nodeString}.`; + break; + case "rescue-mode": + sentence = `${nodeString} cannot be put in rescue mode.`; + break; + case "set-pool": + sentence = `Cannot set pool of ${nodeString}.`; + break; + case "set-zone": + sentence = `Cannot set zone of ${nodeString}.`; + break; + case "unlock": + sentence = `${nodeString} cannot be unlocked.`; + break; + default: + sentence = `${nodeString} cannot be ${actionOption.sentence}.`; + } + } - // The ScriptsManager is only needed for selecting testing or - // commissioning scripts. - var page_managers = [$scope.tabs[$scope.currentpage].manager]; - if ($scope.currentpage === "machines" || - $scope.currentpage === "controllers") { - page_managers.push(ScriptsManager); - } - if ($scope.currentpage === "controllers") { - // VLANsManager is used during controller delete to see if its - // managing a VLAN when confirming delete. - page_managers.push(VLANsManager); - } - - // Load the required managers for this controller. The ServicesManager - // is required by the maasControllerStatus directive that is used - // in the partial for this controller. - ManagerHelperService.loadManagers($scope, page_managers.concat([ - GeneralManager, ZonesManager, UsersManager, ResourcePoolsManager, - ServicesManager, TagsManager])) - .then(function() { - $scope.loading = false; - }); + $scope.failedActionSentence = sentence; + }; + $scope.getHardwareTestErrorText = function(error, tab) { + var selectedItemsCount = $scope.tabs[tab].selectedItems.length; - // Stop polling and save the current filter when the scope is destroyed. - $scope.$on("$destroy", function() { - $interval.cancel($scope.statusPoll); - SearchService.storeFilters( - "machines", $scope.tabs.machines.filters); - SearchService.storeFilters("devices", $scope.tabs.devices.filters); - SearchService.storeFilters( - "controllers", $scope.tabs.controllers.filters); - SearchService.storeFilters( - "switches", $scope.tabs.switches.filters); - }); + if (error === "Unable to run destructive test while deployed!") { + var singular = false; + var machinesText = ""; + + if (selectedItemsCount === 1) { + var singular = true; + } + + if (singular) { + machinesText += "1 machine"; + } else { + machinesText += selectedItemsCount + " machines"; + } + + return ( + machinesText + + " cannot run hardware testing. The selected hardware tests contain" + + " one or more destructive tests. Destructive tests cannot run on" + + " deployed machines." + ); + } else { + return error; + } + }; - // Restore the filters if any saved. - var machinesFilter = SearchService.retrieveFilters("machines"); - if (angular.isObject(machinesFilter)) { - $scope.tabs.machines.search = SearchService.filtersToString( - machinesFilter); - $scope.updateFilters("machines"); - } - var devicesFilter = SearchService.retrieveFilters("devices"); - if (angular.isObject(devicesFilter)) { - $scope.tabs.devices.search = SearchService.filtersToString( - devicesFilter); - $scope.updateFilters("devices"); - } - var controllersFilter = SearchService.retrieveFilters("controllers"); - if (angular.isObject(controllersFilter)) { - $scope.tabs.controllers.search = SearchService.filtersToString( - controllersFilter); - $scope.updateFilters("controllers"); - } - var switchesFilter = SearchService.retrieveFilters("switches"); - if (angular.isObject(switchesFilter)) { - $scope.tabs.switches.search = SearchService.filtersToString( - switchesFilter); - $scope.updateFilters("switches"); - } - - // Set the query if the present in $routeParams. - var query = $routeParams.query; - if (angular.isString(query)) { - $scope.tabs[$scope.currentpage].search = query; - $scope.updateFilters($scope.currentpage); + // Reload osinfo when the page reloads + $scope.$on("$routeUpdate", function() { + GeneralManager.loadItems(["osinfo"]); + }); + + // Switch to the specified tab, if specified. + angular.forEach( + ["machines", "pools", "devices", "controllers", "switches"], + function(node_type) { + if ($location.path().indexOf("/" + node_type) !== -1) { + $scope.toggleTab(node_type); + } } + ); + + // The ScriptsManager is only needed for selecting testing or + // commissioning scripts. + var page_managers = [$scope.tabs[$scope.currentpage].manager]; + if ( + $scope.currentpage === "machines" || + $scope.currentpage === "controllers" + ) { + page_managers.push(ScriptsManager); + } + if ($scope.currentpage === "controllers") { + // VLANsManager is used during controller delete to see if its + // managing a VLAN when confirming delete. + page_managers.push(VLANsManager); + } + + // Load the required managers for this controller. The ServicesManager + // is required by the maasControllerStatus directive that is used + // in the partial for this controller. + ManagerHelperService.loadManagers( + $scope, + page_managers.concat([ + GeneralManager, + ZonesManager, + UsersManager, + ResourcePoolsManager, + ServicesManager, + TagsManager + ]) + ).then(function() { + $scope.loading = false; + }); + + // Stop polling and save the current filter when the scope is destroyed. + $scope.$on("$destroy", function() { + $interval.cancel($scope.statusPoll); + SearchService.storeFilters("machines", $scope.tabs.machines.filters); + SearchService.storeFilters("devices", $scope.tabs.devices.filters); + SearchService.storeFilters("controllers", $scope.tabs.controllers.filters); + SearchService.storeFilters("switches", $scope.tabs.switches.filters); + }); + + // Restore the filters if any saved. + var machinesFilter = SearchService.retrieveFilters("machines"); + if (angular.isObject(machinesFilter)) { + $scope.tabs.machines.search = SearchService.filtersToString(machinesFilter); + $scope.updateFilters("machines"); + } + var devicesFilter = SearchService.retrieveFilters("devices"); + if (angular.isObject(devicesFilter)) { + $scope.tabs.devices.search = SearchService.filtersToString(devicesFilter); + $scope.updateFilters("devices"); + } + var controllersFilter = SearchService.retrieveFilters("controllers"); + if (angular.isObject(controllersFilter)) { + $scope.tabs.controllers.search = SearchService.filtersToString( + controllersFilter + ); + $scope.updateFilters("controllers"); + } + var switchesFilter = SearchService.retrieveFilters("switches"); + if (angular.isObject(switchesFilter)) { + $scope.tabs.switches.search = SearchService.filtersToString(switchesFilter); + $scope.updateFilters("switches"); + } + + // Set the query if the present in $routeParams. + var query = $routeParams.query; + if (angular.isString(query)) { + $scope.tabs[$scope.currentpage].search = query; + $scope.updateFilters($scope.currentpage); + } } export default NodesListController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/pod_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/pod_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/pod_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/pod_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -1,4 +1,4 @@ -/* Copyright 2017-2018 Canonical Ltd. This software is licensed under the +/* Copyright 2017-2019 Canonical Ltd. This software is licensed under the * GNU Affero General Public License version 3 (see the file LICENSE). * * MAAS Pod Details Controller @@ -6,701 +6,743 @@ /* @ngInject */ function PodDetailsController( - $scope, $rootScope, $location, $routeParams, $filter, - PodsManager, GeneralManager, UsersManager, DomainsManager, - ZonesManager, MachinesManager, ManagerHelperService, ErrorService, - ResourcePoolsManager, SubnetsManager, VLANsManager, FabricsManager, - SpacesManager, ValidationService) { - - // Set title and page. - $rootScope.title = "Loading..."; - $rootScope.page = "pods"; - - // Initial values. - $scope.loaded = false; - $scope.pod = null; - $scope.podManager = PodsManager; - $scope.action = { - option: null, - options: [ - { - name: 'refresh', - title: 'Refresh', - sentence: 'refresh', - operation: angular.bind(PodsManager, PodsManager.refresh) - }, - { - name: 'delete', - title: 'Delete', - sentence: 'delete', - operation: angular.bind(PodsManager, PodsManager.deleteItem) - } - ], - inProgress: false, - error: null - }; - $scope.defaultInterface = { - name: 'default' - }; - $scope.compose = { - action: { - name: 'compose', - title: 'Compose', - sentence: 'compose' - }, - obj: { - storage: [{ - type: 'local', - size: 8, - tags: [], - pool: {}, - boot: true - }], - requests: [], - interfaces: [$scope.defaultInterface] - } - }; - $scope.power_types = GeneralManager.getData("power_types"); - $scope.domains = DomainsManager.getItems(); - $scope.zones = ZonesManager.getItems(); - $scope.pools = ResourcePoolsManager.getItems(); - $scope.section = { - area: 'summary' - }; - $scope.machinesSearch = 'pod-id:=invalid'; - $scope.editing = false; - - // Pod name section. - $scope.name = { - editing: false, - value: "", - }; - - // Return true if at least a rack controller is connected to the - // region controller. - $scope.isRackControllerConnected = function() { - // If power_types exist then a rack controller is connected. - return $scope.power_types.length > 0; - }; - - // Return true when the edit buttons can be clicked. - $scope.canEdit = function() { - if ($scope.isRackControllerConnected() && $scope.pod && - $scope.pod.permissions && - $scope.pod.permissions.indexOf('edit') !== -1) { - return true; - } else { - return false; - } - }; - - // Called to edit the pod configuration. - $scope.editPodConfiguration = function() { - if (!$scope.canEdit()) { - return; - } - $scope.editing = true; - }; - - // Called when the cancel or save button is pressed. - $scope.exitEditPodConfiguration = function() { - $scope.editing = false; - }; - - // Called to edit the pod name. - $scope.editName = function() { - if (!$scope.canEdit()) { - return; - } - - // Do nothing if already editing because we don't - // want to reset the current value. - if ($scope.name.editing) { - return; - } - $scope.name.editing = true; - $scope.name.value = $scope.pod.name; - }; - - // Return true when the pod name is invalid. - $scope.editNameInvalid = function() { - // Not invalid unless editing. - if (!$scope.name.editing) { - return false; - } + $scope, + $rootScope, + $location, + $routeParams, + $filter, + PodsManager, + GeneralManager, + UsersManager, + DomainsManager, + ZonesManager, + MachinesManager, + ManagerHelperService, + ErrorService, + ResourcePoolsManager, + SubnetsManager, + VLANsManager, + FabricsManager, + SpacesManager, + ValidationService, + $log, + $document +) { + // Set title and page. + $rootScope.title = "Loading..."; + $rootScope.page = "pods"; + + // Initial values. + $scope.loaded = false; + $scope.pod = null; + $scope.podManager = PodsManager; + $scope.action = { + option: null, + options: [ + { + name: "refresh", + title: "Refresh", + sentence: "refresh", + operation: angular.bind(PodsManager, PodsManager.refresh) + }, + { + name: "delete", + title: "Delete", + sentence: "delete", + operation: angular.bind(PodsManager, PodsManager.deleteItem) + } + ], + inProgress: false, + error: null + }; + $scope.defaultInterface = { + name: "default" + }; + $scope.compose = { + action: { + name: "compose", + title: "Compose", + sentence: "compose" + }, + obj: { + storage: [ + { + type: "local", + size: 8, + tags: [], + pool: {}, + boot: true + } + ], + requests: [], + interfaces: [$scope.defaultInterface] + } + }; + $scope.power_types = GeneralManager.getData("power_types"); + $scope.domains = DomainsManager.getItems(); + $scope.zones = ZonesManager.getItems(); + $scope.pools = ResourcePoolsManager.getItems(); + $scope.section = { + area: "summary" + }; + $scope.machinesSearch = "pod-id:=invalid"; + $scope.editing = false; + + // Pod name section. + $scope.name = { + editing: false, + value: "" + }; + + // Return true if at least a rack controller is connected to the + // region controller. + $scope.isRackControllerConnected = function() { + // If power_types exist then a rack controller is connected. + return $scope.power_types.length > 0; + }; + + // Return true when the edit buttons can be clicked. + $scope.canEdit = function() { + if ( + $scope.isRackControllerConnected() && + $scope.pod && + $scope.pod.permissions && + $scope.pod.permissions.indexOf("edit") !== -1 + ) { + return true; + } else { + return false; + } + }; + + // Called to edit the pod configuration. + $scope.editPodConfiguration = function() { + if (!$scope.canEdit()) { + return; + } + $scope.editing = true; + }; - // The value cannot be blank. - var value = $scope.name.value; - if (value.length === 0) { - return true; - } - return !ValidationService.validateHostname(value); - }; + // Called when the cancel or save button is pressed. + $scope.exitEditPodConfiguration = function() { + $scope.editing = false; + }; - // Called to cancel editing of the pod name. - $scope.cancelEditName = function() { - $scope.name.editing = false; + // Called to edit the pod name. + $scope.editName = function() { + if (!$scope.canEdit()) { + return; + } + + // Do nothing if already editing because we don't + // want to reset the current value. + if ($scope.name.editing) { + return; + } + $scope.name.editing = true; + $scope.name.value = $scope.pod.name; + }; + + // Return true when the pod name is invalid. + $scope.editNameInvalid = function() { + // Not invalid unless editing. + if (!$scope.name.editing) { + return false; + } + + // The value cannot be blank. + var value = $scope.name.value; + if (value.length === 0) { + return true; + } + return !ValidationService.validateHostname(value); + }; + + // Called to cancel editing of the pod name. + $scope.cancelEditName = function() { + $scope.name.editing = false; + updateName(); + }; + + // Called to save editing of pod name. + $scope.saveEditName = function() { + // Does nothing if invalid. + if ($scope.editNameInvalid()) { + return; + } + $scope.name.editing = false; + + // Copy the pod and make the changes. + var pod = angular.copy($scope.pod); + pod.name = $scope.name.value; + + // Update the pod. + $scope.updatePod(pod); + }; + + function updateName() { + // Don't update the value if in editing mode. + // As this would overwrite the users changes. + if ($scope.name.editing) { + return; + } + $scope.name.value = $scope.pod.name; + } + + // Update the pod with new data on the region. + $scope.updatePod = function(pod) { + return $scope.podManager.updateItem(pod).then( + function() { updateName(); - }; - - // Called to save editing of pod name. - $scope.saveEditName = function() { - // Does nothing if invalid. - if ($scope.editNameInvalid()) { - return; - } - $scope.name.editing = false; - - // Copy the pod and make the changes. - var pod = angular.copy($scope.pod); - pod.name = $scope.name.value; - - // Update the pod. - $scope.updatePod(pod); - }; - - function updateName() { - // Don't update the value if in editing mode. - // As this would overwrite the users changes. - if ($scope.name.editing) { - return; + }, + function(error) { + $log.error(error); + updateName(); + } + ); + }; + + // Return true if there is an action error. + $scope.isActionError = function() { + return $scope.action.error !== null; + }; + + // Called when the action.option has changed. + $scope.actionOptionChanged = function() { + // Clear the action error. + $scope.action.error = null; + }; + + // Cancel the action. + $scope.actionCancel = function() { + $scope.action.option = null; + $scope.action.error = null; + }; + + // Perform the action. + $scope.actionGo = function() { + $scope.action.inProgress = true; + $scope.action.option.operation($scope.pod).then( + function() { + // If the action was delete, then go back to listing. + if ($scope.action.option.name === "delete") { + $location.path("/pods"); } - $scope.name.value = $scope.pod.name; - } - - // Update the pod with new data on the region. - $scope.updatePod = function(pod) { - return $scope.podManager.updateItem(pod).then(function(pod) { - updateName(); - }, function(error) { - console.log(error); - updateName(); - }); - }; - - // Return true if there is an action error. - $scope.isActionError = function() { - return $scope.action.error !== null; - }; - - // Called when the action.option has changed. - $scope.actionOptionChanged = function() { - // Clear the action error. - $scope.action.error = null; - }; - - // Cancel the action. - $scope.actionCancel = function() { + $scope.action.inProgress = false; $scope.action.option = null; $scope.action.error = null; - }; - - // Perform the action. - $scope.actionGo = function() { - $scope.action.inProgress = true; - $scope.action.option.operation($scope.pod).then(function() { - // If the action was delete, then go back to listing. - if ($scope.action.option.name === "delete") { - $location.path("/pods"); - } - $scope.action.inProgress = false; - $scope.action.option = null; - $scope.action.error = null; - }, function(error) { - $scope.action.inProgress = false; - $scope.action.error = error; - }); - }; - - $scope.openSubnetOptions = function(iface) { - angular.forEach($scope.compose.obj.interfaces, function(i) { - i.showOptions = false; - }); - iface.showOptions = true; - } - - $scope.validateMachineCompose = function() { - if ($scope.compose.obj.requests.length < 1) { - $scope.updateRequests(); - } - - var requests = $scope.compose.obj.requests; - var valid = true; - - requests.forEach(function(request) { - if (request.size > request.available || request.size === '') { - valid = false; - } - }); - - return valid; - }; - - $scope.totalStoragePercentage = function(storage_pool, storage, other) { - var used = storage_pool.used / storage_pool.total * 100; - var requested = storage / storage_pool.total * 100; - var otherRequested = other / storage_pool.total * 100; - var percent = used + requested; - - if (other) { - percent = used + requested + otherRequested; - } - return percent; - }; - - $scope.updateRequests = function() { - var storages = $scope.compose.obj.storage; - var requests = []; - storages.forEach(function(storage) { - var requestWithPool = requests.find(function(request) { - return request.poolId === storage.pool.id; - }); - if (requestWithPool) { - requestWithPool.size += parseInt(storage.size, 10); - } else { - requests.push({ - poolId: storage.pool.id, - size: storage.size, - available: Math.round( - storage.pool.available / 1000 / 1000 / 1000) - }); - } - }); - $scope.compose.obj.requests = requests; - } - - $scope.getOtherRequests = function(storagePool, storage) { - var requests = $scope.compose.obj.requests; - var request = 0; - - for (var i = 0; i < requests.length; i++) { - if (storagePool.id === requests[i].poolId) { - request = requests[i].size; - } - } - - if (storagePool.id === storage.pool.id) { - request -= storage.size; - } - - return request; - } - - $scope.poolOverCapacity = function(storage) { - var storagePool = $scope.pod.storage_pools.find(function(pool) { - return pool.id === storage.pool.id; - }); - var requests = $scope.compose.obj.requests; - var request = 0; - - for (var i = 0; i < requests.length; i++) { - if (storagePool.id === requests[i].poolId) { - request = requests[i].size; - } - } - - if ($filter('convertGigabyteToBytes')(request) - > storagePool.available) { - return true; - } - return false; - } - - // Prevents key input if input is not a number key code. - $scope.numberOnly = function(evt) { - var charCode = (evt.which) ? evt.which : event.keyCode; - if (charCode > 31 && (charCode < 48 || charCode > 57)) { - evt.preventDefault(); - } - }; - - $scope.openOptions = function(storage) { - angular.forEach($scope.compose.obj.storage, function(disk) { - disk.showOptions = false; - }); - storage.showOptions = true; - $scope.updateRequests(); - }; - - $scope.closeOptions = function(storage) { - storage.showOptions = false; - }; - - $scope.closeStorageOptions = function() { - angular.forEach($scope.compose.obj.storage, function(disk) { - disk.showOptions = false; - }); - }; - - $scope.selectStoragePool = function(storagePool, storage, isDisabled) { - if (!isDisabled) { - storage.pool = storagePool; - $scope.updateRequests(); - } - }; - - // Return the title of the pod type. - $scope.getPodTypeTitle = function() { - var i; - for (i = 0; i < $scope.power_types.length; i++) { - var power_type = $scope.power_types[i]; - if (power_type.name === $scope.pod.type) { - return power_type.description; - } - } - return $scope.pod.type; - }; - - // Returns true if the pod is composable. - $scope.canCompose = function() { - if (angular.isObject($scope.pod) - && angular.isArray($scope.pod.permissions)) { - return ($scope.pod.permissions.indexOf('compose') >= 0 && - $scope.pod.capabilities.indexOf('composable') >= 0); - } else { - return false; - } - }; - - // Opens the compose action menu. - $scope.composeMachine = function() { - $scope.action.option = $scope.compose.action; - $scope.updateRequests(); - }; - - // Calculate the available cores with overcommit applied - $scope.availableWithOvercommit = PodsManager.availableWithOvercommit; - - // Strip trailing zero - $scope.stripTrailingZero = function(value) { - if (value) { - return value.toString().replace(/[.,]0$/, ""); - } - }; - - // Called before the compose params is sent over the websocket. - $scope.composePreProcess = function(params) { - params = angular.copy(params); - params.id = $scope.pod.id; - // Sort boot disk first. - var sorted = $scope.compose.obj.storage.sort(function(a, b) { - if (a.boot === b.boot) { - return 0; - } else if (a.boot && !b.boot) { - return -1; - } else { - return 1; - } - }); - // Create the storage constraint. - var storage = []; - angular.forEach(sorted, function(disk, idx) { - var constraint = idx + ':' + disk.size; - var tags = disk.tags.map(function(tag) { - return tag.text; - }); - if ($scope.pod.type === 'rsd') { - tags.splice(0, 0, disk.type); - } else if ($scope.pod.type === 'virsh') { - tags.splice(0, 0, disk.pool.name); - } - constraint += '(' + tags.join(',') + ')'; - storage.push(constraint); - }); - params.storage = storage.join(','); - - // Create the interface constraint. - // :=[,=];... - var interfaces = []; - angular.forEach($scope.compose.obj.interfaces, function(iface) { - let row = ''; - if (iface.ipaddress) { - row = `${iface.name}:ip=${iface.ipaddress}` - } else if (iface.subnet) { - row = `${iface.name}:subnet_cidr=${iface.subnet.cidr}` - } - if (row) { - interfaces.push(row); - } - }); - params.interfaces = interfaces.join(';'); - - return params; - }; - - $scope.copyToClipboard = function($event) { - var clipboardParent = $event.currentTarget.previousSibling; - var clipboardValue = clipboardParent.previousSibling.value; - var el = document.createElement('textarea'); - el.value = clipboardValue; - document.body.appendChild(el); - el.select(); - document.execCommand('copy'); - document.body.removeChild(el); - }; - - // Called to cancel composition. - $scope.cancelCompose = function() { - $scope.compose.obj = { - storage: [{ - type: 'local', - size: 8, - tags: [], - pool: $scope.getDefaultStoragePool(), - boot: true - }], - requests: [], - interfaces: [$scope.defaultInterface] - }; - $scope.action.option = null; - }; - - $scope.hasMultipleRequests = function(storage) { - if (storage.otherRequests > 0) { - return true; - } - - return false; - } - - // Add another storage device. - $scope.composeAddStorage = function() { - var storage = { - type: 'local', - size: 8, - tags: [], - pool: $scope.getDefaultStoragePool(), - boot: false - }; - - // Close all open option menus in the storage table when creating - // a new storage. - $scope.closeStorageOptions(); - - if ($scope.pod.capabilities.indexOf('iscsi_storage') >= 0) { - storage.type = 'iscsi'; - } - - $scope.compose.obj.storage.push(storage); - $scope.updateRequests(); - }; - - // Change which disk is the boot disk. - $scope.composeSetBootDisk = function(storage) { - angular.forEach($scope.compose.obj.storage, function(disk) { - disk.boot = false; - }); - storage.boot = true; - }; - - // Get the default pod pool - $scope.getDefaultStoragePool = function() { - var defaultPool = {}; - if ($scope.pod.storage_pools && $scope.pod.default_storage_pool) { - defaultPool = $scope.pod.storage_pools.filter( - pool => pool.id == $scope.pod.default_storage_pool - )[0]; - } - - if (!$scope.selectedPoolId) { - $scope.selectedPoolId = defaultPool.id; - } - - return defaultPool; - }; - - // Remove a disk from storage config. - $scope.composeRemoveDisk = function(storage) { - var idx = $scope.compose.obj.storage.indexOf(storage); + }, + function(error) { + $scope.action.inProgress = false; + $scope.action.error = error; + } + ); + }; + + $scope.openSubnetOptions = function(iface) { + angular.forEach($scope.compose.obj.interfaces, function(i) { + i.showOptions = false; + }); + iface.showOptions = true; + }; + + $scope.validateMachineCompose = function() { + if ($scope.compose.obj.requests.length < 1) { + $scope.updateRequests(); + } + + const requests = $scope.compose.obj.requests; + const hostname = $scope.compose.obj.hostname; + let valid = true; + if (angular.isDefined(hostname) && hostname !== "") { + valid = ValidationService.validateHostname(hostname); + } + + requests.forEach(function(request) { + if (request.size > request.available || request.size === "") { + valid = false; + } + }); + + return valid; + }; + + $scope.totalStoragePercentage = function(storage_pool, storage, other) { + var used = (storage_pool.used / storage_pool.total) * 100; + var requested = (storage / storage_pool.total) * 100; + var otherRequested = (other / storage_pool.total) * 100; + var percent = used + requested; + + if (other) { + percent = used + requested + otherRequested; + } + return percent; + }; + + $scope.updateRequests = function() { + var storages = $scope.compose.obj.storage; + var requests = []; + storages.forEach(function(storage) { + var requestWithPool = requests.find(function(request) { + return request.poolId === storage.pool.id; + }); + if (requestWithPool) { + requestWithPool.size += parseInt(storage.size, 10); + } else { + requests.push({ + poolId: storage.pool.id, + size: storage.size, + available: Math.round(storage.pool.available / 1000 / 1000 / 1000) + }); + } + }); + $scope.compose.obj.requests = requests; + }; + + $scope.getOtherRequests = function(storagePool, storage) { + var requests = $scope.compose.obj.requests; + var request = 0; + + for (var i = 0; i < requests.length; i++) { + if (storagePool.id === requests[i].poolId) { + request = requests[i].size; + } + } + + if (storagePool.id === storage.pool.id) { + request -= storage.size; + } + + return request; + }; + + $scope.poolOverCapacity = function(storage) { + var storagePool = $scope.pod.storage_pools.find(function(pool) { + return pool.id === storage.pool.id; + }); + var requests = $scope.compose.obj.requests; + var request = 0; + + for (var i = 0; i < requests.length; i++) { + if (storagePool.id === requests[i].poolId) { + request = requests[i].size; + } + } + + if ($filter("convertGigabyteToBytes")(request) > storagePool.available) { + return true; + } + return false; + }; + + // Prevents key input if input is not a number key code. + $scope.numberOnly = function(evt) { + var charCode = evt.which ? evt.which : event.keyCode; + if (charCode > 31 && (charCode < 48 || charCode > 57)) { + evt.preventDefault(); + } + }; + + $scope.openOptions = function(storage) { + angular.forEach($scope.compose.obj.storage, function(disk) { + disk.showOptions = false; + }); + storage.showOptions = true; + $scope.updateRequests(); + }; + + $scope.closeOptions = function(storage) { + storage.showOptions = false; + }; + + $scope.closeStorageOptions = function() { + angular.forEach($scope.compose.obj.storage, function(disk) { + disk.showOptions = false; + }); + }; + + $scope.selectStoragePool = function(storagePool, storage, isDisabled) { + if (!isDisabled) { + storage.pool = storagePool; + $scope.updateRequests(); + } + }; + + // Return the title of the pod type. + $scope.getPodTypeTitle = function() { + var i; + for (i = 0; i < $scope.power_types.length; i++) { + var power_type = $scope.power_types[i]; + if (power_type.name === $scope.pod.type) { + return power_type.description; + } + } + return $scope.pod.type; + }; + + // Returns true if the pod is composable. + $scope.canCompose = function() { + if ( + angular.isObject($scope.pod) && + angular.isArray($scope.pod.permissions) + ) { + return ( + $scope.pod.permissions.indexOf("compose") >= 0 && + $scope.pod.capabilities.indexOf("composable") >= 0 + ); + } else { + return false; + } + }; + + // Opens the compose action menu. + $scope.composeMachine = function() { + $scope.action.option = $scope.compose.action; + $scope.updateRequests(); + }; + + // Calculate the available cores with overcommit applied + $scope.availableWithOvercommit = PodsManager.availableWithOvercommit; + + // Strip trailing zero + $scope.stripTrailingZero = function(value) { + if (value) { + return value.toString().replace(/[.,]0$/, ""); + } + }; + + // Called before the compose params is sent over the websocket. + $scope.composePreProcess = function(params) { + params = angular.copy(params); + params.id = $scope.pod.id; + // Sort boot disk first. + var sorted = $scope.compose.obj.storage.sort(function(a, b) { + if (a.boot === b.boot) { + return 0; + } else if (a.boot && !b.boot) { + return -1; + } else { + return 1; + } + }); + // Create the storage constraint. + var storage = []; + angular.forEach(sorted, function(disk, idx) { + var constraint = idx + ":" + disk.size; + var tags = disk.tags.map(function(tag) { + return tag.text; + }); + if ($scope.pod.type === "rsd") { + tags.splice(0, 0, disk.type); + } else if ($scope.pod.type === "virsh") { + tags.splice(0, 0, disk.pool.name); + } + constraint += "(" + tags.join(",") + ")"; + storage.push(constraint); + }); + params.storage = storage.join(","); + + // Create the interface constraint. + // :=[,=];... + var interfaces = []; + angular.forEach($scope.compose.obj.interfaces, function(iface) { + let row = ""; + if (iface.ipaddress) { + row = `${iface.name}:ip=${iface.ipaddress}`; + } else if (iface.subnet) { + row = `${iface.name}:subnet_cidr=${iface.subnet.cidr}`; + } + if (row) { + interfaces.push(row); + } + }); + params.interfaces = interfaces.join(";"); + + return params; + }; + + $scope.copyToClipboard = function($event) { + var clipboardParent = $event.currentTarget.previousSibling; + var clipboardValue = clipboardParent.previousSibling.value; + var el = $document.createElement("textarea"); + el.value = clipboardValue; + $document.body.appendChild(el); + el.select(); + $document.execCommand("copy"); + $document.body.removeChild(el); + }; + + // Called to cancel composition. + $scope.cancelCompose = function() { + $scope.compose.obj = { + storage: [ + { + type: "local", + size: 8, + tags: [], + pool: $scope.getDefaultStoragePool(), + boot: true + } + ], + requests: [], + interfaces: [$scope.defaultInterface] + }; + $scope.action.option = null; + }; + + $scope.hasMultipleRequests = function(storage) { + if (storage.otherRequests > 0) { + return true; + } + + return false; + }; + + // Add another storage device. + $scope.composeAddStorage = function() { + var storage = { + type: "local", + size: 8, + tags: [], + pool: $scope.getDefaultStoragePool(), + boot: false + }; + + // Close all open option menus in the storage table when creating + // a new storage. + $scope.closeStorageOptions(); + + if ($scope.pod.capabilities.indexOf("iscsi_storage") >= 0) { + storage.type = "iscsi"; + } + + $scope.compose.obj.storage.push(storage); + $scope.updateRequests(); + }; + + // Change which disk is the boot disk. + $scope.composeSetBootDisk = function(storage) { + angular.forEach($scope.compose.obj.storage, function(disk) { + disk.boot = false; + }); + storage.boot = true; + }; + + // Get the default pod pool + $scope.getDefaultStoragePool = function() { + var defaultPool = {}; + if ($scope.pod.storage_pools && $scope.pod.default_storage_pool) { + defaultPool = $scope.pod.storage_pools.filter( + pool => pool.id == $scope.pod.default_storage_pool + )[0]; + } + + if (!$scope.selectedPoolId) { + $scope.selectedPoolId = defaultPool.id; + } + + return defaultPool; + }; + + // Remove a disk from storage config. + $scope.composeRemoveDisk = function(storage) { + var idx = $scope.compose.obj.storage.indexOf(storage); + if (idx >= 0) { + $scope.compose.obj.storage.splice(idx, 1); + } + $scope.updateRequests(); + }; + + $scope.bySpace = function(spaceName) { + return function(subnet) { + if (spaceName && subnet.space !== null) { + return subnet.space.name == spaceName; + } else if (!spaceName) { + return !subnet.space; + } + }; + }; + + $scope.hasSpacelessSubnets = function() { + return $scope.availableSubnets.some(subnet => !subnet.space); + }; + + $scope.selectSubnet = function(subnet, iface) { + const idx = $scope.compose.obj.interfaces.indexOf(iface); + const thisInterface = $scope.compose.obj.interfaces[idx]; + thisInterface.subnet = subnet; + thisInterface.space = subnet.space; + thisInterface.vlan = subnet.vlan; + thisInterface.fabric = subnet.fabric; + thisInterface.pxe = $scope.pod.boot_vlans.includes(subnet.vlan.id); + $scope.closeOptions(iface); + return subnet; + }; + + $scope.resetSubnetList = function(iface) { + // Select first available subnet or clear list. + let subnets = $scope.availableSubnets; + + if (iface.selectedSpace) { + subnets = subnets.filter(subnet => { + if (subnet.space) { + return subnet.space.name === iface.selectedSpace; + } + }); + } + + if (subnets.length > 0) { + iface.subnet = $scope.selectSubnet(subnets[0], iface); + } else { + iface.subnet = undefined; + } + }; + + $scope.selectSubnetByIP = function(iface) { + if (iface.ipaddress) { + angular.forEach($scope.availableSubnets, function(subnet) { + let inNetwork = ValidationService.validateIPInNetwork( + iface.ipaddress, + subnet.cidr + ); + if (inNetwork) { + iface.subnet = $scope.selectSubnet(subnet, iface); + } + }); + } + }; + + const _getSubnetDetails = function(originalSubnet) { + const subnet = Object.assign({}, originalSubnet); + + if (subnet.name === subnet.cidr) { + subnet.displayName = subnet.cidr; + } else { + subnet.displayName = `${subnet.cidr} (${subnet.name})`; + } + subnet.vlan = VLANsManager.getItemFromList(subnet.vlan); + if (subnet.vlan) { + subnet.fabric = FabricsManager.getItemFromList(subnet.vlan.fabric); + } + subnet.space = SpacesManager.getItemFromList(subnet.space); + return subnet; + }; + + // Add interfaces. + $scope.composeAddInterface = function() { + // Remove default auto-assigned interface when + // adding custom interfaces + let defaultIdx = $scope.compose.obj.interfaces.indexOf( + $scope.defaultInterface + ); + if (defaultIdx >= 0) { + $scope.compose.obj.interfaces.splice(defaultIdx, 1); + } + var iface = { + name: `eth${$scope.compose.obj.interfaces.length}` + }; + $scope.compose.obj.interfaces.push(iface); + }; + + // Remove an interface from interfaces config. + $scope.composeRemoveInterface = function(iface) { + var idx = $scope.compose.obj.interfaces.indexOf(iface); + if (idx >= 0) { + $scope.compose.obj.interfaces.splice(idx, 1); + } + + // Re-add default interface if all custom interfaces removed + if ($scope.compose.obj.interfaces.length == 0) { + $scope.compose.obj.interfaces.push($scope.defaultInterface); + } + }; + + // Start watching key fields. + $scope.startWatching = function() { + $scope.$watch("subnets", function() { + angular.forEach($scope.subnets, function(subnet) { + // filter subnets from vlans not attached to host + if ($scope.pod.attached_vlans.includes(subnet.vlan)) { + $scope.availableSubnets.push(_getSubnetDetails(subnet)); + } + }); + }); + $scope.$watch("pod.name", function() { + $rootScope.title = "Pod " + $scope.pod.name; + updateName(); + }); + $scope.$watch("pod.capabilities", function() { + // Show the composable action if the pod supports composition. + var idx = $scope.action.options.indexOf($scope.compose.action); + if (!$scope.canCompose()) { if (idx >= 0) { - $scope.compose.obj.storage.splice(idx, 1); - } - $scope.updateRequests(); - }; - - $scope.bySpace = function(spaceName) { - return function(subnet) { - if (spaceName && subnet.space !== null) { - return subnet.space.name == spaceName; - } else if (!spaceName) { - return !subnet.space; - } + $scope.action.options.splice(idx, 1); } - } - - $scope.hasSpacelessSubnets = function() { - return $scope.availableSubnets.some(subnet => !subnet.space); - } - - $scope.selectSubnet = function(subnet, iface) { - const idx = $scope.compose.obj.interfaces.indexOf(iface); - const thisInterface = $scope.compose.obj.interfaces[idx]; - thisInterface.subnet = subnet; - thisInterface.space = subnet.space; - thisInterface.vlan = subnet.vlan; - thisInterface.fabric = subnet.fabric; - thisInterface.pxe = $scope.pod.boot_vlans.includes(subnet.vlan.id); - $scope.closeOptions(iface); - return subnet; - } - - $scope.resetSubnetList = function(iface) { - // Select first available subnet or clear list. - let subnets = $scope.availableSubnets; - - if (iface.selectedSpace) { - subnets = subnets.filter(subnet => { - if (subnet.space) { - return subnet.space.name === iface.selectedSpace; - } - }); - } - - if (subnets.length > 0) { - iface.subnet = $scope.selectSubnet(subnets[0], iface); - } else { - iface.subnet = undefined; - } - } - - $scope.selectSubnetByIP = function(iface) { - if (iface.ipaddress) { - angular.forEach($scope.availableSubnets, function(subnet, idx) { - let inNetwork = ValidationService.validateIPInNetwork( - iface.ipaddress, subnet.cidr) - if (inNetwork) { - iface.subnet = $scope.selectSubnet(subnet, iface); - } - }); - } - } - - const _getSubnetDetails = function(originalSubnet) { - const subnet = Object.assign({}, originalSubnet); - - if (subnet.name === subnet.cidr) { - subnet.displayName = subnet.cidr; - } else { - subnet.displayName = `${subnet.cidr} (${subnet.name})`; - } - subnet.vlan = VLANsManager.getItemFromList(subnet.vlan); - if (subnet.vlan) { - subnet.fabric = - FabricsManager.getItemFromList(subnet.vlan.fabric); + } else { + if (idx === -1) { + $scope.action.options.splice(0, 0, $scope.compose.action); + } + } + }); + $scope.$watch("action.option", function(now, then) { + // When the compose action is selected set the default + // parameters. + if (now && now.name === "compose") { + if (!then || then.name !== "compose") { + $scope.compose.obj.domain = DomainsManager.getDefaultDomain().id; + $scope.compose.obj.zone = ZonesManager.getDefaultZone($scope.pod).id; + $scope.compose.obj.pool = $scope.pod.default_pool; + } + } + }); + }; + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ + PodsManager, + GeneralManager, + UsersManager, + DomainsManager, + ZonesManager, + MachinesManager, + ResourcePoolsManager, + SubnetsManager, + VLANsManager, + FabricsManager, + SpacesManager + ]).then(function() { + $scope.spaces = SpacesManager.getItems(); + $scope.subnets = SubnetsManager.getItems(); + $scope.availableSubnets = []; + + // Possibly redirected from another controller that already had + // this pod set to active. Only call setActiveItem if not already + // the activeItem. + var activePod = PodsManager.getActiveItem(); + if ( + angular.isObject(activePod) && + activePod.id === parseInt($routeParams.id, 10) + ) { + $scope.pod = activePod; + $scope.compose.obj.storage[0].pool = $scope.getDefaultStoragePool(); + $scope.loaded = true; + $scope.machinesSearch = "pod-id:=" + $scope.pod.id; + $scope.startWatching(); + } else { + PodsManager.setActiveItem(parseInt($routeParams.id, 10)).then( + function(pod) { + $scope.pod = pod; + $scope.compose.obj.storage[0].pool = $scope.getDefaultStoragePool(); + $scope.loaded = true; + $scope.machinesSearch = "pod-id:=" + $scope.pod.id; + $scope.startWatching(); + }, + function(error) { + ErrorService.raiseError(error); } - subnet.space = SpacesManager.getItemFromList(subnet.space); - return subnet; + ); } - - // Add interfaces. - $scope.composeAddInterface = function() { - // Remove default auto-assigned interface when - // adding custom interfaces - let defaultIdx = $scope.compose.obj.interfaces.indexOf( - $scope.defaultInterface); - if (defaultIdx >= 0) { - $scope.compose.obj.interfaces.splice(defaultIdx, 1); - } - var iface = { - name: `eth${$scope.compose.obj.interfaces.length}` - }; - $scope.compose.obj.interfaces.push(iface); - }; - - // Remove an interface from interfaces config. - $scope.composeRemoveInterface = function(iface) { - var idx = $scope.compose.obj.interfaces.indexOf(iface); - if (idx >= 0) { - $scope.compose.obj.interfaces.splice(idx, 1); - } - - // Re-add default interface if all custom interfaces removed - if ($scope.compose.obj.interfaces.length == 0) { - $scope.compose.obj.interfaces.push($scope.defaultInterface); - } - }; - - // Start watching key fields. - $scope.startWatching = function() { - $scope.$watch("subnets", function() { - angular.forEach($scope.subnets, function(subnet, idx) { - // filter subnets from vlans not attached to host - if ($scope.pod.attached_vlans.includes(subnet.vlan)) { - $scope.availableSubnets.push(_getSubnetDetails(subnet)); - } - }); - }); - $scope.$watch("pod.name", function() { - $rootScope.title = 'Pod ' + $scope.pod.name; - updateName(); - }); - $scope.$watch("pod.capabilities", function() { - // Show the composable action if the pod supports composition. - var idx = $scope.action.options.indexOf( - $scope.compose.action); - if (!$scope.canCompose()) { - if (idx >= 0) { - $scope.action.options.splice(idx, 1); - } - } else { - if (idx === -1) { - $scope.action.options.splice( - 0, 0, $scope.compose.action); - } - } - }); - $scope.$watch("action.option", function(now, then) { - // When the compose action is selected set the default - // parameters. - if (now && now.name === 'compose') { - if (!then || then.name !== 'compose') { - $scope.compose.obj.domain = ( - DomainsManager.getDefaultDomain().id); - $scope.compose.obj.zone = ( - ZonesManager.getDefaultZone($scope.pod).id); - $scope.compose.obj.pool = $scope.pod.default_pool; - } - } - }); - }; - - // Load all the required managers. - ManagerHelperService.loadManagers($scope, [ - PodsManager, GeneralManager, UsersManager, - DomainsManager, ZonesManager, MachinesManager, - ResourcePoolsManager, SubnetsManager, VLANsManager, - FabricsManager, SpacesManager]).then(function() { - - $scope.spaces = SpacesManager.getItems(); - $scope.subnets = SubnetsManager.getItems(); - $scope.availableSubnets = []; - - // Possibly redirected from another controller that already had - // this pod set to active. Only call setActiveItem if not already - // the activeItem. - var activePod = PodsManager.getActiveItem(); - if (angular.isObject(activePod) && - activePod.id === parseInt($routeParams.id, 10)) { - $scope.pod = activePod; - $scope.compose.obj.storage[0].pool = ( - $scope.getDefaultStoragePool()); - $scope.loaded = true; - $scope.machinesSearch = 'pod-id:=' + $scope.pod.id; - $scope.startWatching(); - } else { - PodsManager.setActiveItem( - parseInt($routeParams.id, 10)).then(function(pod) { - $scope.pod = pod; - $scope.compose.obj.storage[0].pool = ( - $scope.getDefaultStoragePool()); - $scope.loaded = true; - $scope.machinesSearch = 'pod-id:=' + $scope.pod.id; - $scope.startWatching(); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); + }); } export default PodDetailsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/pods_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/pods_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/pods_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/pods_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,260 +6,276 @@ /* @ngInject */ function PodsListController( - $scope, $rootScope, PodsManager, UsersManager, GeneralManager, - ZonesManager, ManagerHelperService, ResourcePoolsManager) { - - // Set title and page. - $rootScope.title = "Pods"; - $rootScope.page = "pods"; - - // Set initial values. - $scope.podManager = PodsManager; - $scope.pods = PodsManager.getItems(); - $scope.loading = true; - - $scope.filteredItems = []; - $scope.selectedItems = PodsManager.getSelectedItems(); - $scope.predicate = 'name'; - $scope.allViewableChecked = false; - $scope.action = { - option: null, - options: [ - { - name: 'refresh', - title: 'Refresh', - sentence: 'refresh', - operation: angular.bind(PodsManager, PodsManager.refresh) - }, - { - name: 'delete', - title: 'Delete', - sentence: 'delete', - operation: angular.bind(PodsManager, PodsManager.deleteItem) - } - ], - progress: { - total: 0, - completed: 0, - errors: 0 - } - }; - $scope.add = { - open: false, - obj: { - cpu_over_commit_ratio: 1, - memory_over_commit_ratio: 1 - } - }; - $scope.powerTypes = GeneralManager.getData("power_types"); - $scope.zones = ZonesManager.getItems(); - $scope.pools = ResourcePoolsManager.getItems(); - - // Called to update `allViewableChecked`. - function updateAllViewableChecked() { - // Not checked when no pods. - if ($scope.pods.length === 0) { - $scope.allViewableChecked = false; - return; - } + $scope, + $rootScope, + PodsManager, + UsersManager, + GeneralManager, + ZonesManager, + ManagerHelperService, + ResourcePoolsManager +) { + // Set title and page. + $rootScope.title = "Pods"; + $rootScope.page = "pods"; + + // Set initial values. + $scope.podManager = PodsManager; + $scope.pods = PodsManager.getItems(); + $scope.loading = true; + + $scope.filteredItems = []; + $scope.selectedItems = PodsManager.getSelectedItems(); + $scope.predicate = "name"; + $scope.allViewableChecked = false; + $scope.action = { + option: null, + options: [ + { + name: "refresh", + title: "Refresh", + sentence: "refresh", + operation: angular.bind(PodsManager, PodsManager.refresh) + }, + { + name: "delete", + title: "Delete", + sentence: "delete", + operation: angular.bind(PodsManager, PodsManager.deleteItem) + } + ], + progress: { + total: 0, + completed: 0, + errors: 0 + } + }; + $scope.add = { + open: false, + obj: { + cpu_over_commit_ratio: 1, + memory_over_commit_ratio: 1 + } + }; + $scope.powerTypes = GeneralManager.getData("power_types"); + $scope.zones = ZonesManager.getItems(); + $scope.pools = ResourcePoolsManager.getItems(); + + // Called to update `allViewableChecked`. + function updateAllViewableChecked() { + // Not checked when no pods. + if ($scope.pods.length === 0) { + $scope.allViewableChecked = false; + return; + } - // Loop through all filtered pods and see if all are checked. - var i; - for (i = 0; i < $scope.pods.length; i++) { - if (!$scope.pods[i].$selected) { - $scope.allViewableChecked = false; - return; - } - } - $scope.allViewableChecked = true; + // Loop through all filtered pods and see if all are checked. + var i; + for (i = 0; i < $scope.pods.length; i++) { + if (!$scope.pods[i].$selected) { + $scope.allViewableChecked = false; + return; + } } + $scope.allViewableChecked = true; + } - function clearAction() { - resetActionProgress(); + function clearAction() { + resetActionProgress(); + $scope.action.option = null; + } + + // Clear the action if required. + function shouldClearAction() { + if ($scope.selectedItems.length === 0) { + clearAction(); + if ($scope.action.option) { $scope.action.option = null; + } } + } - // Clear the action if required. - function shouldClearAction() { - if ($scope.selectedItems.length === 0) { - clearAction(); - if ($scope.action.option) { - $scope.action.option = null; - } - } - } + // Reset actionProgress to zero. + function resetActionProgress() { + var progress = $scope.action.progress; + progress.completed = progress.total = progress.errors = 0; + angular.forEach($scope.pods, function(pod) { + delete pod.action_failed; + }); + } - // Reset actionProgress to zero. - function resetActionProgress() { - var progress = $scope.action.progress; - progress.completed = progress.total = progress.errors = 0; - angular.forEach($scope.pods, function(pod) { - delete pod.action_failed; - }); + // After an action has been performed check if we can leave all pods + // selected or if an error occurred and we should only show the failed + // pods. + function updateSelectedItems() { + if (!$scope.hasActionsFailed()) { + if (!$scope.hasActionsInProgress()) { + clearAction(); + } + return; } + angular.forEach($scope.pods, function(pod) { + if (pod.action_failed === false) { + PodsManager.unselectItem(pod.id); + } + }); + shouldClearAction(); + } - // After an action has been performed check if we can leave all pods - // selected or if an error occurred and we should only show the failed - // pods. - function updateSelectedItems() { - if (!$scope.hasActionsFailed()) { - if (!$scope.hasActionsInProgress()) { - clearAction(); - } - return; - } - angular.forEach($scope.pods, function(pod) { - if (pod.action_failed === false) { - PodsManager.unselectItem(pod.id); - } - }); - shouldClearAction(); + // Mark a pod as selected or unselected. + $scope.toggleChecked = function(pod) { + if (PodsManager.isSelected(pod.id)) { + PodsManager.unselectItem(pod.id); + } else { + PodsManager.selectItem(pod.id); } - - // Mark a pod as selected or unselected. - $scope.toggleChecked = function(pod) { - if (PodsManager.isSelected(pod.id)) { - PodsManager.unselectItem(pod.id); - } else { - PodsManager.selectItem(pod.id); - } - updateAllViewableChecked(); - shouldClearAction(); - }; - - // Select all viewable pods or deselect all viewable pods. - $scope.toggleCheckAll = function() { - if ($scope.allViewableChecked) { - angular.forEach($scope.pods, function(pod) { - PodsManager.unselectItem(pod.id); - }); - } else { - angular.forEach($scope.pods, function(pod) { - PodsManager.selectItem(pod.id); - }); + updateAllViewableChecked(); + shouldClearAction(); + }; + + // Select all viewable pods or deselect all viewable pods. + $scope.toggleCheckAll = function() { + if ($scope.allViewableChecked) { + angular.forEach($scope.pods, function(pod) { + PodsManager.unselectItem(pod.id); + }); + } else { + angular.forEach($scope.pods, function(pod) { + PodsManager.selectItem(pod.id); + }); + } + updateAllViewableChecked(); + shouldClearAction(); + }; + + // When the pods change update if all check buttons should be + // checked or not. + $scope.$watchCollection("pods", function() { + updateAllViewableChecked(); + }); + + // Sorts the table by predicate. + $scope.sortTable = function(predicate) { + $scope.predicate = predicate; + $scope.reverse = !$scope.reverse; + }; + + // Called when the current action is cancelled. + $scope.actionCancel = function() { + resetActionProgress(); + $scope.action.option = null; + }; + + // Calculate the available cores with overcommit applied + $scope.availableWithOvercommit = PodsManager.availableWithOvercommit; + + // Perform the action on all pods. + $scope.actionGo = function() { + var extra = {}; + + // Setup actionProgress. + resetActionProgress(); + $scope.action.progress.total = $scope.selectedItems.length; + + // Perform the action on all selected items. + var operation = $scope.action.option.operation; + angular.forEach($scope.selectedItems, function(pod) { + operation(pod).then( + function() { + $scope.action.progress.completed += 1; + pod.action_failed = false; + updateSelectedItems(); + }, + function(error) { + $scope.action.progress.errors += 1; + pod.action_error = error; + pod.action_failed = true; + updateSelectedItems(); } - updateAllViewableChecked(); - shouldClearAction(); - }; - - // When the pods change update if all check buttons should be - // checked or not. - $scope.$watchCollection("pods", function() { - updateAllViewableChecked(); + ); }); + }; - // Sorts the table by predicate. - $scope.sortTable = function(predicate) { - $scope.predicate = predicate; - $scope.reverse = !$scope.reverse; - }; - - // Called when the current action is cancelled. - $scope.actionCancel = function() { - resetActionProgress(); - $scope.action.option = null; - }; - - // Calculate the available cores with overcommit applied - $scope.availableWithOvercommit = PodsManager.availableWithOvercommit; - - // Perform the action on all pods. - $scope.actionGo = function() { - var extra = {}; - - // Setup actionProgress. - resetActionProgress(); - $scope.action.progress.total = $scope.selectedItems.length; - - // Perform the action on all selected items. - var operation = $scope.action.option.operation; - angular.forEach($scope.selectedItems, function(pod) { - operation(pod).then(function() { - $scope.action.progress.completed += 1; - pod.action_failed = false; - updateSelectedItems(); - }, function(error) { - $scope.action.progress.errors += 1; - pod.action_error = error; - pod.action_failed = true; - updateSelectedItems(); - }); - }); - }; - - // Returns true when actions are being performed. - $scope.hasActionsInProgress = function() { - var progress = $scope.action.progress; - return progress.total > 0 && ( - progress.completed + progress.errors) !== progress.total; - }; - - // Returns true if any of the actions have failed. - $scope.hasActionsFailed = function() { - var progress = $scope.action.progress; - return progress.errors > 0; - }; - - // Called when the add pod button is pressed. - $scope.addPod = function() { - $scope.add.open = true; - $scope.add.obj.zone = ZonesManager.getDefaultZone().id; - $scope.add.obj.default_pool = ( - ResourcePoolsManager.getDefaultPool().id); - $scope.add.obj.cpu_over_commit_ratio = 1; - $scope.add.obj.memory_over_commit_ratio = 1; - }; - - // Called when the cancel add pod button is pressed. - $scope.cancelAddPod = function() { - $scope.add.open = false; - $scope.add.obj = {}; - }; - - // Return true if at least a rack controller is connected to the - // region controller. - $scope.isRackControllerConnected = function() { - // If powerTypes exist then a rack controller is connected. - return $scope.powerTypes.length > 0; - }; - - // Return true when the add pod buttons can be clicked. - $scope.canAddPod = function() { - return ( - $scope.isRackControllerConnected() && - UsersManager.hasGlobalPermission('pod_create')); - }; - - // Return true if the actions should be shown. - $scope.showActions = function() { - for (var i = 0; i < $scope.pods.length; i++) { - if ($scope.pods[i].permissions && - $scope.pods[i].permissions.indexOf('edit') >= 0) { - return true; - } - } - return false; - }; + // Returns true when actions are being performed. + $scope.hasActionsInProgress = function() { + var progress = $scope.action.progress; + return ( + progress.total > 0 && + progress.completed + progress.errors !== progress.total + ); + }; + + // Returns true if any of the actions have failed. + $scope.hasActionsFailed = function() { + var progress = $scope.action.progress; + return progress.errors > 0; + }; + + // Called when the add pod button is pressed. + $scope.addPod = function() { + $scope.add.open = true; + $scope.add.obj.zone = ZonesManager.getDefaultZone().id; + $scope.add.obj.default_pool = ResourcePoolsManager.getDefaultPool().id; + $scope.add.obj.cpu_over_commit_ratio = 1; + $scope.add.obj.memory_over_commit_ratio = 1; + }; + + // Called when the cancel add pod button is pressed. + $scope.cancelAddPod = function() { + $scope.add.open = false; + $scope.add.obj = {}; + }; + + // Return true if at least a rack controller is connected to the + // region controller. + $scope.isRackControllerConnected = function() { + // If powerTypes exist then a rack controller is connected. + return $scope.powerTypes.length > 0; + }; + + // Return true when the add pod buttons can be clicked. + $scope.canAddPod = function() { + return ( + $scope.isRackControllerConnected() && + UsersManager.hasGlobalPermission("pod_create") + ); + }; + + // Return true if the actions should be shown. + $scope.showActions = function() { + for (var i = 0; i < $scope.pods.length; i++) { + if ( + $scope.pods[i].permissions && + $scope.pods[i].permissions.indexOf("edit") >= 0 + ) { + return true; + } + } + return false; + }; - // Return the title of the power type. - $scope.getPowerTypeTitle = function(power_type) { - var i; - for (i = 0; i < $scope.powerTypes.length; i++) { - var powerType = $scope.powerTypes[i]; - if (powerType.name === power_type) { - return powerType.description; - } - } - return power_type; - }; + // Return the title of the power type. + $scope.getPowerTypeTitle = function(power_type) { + var i; + for (i = 0; i < $scope.powerTypes.length; i++) { + var powerType = $scope.powerTypes[i]; + if (powerType.name === power_type) { + return powerType.description; + } + } + return power_type; + }; - // Load the required managers for this controller. - ManagerHelperService.loadManagers($scope, [ - PodsManager, UsersManager, GeneralManager, ZonesManager, - ResourcePoolsManager]).then( - function() { - $scope.loading = false; - }); + // Load the required managers for this controller. + ManagerHelperService.loadManagers($scope, [ + PodsManager, + UsersManager, + GeneralManager, + ZonesManager, + ResourcePoolsManager + ]).then(function() { + $scope.loading = false; + }); } export default PodsListController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/prefs.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/prefs.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/prefs.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/prefs.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,13 +5,11 @@ */ /* @ngInject */ -function PreferencesController( - $scope, UsersManager, ManagerHelperService) { - $scope.loading = true; - ManagerHelperService.loadManager( - $scope, UsersManager).then(function() { - $scope.loading = false; - }); +function PreferencesController($scope, UsersManager, ManagerHelperService) { + $scope.loading = true; + ManagerHelperService.loadManager($scope, UsersManager).then(function() { + $scope.loading = false; + }); } export default PreferencesController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/settings.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/settings.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/settings.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/settings.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,256 +5,263 @@ */ /* @ngInject */ -function SettingsController($scope, $rootScope, $routeParams, - PackageRepositoriesManager, DHCPSnippetsManager, - MachinesManager, ControllersManager, - DevicesManager, SubnetsManager, GeneralManager, - ManagerHelperService) { - - // Set the title and page. - $rootScope.title = "Loading..."; - $rootScope.page = "settings"; - - // Initial values. - $scope.loading = true; - $scope.snippetsManager = DHCPSnippetsManager; - $scope.snippets = DHCPSnippetsManager.getItems(); - $scope.subnets = SubnetsManager.getItems(); - $scope.machines = MachinesManager.getItems(); - $scope.devices = DevicesManager.getItems(); - $scope.controllers = ControllersManager.getItems(); - $scope.known_architectures = - GeneralManager.getData("known_architectures"); - $scope.pockets_to_disable = - GeneralManager.getData("pockets_to_disable"); - $scope.components_to_disable = - GeneralManager.getData("components_to_disable"); - $scope.packageRepositoriesManager = PackageRepositoriesManager; - $scope.repositories = - PackageRepositoriesManager.getItems(); - $scope.newSnippet = null; - $scope.editSnippet = null; - $scope.deleteSnippet = null; - $scope.snippetTypes = ["Global", "Subnet", "Node"]; +function SettingsController( + $scope, + $rootScope, + $routeParams, + PackageRepositoriesManager, + DHCPSnippetsManager, + MachinesManager, + ControllersManager, + DevicesManager, + SubnetsManager, + GeneralManager, + ManagerHelperService +) { + // Set the title and page. + $rootScope.title = "Loading..."; + $rootScope.page = "settings"; + + // Initial values. + $scope.loading = true; + $scope.snippetsManager = DHCPSnippetsManager; + $scope.snippets = DHCPSnippetsManager.getItems(); + $scope.subnets = SubnetsManager.getItems(); + $scope.machines = MachinesManager.getItems(); + $scope.devices = DevicesManager.getItems(); + $scope.controllers = ControllersManager.getItems(); + $scope.known_architectures = GeneralManager.getData("known_architectures"); + $scope.pockets_to_disable = GeneralManager.getData("pockets_to_disable"); + $scope.components_to_disable = GeneralManager.getData( + "components_to_disable" + ); + $scope.packageRepositoriesManager = PackageRepositoriesManager; + $scope.repositories = PackageRepositoriesManager.getItems(); + $scope.newSnippet = null; + $scope.editSnippet = null; + $scope.deleteSnippet = null; + $scope.snippetTypes = ["Global", "Subnet", "Node"]; + $scope.newRepository = null; + $scope.editRepository = null; + $scope.deleteRepository = null; + + // Called when the enabled toggle is changed. + $scope.repositoryEnabledToggle = function(repository) { + PackageRepositoriesManager.updateItem(repository); + }; + + // Called to enter remove mode for a repository. + $scope.repositoryEnterRemove = function(repository) { $scope.newRepository = null; $scope.editRepository = null; - $scope.deleteRepository = null; - - // Called when the enabled toggle is changed. - $scope.repositoryEnabledToggle = function(repository) { - PackageRepositoriesManager.updateItem(repository); - }; + $scope.deleteRepository = repository; + }; - // Called to enter remove mode for a repository. - $scope.repositoryEnterRemove = function(repository) { - $scope.newRepository = null; - $scope.editRepository = null; - $scope.deleteRepository = repository; - }; - - // Called to exit remove mode for a repository. - $scope.repositoryExitRemove = function() { - $scope.deleteRepository = null; - }; - - // Called to confirm the removal of a repository. - $scope.repositoryConfirmRemove = function() { - PackageRepositoriesManager.deleteItem( - $scope.deleteRepository).then(function() { - $scope.repositoryExitRemove(); - }); - }; - - // Return true if the repository is a PPA. - $scope.isPPA = function(data) { - if (!angular.isObject(data)) { - return false; - } - if (!angular.isString(data.url)) { - return false; - } - return data.url.indexOf("ppa:") === 0 || - data.url.indexOf("ppa.launchpad.net") > -1; - }; + // Called to exit remove mode for a repository. + $scope.repositoryExitRemove = function() { + $scope.deleteRepository = null; + }; - // Return true if the repository is a mirror. - $scope.isMirror = function(data) { - if (!angular.isObject(data)) { - return false; - } - if (!angular.isString(data.name)) { - return false; - } - return data.name === "main_archive" || - data.name === "ports_archive"; - }; + // Called to confirm the removal of a repository. + $scope.repositoryConfirmRemove = function() { + PackageRepositoriesManager.deleteItem($scope.deleteRepository).then( + function() { + $scope.repositoryExitRemove(); + } + ); + }; + + // Return true if the repository is a PPA. + $scope.isPPA = function(data) { + if (!angular.isObject(data)) { + return false; + } + if (!angular.isString(data.url)) { + return false; + } + return ( + data.url.indexOf("ppa:") === 0 || + data.url.indexOf("ppa.launchpad.net") > -1 + ); + }; + + // Return true if the repository is a mirror. + $scope.isMirror = function(data) { + if (!angular.isObject(data)) { + return false; + } + if (!angular.isString(data.name)) { + return false; + } + return data.name === "main_archive" || data.name === "ports_archive"; + }; - // Called to enter edit mode for a repository. - $scope.repositoryEnterEdit = function(repository) { - $scope.newRepository = null; - $scope.deleteRepository = null; - $scope.editRepository = repository; - }; + // Called to enter edit mode for a repository. + $scope.repositoryEnterEdit = function(repository) { + $scope.newRepository = null; + $scope.deleteRepository = null; + $scope.editRepository = repository; + }; - // Called to exit edit mode for a repository. - $scope.repositoryExitEdit = function() { - $scope.editRepository = null; - }; + // Called to exit edit mode for a repository. + $scope.repositoryExitEdit = function() { + $scope.editRepository = null; + }; - // Called to start adding a new repository. - $scope.repositoryAdd = function(isPPA) { - var repo = { - name: "", - enabled: true, - url: "", - key: "", - arches: ["i386", "amd64"], - distributions: [], - components: [] - }; - if (isPPA) { - repo.url = "ppa:"; - } - $scope.newRepository = repo; + // Called to start adding a new repository. + $scope.repositoryAdd = function(isPPA) { + var repo = { + name: "", + enabled: true, + url: "", + key: "", + arches: ["i386", "amd64"], + distributions: [], + components: [] }; + if (isPPA) { + repo.url = "ppa:"; + } + $scope.newRepository = repo; + }; - // Called to cancel addind a new repository. - $scope.repositoryAddCancel = function() { - $scope.newRepository = null; - }; + // Called to cancel addind a new repository. + $scope.repositoryAddCancel = function() { + $scope.newRepository = null; + }; - // Return the node from either the machines, devices, or controllers - // manager. - function getNode(system_id) { - var node = MachinesManager.getItemFromList(system_id); - if (angular.isObject(node)) { - return node; - } - node = DevicesManager.getItemFromList(system_id); - if (angular.isObject(node)) { - return node; - } - node = ControllersManager.getItemFromList(system_id); - if (angular.isObject(node)) { - return node; - } - } - - // Return the name of the subnet. - $scope.getSubnetName = function(subnet) { - return SubnetsManager.getName(subnet); - }; + // Return the node from either the machines, devices, or controllers + // manager. + function getNode(system_id) { + var node = MachinesManager.getItemFromList(system_id); + if (angular.isObject(node)) { + return node; + } + node = DevicesManager.getItemFromList(system_id); + if (angular.isObject(node)) { + return node; + } + node = ControllersManager.getItemFromList(system_id); + if (angular.isObject(node)) { + return node; + } + } - // Return the text for the type of snippet. - $scope.getSnippetTypeText = function(snippet) { - if (angular.isString(snippet.node)) { - return "Node"; - } else if (angular.isNumber(snippet.subnet)) { - return "Subnet"; - } else { - return "Global"; - } - }; + // Return the name of the subnet. + $scope.getSubnetName = function(subnet) { + return SubnetsManager.getName(subnet); + }; + + // Return the text for the type of snippet. + $scope.getSnippetTypeText = function(snippet) { + if (angular.isString(snippet.node)) { + return "Node"; + } else if (angular.isNumber(snippet.subnet)) { + return "Subnet"; + } else { + return "Global"; + } + }; - // Return the object the snippet applies to. - $scope.getSnippetAppliesToObject = function(snippet) { - if (angular.isString(snippet.node)) { - return getNode(snippet.node); - } else if (angular.isNumber(snippet.subnet)) { - return SubnetsManager.getItemFromList(snippet.subnet); - } - }; + // Return the object the snippet applies to. + $scope.getSnippetAppliesToObject = function(snippet) { + if (angular.isString(snippet.node)) { + return getNode(snippet.node); + } else if (angular.isNumber(snippet.subnet)) { + return SubnetsManager.getItemFromList(snippet.subnet); + } + }; - // Return the applies to text that is disabled in none edit mode. - $scope.getSnippetAppliesToText = function(snippet) { - var obj = $scope.getSnippetAppliesToObject(snippet); - if (angular.isString(snippet.node) && angular.isObject(obj)) { - return obj.fqdn; - } else if (angular.isNumber(snippet.subnet) && - angular.isObject(obj)) { - return SubnetsManager.getName(obj); - } else { - return ""; - } - }; + // Return the applies to text that is disabled in none edit mode. + $scope.getSnippetAppliesToText = function(snippet) { + var obj = $scope.getSnippetAppliesToObject(snippet); + if (angular.isString(snippet.node) && angular.isObject(obj)) { + return obj.fqdn; + } else if (angular.isNumber(snippet.subnet) && angular.isObject(obj)) { + return SubnetsManager.getName(obj); + } else { + return ""; + } + }; - // Called to enter remove mode for a DHCP snippet. - $scope.snippetEnterRemove = function(snippet) { - $scope.newSnippet = null; - $scope.editSnippet = null; - $scope.deleteSnippet = snippet; - }; + // Called to enter remove mode for a DHCP snippet. + $scope.snippetEnterRemove = function(snippet) { + $scope.newSnippet = null; + $scope.editSnippet = null; + $scope.deleteSnippet = snippet; + }; - // Called to exit remove mode for a DHCP snippet. - $scope.snippetExitRemove = function() { - $scope.deleteSnippet = null; - }; + // Called to exit remove mode for a DHCP snippet. + $scope.snippetExitRemove = function() { + $scope.deleteSnippet = null; + }; - // Called to confirm the removal of a snippet. - $scope.snippetConfirmRemove = function() { - DHCPSnippetsManager.deleteItem($scope.deleteSnippet).then( - function() { - $scope.snippetExitRemove(); - }); - }; + // Called to confirm the removal of a snippet. + $scope.snippetConfirmRemove = function() { + DHCPSnippetsManager.deleteItem($scope.deleteSnippet).then(function() { + $scope.snippetExitRemove(); + }); + }; - // Called to enter edit mode for a DHCP snippet. - $scope.snippetEnterEdit = function(snippet) { - $scope.newSnippet = null; - $scope.deleteSnippet = null; - $scope.editSnippet = snippet; - $scope.editSnippet.type = $scope.getSnippetTypeText(snippet); - }; + // Called to enter edit mode for a DHCP snippet. + $scope.snippetEnterEdit = function(snippet) { + $scope.newSnippet = null; + $scope.deleteSnippet = null; + $scope.editSnippet = snippet; + $scope.editSnippet.type = $scope.getSnippetTypeText(snippet); + }; - // Called to exit edit mode for a DHCP snippet. - $scope.snippetExitEdit = function() { - $scope.editSnippet = null; - }; + // Called to exit edit mode for a DHCP snippet. + $scope.snippetExitEdit = function() { + $scope.editSnippet = null; + }; - // Called when the active toggle is changed. - $scope.snippetToggle = function(snippet) { - DHCPSnippetsManager.updateItem(snippet).then(null, - function(error) { - // Revert state change and clear toggling. - snippet.enabled = !snippet.enabled; - console.log(error); - }); - }; + // Called when the active toggle is changed. + $scope.snippetToggle = function(snippet) { + DHCPSnippetsManager.updateItem(snippet).then(null, function(error) { + // Revert state change and clear toggling. + snippet.enabled = !snippet.enabled; + console.log(error); + }); + }; - // Called to start adding a new snippet. - $scope.snippetAdd = function() { - $scope.editSnippet = null; - $scope.deleteSnippet = null; - $scope.newSnippet = { - name: "", - type: "Global", - enabled: true - }; + // Called to start adding a new snippet. + $scope.snippetAdd = function() { + $scope.editSnippet = null; + $scope.deleteSnippet = null; + $scope.newSnippet = { + name: "", + type: "Global", + enabled: true }; + }; - // Called to cancel addind a new snippet. - $scope.snippetAddCancel = function() { - $scope.newSnippet = null; - }; + // Called to cancel addind a new snippet. + $scope.snippetAddCancel = function() { + $scope.newSnippet = null; + }; - // Setup page variables based on section. - if ($routeParams.section === "dhcp") { - $rootScope.title = "DHCP snippets"; - $scope.currentpage = 'dhcp'; - } - else if ($routeParams.section === "repositories") { - $rootScope.title = "Package repositories"; - $scope.currentpage = 'repositories'; - } - - // Load the required managers. - ManagerHelperService.loadManagers($scope, [ - PackageRepositoriesManager, DHCPSnippetsManager, - MachinesManager, DevicesManager, ControllersManager, - SubnetsManager, GeneralManager]).then( - function() { - $scope.loading = false; - }); + // Setup page variables based on section. + if ($routeParams.section === "dhcp") { + $rootScope.title = "DHCP snippets"; + $scope.currentpage = "dhcp"; + } else if ($routeParams.section === "repositories") { + $rootScope.title = "Package repositories"; + $scope.currentpage = "repositories"; + } + + // Load the required managers. + ManagerHelperService.loadManagers($scope, [ + PackageRepositoriesManager, + DHCPSnippetsManager, + MachinesManager, + DevicesManager, + ControllersManager, + SubnetsManager, + GeneralManager + ]).then(function() { + $scope.loading = false; + }); } export default SettingsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/space_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/space_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/space_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/space_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,138 +6,157 @@ /* @ngInject */ function SpaceDetailsController( - $scope, $rootScope, $routeParams, $filter, $location, - SpacesManager, VLANsManager, SubnetsManager, FabricsManager, - UsersManager, ManagerHelperService, ErrorService) { - - // Set title and page. - $rootScope.title = "Loading..."; - - // Note: this value must match the top-level tab, in order for - // highlighting to occur properly. - $rootScope.page = "networks"; - - // Initial values. - $scope.space = null; - $scope.spaceManager = SpacesManager; - $scope.subnets = SubnetsManager.getItems(); - $scope.loaded = false; - $scope.editSummary = false; + $scope, + $rootScope, + $routeParams, + $filter, + $location, + SpacesManager, + VLANsManager, + SubnetsManager, + FabricsManager, + UsersManager, + ManagerHelperService, + ErrorService +) { + // Set title and page. + $rootScope.title = "Loading..."; + + // Note: this value must match the top-level tab, in order for + // highlighting to occur properly. + $rootScope.page = "networks"; + + // Initial values. + $scope.space = null; + $scope.spaceManager = SpacesManager; + $scope.subnets = SubnetsManager.getItems(); + $scope.loaded = false; + $scope.editSummary = false; + + // Updates the page title. + function updateTitle() { + $rootScope.title = $scope.space.name; + } + + // Called when the space has been loaded. + function spaceLoaded(space) { + $scope.space = space; + updateTitle(); + $scope.predicate = "[subnet_name, vlan_name]"; + $scope.$watch("subnets", updateSubnets, true); + updateSubnets(); + $scope.loaded = true; + } + + // Generate a table that can easily be rendered in the view. + function updateSubnets() { + var rows = []; + angular.forEach( + $filter("filter")($scope.subnets, { space: $scope.space.id }, true), + function(subnet) { + var vlan = VLANsManager.getItemFromList(subnet.vlan); + var fabric = FabricsManager.getItemFromList(vlan.fabric); + var row = { + vlan: vlan, + vlan_name: VLANsManager.getName(vlan), + subnet: subnet, + subnet_name: SubnetsManager.getName(subnet), + fabric: fabric, + fabric_name: fabric.name + }; + rows.push(row); + } + ); + $scope.rows = rows; + } + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Called when the "edit" button is cliked in the space summary + $scope.enterEditSummary = function() { + $scope.editSummary = true; + }; - // Updates the page title. - function updateTitle() { - $rootScope.title = $scope.space.name; - } + // Called when the "cancel" button is cliked in the space summary + $scope.exitEditSummary = function() { + $scope.editSummary = false; + }; - // Called when the space has been loaded. - function spaceLoaded(space) { - $scope.space = space; - updateTitle(); - $scope.predicate = "[subnet_name, vlan_name]"; - $scope.$watch("subnets", updateSubnets, true); - updateSubnets(); - $scope.loaded = true; + // Return true if this is the default Space + $scope.isDefaultSpace = function() { + if (!angular.isObject($scope.space)) { + return false; } + return $scope.space.id === 0; + }; - // Generate a table that can easily be rendered in the view. - function updateSubnets() { - var rows = []; - angular.forEach($filter('filter')( - $scope.subnets, { space: $scope.space.id }, true), - function(subnet) { - var vlan = VLANsManager.getItemFromList(subnet.vlan); - var fabric = FabricsManager.getItemFromList(vlan.fabric); - var row = { - vlan: vlan, - vlan_name: VLANsManager.getName(vlan), - subnet: subnet, - subnet_name: SubnetsManager.getName(subnet), - fabric: fabric, - fabric_name: fabric.name - }; - rows.push(row); - }); - $scope.rows = rows; + // Called to check if the space can be deleted. + $scope.canBeDeleted = function() { + if (angular.isObject($scope.space)) { + return $scope.space.subnet_ids.length === 0; } + return false; + }; - - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Called when the "edit" button is cliked in the space summary - $scope.enterEditSummary = function() { - $scope.editSummary = true; - }; - - // Called when the "cancel" button is cliked in the space summary - $scope.exitEditSummary = function() { - $scope.editSummary = false; - }; - - // Return true if this is the default Space - $scope.isDefaultSpace = function() { - if (!angular.isObject($scope.space)) { - return false; - } - return $scope.space.id === 0; - }; - - // Called to check if the space can be deleted. - $scope.canBeDeleted = function() { - if (angular.isObject($scope.space)) { - return $scope.space.subnet_ids.length === 0; - } - return false; - }; - - // Called when the delete space button is pressed. - $scope.deleteButton = function() { - $scope.error = null; - $scope.confirmingDelete = true; - }; - - // Called when the cancel delete space button is pressed. - $scope.cancelDeleteButton = function() { + // Called when the delete space button is pressed. + $scope.deleteButton = function() { + $scope.error = null; + $scope.confirmingDelete = true; + }; + + // Called when the cancel delete space button is pressed. + $scope.cancelDeleteButton = function() { + $scope.confirmingDelete = false; + }; + + // Called when the confirm delete space button is pressed. + $scope.deleteConfirmButton = function() { + SpacesManager.deleteSpace($scope.space).then( + function() { $scope.confirmingDelete = false; - }; - - // Called when the confirm delete space button is pressed. - $scope.deleteConfirmButton = function() { - SpacesManager.deleteSpace($scope.space).then(function() { - $scope.confirmingDelete = false; - $location.path("/networks"); - $location.search('by', 'space'); - }, function(error) { - $scope.error = - ManagerHelperService.parseValidationError(error); - }); - }; - - // Load all the required managers. - ManagerHelperService.loadManagers($scope, [ - SpacesManager, SubnetsManager, VLANsManager, FabricsManager, - UsersManager]).then(function() { - // Possibly redirected from another controller that already had - // this space set to active. Only call setActiveItem if not - // already the activeItem. - var activeSpace = SpacesManager.getActiveItem(); - var requestedSpace = parseInt($routeParams.space_id, 10); - if (isNaN(requestedSpace)) { - ErrorService.raiseError("Invalid space identifier."); - } else if (angular.isObject(activeSpace) && - activeSpace.id === requestedSpace) { - spaceLoaded(activeSpace); - } else { - SpacesManager.setActiveItem( - requestedSpace).then(function(space) { - spaceLoaded(space); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); + $location.path("/networks"); + $location.search("by", "space"); + }, + function(error) { + $scope.error = ManagerHelperService.parseValidationError(error); + } + ); + }; + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ + SpacesManager, + SubnetsManager, + VLANsManager, + FabricsManager, + UsersManager + ]).then(function() { + // Possibly redirected from another controller that already had + // this space set to active. Only call setActiveItem if not + // already the activeItem. + var activeSpace = SpacesManager.getActiveItem(); + var requestedSpace = parseInt($routeParams.space_id, 10); + if (isNaN(requestedSpace)) { + ErrorService.raiseError("Invalid space identifier."); + } else if ( + angular.isObject(activeSpace) && + activeSpace.id === requestedSpace + ) { + spaceLoaded(activeSpace); + } else { + SpacesManager.setActiveItem(requestedSpace).then( + function(space) { + spaceLoaded(space); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } + }); } export default SpaceDetailsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/subnet_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/subnet_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/subnet_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/subnet_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,400 +5,416 @@ */ export function filterSource() { - return function(subnets, source) { - var filtered = []; - angular.forEach(subnets, function(subnet) { - if (subnet.id !== source.id && - subnet.version === source.version) { - filtered.push(subnet); - } - }); - return filtered; - }; + return function(subnets, source) { + var filtered = []; + angular.forEach(subnets, function(subnet) { + if (subnet.id !== source.id && subnet.version === source.version) { + filtered.push(subnet); + } + }); + return filtered; + }; } /* @ngInject */ export function SubnetDetailsController( - $scope, $rootScope, $routeParams, $location, ConfigsManager, - SubnetsManager, SpacesManager, VLANsManager, UsersManager, - FabricsManager, StaticRoutesManager, ManagerHelperService, ErrorService, - ConverterService) { - - // Set title and page. - $rootScope.title = "Loading..."; - - // Note: this value must match the top-level tab, in order for - // highlighting to occur properly. - $rootScope.page = "networks"; - - // Initial values. - $scope.loaded = false; - $scope.subnet = null; - $scope.editSummary = false; - $scope.active_discovery_data = null; - $scope.active_discovery_interval = null; - $scope.subnets = SubnetsManager.getItems(); - $scope.subnetManager = SubnetsManager; - $scope.staticRoutes = StaticRoutesManager.getItems(); - $scope.staticRoutesManager = StaticRoutesManager; - $scope.space = null; - $scope.vlans = VLANsManager.getItems(); - $scope.fabrics = FabricsManager.getItems(); - $scope.actionError = null; - $scope.actionOption = null; - $scope.actionOptions = []; - $scope.reverse = false; - $scope.newStaticRoute = null; - $scope.editStaticRoute = null; - $scope.deleteStaticRoute = null; - - $scope.MAP_SUBNET_ACTION = { - name: "map_subnet", - title: "Map subnet" - }; - $scope.DELETE_ACTION = { - name: "delete", - title: "Delete" - }; - - // Alloc type mapping. - var ALLOC_TYPES = { - 0: 'Automatic', - 1: 'Static', - 4: 'User reserved', - 5: 'DHCP', - 6: 'Observed' - }; - - // Node type mapping. - var NODE_TYPES = { - 0: 'Machine', - 1: 'Device', - 2: 'Rack controller', - 3: 'Region controller', - 4: 'Rack and region controller', - 5: 'Chassis', - 6: 'Storage' - }; - - // Updates the page title. - function updateTitle() { - const subnet = $scope.subnet; - if (subnet && subnet.cidr) { - $rootScope.title = subnet.cidr; - if (subnet.name && subnet.cidr !== subnet.name) { - $rootScope.title += " (" + subnet.name + ")"; - } - } + $scope, + $rootScope, + $routeParams, + $location, + ConfigsManager, + SubnetsManager, + SpacesManager, + VLANsManager, + UsersManager, + FabricsManager, + StaticRoutesManager, + ManagerHelperService, + ErrorService, + ConverterService +) { + // Set title and page. + $rootScope.title = "Loading..."; + + // Note: this value must match the top-level tab, in order for + // highlighting to occur properly. + $rootScope.page = "networks"; + + // Initial values. + $scope.loaded = false; + $scope.subnet = null; + $scope.editSummary = false; + $scope.active_discovery_data = null; + $scope.active_discovery_interval = null; + $scope.subnets = SubnetsManager.getItems(); + $scope.subnetManager = SubnetsManager; + $scope.staticRoutes = StaticRoutesManager.getItems(); + $scope.staticRoutesManager = StaticRoutesManager; + $scope.space = null; + $scope.vlans = VLANsManager.getItems(); + $scope.fabrics = FabricsManager.getItems(); + $scope.actionError = null; + $scope.actionOption = null; + $scope.actionOptions = []; + $scope.reverse = false; + $scope.newStaticRoute = null; + $scope.editStaticRoute = null; + $scope.deleteStaticRoute = null; + + $scope.MAP_SUBNET_ACTION = { + name: "map_subnet", + title: "Map subnet" + }; + $scope.DELETE_ACTION = { + name: "delete", + title: "Delete" + }; + + // Alloc type mapping. + var ALLOC_TYPES = { + 0: "Automatic", + 1: "Static", + 4: "User reserved", + 5: "DHCP", + 6: "Observed" + }; + + // Node type mapping. + var NODE_TYPES = { + 0: "Machine", + 1: "Device", + 2: "Rack controller", + 3: "Region controller", + 4: "Rack and region controller", + 5: "Chassis", + 6: "Storage" + }; + + // Updates the page title. + function updateTitle() { + const subnet = $scope.subnet; + if (subnet && subnet.cidr) { + $rootScope.title = subnet.cidr; + if (subnet.name && subnet.cidr !== subnet.name) { + $rootScope.title += " (" + subnet.name + ")"; + } } + } - // Update the IP version of the CIDR. - function updateIPVersion() { - var ip = $scope.subnet.cidr.split('/')[0]; - if (ip.indexOf(':') === -1) { - $scope.ipVersion = 4; - } else { - $scope.ipVersion = 6; - } + // Update the IP version of the CIDR. + function updateIPVersion() { + var ip = $scope.subnet.cidr.split("/")[0]; + if (ip.indexOf(":") === -1) { + $scope.ipVersion = 4; + } else { + $scope.ipVersion = 6; } + } - // Sort for IP address. - $scope.ipSort = function(ipAddress) { - if ($scope.ipVersion === 4) { - return ConverterService.ipv4ToInteger(ipAddress.ip); - } else { - return ConverterService.ipv6Expand(ipAddress.ip); - } - }; - - // Set default predicate to the ipSort function. - $scope.predicate = $scope.ipSort; - - // Return the name of the allocation type. - $scope.getAllocType = function(allocType) { - var str = ALLOC_TYPES[allocType]; - if (angular.isString(str)) { - return str; - } else { - return "Unknown"; - } - }; - - $scope.getSubnetCIDR = function(destId) { - return SubnetsManager.getItemFromList(destId).cidr; - }; - - // Sort based on the name of the allocation type. - $scope.allocTypeSort = function(ipAddress) { - return $scope.getAllocType(ipAddress.alloc_type); - }; - - // Return the name of the node type for the given IP. - $scope.getUsageForIP = function(ip) { - if (angular.isObject(ip.node_summary)) { - var isContainer = ip.node_summary.is_container; - var nodeType = ip.node_summary.node_type; - if (nodeType === 1 && isContainer === true) { - return "Container"; - } - var str = NODE_TYPES[nodeType]; - if (angular.isString(str)) { - return str; - } else { - return "Unknown"; - } - } else if (angular.isObject(ip.bmcs)) { - return "BMC"; - } else if (angular.isObject(ip.dns_records)) { - return "DNS"; - } else { - return "Unknown"; - } - }; - - // Sort based on the node type string. - $scope.nodeTypeSort = function(ipAddress) { - return $scope.getUsageForIP(ipAddress); - }; + // Sort for IP address. + $scope.ipSort = function(ipAddress) { + if ($scope.ipVersion === 4) { + return ConverterService.ipv4ToInteger(ipAddress.ip); + } else { + return ConverterService.ipv6Expand(ipAddress.ip); + } + }; - // Sort based on the owner name. - $scope.ownerSort = function(ipAddress) { - var owner = ipAddress.user; - if (angular.isString(owner) && owner.length > 0) { - return owner; - } else { - return "MAAS"; - } - }; + // Set default predicate to the ipSort function. + $scope.predicate = $scope.ipSort; - // Called to change the sort order of the IP table. - $scope.sortIPTable = function(predicate) { - $scope.predicate = predicate; - $scope.reverse = !$scope.reverse; - }; + // Return the name of the allocation type. + $scope.getAllocType = function(allocType) { + var str = ALLOC_TYPES[allocType]; + if (angular.isString(str)) { + return str; + } else { + return "Unknown"; + } + }; - // Return the name of the VLAN. - $scope.getVLANName = function(vlan) { - return VLANsManager.getName(vlan); - }; + $scope.getSubnetCIDR = function(destId) { + return SubnetsManager.getItemFromList(destId).cidr; + }; + + // Sort based on the name of the allocation type. + $scope.allocTypeSort = function(ipAddress) { + return $scope.getAllocType(ipAddress.alloc_type); + }; + + // Return the name of the node type for the given IP. + $scope.getUsageForIP = function(ip) { + if (angular.isObject(ip.node_summary)) { + var isContainer = ip.node_summary.is_container; + var nodeType = ip.node_summary.node_type; + if (nodeType === 1 && isContainer === true) { + return "Container"; + } + var str = NODE_TYPES[nodeType]; + if (angular.isString(str)) { + return str; + } else { + return "Unknown"; + } + } else if (angular.isObject(ip.bmcs)) { + return "BMC"; + } else if (angular.isObject(ip.dns_records)) { + return "DNS"; + } else { + return "Unknown"; + } + }; - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; + // Sort based on the node type string. + $scope.nodeTypeSort = function(ipAddress) { + return $scope.getUsageForIP(ipAddress); + }; + + // Sort based on the owner name. + $scope.ownerSort = function(ipAddress) { + var owner = ipAddress.user; + if (angular.isString(owner) && owner.length > 0) { + return owner; + } else { + return "MAAS"; + } + }; - $scope.actionRetry = function() { - // When we clear actionError, the HTML will be re-rendered to - // hide the error message (and the user will be taken back to - // the previous action they were performing, since we reset - // the actionOption in the error handler. - $scope.actionError = null; - }; + // Called to change the sort order of the IP table. + $scope.sortIPTable = function(predicate) { + $scope.predicate = predicate; + $scope.reverse = !$scope.reverse; + }; + + // Return the name of the VLAN. + $scope.getVLANName = function(vlan) { + return VLANsManager.getName(vlan); + }; + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + $scope.actionRetry = function() { + // When we clear actionError, the HTML will be re-rendered to + // hide the error message (and the user will be taken back to + // the previous action they were performing, since we reset + // the actionOption in the error handler. + $scope.actionError = null; + }; - // Perform the action. - $scope.actionGo = function() { - if ($scope.actionOption.name === "map_subnet") { - SubnetsManager.scanSubnet($scope.subnet).then(function(result) { - if (result && result.scan_started_on.length === 0) { - $scope.actionError = - ManagerHelperService.parseValidationError( - result.result); - } else { - $scope.actionOption = null; - $scope.actionError = null; - } - }, function(error) { - $scope.actionError = - ManagerHelperService.parseValidationError(error); - }); - } else if ($scope.actionOption.name === "delete") { - SubnetsManager.deleteSubnet( - $scope.subnet).then(function(result) { - $scope.actionOption = null; - $scope.actionError = null; - $location.path("/networks"); - }, function(error) { - $scope.actionError = - ManagerHelperService.parseValidationError(error); - }); + // Perform the action. + $scope.actionGo = function() { + if ($scope.actionOption.name === "map_subnet") { + SubnetsManager.scanSubnet($scope.subnet).then( + function(result) { + if (result && result.scan_started_on.length === 0) { + $scope.actionError = ManagerHelperService.parseValidationError( + result.result + ); + } else { + $scope.actionOption = null; + $scope.actionError = null; + } + }, + function(error) { + $scope.actionError = ManagerHelperService.parseValidationError(error); + } + ); + } else if ($scope.actionOption.name === "delete") { + SubnetsManager.deleteSubnet($scope.subnet).then( + function(result) { + $scope.actionOption = null; + $scope.actionError = null; + $location.path("/networks"); + }, + function(error) { + $scope.actionError = ManagerHelperService.parseValidationError(error); } - }; + ); + } + }; - // Called when a action is selected. - $scope.actionChanged = function() { - $scope.actionError = null; - }; + // Called when a action is selected. + $scope.actionChanged = function() { + $scope.actionError = null; + }; - // Called when the "Cancel" button is pressed. - $scope.cancelAction = function() { - $scope.actionOption = null; - $scope.actionError = null; - }; + // Called when the "Cancel" button is pressed. + $scope.cancelAction = function() { + $scope.actionOption = null; + $scope.actionError = null; + }; - // Called when the managers load to populate the actions the user - // is allowed to perform. - $scope.updateActions = function() { - if (UsersManager.isSuperUser()) { - $scope.actionOptions = [ - $scope.MAP_SUBNET_ACTION, - $scope.DELETE_ACTION - ]; - } else { - $scope.actionOptions = []; - } - }; + // Called when the managers load to populate the actions the user + // is allowed to perform. + $scope.updateActions = function() { + if (UsersManager.isSuperUser()) { + $scope.actionOptions = [$scope.MAP_SUBNET_ACTION, $scope.DELETE_ACTION]; + } else { + $scope.actionOptions = []; + } + }; - // Called when the "edit" button is cliked in the subnet summary - $scope.enterEditSummary = function() { - $scope.editSummary = true; - }; + // Called when the "edit" button is cliked in the subnet summary + $scope.enterEditSummary = function() { + $scope.editSummary = true; + }; - // Called when the "cancel" button is cliked in the subnet summary - $scope.exitEditSummary = function() { - $scope.editSummary = false; - }; + // Called when the "cancel" button is cliked in the subnet summary + $scope.exitEditSummary = function() { + $scope.editSummary = false; + }; - // Called by maas-obj-form before it saves the subnet. The passed - // subnet is the object right before its sent over the websocket. - $scope.subnetPreSave = function(subnet, changedFields) { - // Adjust the subnet object if the fabric changed. - if (changedFields.indexOf("fabric") !== -1) { - // Fabric changed, the websocket expects VLAN to be updated, so - // we set the VLAN to the default VLAN for the new fabric. - subnet.vlan = FabricsManager.getItemFromList( - subnet.fabric).default_vlan_id; - } - return subnet; - }; + // Called by maas-obj-form before it saves the subnet. The passed + // subnet is the object right before its sent over the websocket. + $scope.subnetPreSave = function(subnet, changedFields) { + // Adjust the subnet object if the fabric changed. + if (changedFields.indexOf("fabric") !== -1) { + // Fabric changed, the websocket expects VLAN to be updated, so + // we set the VLAN to the default VLAN for the new fabric. + subnet.vlan = FabricsManager.getItemFromList( + subnet.fabric + ).default_vlan_id; + } + return subnet; + }; - // Called to start adding a new static route. - $scope.addStaticRoute = function() { - $scope.editStaticRoute = null; - $scope.deleteStaticRoute = null; - $scope.newStaticRoute = { - source: $scope.subnet.id, - gateway_ip: "", - destination: null, - metric: 0 - }; + // Called to start adding a new static route. + $scope.addStaticRoute = function() { + $scope.editStaticRoute = null; + $scope.deleteStaticRoute = null; + $scope.newStaticRoute = { + source: $scope.subnet.id, + gateway_ip: "", + destination: null, + metric: 0 }; + }; - // Cancel adding the new static route. - $scope.cancelAddStaticRoute = function() { - $scope.newStaticRoute = null; - }; + // Cancel adding the new static route. + $scope.cancelAddStaticRoute = function() { + $scope.newStaticRoute = null; + }; - // Return true if the static route is in edit mode. - $scope.isStaticRouteInEditMode = function(route) { - return $scope.editStaticRoute === route; - }; + // Return true if the static route is in edit mode. + $scope.isStaticRouteInEditMode = function(route) { + return $scope.editStaticRoute === route; + }; - // Toggle edit mode for the static route. - $scope.staticRouteToggleEditMode = function(route) { - $scope.newStaticRoute = null; - $scope.deleteStaticRoute = null; - if ($scope.isStaticRouteInEditMode(route)) { - $scope.editStaticRoute = null; - } else { - $scope.editStaticRoute = route; - } - }; + // Toggle edit mode for the static route. + $scope.staticRouteToggleEditMode = function(route) { + $scope.newStaticRoute = null; + $scope.deleteStaticRoute = null; + if ($scope.isStaticRouteInEditMode(route)) { + $scope.editStaticRoute = null; + } else { + $scope.editStaticRoute = route; + } + }; - // Return true if the static route is in delete mode. - $scope.isStaticRouteInDeleteMode = function(route) { - return $scope.deleteStaticRoute === route; - }; + // Return true if the static route is in delete mode. + $scope.isStaticRouteInDeleteMode = function(route) { + return $scope.deleteStaticRoute === route; + }; - // Enter delete mode for the static route. - $scope.staticRouteEnterDeleteMode = function(route) { - $scope.newStaticRoute = null; - $scope.editStaticRoute = null; - $scope.deleteStaticRoute = route; - }; + // Enter delete mode for the static route. + $scope.staticRouteEnterDeleteMode = function(route) { + $scope.newStaticRoute = null; + $scope.editStaticRoute = null; + $scope.deleteStaticRoute = route; + }; - // Exit delete mode for the statc route. - $scope.staticRouteCancelDelete = function() { - $scope.deleteStaticRoute = null; - }; + // Exit delete mode for the statc route. + $scope.staticRouteCancelDelete = function() { + $scope.deleteStaticRoute = null; + }; - // Perform the delete operation on the static route. - $scope.staticRouteConfirmDelete = function() { - StaticRoutesManager.deleteItem($scope.deleteStaticRoute).then( - function() { - $scope.deleteStaticRoute = null; - }); - }; + // Perform the delete operation on the static route. + $scope.staticRouteConfirmDelete = function() { + StaticRoutesManager.deleteItem($scope.deleteStaticRoute).then(function() { + $scope.deleteStaticRoute = null; + }); + }; - // Called when the subnet has been loaded. - function subnetLoaded(subnet) { - $scope.subnet = subnet; - $scope.loaded = true; - - updateTitle(); - - // Watch the vlan and fabric field so if its changed on the subnet - // we make sure that the fabric is updated. It is possible that - // fabric is removed from the subnet since it is injected from this - // controller, so when it is removed we add it back. - var updateFabric = function() { - $scope.subnet.fabric = ( - VLANsManager.getItemFromList($scope.subnet.vlan).fabric); - $scope.subnet.fabric_name = ( - FabricsManager.getItemFromList(subnet.fabric).name); - }; - var updateSpace = function() { - $scope.space = ( - SpacesManager.getItemFromList($scope.subnet.space)); - }; - var updateVlan = function() { - var vlan = VLANsManager.getItemFromList($scope.subnet.vlan); - $scope.subnet.vlan_name = ( - VLANsManager.getName(vlan) - ); - }; + // Called when the subnet has been loaded. + function subnetLoaded(subnet) { + $scope.subnet = subnet; + $scope.loaded = true; + + updateTitle(); + + // Watch the vlan and fabric field so if its changed on the subnet + // we make sure that the fabric is updated. It is possible that + // fabric is removed from the subnet since it is injected from this + // controller, so when it is removed we add it back. + var updateFabric = function() { + $scope.subnet.fabric = VLANsManager.getItemFromList( + $scope.subnet.vlan + ).fabric; + $scope.subnet.fabric_name = FabricsManager.getItemFromList( + subnet.fabric + ).name; + }; + var updateSpace = function() { + $scope.space = SpacesManager.getItemFromList($scope.subnet.space); + }; + var updateVlan = function() { + var vlan = VLANsManager.getItemFromList($scope.subnet.vlan); + $scope.subnet.vlan_name = VLANsManager.getName(vlan); + }; + + $scope.$watch("subnet.fabric", updateFabric); + $scope.$watch("subnet.fabric_name", updateFabric); + $scope.$watch("subnet.vlan", updateFabric); + $scope.$watch("subnet.vlan_name", updateVlan); + $scope.$watch("subnet.space", updateSpace); + $scope.$watch("subnet.cidr", updateIPVersion); + } + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ + ConfigsManager, + SubnetsManager, + SpacesManager, + VLANsManager, + UsersManager, + FabricsManager, + StaticRoutesManager + ]).then(function() { + $scope.updateActions(); + $scope.active_discovery_data = ConfigsManager.getItemFromList( + "active_discovery_interval" + ); + // Find active discovery interval + angular.forEach($scope.active_discovery_data.choices, function(choice) { + if (choice[0] === $scope.active_discovery_data.value) { + $scope.active_discovery_interval = choice[1]; + } + }); - $scope.$watch("subnet.fabric", updateFabric); - $scope.$watch("subnet.fabric_name", updateFabric); - $scope.$watch("subnet.vlan", updateFabric); - $scope.$watch("subnet.vlan_name", updateVlan); - $scope.$watch("subnet.space", updateSpace); - $scope.$watch("subnet.cidr", updateIPVersion); - } - - // Load all the required managers. - ManagerHelperService.loadManagers($scope, [ - ConfigsManager, SubnetsManager, SpacesManager, VLANsManager, - UsersManager, FabricsManager, StaticRoutesManager - ]).then(function() { - - $scope.updateActions(); - $scope.active_discovery_data = ConfigsManager.getItemFromList( - "active_discovery_interval"); - // Find active discovery interval - angular.forEach( - $scope.active_discovery_data.choices, function(choice) { - if (choice[0] === $scope.active_discovery_data.value) { - $scope.active_discovery_interval = choice[1]; - } - }); - - // Possibly redirected from another controller that already had - // this subnet set to active. Only call setActiveItem if not - // already the activeItem. - var activeSubnet = SubnetsManager.getActiveItem(); - var requestedSubnet = parseInt($routeParams.subnet_id, 10); - if (isNaN(requestedSubnet)) { - ErrorService.raiseError("Invalid subnet identifier."); - } else if (angular.isObject(activeSubnet) && - activeSubnet.id === requestedSubnet) { - subnetLoaded(activeSubnet); - } else { - SubnetsManager.setActiveItem( - requestedSubnet).then(function(subnet) { - subnetLoaded(subnet); - }, function(error) { - ErrorService.raiseError(error); - }); + // Possibly redirected from another controller that already had + // this subnet set to active. Only call setActiveItem if not + // already the activeItem. + var activeSubnet = SubnetsManager.getActiveItem(); + var requestedSubnet = parseInt($routeParams.subnet_id, 10); + if (isNaN(requestedSubnet)) { + ErrorService.raiseError("Invalid subnet identifier."); + } else if ( + angular.isObject(activeSubnet) && + activeSubnet.id === requestedSubnet + ) { + subnetLoaded(activeSubnet); + } else { + SubnetsManager.setActiveItem(requestedSubnet).then( + function(subnet) { + subnetLoaded(subnet); + }, + function(error) { + ErrorService.raiseError(error); } - }); + ); + } + }); } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_add_device.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_add_device.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_add_device.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_add_device.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,785 +4,778 @@ * Unit tests for AddDeviceController. */ -describe("AddDeviceController", function() { +import { makeName } from "testing/utils"; - // Load the MAAS module. - beforeEach(module("MAAS")); +describe("AddDeviceController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Grab the needed angular pieces. - var $controller, $rootScope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the AddDeviceController - // and mock the websocket connection. - var SubnetsManager, DevicesManager, DomainsManager, ManagerHelperService; - var ValidationService, RegionConnection, webSocket; - beforeEach(inject(function($injector) { - SubnetsManager = $injector.get("SubnetsManager"); - DevicesManager = $injector.get("DevicesManager"); - DomainsManager = $injector.get("DomainsManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ValidationService = $injector.get("ValidationService"); - RegionConnection = $injector.get("RegionConnection"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Create the parent scope and the scope for the controller. - var parentScope, $scope; - beforeEach(function() { - parentScope = $rootScope.$new(); - parentScope.addDeviceScope = null; - $scope = parentScope.$new(); - }); - - // Makes the AddDeviceController - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - return $controller("AddDeviceController", { - $scope: $scope, - SubnetsManager: SubnetsManager, - DevicesManager: DevicesManager, - DomainsManager: DomainsManager, - ValidationService: ValidationService, - ManagerHelperService: ManagerHelperService - }); + // Grab the needed angular pieces. + var $controller, $rootScope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the AddDeviceController + // and mock the websocket connection. + var SubnetsManager, DevicesManager, DomainsManager, ManagerHelperService; + var ValidationService, RegionConnection, webSocket; + beforeEach(inject(function($injector) { + SubnetsManager = $injector.get("SubnetsManager"); + DevicesManager = $injector.get("DevicesManager"); + DomainsManager = $injector.get("DomainsManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ValidationService = $injector.get("ValidationService"); + RegionConnection = $injector.get("RegionConnection"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Create the parent scope and the scope for the controller. + var parentScope, $scope; + beforeEach(function() { + parentScope = $rootScope.$new(); + parentScope.addDeviceScope = null; + $scope = parentScope.$new(); + }); + + // Makes the AddDeviceController + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - - // Make the AddDeviceController with the $scope.device already initialized. - function makeControllerWithDevice() { - var defer = $q.defer(); - var controller = makeController(defer); - $scope.show(); - defer.resolve(); - $rootScope.$digest(); - return controller; + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + return $controller("AddDeviceController", { + $scope: $scope, + SubnetsManager: SubnetsManager, + DevicesManager: DevicesManager, + DomainsManager: DomainsManager, + ValidationService: ValidationService, + ManagerHelperService: ManagerHelperService + }); + } + + // Make the AddDeviceController with the $scope.device already initialized. + function makeControllerWithDevice() { + var defer = $q.defer(); + var controller = makeController(defer); + $scope.show(); + defer.resolve(); + $rootScope.$digest(); + return controller; + } + + // Generating random subnets is difficult, so we just use an array + // of random subnets and select one from it. + var subnets = [ + { + cidr: "192.168.1.0/24", + name: "192.168.1.0/24", + first_ip: "192.168.1.1" + }, + { + cidr: "192.168.2.0/24", + name: "192.168.2.0/24", + first_ip: "192.168.2.1" + }, + { + cidr: "172.16.0.0/16", + name: "172.16.0.0/16", + first_ip: "172.16.1.1" + }, + { + cidr: "172.17.0.0/16", + name: "172.17.0.0/16", + first_ip: "172.17.1.1" } - - // Generating random subnets is difficult, so we just use an array - // of random subnets and select one from it. - var subnets = [ - { - cidr: "192.168.1.0/24", - name: "192.168.1.0/24", - first_ip: "192.168.1.1" - }, - { - cidr: "192.168.2.0/24", - name: "192.168.2.0/24", - first_ip: "192.168.2.1" - }, - { - cidr: "172.16.0.0/16", - name: "172.16.0.0/16", - first_ip: "172.16.1.1" - }, - { - cidr: "172.17.0.0/16", - name: "172.17.0.0/16", - first_ip: "172.17.1.1" - } - ]; - var _nextSubnet = 0; - beforeEach(function() { - // Reset the next network before each test. - _nextSubnet = 0; - }); - - // Make a subnet. - var _subnetId = 0; - function makeSubnet() { - if(_nextSubnet >= subnets.length) { - throw new Error("Out of fake subnets."); - } - var subnet = subnets[_nextSubnet++]; - subnet.id = _subnetId++; - return subnet; + ]; + var _nextSubnet = 0; + beforeEach(function() { + // Reset the next network before each test. + _nextSubnet = 0; + }); + + // Make a subnet. + var _subnetId = 0; + function makeSubnet() { + if (_nextSubnet >= subnets.length) { + throw new Error("Out of fake subnets."); } - - // Make a interface - function makeInterface(mac, ipAssignment, subnetId, ipAddress) { - if(angular.isUndefined(mac)) { - mac = ""; - } - if(angular.isUndefined(ipAssignment)) { - ipAssignment = null; - } - if(angular.isUndefined(subnetId)) { - subnetId = null; - } - if(angular.isUndefined(ipAddress)) { - ipAddress = ""; - } - return { - mac: mac, - ipAssignment: ipAssignment, - subnetId: subnetId, - ipAddress: ipAddress - }; + var subnet = subnets[_nextSubnet++]; + subnet.id = _subnetId++; + return subnet; + } + + // Make a interface + function makeInterface(mac, ipAssignment, subnetId, ipAddress) { + if (angular.isUndefined(mac)) { + mac = ""; } - - it("sets addDeviceScope on $scope.$parent", function() { - makeController(); - expect(parentScope.addDeviceScope).toBe($scope); - }); - - it("sets initial values on $scope", function() { - makeController(); - expect($scope.viewable).toBe(false); - expect($scope.error).toBe(null); - expect($scope.ipAssignments).toEqual([ - { - name: "external", - title: "External" - }, - { - name: "dynamic", - title: "Dynamic" + if (angular.isUndefined(ipAssignment)) { + ipAssignment = null; + } + if (angular.isUndefined(subnetId)) { + subnetId = null; + } + if (angular.isUndefined(ipAddress)) { + ipAddress = ""; + } + return { + mac: mac, + ipAssignment: ipAssignment, + subnetId: subnetId, + ipAddress: ipAddress + }; + } + + it("sets addDeviceScope on $scope.$parent", function() { + makeController(); + expect(parentScope.addDeviceScope).toBe($scope); + }); + + it("sets initial values on $scope", function() { + makeController(); + expect($scope.viewable).toBe(false); + expect($scope.error).toBe(null); + expect($scope.ipAssignments).toEqual([ + { + name: "external", + title: "External" + }, + { + name: "dynamic", + title: "Dynamic" + }, + { + name: "static", + title: "Static" + } + ]); + }); + + it("doesn't call loadManagers when initialized", function() { + // add_hardware is loaded on the listing and details page. Managers + // should be loaded when shown. Otherwise all Zones and Domains are + // loaded and updated even though they are not needed. + makeController(); + expect(ManagerHelperService.loadManagers).not.toHaveBeenCalled(); + }); + + describe("show", function() { + it("does nothing if already viewable", function() { + var defer = $q.defer(); + makeController(defer); + $scope.viewable = true; + var name = makeName("name"); + $scope.device = { name: name }; + $scope.show(); + + defer.resolve(); + $rootScope.$digest(); + // The device name should have stayed the same, showing that + // the call did nothing. + expect($scope.device.name).toBe(name); + }); + + it("clears device and sets viewable to true", function() { + var defer = $q.defer(); + makeController(defer); + $scope.device = { name: makeName("name") }; + $scope.show(); + + defer.resolve(); + $rootScope.$digest(); + expect($scope.device).toEqual({ + name: "", + domain: undefined, + interfaces: [ + { + mac: "", + ipAssignment: null, + subnetId: null, + ipAddress: "" + } + ] + }); + expect($scope.domains).toBe(DomainsManager.getItems()); + expect($scope.viewable).toBe(true); + }); + + it("calls loadManagers for Subnets/Domains Manager", function() { + var defer = $q.defer(); + makeController(defer); + $scope.show(); + + defer.resolve(); + $rootScope.$digest(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + SubnetsManager, + DomainsManager + ]); + }); + }); + + describe("hide", function() { + it("sets viewable to false", function() { + makeController(); + $scope.viewable = true; + $scope.hide(); + expect($scope.viewable).toBe(false); + }); + + it("emits event addDeviceHidden", function(done) { + makeController(); + $scope.viewable = true; + $scope.$on("addDeviceHidden", function() { + done(); + }); + $scope.hide(); + }); + + it("unloadManagers", function() { + var unloadManagers = spyOn(ManagerHelperService, "unloadManagers"); + makeController(); + $scope.viewable = true; + $scope.hide(); + expect(unloadManagers).toHaveBeenCalledWith($scope, [ + SubnetsManager, + DomainsManager + ]); + }); + }); + + describe("nameHasError", function() { + it("returns false if name is empty", function() { + makeController(); + expect($scope.nameHasError()).toBe(false); + }); + + it("returns false if valid name", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + expect($scope.nameHasError()).toBe(false); + }); + + it("returns true if invalid name", function() { + makeControllerWithDevice(); + $scope.device.name = "a_bc.local"; + expect($scope.nameHasError()).toBe(true); + }); + }); + + describe("macHasError", function() { + it("returns false if mac is empty", function() { + makeController(); + var nic = makeInterface(); + expect($scope.macHasError(nic)).toBe(false); + }); + + it("returns false if valid mac", function() { + makeControllerWithDevice(); + var nic = makeInterface("00:00:11:22:33:44"); + expect($scope.macHasError(nic)).toBe(false); + }); + + it("returns false if not repeat mac", function() { + makeControllerWithDevice(); + var nic = makeInterface("00:00:11:22:33:44"); + var nic2 = makeInterface("00:00:11:22:33:55"); + $scope.device.interfaces = [nic, nic2]; + expect($scope.macHasError(nic)).toBe(false); + expect($scope.macHasError(nic2)).toBe(false); + }); + + it("returns true if invalid mac", function() { + makeController(); + var nic = makeInterface("00:00:11:22:33"); + expect($scope.macHasError(nic)).toBe(true); + }); + + it("returns true if repeat mac", function() { + makeControllerWithDevice(); + var nic = makeInterface("00:00:11:22:33:44"); + var nic2 = makeInterface("00:00:11:22:33:44"); + $scope.device.interfaces = [nic, nic2]; + expect($scope.macHasError(nic)).toBe(true); + expect($scope.macHasError(nic2)).toBe(true); + }); + }); + + describe("ipHasError", function() { + it("returns false if ip is empty", function() { + makeController(); + var nic = makeInterface(); + expect($scope.ipHasError(nic)).toBe(false); + }); + + it("returns false if valid ipv4", function() { + makeController(); + var nic = makeInterface(); + nic.ipAddress = "192.168.1.1"; + expect($scope.ipHasError(nic)).toBe(false); + }); + + it("returns false if valid ipv6", function() { + makeController(); + var nic = makeInterface(); + nic.ipAddress = "2001:db8::1"; + expect($scope.ipHasError(nic)).toBe(false); + }); + + it("returns true if invalid ipv4", function() { + makeController(); + var nic = makeInterface(); + nic.ipAddress = "192.168.1"; + expect($scope.ipHasError(nic)).toBe(true); + }); + + it("returns true if invalid ipv6", function() { + makeController(); + var nic = makeInterface(); + nic.ipAddress = "2001::db8::1"; + expect($scope.ipHasError(nic)).toBe(true); + }); + + it("returns false if external ip out of managed network", function() { + makeController(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + // No class A address is in the fake networks. + var deviceInterface = makeInterface(); + deviceInterface.ipAddress = "10.0.1.1"; + deviceInterface.ipAssignment = { + name: "external" + }; + expect($scope.ipHasError(deviceInterface)).toBe(false); + }); + + it("returns true if external ip in managed network", function() { + makeController(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + var deviceInterface = makeInterface(); + deviceInterface.ipAddress = subnet.first_ip; + deviceInterface.ipAssignment = { + name: "external" + }; + expect($scope.ipHasError(deviceInterface)).toBe(true); + }); + + it("returns false if static in managed network", function() { + makeController(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + var deviceInterface = makeInterface(); + deviceInterface.ipAddress = subnet.first_ip; + deviceInterface.ipAssignment = { + name: "static" + }; + expect($scope.ipHasError(deviceInterface)).toBe(false); + }); + + it("returns false if static ip in select network", function() { + makeController(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + var deviceInterface = makeInterface(); + deviceInterface.ipAddress = subnet.first_ip; + deviceInterface.subnetId = subnet.id; + deviceInterface.ipAssignment = { + name: "static" + }; + expect($scope.ipHasError(deviceInterface)).toBe(false); + }); + + it("returns true if static ip out of select network", function() { + makeController(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + var deviceInterface = makeInterface(); + deviceInterface.ipAddress = "120.22.22.1"; + deviceInterface.subnetId = subnet.id; + deviceInterface.ipAssignment = { + name: "static" + }; + expect($scope.ipHasError(deviceInterface)).toBe(true); + }); + }); + + describe("deviceHasError", function() { + it("returns true if name empty", function() { + makeControllerWithDevice(); + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns true if mac empty", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns true if name invalid", function() { + makeControllerWithDevice(); + $scope.device.name = "ab_c.local"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns true if mac invalid", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44"; + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns true if missing ip assignment selection", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns false if dynamic ip assignment selection", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + expect($scope.deviceHasError()).toBe(false); + }); + + it("returns true if external ip assignment and ip empty", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "external" + }; + $scope.device.interfaces[0].ipAddress = ""; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns true if external ip assignment and ip invalid", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "external" + }; + $scope.device.interfaces[0].ipAddress = "192.168"; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns false if external ip assignment and ip valid", function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "external" + }; + $scope.device.interfaces[0].ipAddress = "192.168.1.1"; + expect($scope.deviceHasError()).toBe(false); + }); + + it(`returns true if static ip assignment + and no cluster interface`, function() { + makeControllerWithDevice(); + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "static" + }; + expect($scope.deviceHasError()).toBe(true); + }); + + it("returns false if static ip assignment and subnet", function() { + makeControllerWithDevice(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "static" + }; + $scope.device.interfaces[0].subnetId = subnet.id; + expect($scope.deviceHasError()).toBe(false); + }); + + it( + "returns true if static ip assignment, subnet, and " + + "invalid ip address", + function() { + makeControllerWithDevice(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "static" + }; + $scope.device.interfaces[0].subnetId = subnet.id; + $scope.device.interfaces[0].ipAddress = "192.168"; + expect($scope.deviceHasError()).toBe(true); + } + ); + + it( + "returns true if static ip assignment, subnet, and " + + "ip address out of network", + function() { + makeControllerWithDevice(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "static" + }; + $scope.device.interfaces[0].subnetId = subnet.id; + $scope.device.interfaces[0].ipAddress = "122.10.1.0"; + expect($scope.deviceHasError()).toBe(true); + } + ); + + it( + "returns false if static ip assignment, subnet, and " + + "ip address in network", + function() { + makeControllerWithDevice(); + var subnet = makeSubnet(); + SubnetsManager._items = [subnet]; + $scope.subnets = [subnet]; + $scope.device.name = "abc"; + $scope.device.interfaces[0].mac = "00:11:22:33:44:55"; + $scope.device.interfaces[0].ipAssignment = { + name: "static" + }; + $scope.device.interfaces[0].subnetId = subnet.id; + $scope.device.interfaces[0].ipAddress = subnet.first_ip; + expect($scope.deviceHasError()).toBe(false); + } + ); + }); + + describe("addInterface", function() { + it("adds another interface", function() { + makeControllerWithDevice(); + $scope.addInterface(); + expect($scope.device.interfaces.length).toBe(2); + }); + }); + + describe("isPrimaryInterface", function() { + it("returns true for first interface", function() { + makeControllerWithDevice(); + $scope.addInterface(); + expect($scope.isPrimaryInterface($scope.device.interfaces[0])).toBe(true); + }); + + it("returns false for second interface", function() { + makeControllerWithDevice(); + $scope.addInterface(); + expect($scope.isPrimaryInterface($scope.device.interfaces[1])).toBe( + false + ); + }); + }); + + describe("deleteInterface", function() { + it("doesnt remove primary interface", function() { + makeControllerWithDevice(); + var nic = $scope.device.interfaces[0]; + $scope.deleteInterface(nic); + expect($scope.device.interfaces[0]).toBe(nic); + }); + + it("removes interface", function() { + makeControllerWithDevice(); + $scope.addInterface(); + var nic = $scope.device.interfaces[1]; + $scope.deleteInterface(nic); + expect($scope.device.interfaces.indexOf(nic)).toBe(-1); + }); + }); + + describe("cancel", function() { + it("clears device", function() { + makeControllerWithDevice(); + $scope.device.name = makeName("name"); + $scope.cancel(); + expect($scope.device.name).toBe(""); + }); + + it("calls hide", function() { + makeController(); + spyOn($scope, "hide"); + $scope.cancel(); + expect($scope.hide).toHaveBeenCalled(); + }); + }); + + describe("save", function() { + it("doest nothing if device in error", function() { + makeController(); + var error = makeName("error"); + $scope.error = error; + spyOn($scope, "deviceHasError").and.returnValue(true); + $scope.save(); + // Error would have been cleared if save did anything. + expect($scope.error).toBe(error); + }); + + it("clears error before calling create", function() { + makeControllerWithDevice(); + $scope.error = makeName("error"); + spyOn($scope, "deviceHasError").and.returnValue(false); + spyOn(DevicesManager, "create").and.returnValue($q.defer().promise); + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + $scope.save(); + expect($scope.error).toBeNull(); + }); + + it("calls create with converted device", function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "deviceHasError").and.returnValue(false); + spyOn(DevicesManager, "create").and.returnValue($q.defer().promise); + var name = makeName("name"); + var domain = makeName("domain"); + var mac = makeName("mac"); + var assignment = "static"; + var ipAddress = makeName("ip"); + var subnet = makeSubnet(); + $scope.device = { + name: name, + domain: domain, + interfaces: [ + { + mac: mac, + ipAssignment: { + name: assignment }, - { - name: "static", - title: "Static" - } - ]); - }); - - it("doesn't call loadManagers when initialized", function() { - // add_hardware is loaded on the listing and details page. Managers - // should be loaded when shown. Otherwise all Zones and Domains are - // loaded and updated even though they are not needed. - makeController(); - expect(ManagerHelperService.loadManagers).not.toHaveBeenCalled(); - }); - - describe("show", function() { - - it("does nothing if already viewable", function() { - var defer = $q.defer(); - makeController(defer); - $scope.viewable = true; - var name = makeName("name"); - $scope.device = {name: name}; - $scope.show(); - - defer.resolve(); - $rootScope.$digest(); - // The device name should have stayed the same, showing that - // the call did nothing. - expect($scope.device.name).toBe(name); - }); - - it("clears device and sets viewable to true", function() { - var defer = $q.defer(); - makeController(defer); - $scope.device = {name: makeName("name")}; - $scope.show(); - - defer.resolve(); - $rootScope.$digest(); - expect($scope.device).toEqual({ - name: "", - domain: undefined, - interfaces: [{ - mac: "", - ipAssignment: null, - subnetId: null, - ipAddress: "" - }] - }); - expect($scope.domains).toBe(DomainsManager.getItems()); - expect($scope.viewable).toBe(true); - }); - - it("calls loadManagers for Subnets/Domains Manager", function() { - var defer = $q.defer(); - makeController(defer); - $scope.show(); - - defer.resolve(); - $rootScope.$digest(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [SubnetsManager, DomainsManager]); - }); - }); - - describe("hide", function() { - - it("sets viewable to false", function() { - makeController(); - $scope.viewable = true; - $scope.hide(); - expect($scope.viewable).toBe(false); - }); - - it("emits event addDeviceHidden", function(done) { - makeController(); - $scope.viewable = true; - $scope.$on("addDeviceHidden", function() { - done(); - }); - $scope.hide(); - }); - - it("unloadManagers", function() { - var unloadManagers = spyOn(ManagerHelperService, "unloadManagers"); - makeController(); - $scope.viewable = true; - $scope.hide(); - expect(unloadManagers).toHaveBeenCalledWith( - $scope, [SubnetsManager, DomainsManager]); - }); - }); - - describe("nameHasError", function() { - - it("returns false if name is empty", function() { - makeController(); - expect($scope.nameHasError()).toBe(false); - }); - - it("returns false if valid name", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - expect($scope.nameHasError()).toBe(false); - }); - - it("returns true if invalid name", function() { - makeControllerWithDevice(); - $scope.device.name = "a_bc.local"; - expect($scope.nameHasError()).toBe(true); - }); - }); - - describe("macHasError", function() { - - it("returns false if mac is empty", function() { - makeController(); - var nic = makeInterface(); - expect($scope.macHasError(nic)).toBe(false); - }); - - it("returns false if valid mac", function() { - makeControllerWithDevice(); - var nic = makeInterface("00:00:11:22:33:44"); - expect($scope.macHasError(nic)).toBe(false); - }); - - it("returns false if not repeat mac", function() { - makeControllerWithDevice(); - var nic = makeInterface("00:00:11:22:33:44"); - var nic2 = makeInterface("00:00:11:22:33:55"); - $scope.device.interfaces = [ - nic, - nic2 - ]; - expect($scope.macHasError(nic)).toBe(false); - expect($scope.macHasError(nic2)).toBe(false); - }); - - it("returns true if invalid mac", function() { - makeController(); - var nic = makeInterface("00:00:11:22:33"); - expect($scope.macHasError(nic)).toBe(true); - }); - - it("returns true if repeat mac", function() { - makeControllerWithDevice(); - var nic = makeInterface("00:00:11:22:33:44"); - var nic2 = makeInterface("00:00:11:22:33:44"); - $scope.device.interfaces = [ - nic, - nic2 - ]; - expect($scope.macHasError(nic)).toBe(true); - expect($scope.macHasError(nic2)).toBe(true); - }); - }); - - describe("ipHasError", function() { - - it("returns false if ip is empty", function() { - makeController(); - var nic = makeInterface(); - expect($scope.ipHasError(nic)).toBe(false); - }); - - it("returns false if valid ipv4", function() { - makeController(); - var nic = makeInterface(); - nic.ipAddress = "192.168.1.1"; - expect($scope.ipHasError(nic)).toBe(false); - }); - - it("returns false if valid ipv6", function() { - makeController(); - var nic = makeInterface(); - nic.ipAddress = "2001:db8::1"; - expect($scope.ipHasError(nic)).toBe(false); - }); - - it("returns true if invalid ipv4", function() { - makeController(); - var nic = makeInterface(); - nic.ipAddress = "192.168.1"; - expect($scope.ipHasError(nic)).toBe(true); - }); - - it("returns true if invalid ipv6", function() { - makeController(); - var nic = makeInterface(); - nic.ipAddress = "2001::db8::1"; - expect($scope.ipHasError(nic)).toBe(true); - }); - - it("returns false if external ip out of managed network", function() { - makeController(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - // No class A address is in the fake networks. - var deviceInterface = makeInterface(); - deviceInterface.ipAddress = "10.0.1.1"; - deviceInterface.ipAssignment = { - name: "external" - }; - expect($scope.ipHasError(deviceInterface)).toBe(false); - }); - - it("returns true if external ip in managed network", function() { - makeController(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - var deviceInterface = makeInterface(); - deviceInterface.ipAddress = subnet.first_ip; - deviceInterface.ipAssignment = { - name: "external" - }; - expect($scope.ipHasError(deviceInterface)).toBe(true); - }); - - it("returns false if static in managed network", function() { - makeController(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - var deviceInterface = makeInterface(); - deviceInterface.ipAddress = subnet.first_ip; - deviceInterface.ipAssignment = { - name: "static" - }; - expect($scope.ipHasError(deviceInterface)).toBe(false); - }); - - it("returns false if static ip in select network", function() { - makeController(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - var deviceInterface = makeInterface(); - deviceInterface.ipAddress = subnet.first_ip; - deviceInterface.subnetId = subnet.id; - deviceInterface.ipAssignment = { - name: "static" - }; - expect($scope.ipHasError(deviceInterface)).toBe(false); - }); - - it("returns true if static ip out of select network", function() { - makeController(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - var deviceInterface = makeInterface(); - deviceInterface.ipAddress = "120.22.22.1"; - deviceInterface.subnetId = subnet.id; - deviceInterface.ipAssignment = { - name: "static" - }; - expect($scope.ipHasError(deviceInterface)).toBe(true); - }); - }); - - describe("deviceHasError", function() { - - it("returns true if name empty", function() { - makeControllerWithDevice(); - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns true if mac empty", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns true if name invalid", function() { - makeControllerWithDevice(); - $scope.device.name = "ab_c.local"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns true if mac invalid", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44'; - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns true if missing ip assignment selection", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns false if dynamic ip assignment selection", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - expect($scope.deviceHasError()).toBe(false); - }); - - it("returns true if external ip assignment and ip empty", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "external" - }; - $scope.device.interfaces[0].ipAddress = ""; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns true if external ip assignment and ip invalid", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "external" - }; - $scope.device.interfaces[0].ipAddress = "192.168"; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns false if external ip assignment and ip valid", function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "external" - }; - $scope.device.interfaces[0].ipAddress = "192.168.1.1"; - expect($scope.deviceHasError()).toBe(false); - }); - - it("returns true if static ip assignment and no cluster interface", - function() { - makeControllerWithDevice(); - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "static" - }; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns false if static ip assignment and subnet", - function() { - makeControllerWithDevice(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "static" - }; - $scope.device.interfaces[0].subnetId = subnet.id; - expect($scope.deviceHasError()).toBe(false); - }); - - it("returns true if static ip assignment, subnet, and " + - "invalid ip address", - function() { - makeControllerWithDevice(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "static" - }; - $scope.device.interfaces[0].subnetId = subnet.id; - $scope.device.interfaces[0].ipAddress = "192.168"; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns true if static ip assignment, subnet, and " + - "ip address out of network", - function() { - makeControllerWithDevice(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "static" - }; - $scope.device.interfaces[0].subnetId = subnet.id; - $scope.device.interfaces[0].ipAddress = "122.10.1.0"; - expect($scope.deviceHasError()).toBe(true); - }); - - it("returns false if static ip assignment, subnet, and " + - "ip address in network", - function() { - makeControllerWithDevice(); - var subnet = makeSubnet(); - SubnetsManager._items = [subnet]; - $scope.subnets = [subnet]; - $scope.device.name = "abc"; - $scope.device.interfaces[0].mac = '00:11:22:33:44:55'; - $scope.device.interfaces[0].ipAssignment = { - name: "static" - }; - $scope.device.interfaces[0].subnetId = subnet.id; - $scope.device.interfaces[0].ipAddress = subnet.first_ip; - expect($scope.deviceHasError()).toBe(false); - }); - }); - - describe("addInterface", function() { - - it("adds another interface", function() { - makeControllerWithDevice(); - $scope.addInterface(); - expect($scope.device.interfaces.length).toBe(2); - }); - }); - - describe("isPrimaryInterface", function() { - - it("returns true for first interface", function() { - makeControllerWithDevice(); - $scope.addInterface(); - expect( - $scope.isPrimaryInterface( - $scope.device.interfaces[0])).toBe(true); - }); - - it("returns false for second interface", function() { - makeControllerWithDevice(); - $scope.addInterface(); - expect( - $scope.isPrimaryInterface( - $scope.device.interfaces[1])).toBe(false); - }); - }); - - describe("deleteInterface", function() { - - it("doesnt remove primary interface", function() { - makeControllerWithDevice(); - var nic = $scope.device.interfaces[0]; - $scope.deleteInterface(nic); - expect($scope.device.interfaces[0]).toBe(nic); - }); - - it("removes interface", function() { - makeControllerWithDevice(); - $scope.addInterface(); - var nic = $scope.device.interfaces[1]; - $scope.deleteInterface(nic); - expect($scope.device.interfaces.indexOf(nic)).toBe(-1); - }); - }); - - describe("cancel", function() { - - it("clears device", function() { - makeControllerWithDevice(); - $scope.device.name = makeName("name"); - $scope.cancel(); - expect($scope.device.name).toBe(""); - }); - - it("calls hide", function() { - makeController(); - spyOn($scope, "hide"); - $scope.cancel(); - expect($scope.hide).toHaveBeenCalled(); - }); - }); - - describe("save", function() { - - it("doest nothing if device in error", function() { - makeController(); - var error = makeName("error"); - $scope.error = error; - spyOn($scope, "deviceHasError").and.returnValue(true); - $scope.save(); - // Error would have been cleared if save did anything. - expect($scope.error).toBe(error); - }); - - it("clears error before calling create", function() { - makeControllerWithDevice(); - $scope.error = makeName("error"); - spyOn($scope, "deviceHasError").and.returnValue(false); - spyOn(DevicesManager, "create").and.returnValue( - $q.defer().promise); - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - $scope.save(); - expect($scope.error).toBeNull(); - }); - - it("calls create with converted device", function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "deviceHasError").and.returnValue(false); - spyOn(DevicesManager, "create").and.returnValue( - $q.defer().promise); - var name = makeName("name"); - var domain = makeName("domain"); - var mac = makeName("mac"); - var assignment = "static"; - var ipAddress = makeName("ip"); - var subnet = makeSubnet(); - $scope.device = { - name: name, - domain: domain, - interfaces: [{ - mac: mac, - ipAssignment: { - name: assignment - }, - subnetId: subnet.id, - ipAddress: ipAddress - }] - }; - $scope.save(); - expect(DevicesManager.create).toHaveBeenCalledWith({ - hostname: name, - domain: domain, - primary_mac: mac, - extra_macs: [], - interfaces: [{ - mac: mac, - ip_assignment: assignment, - ip_address: ipAddress, - subnet: subnet.id - }] - }); - }); - - it("on create resolve device is cleared", function() { - makeControllerWithDevice(); - $scope.error = makeName("error"); - spyOn($scope, "deviceHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DevicesManager, "create").and.returnValue(defer.promise); - $scope.device.name = makeName("name"); - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - $scope.save(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.device.name).toBe(""); - }); - - it("on create resolve hide is called when addAnother is false", - function() { - makeControllerWithDevice(); - $scope.error = makeName("error"); - spyOn($scope, "deviceHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DevicesManager, "create").and.returnValue(defer.promise); - $scope.device.name = makeName("name"); - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - spyOn($scope, "hide"); - $scope.save(false); - defer.resolve(); - $rootScope.$digest(); - expect($scope.hide).toHaveBeenCalled(); - }); - - it("on create resolve hide is not called when addAnother is true", - function() { - makeControllerWithDevice(); - $scope.error = makeName("error"); - spyOn($scope, "deviceHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DevicesManager, "create").and.returnValue(defer.promise); - $scope.device.name = makeName("name"); - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - spyOn($scope, "hide"); - $scope.save(true); - defer.resolve(); - $rootScope.$digest(); - expect($scope.hide).not.toHaveBeenCalled(); - }); - - it("on create reject error is set", - function() { - makeControllerWithDevice(); - $scope.error = makeName("error"); - spyOn($scope, "deviceHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DevicesManager, "create").and.returnValue(defer.promise); - $scope.device.name = makeName("name"); - $scope.device.interfaces[0].ipAssignment = { - name: "dynamic" - }; - $scope.save(); - var errorMsg = makeName("error"); - var error = '{"hostname": ["' + errorMsg + '"]}'; - defer.reject(error); - $rootScope.$digest(); - expect($scope.error).toBe(errorMsg); - }); + subnetId: subnet.id, + ipAddress: ipAddress + } + ] + }; + $scope.save(); + expect(DevicesManager.create).toHaveBeenCalledWith({ + hostname: name, + domain: domain, + primary_mac: mac, + extra_macs: [], + interfaces: [ + { + mac: mac, + ip_assignment: assignment, + ip_address: ipAddress, + subnet: subnet.id + } + ] + }); + }); + + it("on create resolve device is cleared", function() { + makeControllerWithDevice(); + $scope.error = makeName("error"); + spyOn($scope, "deviceHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DevicesManager, "create").and.returnValue(defer.promise); + $scope.device.name = makeName("name"); + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + $scope.save(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.device.name).toBe(""); + }); + + it("on create resolve hide is called when addAnother is false", function() { + makeControllerWithDevice(); + $scope.error = makeName("error"); + spyOn($scope, "deviceHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DevicesManager, "create").and.returnValue(defer.promise); + $scope.device.name = makeName("name"); + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + spyOn($scope, "hide"); + $scope.save(false); + defer.resolve(); + $rootScope.$digest(); + expect($scope.hide).toHaveBeenCalled(); + }); + + it(`on create resolve hide is not called + when addAnother is true`, function() { + makeControllerWithDevice(); + $scope.error = makeName("error"); + spyOn($scope, "deviceHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DevicesManager, "create").and.returnValue(defer.promise); + $scope.device.name = makeName("name"); + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + spyOn($scope, "hide"); + $scope.save(true); + defer.resolve(); + $rootScope.$digest(); + expect($scope.hide).not.toHaveBeenCalled(); + }); + + it("on create reject error is set", function() { + makeControllerWithDevice(); + $scope.error = makeName("error"); + spyOn($scope, "deviceHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DevicesManager, "create").and.returnValue(defer.promise); + $scope.device.name = makeName("name"); + $scope.device.interfaces[0].ipAssignment = { + name: "dynamic" + }; + $scope.save(); + var errorMsg = makeName("error"); + var error = '{"hostname": ["' + errorMsg + '"]}'; + defer.reject(error); + $rootScope.$digest(); + expect($scope.error).toBe(errorMsg); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_add_domain.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_add_domain.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_add_domain.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_add_domain.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,270 +4,261 @@ * Unit tests for AddDomainController. */ +import { makeName } from "testing/utils"; + describe("AddDomainController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); + + // Grab the needed angular pieces. + var $controller, $rootScope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the AddDomainController + // and mock the websocket connection. + var DomainsManager, ManagerHelperService; + var ValidationService, RegionConnection, webSocket; + beforeEach(inject(function($injector) { + DomainsManager = $injector.get("DomainsManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ValidationService = $injector.get("ValidationService"); + RegionConnection = $injector.get("RegionConnection"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Create the parent scope and the scope for the controller. + var parentScope, $scope; + beforeEach(function() { + parentScope = $rootScope.$new(); + parentScope.addDomainScope = null; + $scope = parentScope.$new(); + }); + + // Makes the AddDomainController + function makeController() { + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + return $controller("AddDomainController", { + $scope: $scope, + DomainsManager: DomainsManager, + ValidationService: ValidationService, + ManagerHelperService: ManagerHelperService + }); + } + + it("sets addDomainScope on $scope.$parent", function() { + makeController(); + expect(parentScope.addDomainScope).toBe($scope); + }); + + it("sets initial values on $scope", function() { + makeController(); + expect($scope.viewable).toBe(false); + expect($scope.error).toBe(null); + expect($scope.domain).toEqual({ + name: "", + authoritative: true + }); + }); + + describe("show", function() { + it("does nothing if already viewable", function() { + makeController(); + $scope.viewable = true; + var name = makeName("name"); + $scope.domain.name = name; + $scope.show(); + // The domain name should have stayed the same, showing that + // the call did nothing. + expect($scope.domain.name).toBe(name); + }); + + it("clears domain and sets viewable to true", function() { + makeController(); + $scope.domain.name = makeName("name"); + $scope.show(); + expect($scope.domain.name).toBe(""); + expect($scope.viewable).toBe(true); + }); + }); + + describe("hide", function() { + it("sets viewable to false", function() { + makeController(); + $scope.viewable = true; + $scope.hide(); + expect($scope.viewable).toBe(false); + }); + + it("emits event addDomainHidden", function(done) { + makeController(); + $scope.viewable = true; + $scope.$on("addDomainHidden", function() { + done(); + }); + $scope.hide(); + }); + }); + + describe("nameHasError", function() { + it("returns false if name is empty", function() { + makeController(); + expect($scope.nameHasError()).toBe(false); + }); + + it("returns false if valid name", function() { + makeController(); + $scope.domain.name = "abc"; + expect($scope.nameHasError()).toBe(false); + }); + + it("returns true if invalid name", function() { + makeController(); + $scope.domain.name = "a_bc.local"; + expect($scope.nameHasError()).toBe(true); + }); + }); + + describe("domainHasError", function() { + it("returns true if name empty", function() { + makeController(); + $scope.domain.authoritative = true; + expect($scope.domainHasError()).toBe(true); + }); + + it("returns true if name invalid", function() { + makeController(); + $scope.domain.name = "ab_c.local"; + expect($scope.domainHasError()).toBe(true); + }); + }); + + describe("cancel", function() { + it("clears error", function() { + makeController(); + $scope.error = makeName("error"); + $scope.cancel(); + expect($scope.error).toBeNull(); + }); + + it("clears domain", function() { + makeController(); + $scope.domain.name = makeName("name"); + $scope.cancel(); + expect($scope.domain.name).toBe(""); + }); + + it("calls hide", function() { + makeController(); + spyOn($scope, "hide"); + $scope.cancel(); + expect($scope.hide).toHaveBeenCalled(); + }); + }); + + describe("save", function() { + it("doest nothing if domain in error", function() { + makeController(); + var error = makeName("error"); + $scope.error = error; + spyOn($scope, "domainHasError").and.returnValue(true); + $scope.save(); + // Error would have been cleared if save did anything. + expect($scope.error).toBe(error); + }); + + it("clears error before calling create", function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "domainHasError").and.returnValue(false); + spyOn(DomainsManager, "create").and.returnValue($q.defer().promise); + $scope.domain.authoritative = true; + $scope.save(); + expect($scope.error).toBeNull(); + }); + + it("calls create with converted domain", function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "domainHasError").and.returnValue(false); + spyOn(DomainsManager, "create").and.returnValue($q.defer().promise); + var name = makeName("name"); + var authoritative = true; + $scope.domain = { + name: name, + authoritative: authoritative + }; + $scope.save(); + expect(DomainsManager.create).toHaveBeenCalledWith({ + name: name, + authoritative: authoritative + }); + }); - // Load the MAAS module. - beforeEach(module("MAAS")); + it("on create resolve domain is cleared", function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "domainHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DomainsManager, "create").and.returnValue(defer.promise); + $scope.domain.name = makeName("name"); + $scope.save(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.domain.name).toBe(""); + }); + + it("on create resolve hide is called when addAnother is false", function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "domainHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DomainsManager, "create").and.returnValue(defer.promise); + $scope.domain.name = makeName("name"); + spyOn($scope, "hide"); + $scope.save(false); + defer.resolve(); + $rootScope.$digest(); + expect($scope.hide).toHaveBeenCalled(); + }); + + it(`on create resolve hide is not called + when addAnother is true`, function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "domainHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DomainsManager, "create").and.returnValue(defer.promise); + $scope.domain.name = makeName("name"); + spyOn($scope, "hide"); + $scope.save(true); + defer.resolve(); + $rootScope.$digest(); + expect($scope.hide).not.toHaveBeenCalled(); + }); - // Grab the needed angular pieces. - var $controller, $rootScope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the AddDomainController - // and mock the websocket connection. - var DomainsManager, ManagerHelperService; - var ValidationService, RegionConnection, webSocket; - beforeEach(inject(function($injector) { - DomainsManager = $injector.get("DomainsManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ValidationService = $injector.get("ValidationService"); - RegionConnection = $injector.get("RegionConnection"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Create the parent scope and the scope for the controller. - var parentScope, $scope; - beforeEach(function() { - parentScope = $rootScope.$new(); - parentScope.addDomainScope = null; - $scope = parentScope.$new(); - }); - - // Makes the AddDomainController - function makeController() { - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - return $controller("AddDomainController", { - $scope: $scope, - DomainsManager: DomainsManager, - ValidationService: ValidationService, - ManagerHelperService: ManagerHelperService - }); - } - - it("sets addDomainScope on $scope.$parent", function() { - makeController(); - expect(parentScope.addDomainScope).toBe($scope); - }); - - it("sets initial values on $scope", function() { - makeController(); - expect($scope.viewable).toBe(false); - expect($scope.error).toBe(null); - expect($scope.domain).toEqual({ - name: "", - authoritative: true - }); - }); - - describe("show", function() { - - it("does nothing if already viewable", function() { - makeController(); - $scope.viewable = true; - var name = makeName("name"); - $scope.domain.name = name; - $scope.show(); - // The domain name should have stayed the same, showing that - // the call did nothing. - expect($scope.domain.name).toBe(name); - }); - - it("clears domain and sets viewable to true", function() { - makeController(); - $scope.domain.name = makeName("name"); - $scope.show(); - expect($scope.domain.name).toBe(""); - expect($scope.viewable).toBe(true); - }); - }); - - describe("hide", function() { - - it("sets viewable to false", function() { - makeController(); - $scope.viewable = true; - $scope.hide(); - expect($scope.viewable).toBe(false); - }); - - it("emits event addDomainHidden", function(done) { - makeController(); - $scope.viewable = true; - $scope.$on("addDomainHidden", function() { - done(); - }); - $scope.hide(); - }); - }); - - describe("nameHasError", function() { - - it("returns false if name is empty", function() { - makeController(); - expect($scope.nameHasError()).toBe(false); - }); - - it("returns false if valid name", function() { - makeController(); - $scope.domain.name = "abc"; - expect($scope.nameHasError()).toBe(false); - }); - - it("returns true if invalid name", function() { - makeController(); - $scope.domain.name = "a_bc.local"; - expect($scope.nameHasError()).toBe(true); - }); - }); - - describe("domainHasError", function() { - - it("returns true if name empty", function() { - makeController(); - $scope.domain.authoritative = true; - expect($scope.domainHasError()).toBe(true); - }); - - it("returns true if name invalid", function() { - makeController(); - $scope.domain.name = "ab_c.local"; - expect($scope.domainHasError()).toBe(true); - }); - }); - - describe("cancel", function() { - - it("clears error", function() { - makeController(); - $scope.error = makeName("error"); - $scope.cancel(); - expect($scope.error).toBeNull(); - }); - - it("clears domain", function() { - makeController(); - $scope.domain.name = makeName("name"); - $scope.cancel(); - expect($scope.domain.name).toBe(""); - }); - - it("calls hide", function() { - makeController(); - spyOn($scope, "hide"); - $scope.cancel(); - expect($scope.hide).toHaveBeenCalled(); - }); - }); - - describe("save", function() { - - it("doest nothing if domain in error", function() { - makeController(); - var error = makeName("error"); - $scope.error = error; - spyOn($scope, "domainHasError").and.returnValue(true); - $scope.save(); - // Error would have been cleared if save did anything. - expect($scope.error).toBe(error); - }); - - it("clears error before calling create", function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "domainHasError").and.returnValue(false); - spyOn(DomainsManager, "create").and.returnValue( - $q.defer().promise); - $scope.domain.authoritative = true; - $scope.save(); - expect($scope.error).toBeNull(); - }); - - it("calls create with converted domain", function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "domainHasError").and.returnValue(false); - spyOn(DomainsManager, "create").and.returnValue( - $q.defer().promise); - var name = makeName("name"); - var authoritative = true; - $scope.domain = { - name: name, - authoritative: authoritative - }; - $scope.save(); - expect(DomainsManager.create).toHaveBeenCalledWith({ - name: name, - authoritative: authoritative - }); - }); - - it("on create resolve domain is cleared", function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "domainHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DomainsManager, "create").and.returnValue(defer.promise); - $scope.domain.name = makeName("name"); - $scope.save(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.domain.name).toBe(""); - }); - - it("on create resolve hide is called when addAnother is false", - function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "domainHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DomainsManager, "create").and.returnValue(defer.promise); - $scope.domain.name = makeName("name"); - spyOn($scope, "hide"); - $scope.save(false); - defer.resolve(); - $rootScope.$digest(); - expect($scope.hide).toHaveBeenCalled(); - }); - - it("on create resolve hide is not called when addAnother is true", - function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "domainHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DomainsManager, "create").and.returnValue(defer.promise); - $scope.domain.name = makeName("name"); - spyOn($scope, "hide"); - $scope.save(true); - defer.resolve(); - $rootScope.$digest(); - expect($scope.hide).not.toHaveBeenCalled(); - }); - - it("on create reject error is set", - function() { - makeController(); - $scope.error = makeName("error"); - spyOn($scope, "domainHasError").and.returnValue(false); - var defer = $q.defer(); - spyOn(DomainsManager, "create").and.returnValue(defer.promise); - $scope.domain.name = makeName("name"); - $scope.save(); - var errorMsg = makeName("error"); - var error = '{"name": ["' + errorMsg + '"]}'; - defer.reject(error); - $rootScope.$digest(); - expect($scope.error).toBe(errorMsg); - }); + it("on create reject error is set", function() { + makeController(); + $scope.error = makeName("error"); + spyOn($scope, "domainHasError").and.returnValue(false); + var defer = $q.defer(); + spyOn(DomainsManager, "create").and.returnValue(defer.promise); + $scope.domain.name = makeName("name"); + $scope.save(); + var errorMsg = makeName("error"); + var error = '{"name": ["' + errorMsg + '"]}'; + defer.reject(error); + $rootScope.$digest(); + expect($scope.error).toBe(errorMsg); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_add_hardware.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_add_hardware.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_add_hardware.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_add_hardware.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,751 +4,743 @@ * Unit tests for AddHardwareController. */ +import { makeName } from "testing/utils"; + describe("AddHardwareController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); + // Grab the needed angular pieces. + var $controller, $rootScope, $timeout, $http, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $timeout = $injector.get("$timeout"); + $http = $injector.get("$http"); + $q = $injector.get("$q"); + })); + + // Load the ZonesManager, ResourcePoolsManager, MachinesManager, + // RegionConnection, DomainManager, and mock the websocket connection. + var ZonesManager, + ResourcePoolsManager, + MachinesManager, + GeneralManager, + DomainsManager; + var RegionConnection, ManagerHelperService, webSocket; + beforeEach(inject(function($injector) { + ZonesManager = $injector.get("ZonesManager"); + ResourcePoolsManager = $injector.get("ResourcePoolsManager"); + MachinesManager = $injector.get("MachinesManager"); + GeneralManager = $injector.get("GeneralManager"); + DomainsManager = $injector.get("DomainsManager"); + RegionConnection = $injector.get("RegionConnection"); + ManagerHelperService = $injector.get("ManagerHelperService"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Create the parent scope and the scope for the controller. + var parentScope, $scope; + beforeEach(function() { + parentScope = $rootScope.$new(); + parentScope.addHardwareScope = null; + $scope = parentScope.$new(); + }); + + // Makes the AddHardwareController + function makeController(loadManagersDefer, loadItemsDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); + } - // Grab the needed angular pieces. - var $controller, $rootScope, $timeout, $http, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $timeout = $injector.get("$timeout"); - $http = $injector.get("$http"); - $q = $injector.get("$q"); - })); - - // Load the ZonesManager, ResourcePoolsManager, MachinesManager, - // RegionConnection, DomainManager, and mock the websocket connection. - var ZonesManager, ResourcePoolsManager, MachinesManager, GeneralManager, - DomainsManager; - var RegionConnection, ManagerHelperService, webSocket; - beforeEach(inject(function($injector) { - ZonesManager = $injector.get("ZonesManager"); - ResourcePoolsManager = $injector.get("ResourcePoolsManager"); - MachinesManager = $injector.get("MachinesManager"); - GeneralManager = $injector.get("GeneralManager"); - DomainsManager = $injector.get("DomainsManager"); - RegionConnection = $injector.get("RegionConnection"); - ManagerHelperService = $injector.get("ManagerHelperService"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); + var loadItems = spyOn(GeneralManager, "loadItems"); + if (angular.isObject(loadItemsDefer)) { + loadItems.and.returnValue(loadItemsDefer.promise); + } else { + loadItems.and.returnValue($q.defer().promise); + } - // Create the parent scope and the scope for the controller. - var parentScope, $scope; - beforeEach(function() { - parentScope = $rootScope.$new(); - parentScope.addHardwareScope = null; - $scope = parentScope.$new(); + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + var controller = $controller("AddHardwareController", { + $scope: $scope, + $timeout: $timeout, + $http: $http, + ZonesManager: ZonesManager, + ResourcePoolsManager: ResourcePoolsManager, + MachinesManager: MachinesManager, + GeneralManager: GeneralManager, + DomainsManager: DomainsManager, + RegionConnection: RegionConnection, + ManagerHelperService: ManagerHelperService + }); + return controller; + } + + // Makes the AddHardwareController with the $scope.machine already + // initialized. + function makeControllerWithMachine() { + var loadManagers_defer = $q.defer(); + var loadItems_defer = $q.defer(); + var controller = makeController(loadManagers_defer, loadItems_defer); + $scope.show(); + loadManagers_defer.resolve(); + loadItems_defer.resolve(); + $rootScope.$digest(); + return controller; + } + + it("sets addHardwareScope on $scope.$parent", function() { + makeController(); + expect(parentScope.addHardwareScope).toBe($scope); + }); + + it("sets initial values on $scope", function() { + makeController(); + expect($scope.viewable).toBe(false); + expect($scope.zones).toBe(ZonesManager.getItems()); + expect($scope.pools).toBe(ResourcePoolsManager.getItems()); + expect($scope.domains).toBe(DomainsManager.getItems()); + expect($scope.architectures).toEqual(["Choose an architecture"]); + expect($scope.hwe_kernels).toEqual([]); + expect($scope.power_types).toEqual([]); + expect($scope.error).toBeNull(); + expect($scope.machine).toBeNull(); + expect($scope.chassis).toBeNull(); + }); + + it("doesn't call loadManagers when initialized", function() { + // add_hardware is loaded on the listing and details page. Managers + // should be loaded when shown. Otherwise all Zones and Domains are + // loaded and updated even though they are not needed. + makeController(); + expect(ManagerHelperService.loadManagers).not.toHaveBeenCalled(); + }); + + it("initializes machine architecture with first arch", function() { + var loadManagers_defer = $q.defer(); + var loadItems_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + var arch = makeName("arch"); + $scope.architectures = [arch]; + $scope.machine = { + architecture: "", + power: { type: makeName("power_type") } + }; + $scope.show(); + + loadManagers_defer.resolve(); + loadItems_defer.resolve(); + $scope.$digest(); + expect($scope.machine.architecture).toEqual(arch); + }); + + it("initializes machine arch with amd64 arch", function() { + var loadManagers_defer = $q.defer(); + var loadItems_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + var arch = makeName("arch"); + $scope.architectures = [arch, "amd64/generic"]; + $scope.machine = { + architecture: "", + power: { type: makeName("power_type") } + }; + $scope.show(); + + loadManagers_defer.resolve(); + loadItems_defer.resolve(); + $scope.$digest(); + expect($scope.machine.architecture).toEqual("amd64/generic"); + }); + + it("doesnt initializes machine architecture if set", function() { + var loadManagers_defer = $q.defer(); + var loadItems_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + var arch = makeName("arch"); + var newArch = makeName("arch"); + $scope.architectures = [newArch]; + $scope.machine = { + architecture: arch, + power: { type: makeName("power_type") } + }; + $scope.show(); + + loadManagers_defer.resolve(); + loadItems_defer.resolve(); + $scope.$digest(); + expect($scope.machine.architecture).toEqual(arch); + }); + + it("initializes machine min_hwe_kernel with hwe-t", function() { + var loadManagers_defer = $q.defer(); + var loadItems_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + var arch = makeName("arch"); + var min_hwe_kernel = "hwe-t"; + $scope.architectures = [arch]; + $scope.machine = { + architecture: "", + min_hwe_kernel: min_hwe_kernel, + power: { type: makeName("power_type") } + }; + $scope.show(); + + loadManagers_defer.resolve(); + loadItems_defer.resolve(); + $scope.$digest(); + expect($scope.machine.min_hwe_kernel).toEqual(min_hwe_kernel); + }); + + describe("show", function() { + it("sets viewable to true", function() { + var loadItems_defer = $q.defer(); + var loadManagers_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + $scope.show(); + + loadItems_defer.resolve(); + loadManagers_defer.resolve(); + $rootScope.$digest(); + expect($scope.viewable).toBe(true); + }); + + it("reloads arches and kernels", function() { + var loadItems_defer = $q.defer(); + var loadManagers_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + $scope.show(); + + loadItems_defer.resolve(); + loadManagers_defer.resolve(); + $rootScope.$digest(); + expect(GeneralManager.loadItems).toHaveBeenCalledWith([ + "architectures", + "hwe_kernels", + "default_min_hwe_kernel" + ]); + }); + + it("calls loadManagers with ZonesManager, DomainsManager", function() { + var loadItems_defer = $q.defer(); + var loadManagers_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + $scope.show(); + + loadItems_defer.resolve(); + loadManagers_defer.resolve(); + $rootScope.$digest(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + DomainsManager + ]); + }); + + it(`initializes machine/chassis when + Zones/Domains manager loaded`, function() { + var loadItems_defer = $q.defer(); + var loadManagers_defer = $q.defer(); + makeController(loadManagers_defer, loadItems_defer); + $scope.show(); + + loadItems_defer.resolve(); + loadManagers_defer.resolve(); + $rootScope.$digest(); + expect($scope.machine).not.toBeNull(); + expect($scope.chassis).not.toBeNull(); + }); + }); + + describe("hide", function() { + it("sets viewable to false", function() { + makeController(); + $scope.viewable = true; + $scope.hide(); + expect($scope.viewable).toBe(false); + }); + + it("emits addHardwareHidden event", function(done) { + makeController(); + $scope.$on("addHardwareHidden", function() { + done(); + }); + $scope.hide(); + }); + + it("unloadManagers", function() { + var unloadManagers = spyOn(ManagerHelperService, "unloadManagers"); + makeController(); + $scope.viewable = true; + $scope.hide(); + expect(unloadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + DomainsManager + ]); + }); + }); + + describe("addMac", function() { + it("adds mac address object to machine", function() { + makeControllerWithMachine(); + $scope.addMac(); + expect($scope.machine.macs.length).toBe(2); + }); + }); + + describe("removeMac", function() { + it("removes mac address object from machine", function() { + makeControllerWithMachine(); + $scope.addMac(); + var mac = $scope.machine.macs[1]; + $scope.removeMac(mac); + expect($scope.machine.macs.length).toBe(1); + }); + + it("ignores second remove if mac object removed again", function() { + makeControllerWithMachine(); + $scope.addMac(); + var mac = $scope.machine.macs[1]; + $scope.removeMac(mac); + $scope.removeMac(mac); + expect($scope.machine.macs.length).toBe(1); + }); + }); + + describe("invalidName", function() { + it("return false if machine name empty", function() { + makeControllerWithMachine(); + expect($scope.invalidName($scope.machine)).toBe(false); + }); + + it("return false if machine name valid", function() { + makeControllerWithMachine(); + $scope.machine.name = "abc"; + expect($scope.invalidName($scope.machine)).toBe(false); + }); + + it("return true if machine name invalid", function() { + makeControllerWithMachine(); + $scope.machine.name = "ab_c.local"; + expect($scope.invalidName($scope.machine)).toBe(true); + }); + }); + + describe("validateMac", function() { + it("sets error to false if blank", function() { + makeController(); + var mac = { + mac: "", + error: true + }; + $scope.validateMac(mac); + expect(mac.error).toBe(false); + }); + + it("sets error to true if invalid", function() { + makeController(); + var mac = { + mac: "00:11:22", + error: false + }; + $scope.validateMac(mac); + expect(mac.error).toBe(true); + }); + + it("sets error to false if valid", function() { + makeController(); + var mac = { + mac: "00:11:22:33:44:55", + error: true + }; + $scope.validateMac(mac); + expect(mac.error).toBe(false); + }); + }); + + describe("machineHasError", function() { + it("returns true if machine is null", function() { + makeControllerWithMachine(); + $scope.machine = null; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if zone is null", function() { + makeControllerWithMachine(); + $scope.machine.zone = null; + $scope.machine.pool = null; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if architecture is not chosen", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.pool = {}; + $scope.machine.architecture = "Choose an architecture"; + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if power.type is null", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = null; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if machine.name invalid", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.name = "ab_c.local"; + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if mac[0] is empty", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = ""; + $scope.machine.macs[0].error = false; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if mac[0] is in error", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44"; + $scope.machine.macs[0].error = true; + expect($scope.machineHasError()).toBe(true); + }); + + it("returns true if mac[1] is in error", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + $scope.machine.macs.push({ + mac: "00:11:22:33:55", + error: true + }); + expect($scope.machineHasError()).toBe(true); + }); + + it("returns false if all is correct", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.pool = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + expect($scope.machineHasError()).toBe(false); + }); + + it("returns false if all is correct and mac[1] is blank", function() { + makeControllerWithMachine(); + $scope.machine.zone = {}; + $scope.machine.pool = {}; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = {}; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + $scope.machine.macs.push({ + mac: "", + error: false + }); + expect($scope.machineHasError()).toBe(false); + }); + }); + + describe("chassisHasErrors", function() { + it("returns true if chassis is null", function() { + makeController(); + $scope.chassis = null; + expect($scope.chassisHasErrors()).toBe(true); + }); + + it("returns true if power.type is null", function() { + makeController(); + $scope.chassis = { + power: { + type: null, + parameters: {} + } + }; + expect($scope.chassisHasErrors()).toBe(true); }); - // Makes the AddHardwareController - function makeController(loadManagersDefer, loadItemsDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); + it("returns true if power.parameters is invalid", function() { + makeController(); + $scope.chassis = { + power: { + type: { + fields: [ + { + name: "test", + required: true + } + ] + }, + parameters: { + test: "" + } } + }; + expect($scope.chassisHasErrors()).toBe(true); + }); - var loadItems = spyOn(GeneralManager, "loadItems"); - if(angular.isObject(loadItemsDefer)) { - loadItems.and.returnValue(loadItemsDefer.promise); - } else { - loadItems.and.returnValue($q.defer().promise); + it("returns false if all valid", function() { + makeController(); + $scope.chassis = { + power: { + type: { + fields: [ + { + name: "test", + required: true + } + ] + }, + parameters: { + test: "data" + } } + }; + expect($scope.chassisHasErrors()).toBe(false); + }); + }); - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - var controller = $controller("AddHardwareController", { - $scope: $scope, - $timeout: $timeout, - $http: $http, - ZonesManager: ZonesManager, - ResourcePoolsManager: ResourcePoolsManager, - MachinesManager: MachinesManager, - GeneralManager: GeneralManager, - DomainsManager: DomainsManager, - RegionConnection: RegionConnection, - ManagerHelperService: ManagerHelperService - }); - return controller; - } + describe("cancel", function() { + it("clears error", function() { + makeControllerWithMachine(); + $scope.error = makeName("error"); + $scope.cancel(); - // Makes the AddHardwareController with the $scope.machine already - // initialized. - function makeControllerWithMachine() { - var loadManagers_defer = $q.defer(); - var loadItems_defer = $q.defer(); - var controller = makeController(loadManagers_defer, loadItems_defer); - $scope.show(); - loadManagers_defer.resolve(); - loadItems_defer.resolve(); - $rootScope.$digest(); - return controller; - } + expect($scope.showErrors).toEqual(false); + }); + + it("clears machine and adds a new one", function() { + makeControllerWithMachine(); + $scope.machine.name = makeName("name"); + $scope.cancel(); + expect($scope.machine.name).toBe(""); + }); + + it("clears chassis and adds a new one", function() { + makeControllerWithMachine(); + $scope.chassis.power.type = makeName("type"); + $scope.cancel(); + expect($scope.chassis.power.type).toBeNull(); + }); + + it("calls hide", function() { + makeControllerWithMachine(); + spyOn($scope, "hide"); + $scope.cancel(); + expect($scope.hide).toHaveBeenCalled(); + }); + }); + + describe("saveMachine", function() { + // Setup a valid machine before each test. + beforeEach(function() { + makeControllerWithMachine(); + + $scope.addMac(); + $scope.machine.name = makeName("name").replace("_", ""); + $scope.machine.domain = makeName("domain").replace("_", ""); + $scope.machine.zone = { + id: 1, + name: makeName("zone") + }; + $scope.machine.pool = { + id: 2, + name: makeName("pool") + }; + $scope.machine.architecture = makeName("arch"); + $scope.machine.power.type = { + name: "virsh" + }; + $scope.machine.power.parameters = { + mac_address: "00:11:22:33:44:55" + }; + $scope.machine.macs[0].mac = "00:11:22:33:44:55"; + $scope.machine.macs[0].error = false; + $scope.machine.macs[1].mac = "00:11:22:33:44:66"; + $scope.machine.macs[1].error = false; + }); + + it("Converts power and macs to machine protocol", function() { + $scope.saveMachine(false); + $rootScope.$digest(); + + expect($scope.newMachineObj).toEqual({ + name: $scope.machine.name, + domain: $scope.machine.domain, + architecture: $scope.machine.architecture, + min_hwe_kernel: $scope.machine.min_hwe_kernel, + pxe_mac: $scope.machine.macs[0].mac, + extra_macs: [$scope.machine.macs[1].mac], + power_type: $scope.machine.power.type.name, + power_parameters: $scope.machine.power.parameters, + zone: $scope.machine.zone, + pool: $scope.machine.pool + }); + }); + + it("calls hide once the maas-form's after-save is called", function() { + spyOn($scope, "hide"); + $scope.afterSaveMachine(); + $rootScope.$digest(); + + expect($scope.hide).toHaveBeenCalled(); + }); + + it("resets machine once the maas-form's after-save is called", function() { + $scope.afterSaveMachine(); + $rootScope.$digest(); + + expect($scope.machine.name).toBe(""); + }); + + it("clones machine once after-save is called with addAnother", function() { + $scope.saveMachine(true); + $rootScope.$digest(); + $scope.afterSaveMachine(); + $rootScope.$digest(); + + expect($scope.machine.name).toBe(""); + }); + + it("doesn't call hide if addAnother is true", function() { + spyOn($scope, "hide"); + $scope.saveMachine(true); + $rootScope.$digest(); + $scope.afterSaveMachine(); + $rootScope.$digest(); + + expect($scope.hide).not.toHaveBeenCalled(); + }); + }); + + describe("saveChassis", function() { + // Setup a valid chassis before each test. + var httpDefer; + beforeEach(function() { + httpDefer = $q.defer(); + + // Mock $http. + $http = jasmine.createSpy("$http"); + $http.and.returnValue(httpDefer.promise); + + // Create the controller and the valid chassis. + makeController(); + $scope.chassis = { + domain: makeName("domain"), + power: { + type: { + name: makeName("model"), + fields: [ + { + name: "one", + required: true + }, + { + name: "one", + required: true + } + ] + }, + parameters: { + one: makeName("one"), + two: makeName("two") + } + } + }; + }); + + it("does nothing if errors", function() { + var error = makeName("error"); + $scope.error = error; + spyOn($scope, "chassisHasErrors").and.returnValue(true); + $scope.saveChassis(false); + expect($scope.error).toBe(error); + }); + + it("calls $http with correct parameters", function() { + $scope.saveChassis(false); + + var parameters = $scope.chassis.power.parameters; + parameters.chassis_type = $scope.chassis.power.type.name; + parameters.domain = $scope.chassis.domain.name; + expect($http).toHaveBeenCalledWith({ + method: "POST", + url: "api/2.0/machines/?op=add_chassis", + data: $.param(parameters), + headers: { + "Content-Type": "application/x-www-form-urlencoded" + } + }); + }); + + it("creates new chassis when $http resolves", function() { + $scope.saveChassis(false); + httpDefer.resolve(); + $rootScope.$digest(); + + expect($scope.chassis.power.type).toBeNull(); + }); + + it("calls hide if addAnother false when $http resolves", function() { + spyOn($scope, "hide"); + $scope.saveChassis(false); + httpDefer.resolve(); + $rootScope.$digest(); + + expect($scope.hide).toHaveBeenCalled(); + }); + + it("doesnt call hide if addAnother true when $http resolves", function() { + spyOn($scope, "hide"); + $scope.saveChassis(true); + httpDefer.resolve(); + $rootScope.$digest(); + + expect($scope.hide).not.toHaveBeenCalled(); + }); - it("sets addHardwareScope on $scope.$parent", function() { - makeController(); - expect(parentScope.addHardwareScope).toBe($scope); - }); - - it("sets initial values on $scope", function() { - makeController(); - expect($scope.viewable).toBe(false); - expect($scope.zones).toBe(ZonesManager.getItems()); - expect($scope.pools).toBe(ResourcePoolsManager.getItems()); - expect($scope.domains).toBe(DomainsManager.getItems()); - expect($scope.architectures).toEqual(['Choose an architecture']); - expect($scope.hwe_kernels).toEqual([]); - expect($scope.power_types).toEqual([]); - expect($scope.error).toBeNull(); - expect($scope.machine).toBeNull(); - expect($scope.chassis).toBeNull(); - }); - - it("doesn't call loadManagers when initialized", function() { - // add_hardware is loaded on the listing and details page. Managers - // should be loaded when shown. Otherwise all Zones and Domains are - // loaded and updated even though they are not needed. - makeController(); - expect(ManagerHelperService.loadManagers).not.toHaveBeenCalled(); - }); - - it("initializes machine architecture with first arch", function() { - var loadManagers_defer = $q.defer(); - var loadItems_defer = $q.defer(); - makeController(loadManagers_defer, loadItems_defer); - var arch = makeName("arch"); - $scope.architectures = [arch]; - $scope.machine = { - architecture: '', - power: {type: makeName("power_type")} - }; - $scope.show() - - loadManagers_defer.resolve(); - loadItems_defer.resolve(); - $scope.$digest(); - expect($scope.machine.architecture).toEqual(arch); - }); - - it("initializes machine arch with amd64 arch", function() { - var loadManagers_defer = $q.defer(); - var loadItems_defer = $q.defer(); - makeController(loadManagers_defer, loadItems_defer); - var arch = makeName("arch"); - $scope.architectures = [arch, "amd64/generic"]; - $scope.machine = { - architecture: '', - power: {type: makeName("power_type")} - }; - $scope.show(); - - loadManagers_defer.resolve(); - loadItems_defer.resolve(); - $scope.$digest(); - expect($scope.machine.architecture).toEqual("amd64/generic"); - }); - - it("doesnt initializes machine architecture if set", function() { - var loadManagers_defer = $q.defer(); - var loadItems_defer = $q.defer(); - makeController(loadManagers_defer, loadItems_defer); - var arch = makeName("arch"); - var newArch = makeName("arch"); - $scope.architectures = [newArch]; - $scope.machine = { - architecture: arch, - power: {type: makeName("power_type")} - }; - $scope.show(); - - loadManagers_defer.resolve(); - loadItems_defer.resolve(); - $scope.$digest(); - expect($scope.machine.architecture).toEqual(arch); - }); - - it("initializes machine min_hwe_kernel with hwe-t", function() { - var loadManagers_defer = $q.defer(); - var loadItems_defer = $q.defer(); - makeController(loadManagers_defer, loadItems_defer); - var arch = makeName("arch"); - var min_hwe_kernel = "hwe-t"; - $scope.architectures = [arch]; - $scope.machine = { - architecture: '', - min_hwe_kernel: min_hwe_kernel, - power: {type: makeName("power_type")} - }; - $scope.show(); - - loadManagers_defer.resolve(); - loadItems_defer.resolve(); - $scope.$digest(); - expect($scope.machine.min_hwe_kernel).toEqual(min_hwe_kernel); - }); - - describe("show", function() { - - it("sets viewable to true", function() { - var loadItems_defer = $q.defer(); - var loadManagers_defer = $q.defer(); - makeController( - loadManagers_defer, loadItems_defer); - $scope.show(); - - loadItems_defer.resolve(); - loadManagers_defer.resolve(); - $rootScope.$digest(); - expect($scope.viewable).toBe(true); - }); - - it("reloads arches and kernels", function() { - var loadItems_defer = $q.defer(); - var loadManagers_defer = $q.defer(); - makeController( - loadManagers_defer, loadItems_defer); - $scope.show(); - - loadItems_defer.resolve(); - loadManagers_defer.resolve(); - $rootScope.$digest(); - expect(GeneralManager.loadItems).toHaveBeenCalledWith([ - "architectures", "hwe_kernels", "default_min_hwe_kernel"]); - }); - - it("calls loadManagers with ZonesManager, DomainsManager", function() { - var loadItems_defer = $q.defer(); - var loadManagers_defer = $q.defer(); - makeController( - loadManagers_defer, loadItems_defer); - $scope.show(); - - loadItems_defer.resolve(); - loadManagers_defer.resolve(); - $rootScope.$digest(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ZonesManager, DomainsManager]); - }); - - it("initializes machine/chassis when Zones/Domains manager loaded", - function() { - var loadItems_defer = $q.defer(); - var loadManagers_defer = $q.defer(); - makeController( - loadManagers_defer, loadItems_defer); - $scope.show(); - - loadItems_defer.resolve(); - loadManagers_defer.resolve(); - $rootScope.$digest(); - expect($scope.machine).not.toBeNull(); - expect($scope.chassis).not.toBeNull(); - }); - }); - - describe("hide", function() { - - it("sets viewable to false", function() { - makeController(); - $scope.viewable = true; - $scope.hide(); - expect($scope.viewable).toBe(false); - }); - - it("emits addHardwareHidden event", function(done) { - makeController(); - $scope.$on("addHardwareHidden", function() { - done(); - }); - $scope.hide(); - }); - - it("unloadManagers", function() { - var unloadManagers = spyOn(ManagerHelperService, "unloadManagers"); - makeController(); - $scope.viewable = true; - $scope.hide(); - expect(unloadManagers).toHaveBeenCalledWith( - $scope, [ZonesManager, DomainsManager]); - }); - }); - - describe("addMac", function() { - - it("adds mac address object to machine", function() { - makeControllerWithMachine(); - $scope.addMac(); - expect($scope.machine.macs.length).toBe(2); - }); - }); - - describe("removeMac", function() { - - it("removes mac address object from machine", function() { - makeControllerWithMachine(); - $scope.addMac(); - var mac = $scope.machine.macs[1]; - $scope.removeMac(mac); - expect($scope.machine.macs.length).toBe(1); - }); - - it("ignores second remove if mac object removed again", function() { - makeControllerWithMachine(); - $scope.addMac(); - var mac = $scope.machine.macs[1]; - $scope.removeMac(mac); - $scope.removeMac(mac); - expect($scope.machine.macs.length).toBe(1); - }); - }); - - describe("invalidName", function() { - - it("return false if machine name empty", function() { - makeControllerWithMachine(); - expect($scope.invalidName($scope.machine)).toBe(false); - }); - - it("return false if machine name valid", function() { - makeControllerWithMachine(); - $scope.machine.name = "abc"; - expect($scope.invalidName($scope.machine)).toBe(false); - }); - - it("return true if machine name invalid", function() { - makeControllerWithMachine(); - $scope.machine.name = "ab_c.local"; - expect($scope.invalidName($scope.machine)).toBe(true); - }); - }); - - describe("validateMac", function() { - - it("sets error to false if blank", function() { - makeController(); - var mac = { - mac: '', - error: true - }; - $scope.validateMac(mac); - expect(mac.error).toBe(false); - }); - - it("sets error to true if invalid", function() { - makeController(); - var mac = { - mac: '00:11:22', - error: false - }; - $scope.validateMac(mac); - expect(mac.error).toBe(true); - }); - - it("sets error to false if valid", function() { - makeController(); - var mac = { - mac: '00:11:22:33:44:55', - error: true - }; - $scope.validateMac(mac); - expect(mac.error).toBe(false); - }); - }); - - describe("machineHasError", function() { - - it("returns true if machine is null", function() { - makeControllerWithMachine(); - $scope.machine = null; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if zone is null", function() { - makeControllerWithMachine(); - $scope.machine.zone = null; - $scope.machine.pool = null; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if architecture is not chosen", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.pool = {}; - $scope.machine.architecture = 'Choose an architecture'; - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if power.type is null", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = null; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if machine.name invalid", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.name = "ab_c.local"; - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if mac[0] is empty", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = ''; - $scope.machine.macs[0].error = false; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if mac[0] is in error", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44'; - $scope.machine.macs[0].error = true; - expect($scope.machineHasError()).toBe(true); - }); - - it("returns true if mac[1] is in error", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - $scope.machine.macs.push({ - mac: '00:11:22:33:55', - error: true - }); - expect($scope.machineHasError()).toBe(true); - }); - - it("returns false if all is correct", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.pool = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - expect($scope.machineHasError()).toBe(false); - }); - - it("returns false if all is correct and mac[1] is blank", function() { - makeControllerWithMachine(); - $scope.machine.zone = {}; - $scope.machine.pool = {}; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = {}; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - $scope.machine.macs.push({ - mac: '', - error: false - }); - expect($scope.machineHasError()).toBe(false); - }); - }); - - describe("chassisHasErrors", function() { - - it("returns true if chassis is null", function() { - makeController(); - $scope.chassis = null; - expect($scope.chassisHasErrors()).toBe(true); - }); - - it("returns true if power.type is null", function() { - makeController(); - $scope.chassis = { - power: { - type: null, - parameters: {} - } - }; - expect($scope.chassisHasErrors()).toBe(true); - }); - - it("returns true if power.parameters is invalid", function() { - makeController(); - $scope.chassis = { - power: { - type: { - fields: [ - { - name: "test", - required: true - } - ] - }, - parameters: { - test: "" - } - } - }; - expect($scope.chassisHasErrors()).toBe(true); - }); - - it("returns false if all valid", function() { - makeController(); - $scope.chassis = { - power: { - type: { - fields: [ - { - name: "test", - required: true - } - ] - }, - parameters: { - test: "data" - } - } - }; - expect($scope.chassisHasErrors()).toBe(false); - }); - }); - - describe("cancel", function() { - - it("clears error", function() { - makeControllerWithMachine(); - $scope.error = makeName("error"); - $scope.cancel(); - - expect($scope.showErrors).toEqual(false); - }); - - it("clears machine and adds a new one", function() { - makeControllerWithMachine(); - $scope.machine.name = makeName("name"); - $scope.cancel(); - expect($scope.machine.name).toBe(""); - }); - - it("clears chassis and adds a new one", function() { - makeControllerWithMachine(); - $scope.chassis.power.type = makeName("type"); - $scope.cancel(); - expect($scope.chassis.power.type).toBeNull(); - }); - - it("calls hide", function() { - makeControllerWithMachine(); - spyOn($scope, "hide"); - $scope.cancel(); - expect($scope.hide).toHaveBeenCalled(); - }); - }); - - describe("saveMachine", function() { - - // Setup a valid machine before each test. - beforeEach(function() { - makeControllerWithMachine(); - - $scope.addMac(); - $scope.machine.name = makeName("name").replace("_", ""); - $scope.machine.domain = makeName("domain").replace("_", ""); - $scope.machine.zone = { - id: 1, - name: makeName("zone") - }; - $scope.machine.pool = { - id: 2, - name: makeName("pool") - }; - $scope.machine.architecture = makeName("arch"); - $scope.machine.power.type = { - name: "virsh" - }; - $scope.machine.power.parameters = { - mac_address: "00:11:22:33:44:55" - }; - $scope.machine.macs[0].mac = '00:11:22:33:44:55'; - $scope.machine.macs[0].error = false; - $scope.machine.macs[1].mac = '00:11:22:33:44:66'; - $scope.machine.macs[1].error = false; - }); - - it("Converts power and macs to machine protocol", function() { - $scope.saveMachine(false); - $rootScope.$digest(); - - expect($scope.newMachineObj).toEqual( - { - name: $scope.machine.name, - domain: $scope.machine.domain, - architecture: $scope.machine.architecture, - min_hwe_kernel: $scope.machine.min_hwe_kernel, - pxe_mac: $scope.machine.macs[0].mac, - extra_macs: [$scope.machine.macs[1].mac], - power_type: $scope.machine.power.type.name, - power_parameters: $scope.machine.power.parameters, - zone: $scope.machine.zone, - pool: $scope.machine.pool, - }); - }); - - it("calls hide once the maas-form's after-save is called", function() { - spyOn($scope, "hide"); - $scope.afterSaveMachine(); - $rootScope.$digest(); - - expect($scope.hide).toHaveBeenCalled(); - }); - - it("resets machine once the maas-form's after-save is called", - function() { - $scope.afterSaveMachine(); - $rootScope.$digest(); - - expect($scope.machine.name).toBe(""); - }); - - it("clones machine once after-save is called with addAnother", - function() { - $scope.saveMachine(true); - $rootScope.$digest(); - $scope.afterSaveMachine(); - $rootScope.$digest(); - - expect($scope.machine.name).toBe(""); - }); - - it("doesn't call hide if addAnother is true", function() { - spyOn($scope, "hide"); - $scope.saveMachine(true); - $rootScope.$digest(); - $scope.afterSaveMachine(); - $rootScope.$digest(); - - expect($scope.hide).not.toHaveBeenCalled(); - }); - }); - - describe("saveChassis", function() { - - // Setup a valid chassis before each test. - var httpDefer; - beforeEach(function() { - httpDefer = $q.defer(); - - // Mock $http. - $http = jasmine.createSpy("$http"); - $http.and.returnValue(httpDefer.promise); - - // Create the controller and the valid chassis. - makeController(); - $scope.chassis = { - domain: makeName("domain"), - power: { - type: { - name: makeName("model"), - fields: [ - { - name: "one", - required: true - }, - { - name: "one", - required: true - } - ] - }, - parameters: { - "one": makeName("one"), - "two": makeName("two") - } - } - }; - }); - - it("does nothing if errors", function() { - var error = makeName("error"); - $scope.error = error; - spyOn($scope, "chassisHasErrors").and.returnValue(true); - $scope.saveChassis(false); - expect($scope.error).toBe(error); - }); - - it("calls $http with correct parameters", function() { - $scope.saveChassis(false); - - var parameters = $scope.chassis.power.parameters; - parameters.chassis_type = $scope.chassis.power.type.name; - parameters.domain = $scope.chassis.domain.name; - expect($http).toHaveBeenCalledWith({ - method: 'POST', - url: 'api/2.0/machines/?op=add_chassis', - data: $.param(parameters), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }); - }); - - it("creates new chassis when $http resolves", function() { - $scope.saveChassis(false); - httpDefer.resolve(); - $rootScope.$digest(); - - expect($scope.chassis.power.type).toBeNull(); - }); - - it("calls hide if addAnother false when $http resolves", function() { - spyOn($scope, "hide"); - $scope.saveChassis(false); - httpDefer.resolve(); - $rootScope.$digest(); - - expect($scope.hide).toHaveBeenCalled(); - }); - - it("doesnt call hide if addAnother true when $http resolves", - function() { - spyOn($scope, "hide"); - $scope.saveChassis(true); - httpDefer.resolve(); - $rootScope.$digest(); - - expect($scope.hide).not.toHaveBeenCalled(); - }); - - it("sets error when $http rejects", function() { - $scope.saveChassis(false); - var error = {data:makeName("error")}; - httpDefer.reject(error); - $rootScope.$digest(); + it("sets error when $http rejects", function() { + $scope.saveChassis(false); + var error = { data: makeName("error") }; + httpDefer.reject(error); + $rootScope.$digest(); - expect($scope.error).toBe(error.data); - }); + expect($scope.error).toBe(error.data); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_dashboard.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_dashboard.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_dashboard.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_dashboard.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,655 +4,642 @@ * Unit tests for DashboardController. */ +import { makeInteger, makeName } from "testing/utils"; + describe("DashboardController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q, $location; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load any injected managers and services. + var DiscoveriesManager, DomainsManager, MachinesManager, DevicesManager; + var SubnetsManager, VLANsManager, ConfigsManager, ManagerHelperService; + let SearchService; + beforeEach(inject(function($injector) { + DiscoveriesManager = $injector.get("DiscoveriesManager"); + DomainsManager = $injector.get("DomainsManager"); + MachinesManager = $injector.get("MachinesManager"); + DevicesManager = $injector.get("DevicesManager"); + SubnetsManager = $injector.get("SubnetsManager"); + VLANsManager = $injector.get("VLANsManager"); + ConfigsManager = $injector.get("ConfigsManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + SearchService = $injector.get("SearchService"); + })); + + // Makes the DashboardController + function makeController(loadManagerDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); + } - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q, $routeParams, $location; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load any injected managers and services. - var DiscoveriesManager, DomainsManager, MachinesManager, DevicesManager; - var SubnetsManager, VLANsManager, ConfigsManager, ManagerHelperService; - beforeEach(inject(function($injector) { - DiscoveriesManager = $injector.get("DiscoveriesManager"); - DomainsManager = $injector.get("DomainsManager"); - MachinesManager = $injector.get("MachinesManager"); - DevicesManager = $injector.get("DevicesManager"); - SubnetsManager = $injector.get("SubnetsManager"); - VLANsManager = $injector.get("VLANsManager"); - ConfigsManager = $injector.get("ConfigsManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - SearchService = $injector.get("SearchService"); - })); - - // Makes the DashboardController - function makeController(loadManagerDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); + // Create the controller. + var controller = $controller("DashboardController", { + $scope: $scope, + $rootScope: $rootScope, + DiscoveriesManager: DiscoveriesManager, + DomainsManager: DomainsManager, + MachinesManager: MachinesManager, + DevicesManager: DevicesManager, + SubnetsManager: SubnetsManager, + VLANsManager: VLANsManager, + ConfigsManager: ConfigsManager, + ManagerHelperService: ManagerHelperService + }); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Dashboard"); + expect($rootScope.page).toBe("dashboard"); + }); + + it("calls loadManagers with correct managers", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + DiscoveriesManager, + DomainsManager, + MachinesManager, + DevicesManager, + SubnetsManager, + VLANsManager, + ConfigsManager + ]); + }); + + it("sets initial $scope", function() { + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.discoveredDevices).toBe(DiscoveriesManager.getItems()); + expect($scope.domains).toBe(DomainsManager.getItems()); + expect($scope.machines).toBe(MachinesManager.getItems()); + expect($scope.configManager).toBe(ConfigsManager); + expect($scope.networkDiscovery).toBeNull(); + expect($scope.column).toBe("mac"); + expect($scope.selectedDevice).toBeNull(); + expect($scope.convertTo).toBeNull(); + }); + + describe("proxyManager", function() { + it("calls DevicesManager.createItem when device", function() { + makeController(); + var sentinel = {}; + spyOn(DevicesManager, "createItem").and.returnValue(sentinel); + $scope.convertTo = { + type: "device" + }; + var params = {}; + var observed = $scope.proxyManager.updateItem(params); + expect(observed).toBe(sentinel); + expect(DevicesManager.createItem).toHaveBeenCalledWith(params); + }); + + it("calls DevicesManager.createInterface when interface", function() { + makeController(); + var sentinel = {}; + spyOn(DevicesManager, "createInterface").and.returnValue(sentinel); + $scope.convertTo = { + type: "interface" + }; + var params = {}; + var observed = $scope.proxyManager.updateItem(params); + expect(observed).toBe(sentinel); + expect(DevicesManager.createInterface).toHaveBeenCalledWith(params); + }); + }); + + describe("getDiscoveryName", function() { + it("returns discovery hostname", function() { + makeController(); + var discovery = { hostname: "hostname" }; + expect($scope.getDiscoveryName(discovery)).toBe("hostname"); + }); + + it("returns discovery mac_organization with device octets", function() { + makeController(); + var discovery = { + hostname: null, + mac_organization: "mac-org", + mac_address: "00:11:22:33:44:55" + }; + var expected_name = "unknown"; + expect($scope.getDiscoveryName(discovery)).toBe(expected_name); + }); + + it("returns discovery with device mac", function() { + makeController(); + var discovery = { + hostname: null, + mac_organization: null, + mac_address: "00:11:22:33:44:55" + }; + var expected_name = "unknown"; + expect($scope.getDiscoveryName(discovery)).toBe(expected_name); + }); + }); + + describe("getSubnetName", function() { + it("calls SubnetsManager.getName with subnet", function() { + makeController(); + var subnet = { + id: makeInteger(0, 100) + }; + var sentinel = {}; + SubnetsManager._items = [subnet]; + spyOn(SubnetsManager, "getName").and.returnValue(sentinel); + expect($scope.getSubnetName(subnet.id)).toBe(sentinel); + expect(SubnetsManager.getName).toHaveBeenCalledWith(subnet); + }); + }); + + describe("getVLANName", function() { + it("calls VLANsManager.getName with vlan", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var sentinel = {}; + VLANsManager._items = [vlan]; + spyOn(VLANsManager, "getName").and.returnValue(sentinel); + expect($scope.getVLANName(vlan.id)).toBe(sentinel); + expect(VLANsManager.getName).toHaveBeenCalledWith(vlan); + }); + }); + + describe("toggleSelected", function() { + it("clears selected if already selected", function() { + makeController(); + var id = makeInteger(0, 100); + $scope.selectedDevice = id; + $scope.toggleSelected(id); + expect($scope.selectedDevice).toBeNull(); + }); + + it("sets selectedDevice and convertTo with static", function() { + makeController(); + var id = makeInteger(0, 100); + var defaultDomain = { + id: 0 + }; + DomainsManager._items = [defaultDomain]; + var discovered = { + first_seen: id, + hostname: makeName("hostname"), + subnet: makeInteger(0, 100) + }; + DiscoveriesManager._items = [discovered]; + $scope.toggleSelected(id); + expect($scope.selectedDevice).toBe(id); + expect($scope.convertTo).toEqual({ + type: "device", + hostname: $scope.getDiscoveryName(discovered), + domain: defaultDomain, + parent: null, + ip_assignment: "dynamic", + goTo: false, + saved: false, + deviceIPOptions: [ + ["static", "Static"], + ["dynamic", "Dynamic"], + ["external", "External"] + ] + }); + }); + it("sets handles fqdn correctly", function() { + makeController(); + var id = makeInteger(0, 100); + var defaultDomain = { + id: 0 + }; + var domain = { + id: 1, + name: makeName("domain") + }; + var hostname = makeName("hostname"); + DomainsManager._items = [defaultDomain, domain]; + var discovered = { + first_seen: id, + hostname: hostname + "." + domain.name, + subnet: makeInteger(0, 100) + }; + DiscoveriesManager._items = [discovered]; + $scope.toggleSelected(id); + expect($scope.selectedDevice).toBe(id); + // Just confirm the hostname and domain, the rest is checked in the + // above test. + expect($scope.convertTo.hostname).toBe(hostname); + expect($scope.convertTo.domain).toBe(domain); + }); + + it("sets selectedDevice and convertTo without static", function() { + makeController(); + var id = makeInteger(0, 100); + var defaultDomain = { + id: 0 + }; + DomainsManager._items = [defaultDomain]; + var discovered = { + first_seen: id, + hostname: makeName("hostname"), + subnet: null + }; + DiscoveriesManager._items = [discovered]; + $scope.toggleSelected(id); + expect($scope.selectedDevice).toBe(id); + expect($scope.convertTo).toEqual({ + type: "device", + hostname: $scope.getDiscoveryName(discovered), + domain: defaultDomain, + parent: null, + ip_assignment: "dynamic", + goTo: false, + saved: false, + deviceIPOptions: [["dynamic", "Dynamic"], ["external", "External"]] + }); + }); + }); + + describe("sortTable", function() { + it("sets predicate", function() { + makeController(); + var predicate = makeName("predicate"); + $scope.sortTable(predicate); + expect($scope.predicate).toBe(predicate); + }); + + it("reverses reverse", function() { + makeController(); + $scope.reverse = true; + $scope.sortTable(makeName("predicate")); + expect($scope.reverse).toBe(false); + }); + }); + + describe("preProcess", function() { + it("adjust device to include the needed fields", function() { + makeController(); + var id = makeInteger(0, 100); + var defaultDomain = { + id: 0 + }; + DomainsManager._items = [defaultDomain]; + var discovered = { + first_seen: id, + hostname: makeName("hostname"), + subnet: makeInteger(0, 100), + mac_address: makeName("mac"), + ip: makeName("ip") + }; + DiscoveriesManager._items = [discovered]; + $scope.toggleSelected(id); + var observed = $scope.preProcess($scope.convertTo); + expect(observed).not.toBe($scope.convertTo); + expect(observed).toEqual({ + type: "device", + hostname: $scope.getDiscoveryName(discovered), + domain: defaultDomain, + parent: null, + ip_assignment: "dynamic", + goTo: false, + saved: false, + deviceIPOptions: [ + ["static", "Static"], + ["dynamic", "Dynamic"], + ["external", "External"] + ], + primary_mac: discovered.mac_address, + extra_macs: [], + interfaces: [ + { + mac: discovered.mac_address, + ip_assignment: "dynamic", + ip_address: discovered.ip, + subnet: discovered.subnet + } + ] + }); + }); + + it("adjust interface to include the needed fields", function() { + makeController(); + var id = makeInteger(0, 100); + var defaultDomain = { + id: 0 + }; + DomainsManager._items = [defaultDomain]; + var discovered = { + first_seen: id, + hostname: makeName("hostname"), + subnet: makeInteger(0, 100), + mac_address: makeName("mac"), + ip: makeName("ip") + }; + DiscoveriesManager._items = [discovered]; + $scope.toggleSelected(id); + $scope.convertTo.type = "interface"; + var observed = $scope.preProcess($scope.convertTo); + expect(observed).not.toBe($scope.convertTo); + expect(observed).toEqual({ + type: "interface", + hostname: $scope.getDiscoveryName(discovered), + domain: defaultDomain, + parent: null, + ip_assignment: "dynamic", + goTo: false, + saved: false, + deviceIPOptions: [ + ["static", "Static"], + ["dynamic", "Dynamic"], + ["external", "External"] + ], + mac_address: discovered.mac_address, + ip_address: discovered.ip, + subnet: discovered.subnet + }); + }); + }); + + describe("afterSave", function() { + it("removes item from DiscoveriesManager", function() { + makeController(); + var id = makeInteger(0, 100); + $scope.selectedDevice = id; + $scope.convertTo = { + goTo: false + }; + spyOn(DiscoveriesManager, "_removeItem"); + var newObj = { + hostname: makeName("hostname"), + parent: makeName("parent") + }; + $scope.afterSave(newObj); + expect(DiscoveriesManager._removeItem).toHaveBeenCalledWith(id); + expect($scope.convertTo.hostname).toBe(newObj.hostname); + expect($scope.convertTo.parent).toBe(newObj.parent); + expect($scope.convertTo.saved).toBe(true); + expect($scope.selectedDevice).toBeNull(); + }); + + it("doesn't call $location.path if not goTo", function() { + makeController(); + var id = makeInteger(0, 100); + $scope.selectedDevice = id; + $scope.convertTo = { + goTo: false + }; + spyOn(DiscoveriesManager, "_removeItem"); + spyOn($location, "path"); + $scope.afterSave({ + hostname: makeName("hostname"), + parent: makeName("parent") + }); + expect($location.path).not.toHaveBeenCalled(); + }); + + it("calls $location.path if goTo without parent", function() { + makeController(); + var id = makeInteger(0, 100); + $scope.selectedDevice = id; + $scope.convertTo = { + goTo: true + }; + spyOn(DiscoveriesManager, "_removeItem"); + spyOn($location, "path"); + $scope.afterSave({ + hostname: makeName("hostname"), + parent: null + }); + expect($location.path).toHaveBeenCalledWith("/devices/"); + }); + + it("calls $location.path if goTo with parent", function() { + makeController(); + var id = makeInteger(0, 100); + $scope.selectedDevice = id; + $scope.convertTo = { + goTo: true + }; + spyOn(DiscoveriesManager, "_removeItem"); + spyOn($location, "path"); + var parent = makeName("parent"); + $scope.afterSave({ + hostname: makeName("hostname"), + parent: parent + }); + expect($location.path).toHaveBeenCalledWith("/device/" + parent); + }); + }); + + describe("removeDevice", function() { + it("calls `removeDevice` in `DiscoveriesManager`", function() { + makeController(); + var device = { + ip: "127.0.0.1", + mac_address: "00:25:96:FF:FE:12:34:56" + }; + spyOn(DiscoveriesManager, "removeDevice"); + $scope.removeDevice(device); + expect(DiscoveriesManager.removeDevice).toHaveBeenCalled(); + expect(DiscoveriesManager.removeDevice).toHaveBeenCalledWith(device); + }); + }); + + describe("removeAllDevices", function() { + it("calls `removeDevices` in `DiscoveriesManager`", function() { + makeController(); + var device = { + ip: "127.0.0.1", + mac_address: "00:25:96:FF:FE:12:34:56" + }; + $scope.discoveredDevices.push(device); + spyOn(DiscoveriesManager, "removeDevices").and.callFake(function() { + var deferred = $q.defer(); + return deferred.promise; + }); + $scope.removeAllDevices(); + expect(DiscoveriesManager.removeDevices).toHaveBeenCalled(); + }); + }); + + describe("openClearDiscoveriesPanel", function() { + it("sets `showClearDiscoveriesPanel` to `true`", function() { + makeController(); + $scope.openClearDiscoveriesPanel(); + expect($scope.showClearDiscoveriesPanel).toBe(true); + }); + }); + + describe("closeClearDiscoveriesPanel", function() { + it("sets `showClearDiscoveriesPanel` to `false`", function() { + makeController(); + $scope.closeClearDiscoveriesPanel(); + expect($scope.showClearDiscoveriesPanel).toBe(false); + }); + }); + + describe("getCount", function() { + it("gets count of specified objects", function() { + makeController(); + var device = { + ip: "127.0.0.1", + mac_address: "00:25:96:FF:FE:12:34:56" + }; + $scope.discoveredDevices.push(device); + + expect($scope.getCount("ip", "127.0.0.1")).toBe(1); + expect($scope.getCount("ip", "213.0.0.1")).toBe(0); + }); + }); + + describe("dedupeMetadata", function() { + it("dedupes metadata", function() { + makeController(); + $scope.discoveredDevices = [ + { + fabric_name: "fabric-0", + vlan: 5001, + rack: "bionic-maas", + subnet: "172.16.1.0/24" + }, + { + fabric_name: "fabric-0", + vlan: 5001, + rack: "bionic-maas", + subnet: "172.16.1.0/24" } + ]; - // Create the controller. - var controller = $controller("DashboardController", { - $scope: $scope, - $rootScope: $rootScope, - DiscoveriesManager: DiscoveriesManager, - DomainsManager: DomainsManager, - MachinesManager: MachinesManager, - DevicesManager: DevicesManager, - SubnetsManager: SubnetsManager, - VLANsManager: VLANsManager, - ConfigsManager: ConfigsManager, - ManagerHelperService: ManagerHelperService - }); + expect($scope.dedupeMetadata("fabric_name")).toEqual([ + { + fabric_name: "fabric-0", + vlan: 5001, + rack: "bionic-maas", + subnet: "172.16.1.0/24" + } + ]); + }); + }); - return controller; - } + describe("toggleFilter", function() { + it("calls SearchService.toggleFilter", function() { + makeController(); + spyOn(SearchService, "toggleFilter").and.returnValue( + SearchService.getEmptyFilter() + ); + $scope.toggleFilter("hostname", "test"); + expect(SearchService.toggleFilter).toHaveBeenCalled(); + }); + + it("sets $scope.filters", function() { + makeController(); + var filters = { _: [], other: [] }; + spyOn(SearchService, "toggleFilter").and.returnValue(filters); + $scope.toggleFilter("hostname", "test"); + expect($scope.filters).toBe(filters); + }); + + it("calls SearchService.filtersToString", function() { + makeController(); + spyOn(SearchService, "filtersToString").and.returnValue(""); + $scope.toggleFilter("hostname", "test"); + expect(SearchService.filtersToString).toHaveBeenCalled(); + }); + + it("sets $scope.search", function() { + makeController(); + $scope.toggleFilter("hostname", "test"); + expect($scope.search).toBe("hostname:(=test)"); + }); + }); + + describe("setMetadata", function() { + it("sets metadata for fabrics, vlans, racks and subnets", function() { + makeController(); + $scope.discoveredDevices = [ + { + fabric_name: "fabric-01", + vlan: 5001, + observer_hostname: "happy-rack", + subnet_cidr: "127.0.0.1/24" + }, + { + fabric_name: "fabric-02", + vlan: 5002, + observer_hostname: "happy-rack", + subnet_cidr: "127.0.0.1/25" + }, + { + fabric_name: "fabric-03", + vlan: 5002, + observer_hostname: "happy-rack", + subnet_cidr: "127.0.0.1/24" + } + ]; + + $scope.setMetadata(); - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Dashboard"); - expect($rootScope.page).toBe("dashboard"); - }); - - it("calls loadManagers with correct managers", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - DiscoveriesManager, DomainsManager, MachinesManager, - DevicesManager, SubnetsManager, VLANsManager, ConfigsManager]); - }); - - it("sets initial $scope", function() { - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.discoveredDevices).toBe(DiscoveriesManager.getItems()); - expect($scope.domains).toBe(DomainsManager.getItems()); - expect($scope.machines).toBe(MachinesManager.getItems()); - expect($scope.configManager).toBe(ConfigsManager); - expect($scope.networkDiscovery).toBeNull(); - expect($scope.column).toBe('mac'); - expect($scope.selectedDevice).toBeNull(); - expect($scope.convertTo).toBeNull(); - }); - - describe("proxyManager", function() { - - it("calls DevicesManager.createItem when device", function() { - makeController(); - var sentinel = {}; - spyOn(DevicesManager, "createItem").and.returnValue(sentinel); - $scope.convertTo = { - type: 'device' - }; - var params = {}; - var observed = $scope.proxyManager.updateItem(params); - expect(observed).toBe(sentinel); - expect(DevicesManager.createItem).toHaveBeenCalledWith(params); - }); - - it("calls DevicesManager.createInterface when interface", function() { - makeController(); - var sentinel = {}; - spyOn(DevicesManager, "createInterface").and.returnValue(sentinel); - $scope.convertTo = { - type: 'interface' - }; - var params = {}; - var observed = $scope.proxyManager.updateItem(params); - expect(observed).toBe(sentinel); - expect(DevicesManager.createInterface).toHaveBeenCalledWith(params); - }); - }); - - describe("getDiscoveryName", function() { - - it("returns discovery hostname", function() { - makeController(); - var discovery = { hostname: "hostname" }; - expect($scope.getDiscoveryName(discovery)).toBe("hostname"); - }); - - it("returns discovery mac_organization with device octets", function() { - makeController(); - var discovery = { - hostname: null, - mac_organization: "mac-org", - mac_address: "00:11:22:33:44:55" - }; - var expected_name = "unknown"; - expect($scope.getDiscoveryName(discovery)).toBe(expected_name); - }); - - it("returns discovery with device mac", function() { - makeController(); - var discovery = { - hostname: null, - mac_organization: null, - mac_address: "00:11:22:33:44:55" - }; - var expected_name = "unknown"; - expect($scope.getDiscoveryName(discovery)).toBe(expected_name); - }); - }); - - describe("getSubnetName", function() { - - it("calls SubnetsManager.getName with subnet", function() { - makeController(); - var subnet = { - id: makeInteger(0, 100) - }; - var sentinel = {}; - SubnetsManager._items = [subnet]; - spyOn(SubnetsManager, "getName").and.returnValue(sentinel); - expect($scope.getSubnetName(subnet.id)).toBe(sentinel); - expect(SubnetsManager.getName).toHaveBeenCalledWith(subnet); - }); - }); - - describe("getVLANName", function() { - - it("calls VLANsManager.getName with vlan", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var sentinel = {}; - VLANsManager._items = [vlan]; - spyOn(VLANsManager, "getName").and.returnValue(sentinel); - expect($scope.getVLANName(vlan.id)).toBe(sentinel); - expect(VLANsManager.getName).toHaveBeenCalledWith(vlan); - }); - }); - - describe("toggleSelected", function() { - - it("clears selected if already selected", function() { - makeController(); - var id = makeInteger(0, 100); - $scope.selectedDevice = id; - $scope.toggleSelected(id); - expect($scope.selectedDevice).toBeNull(); - }); - - it("sets selectedDevice and convertTo with static", function() { - makeController(); - var id = makeInteger(0, 100); - var defaultDomain = { - id: 0 - }; - DomainsManager._items = [defaultDomain]; - var discovered = { - first_seen: id, - hostname: makeName("hostname"), - subnet: makeInteger(0, 100) - }; - DiscoveriesManager._items = [discovered]; - $scope.toggleSelected(id); - expect($scope.selectedDevice).toBe(id); - expect($scope.convertTo).toEqual({ - type: 'device', - hostname: $scope.getDiscoveryName(discovered), - domain: defaultDomain, - parent: null, - ip_assignment: 'dynamic', - goTo: false, - saved: false, - deviceIPOptions: [ - ['static', 'Static'], - ['dynamic', 'Dynamic'], - ['external', 'External'] - ] - }); - }); - it("sets handles fqdn correctly", function() { - makeController(); - var id = makeInteger(0, 100); - var defaultDomain = { - id: 0 - }; - var domain = { - id: 1, - name: makeName("domain") - }; - var hostname = makeName("hostname"); - DomainsManager._items = [defaultDomain, domain]; - var discovered = { - first_seen: id, - hostname: hostname + "." + domain.name, - subnet: makeInteger(0, 100) - }; - DiscoveriesManager._items = [discovered]; - $scope.toggleSelected(id); - expect($scope.selectedDevice).toBe(id); - // Just confirm the hostname and domain, the rest is checked in the - // above test. - expect($scope.convertTo.hostname).toBe(hostname); - expect($scope.convertTo.domain).toBe(domain); - }); - - it("sets selectedDevice and convertTo without static", function() { - makeController(); - var id = makeInteger(0, 100); - var defaultDomain = { - id: 0 - }; - DomainsManager._items = [defaultDomain]; - var discovered = { - first_seen: id, - hostname: makeName("hostname"), - subnet: null - }; - DiscoveriesManager._items = [discovered]; - $scope.toggleSelected(id); - expect($scope.selectedDevice).toBe(id); - expect($scope.convertTo).toEqual({ - type: 'device', - hostname: $scope.getDiscoveryName(discovered), - domain: defaultDomain, - parent: null, - ip_assignment: 'dynamic', - goTo: false, - saved: false, - deviceIPOptions: [ - ['dynamic', 'Dynamic'], - ['external', 'External'] - ] - }); - }); - }); - - describe("sortTable", function() { - - it("sets predicate", function() { - makeController(); - var predicate = makeName('predicate'); - $scope.sortTable(predicate); - expect($scope.predicate).toBe(predicate); - }); - - it("reverses reverse", function() { - makeController(); - $scope.reverse = true; - $scope.sortTable(makeName('predicate')); - expect($scope.reverse).toBe(false); - }); - }); - - describe("preProcess", function() { - - it("adjust device to include the needed fields", function() { - makeController(); - var id = makeInteger(0, 100); - var defaultDomain = { - id: 0 - }; - DomainsManager._items = [defaultDomain]; - var discovered = { - first_seen: id, - hostname: makeName("hostname"), - subnet: makeInteger(0, 100), - mac_address: makeName("mac"), - ip: makeName("ip") - }; - DiscoveriesManager._items = [discovered]; - $scope.toggleSelected(id); - var observed = $scope.preProcess($scope.convertTo); - expect(observed).not.toBe($scope.convertTo); - expect(observed).toEqual({ - type: 'device', - hostname: $scope.getDiscoveryName(discovered), - domain: defaultDomain, - parent: null, - ip_assignment: 'dynamic', - goTo: false, - saved: false, - deviceIPOptions: [ - ['static', 'Static'], - ['dynamic', 'Dynamic'], - ['external', 'External'] - ], - primary_mac: discovered.mac_address, - extra_macs: [], - interfaces: [{ - mac: discovered.mac_address, - ip_assignment: 'dynamic', - ip_address: discovered.ip, - subnet: discovered.subnet - }] - }); - }); - - it("adjust interface to include the needed fields", function() { - makeController(); - var id = makeInteger(0, 100); - var defaultDomain = { - id: 0 - }; - DomainsManager._items = [defaultDomain]; - var discovered = { - first_seen: id, - hostname: makeName("hostname"), - subnet: makeInteger(0, 100), - mac_address: makeName("mac"), - ip: makeName("ip") - }; - DiscoveriesManager._items = [discovered]; - $scope.toggleSelected(id); - $scope.convertTo.type = 'interface'; - var observed = $scope.preProcess($scope.convertTo); - expect(observed).not.toBe($scope.convertTo); - expect(observed).toEqual({ - type: 'interface', - hostname: $scope.getDiscoveryName(discovered), - domain: defaultDomain, - parent: null, - ip_assignment: 'dynamic', - goTo: false, - saved: false, - deviceIPOptions: [ - ['static', 'Static'], - ['dynamic', 'Dynamic'], - ['external', 'External'] - ], - mac_address: discovered.mac_address, - ip_address: discovered.ip, - subnet: discovered.subnet - }); - }); - }); - - describe("afterSave", function() { - - it("removes item from DiscoveriesManager", function() { - makeController(); - var id = makeInteger(0, 100); - $scope.selectedDevice = id; - $scope.convertTo = { - goTo: false - }; - spyOn(DiscoveriesManager, "_removeItem"); - var newObj = { - hostname: makeName("hostname"), - parent: makeName("parent") - }; - $scope.afterSave(newObj); - expect(DiscoveriesManager._removeItem).toHaveBeenCalledWith(id); - expect($scope.convertTo.hostname).toBe(newObj.hostname); - expect($scope.convertTo.parent).toBe(newObj.parent); - expect($scope.convertTo.saved).toBe(true); - expect($scope.selectedDevice).toBeNull(); - }); - - it("doesn't call $location.path if not goTo", function() { - makeController(); - var id = makeInteger(0, 100); - $scope.selectedDevice = id; - $scope.convertTo = { - goTo: false - }; - spyOn(DiscoveriesManager, "_removeItem"); - spyOn($location, "path"); - $scope.afterSave({ - hostname: makeName("hostname"), - parent: makeName("parent") - }); - expect($location.path).not.toHaveBeenCalled(); - }); - - it("calls $location.path if goTo without parent", function() { - makeController(); - var id = makeInteger(0, 100); - $scope.selectedDevice = id; - $scope.convertTo = { - goTo: true - }; - spyOn(DiscoveriesManager, "_removeItem"); - spyOn($location, "path"); - $scope.afterSave({ - hostname: makeName("hostname"), - parent: null - }); - expect($location.path).toHaveBeenCalledWith("/devices/"); - }); - - it("calls $location.path if goTo with parent", function() { - makeController(); - var id = makeInteger(0, 100); - $scope.selectedDevice = id; - $scope.convertTo = { - goTo: true - }; - spyOn(DiscoveriesManager, "_removeItem"); - spyOn($location, "path"); - var parent = makeName("parent"); - $scope.afterSave({ - hostname: makeName("hostname"), - parent: parent - }); - expect($location.path).toHaveBeenCalledWith("/device/" + parent); - }); - }); - - describe("removeDevice", function () { - it("calls `removeDevice` in `DiscoveriesManager`", function() { - makeController(); - var device = { - ip: "127.0.0.1", - mac_address: "00:25:96:FF:FE:12:34:56" - }; - spyOn(DiscoveriesManager, "removeDevice"); - $scope.removeDevice(device); - expect(DiscoveriesManager.removeDevice).toHaveBeenCalled(); - expect(DiscoveriesManager.removeDevice) - .toHaveBeenCalledWith(device); - }); - }); - - describe("removeAllDevices", function () { - it("calls `removeDevices` in `DiscoveriesManager`", function () { - makeController(); - var device = { - ip: "127.0.0.1", - mac_address: "00:25:96:FF:FE:12:34:56" - }; - $scope.discoveredDevices.push(device); - spyOn(DiscoveriesManager, "removeDevices").and.callFake(function() { - var deferred = $q.defer(); - return deferred.promise; - }); - $scope.removeAllDevices(); - expect(DiscoveriesManager.removeDevices).toHaveBeenCalled(); - }); - }); - - describe("openClearDiscoveriesPanel", function() { - it("sets `showClearDiscoveriesPanel` to `true`", function() { - makeController(); - $scope.openClearDiscoveriesPanel(); - expect($scope.showClearDiscoveriesPanel).toBe(true); - }); - }); - - describe("closeClearDiscoveriesPanel", function() { - it("sets `showClearDiscoveriesPanel` to `false`", function() { - makeController(); - $scope.closeClearDiscoveriesPanel(); - expect($scope.showClearDiscoveriesPanel).toBe(false); - }); - }); - - describe("getCount", function() { - it("gets count of specified objects", function() { - makeController(); - var device = { - ip: "127.0.0.1", - mac_address: "00:25:96:FF:FE:12:34:56" - }; - $scope.discoveredDevices.push(device); - - expect($scope.getCount("ip", "127.0.0.1")).toBe(1); - expect($scope.getCount("ip", "213.0.0.1")).toBe(0); - }); - }); - - describe("dedupeMetadata", function() { - it("dedupes metadata", function() { - makeController(); - $scope.discoveredDevices = [ - { - fabric_name: "fabric-0", - vlan: 5001, - rack: "bionic-maas", - subnet: "172.16.1.0/24" - }, - { - fabric_name: "fabric-0", - vlan: 5001, - rack: "bionic-maas", - subnet: "172.16.1.0/24" - } - ]; - - expect($scope.dedupeMetadata("fabric_name")).toEqual([ - { - fabric_name: "fabric-0", - vlan: 5001, - rack: "bionic-maas", - subnet: "172.16.1.0/24" - } - ]); - }); - }); - - describe("toggleFilter", function() { - it("calls SearchService.toggleFilter", function () { - makeController(); - spyOn(SearchService, "toggleFilter").and.returnValue( - SearchService.getEmptyFilter()); - $scope.toggleFilter("hostname", "test"); - expect(SearchService.toggleFilter).toHaveBeenCalled(); - }); - - it("sets $scope.filters", function() { - makeController(); - var filters = { _: [], other: [] }; - spyOn(SearchService, "toggleFilter").and.returnValue( - filters); - $scope.toggleFilter("hostname", "test"); - expect($scope.filters).toBe(filters); - }); - - it("calls SearchService.filtersToString", function() { - makeController(); - spyOn(SearchService, "filtersToString").and.returnValue(""); - $scope.toggleFilter("hostname", "test"); - expect(SearchService.filtersToString).toHaveBeenCalled(); - }); - - it("sets $scope.search", function() { - makeController(); - $scope.toggleFilter("hostname", "test"); - expect($scope.search).toBe("hostname:(=test)"); - }); - }); - - describe("setMetadata", function() { - it("sets metadata for fabrics, vlans, racks and subnets", function() { - makeController(); - $scope.discoveredDevices = [ - { - fabric_name: "fabric-01", - vlan: 5001, - observer_hostname: "happy-rack", - subnet_cidr: "127.0.0.1/24" - }, - { - fabric_name: "fabric-02", - vlan: 5002, - observer_hostname: "happy-rack", - subnet_cidr: "127.0.0.1/25" - }, - { - fabric_name: "fabric-03", - vlan: 5002, - observer_hostname: "happy-rack", - subnet_cidr: "127.0.0.1/24" - } - ]; - - $scope.setMetadata(); - - expect($scope.metadata).toEqual({ - fabric: [ - { name: "fabric-01", count: 1 }, - { name: "fabric-02", count: 1 }, - { name: "fabric-03", count: 1 } - ], - vlan: [ - { name: 5001, count: 1 }, - { name: 5002, count: 2 } - ], - rack: [ - { name: "happy-rack", count: 3 } - ], - subnet: [ - { name: "127.0.0.1/24", count: 2 }, - { name: "127.0.0.1/25", count: 1 } - ] - }); - }); - }); - - describe("isFilterActive", function() { - it("returns true when active", function() { - makeController(); - $scope.toggleFilter("hostname", "test"); - expect($scope.isFilterActive("hostname", "test")).toBe(true); - }); - - it("returns false when inactive", function() { - makeController(); - $scope.toggleFilter("hostname", "test2"); - expect($scope.isFilterActive("hostname", "test")).toBe(false); - }); - }); - - describe("updateFilters", function () { - - it("updates filters and sets searchValid to true", function () { - makeController(); - $scope.search = "test hostname:name"; - $scope.updateFilters(); - expect($scope.filters).toEqual({ - _: ["test"], - hostname: ["name"] - }); - expect($scope.searchValid).toBe(true); - }); - - it("updates sets filters empty and sets searchValid to false", - function () { - makeController(); - $scope.search = "test hostname:(name"; - $scope.updateFilters(); - expect( - $scope.filters).toEqual( - SearchService.getEmptyFilter()); - expect($scope.searchValid).toBe(false); - }); + expect($scope.metadata).toEqual({ + fabric: [ + { name: "fabric-01", count: 1 }, + { name: "fabric-02", count: 1 }, + { name: "fabric-03", count: 1 } + ], + vlan: [{ name: 5001, count: 1 }, { name: 5002, count: 2 }], + rack: [{ name: "happy-rack", count: 3 }], + subnet: [ + { name: "127.0.0.1/24", count: 2 }, + { name: "127.0.0.1/25", count: 1 } + ] + }); + }); + }); + + describe("isFilterActive", function() { + it("returns true when active", function() { + makeController(); + $scope.toggleFilter("hostname", "test"); + expect($scope.isFilterActive("hostname", "test")).toBe(true); + }); + + it("returns false when inactive", function() { + makeController(); + $scope.toggleFilter("hostname", "test2"); + expect($scope.isFilterActive("hostname", "test")).toBe(false); + }); + }); + + describe("updateFilters", function() { + it("updates filters and sets searchValid to true", function() { + makeController(); + $scope.search = "test hostname:name"; + $scope.updateFilters(); + expect($scope.filters).toEqual({ + _: ["test"], + hostname: ["name"] + }); + expect($scope.searchValid).toBe(true); + }); + + it("updates sets filters empty and sets searchValid to false", function() { + makeController(); + $scope.search = "test hostname:(name"; + $scope.updateFilters(); + expect($scope.filters).toEqual(SearchService.getEmptyFilter()); + expect($scope.searchValid).toBe(false); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_domain_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_domain_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_domain_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_domain_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,219 +4,215 @@ * Unit tests for DomainsListController. */ -describe("DomainDetailsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Make a fake domain - function makeDomain() { - var domain = { - id: makeInteger(1, 10000), - name: 'example.com', - displayname: 'example.com', - authoritative: true - }; - DomainsManager._items.push(domain); - return domain; - } - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $routeParams; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load any injected managers and services. - var DomainsManager, UsersManager, ManagerHelperService, ErrorService; - beforeEach(inject(function($injector) { - DomainsManager = $injector.get("DomainsManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - })); - - var domain; - beforeEach(function() { - domain = makeDomain(); - }); - - // Makes the NodesListController - function makeController(loadManagerDefer) { - spyOn(UsersManager, "isSuperUser").and.returnValue(true); - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("DomainDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - DomainsManager: DomainsManager, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); +import { makeInteger, makeName } from "testing/utils"; - return controller; - } - - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(DomainsManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - $routeParams.domain_id = domain.id; - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(domain); - $rootScope.$digest(); +describe("DomainDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - return controller; + // Make a fake domain + function makeDomain() { + var domain = { + id: makeInteger(1, 10000), + name: "example.com", + displayname: "example.com", + authoritative: true + }; + DomainsManager._items.push(domain); + return domain; + } + + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load any injected managers and services. + var DomainsManager, UsersManager, ManagerHelperService, ErrorService; + beforeEach(inject(function($injector) { + DomainsManager = $injector.get("DomainsManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + })); + + var domain; + beforeEach(function() { + domain = makeDomain(); + }); + + // Makes the NodesListController + function makeController(loadManagerDefer) { + spyOn(UsersManager, "isSuperUser").and.returnValue(true); + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { + // Create the controller. + var controller = $controller("DomainDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + DomainsManager: DomainsManager, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(DomainsManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + var controller = makeController(defer); + $routeParams.domain_id = domain.id; + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(domain); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("domains"); + }); + + it( + "calls loadManagers with [DomainsManager, UsersManager]" + + function() { makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("domains"); - }); - - it("calls loadManagers with [DomainsManager, UsersManager]" + - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [DomainsManager, UsersManager]); - }); - - it("raises error if domain identifier is invalid", function() { - spyOn(DomainsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ErrorService, "raiseError").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.domain_id = 'xyzzy'; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.domain).toBe(null); - expect($scope.loaded).toBe(false); - expect(DomainsManager.setActiveItem).not.toHaveBeenCalled(); - expect(ErrorService.raiseError).toHaveBeenCalled(); - }); - - it("doesn't call setActiveItem if domain is loaded", function() { - spyOn(DomainsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - DomainsManager._activeItem = domain; - $routeParams.domain_id = domain.id; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.domain).toBe(domain); - expect($scope.loaded).toBe(true); - expect(DomainsManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if domain is not active", function() { - spyOn(DomainsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.domain_id = domain.id; - - defer.resolve(); - $rootScope.$digest(); - - expect(DomainsManager.setActiveItem).toHaveBeenCalledWith( - domain.id); - }); - - it("sets domain and loaded once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.domain).toBe(domain); - expect($scope.loaded).toBe(true); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + DomainsManager, + UsersManager + ]); + } + ); + + it("raises error if domain identifier is invalid", function() { + spyOn(DomainsManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ErrorService, "raiseError").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.domain_id = "xyzzy"; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.domain).toBe(null); + expect($scope.loaded).toBe(false); + expect(DomainsManager.setActiveItem).not.toHaveBeenCalled(); + expect(ErrorService.raiseError).toHaveBeenCalled(); + }); + + it("doesn't call setActiveItem if domain is loaded", function() { + spyOn(DomainsManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + DomainsManager._activeItem = domain; + $routeParams.domain_id = domain.id; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.domain).toBe(domain); + expect($scope.loaded).toBe(true); + expect(DomainsManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if domain is not active", function() { + spyOn(DomainsManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.domain_id = domain.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(DomainsManager.setActiveItem).toHaveBeenCalledWith(domain.id); + }); + + it("sets domain and loaded once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.domain).toBe(domain); + expect($scope.loaded).toBe(true); + }); + + it("title is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(domain.displayname); + }); + + describe("canBeDeleted", function() { + it("returns false if domain is null", function() { + makeControllerResolveSetActiveItem(); + $scope.domain = null; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns false if domain has resources", function() { + makeControllerResolveSetActiveItem(); + $scope.domain.rrsets = [makeInteger()]; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns true if domain has no resources", function() { + makeControllerResolveSetActiveItem(); + $scope.domain.rrsets = []; + expect($scope.canBeDeleted()).toBe(true); + }); + }); + + describe("deleteButton", function() { + it("confirms delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + expect($scope.actionInProgress).toBe(true); + }); + + it("clears error", function() { + makeControllerResolveSetActiveItem(); + $scope.error = makeName("error"); + $scope.deleteButton(); + expect($scope.error).toBeNull(); + }); + }); + + describe("cancelAction", function() { + it("cancels delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + $scope.cancelAction(); + expect($scope.actionInProgress).toBe(false); + }); + }); + + describe("deleteDomain", function() { + it("calls deleteDomain", function() { + makeController(); + var deleteDomain = spyOn(DomainsManager, "deleteDomain"); + var defer = $q.defer(); + deleteDomain.and.returnValue(defer.promise); + $scope.deleteConfirmButton(); + expect(deleteDomain).toHaveBeenCalled(); }); - - it("title is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(domain.displayname); - }); - - describe("canBeDeleted", function() { - - it("returns false if domain is null", function() { - makeControllerResolveSetActiveItem(); - $scope.domain = null; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns false if domain has resources", function() { - makeControllerResolveSetActiveItem(); - $scope.domain.rrsets = [makeInteger()]; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns true if domain has no resources", function() { - makeControllerResolveSetActiveItem(); - $scope.domain.rrsets = []; - expect($scope.canBeDeleted()).toBe(true); - }); - }); - - describe("deleteButton", function() { - - it("confirms delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - expect($scope.actionInProgress).toBe(true); - }); - - it("clears error", function() { - makeControllerResolveSetActiveItem(); - $scope.error = makeName("error"); - $scope.deleteButton(); - expect($scope.error).toBeNull(); - }); - }); - - describe("cancelAction", function() { - - it("cancels delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - $scope.cancelAction(); - expect($scope.actionInProgress).toBe(false); - }); - }); - - describe("deleteDomain", function() { - - it("calls deleteDomain", function() { - makeController(); - var deleteDomain = spyOn(DomainsManager, "deleteDomain"); - var defer = $q.defer(); - deleteDomain.and.returnValue(defer.promise); - $scope.deleteConfirmButton(); - expect(deleteDomain).toHaveBeenCalled(); - }); - }); - + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_domains_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_domains_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_domains_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_domains_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,158 +4,137 @@ * Unit tests for DomainsListController. */ -describe("DomainsListController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); +import { makeInteger } from "testing/utils"; - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q, $routeParams; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load the managers and services. - var DomainsManager, UsersManager; - var ManagerHelperService, RegionConnection; - beforeEach(inject(function($injector) { - DomainsManager = $injector.get("DomainsManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Makes the DomainsListController - function makeController(loadManagerDefer, defaultConnectDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("DomainsListController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - DomainsManager: DomainsManager, - ManagerHelperService: ManagerHelperService - }); +describe("DomainsListController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load the managers and services. + var DomainsManager, UsersManager; + var ManagerHelperService; + beforeEach(inject(function($injector) { + DomainsManager = $injector.get("DomainsManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Makes the DomainsListController + function makeController(loadManagerDefer, defaultConnectDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("DNS"); - expect($rootScope.page).toBe("domains"); - }); - - it("sets initial values on $scope", function() { - // tab-independent variables. - makeController(); - expect($scope.domains).toBe(DomainsManager.getItems()); - expect($scope.loading).toBe(true); - }); - - it("calls loadManagers with [DomainsManager, UsersManager]", - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [DomainsManager, UsersManager]); - }); - - it("sets loading to false when loadManagers resolves", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.loading).toBe(false); - }); - - describe("addDomain", function() { - - it("calls show in addDomainScope", function() { - makeController(); - $scope.addDomainScope = { - show: jasmine.createSpy("show") - }; - $scope.addDomain(); - expect($scope.addDomainScope.show).toHaveBeenCalled(); - }); - }); - - describe("cancelAddDomain", function() { - - it("calls cancel in addDomainScope", function() { - makeController(); - $scope.addDomainScope = { - cancel: jasmine.createSpy("cancel") - }; - $scope.cancelAddDomain(); - expect($scope.addDomainScope.cancel).toHaveBeenCalled(); - }); - }); - - describe("confirmSetDefault", function() { - - it("sets confirmSetDefaultRow to the specified row", function() { - makeController(); - var obj = { - id: makeInteger(0, 100) - }; - $scope.confirmSetDefault(obj); - expect($scope.confirmSetDefaultRow).toBe(obj); - }); - }); - - describe("cancelSetDefault", function() { - - it("sets confirmSetDefaultRow to the specified row", function() { - makeController(); - var obj = { - id: makeInteger(0, 100) - }; - $scope.confirmSetDefaultRow = obj; - $scope.cancelSetDefault(); - expect($scope.confirmSetDefaultRow).toBe(null); - }); - }); - - describe("setDefault", function() { - - it("calls DomainsManager.setDefault and clears selection", function() { - makeController(); - spyOn(DomainsManager, "setDefault"); - var obj = { - id: makeInteger(0, 100) - }; - $scope.confirmSetDefaultRow = obj; - $scope.setDefault(obj); - expect(DomainsManager.setDefault).toHaveBeenCalledWith(obj); - expect($scope.confirmSetDefaultRow).toBe(null); - - }); - }); - - setupController = function(domains) { - var defer = $q.defer(); - var controller = makeController(defer); - $scope.domains = domains; - DomainsManager._items = domains; - defer.resolve(); - $rootScope.$digest(); - return controller; - }; - - testUpdates = function(controller, domains, expectedDomainsData) { - $scope.domains = domains; - DomainsManager._items = domains; - $rootScope.$digest(); - expect($scope.data).toEqual(expectedDomainsData); - }; + // Create the controller. + var controller = $controller("DomainsListController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + DomainsManager: DomainsManager, + ManagerHelperService: ManagerHelperService + }); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("DNS"); + expect($rootScope.page).toBe("domains"); + }); + + it("sets initial values on $scope", function() { + // tab-independent variables. + makeController(); + expect($scope.domains).toBe(DomainsManager.getItems()); + expect($scope.loading).toBe(true); + }); + + it("calls loadManagers with [DomainsManager, UsersManager]", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + DomainsManager, + UsersManager + ]); + }); + + it("sets loading to false when loadManagers resolves", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.loading).toBe(false); + }); + + describe("addDomain", function() { + it("calls show in addDomainScope", function() { + makeController(); + $scope.addDomainScope = { + show: jasmine.createSpy("show") + }; + $scope.addDomain(); + expect($scope.addDomainScope.show).toHaveBeenCalled(); + }); + }); + + describe("cancelAddDomain", function() { + it("calls cancel in addDomainScope", function() { + makeController(); + $scope.addDomainScope = { + cancel: jasmine.createSpy("cancel") + }; + $scope.cancelAddDomain(); + expect($scope.addDomainScope.cancel).toHaveBeenCalled(); + }); + }); + + describe("confirmSetDefault", function() { + it("sets confirmSetDefaultRow to the specified row", function() { + makeController(); + var obj = { + id: makeInteger(0, 100) + }; + $scope.confirmSetDefault(obj); + expect($scope.confirmSetDefaultRow).toBe(obj); + }); + }); + + describe("cancelSetDefault", function() { + it("sets confirmSetDefaultRow to the specified row", function() { + makeController(); + var obj = { + id: makeInteger(0, 100) + }; + $scope.confirmSetDefaultRow = obj; + $scope.cancelSetDefault(); + expect($scope.confirmSetDefaultRow).toBe(null); + }); + }); + + describe("setDefault", function() { + it("calls DomainsManager.setDefault and clears selection", function() { + makeController(); + spyOn(DomainsManager, "setDefault"); + var obj = { + id: makeInteger(0, 100) + }; + $scope.confirmSetDefaultRow = obj; + $scope.setDefault(obj); + expect(DomainsManager.setDefault).toHaveBeenCalledWith(obj); + expect($scope.confirmSetDefaultRow).toBe(null); + }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_fabric_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_fabric_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_fabric_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_fabric_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,295 +4,291 @@ * Unit tests for FabricsListController. */ -describe("FabricDetailsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Make a fake fabric - function makeFabric() { - var fabric = { - id: makeInteger(1, 10000), - name: makeName("fabric") - }; - FabricsManager._items.push(fabric); - return fabric; - } +import { makeInteger, makeName } from "testing/utils"; - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $routeParams; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load any injected managers and services. - var FabricsManager, VLANsManager, SubnetsManager, SpacesManager; - var ControllersManager, UsersManager, ManagerHelperService, ErrorService; - beforeEach(inject(function($injector) { - FabricsManager = $injector.get("FabricsManager"); - VLANsManager = $injector.get("VLANsManager"); - SubnetsManager = $injector.get("SubnetsManager"); - SpacesManager = $injector.get("SpacesManager"); - ControllersManager = $injector.get("ControllersManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - })); - - var fabric; - beforeEach(function() { - fabric = makeFabric(); - }); - - // Makes the NodesListController - function makeController(loadManagerDefer) { - spyOn(UsersManager, "isSuperUser").and.returnValue(true); - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - // Create the controller. - var controller = $controller("FabricDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - FabricsManager: FabricsManager, - VLANsManager: VLANsManager, - SubnetsManager: SubnetsManager, - SpacesManager: SpacesManager, - ControllersManager: ControllersManager, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); - return controller; - } - - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(FabricsManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - $routeParams.fabric_id = fabric.id; - - $rootScope.$digest(); - defer.resolve(); - - $rootScope.$digest(); - setActiveDefer.resolve(fabric); - $rootScope.$digest(); +describe("FabricDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - return controller; + // Make a fake fabric + function makeFabric() { + var fabric = { + id: makeInteger(1, 10000), + name: makeName("fabric") + }; + FabricsManager._items.push(fabric); + return fabric; + } + + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load any injected managers and services. + var FabricsManager, VLANsManager, SubnetsManager, SpacesManager; + var ControllersManager, UsersManager, ManagerHelperService, ErrorService; + beforeEach(inject(function($injector) { + FabricsManager = $injector.get("FabricsManager"); + VLANsManager = $injector.get("VLANsManager"); + SubnetsManager = $injector.get("SubnetsManager"); + SpacesManager = $injector.get("SpacesManager"); + ControllersManager = $injector.get("ControllersManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + })); + + var fabric; + beforeEach(function() { + fabric = makeFabric(); + }); + + // Makes the NodesListController + function makeController(loadManagerDefer) { + spyOn(UsersManager, "isSuperUser").and.returnValue(true); + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - - it("sets title and page on $rootScope", function() { + // Create the controller. + var controller = $controller("FabricDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + FabricsManager: FabricsManager, + VLANsManager: VLANsManager, + SubnetsManager: SubnetsManager, + SpacesManager: SpacesManager, + ControllersManager: ControllersManager, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(FabricsManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + var controller = makeController(defer); + $routeParams.fabric_id = fabric.id; + + $rootScope.$digest(); + defer.resolve(); + + $rootScope.$digest(); + setActiveDefer.resolve(fabric); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("networks"); + }); + + it( + "calls loadManagers with correct managers" + + function() { makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("networks"); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + FabricsManager, + VLANsManager, + SubnetsManager, + SpacesManager, + ControllersManager, + UsersManager + ]); + } + ); + + it("raises error if fabric identifier is invalid", function() { + spyOn(FabricsManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ErrorService, "raiseError").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.fabric_id = "xyzzy"; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.fabric).toBe(null); + expect($scope.loaded).toBe(false); + expect(FabricsManager.setActiveItem).not.toHaveBeenCalled(); + expect(ErrorService.raiseError).toHaveBeenCalled(); + }); + + it("doesn't call setActiveItem if fabric is loaded", function() { + spyOn(FabricsManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + FabricsManager._activeItem = fabric; + $routeParams.fabric_id = fabric.id; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.fabric).toBe(fabric); + expect($scope.loaded).toBe(true); + expect(FabricsManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if fabric is not active", function() { + spyOn(FabricsManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.fabric_id = fabric.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(FabricsManager.setActiveItem).toHaveBeenCalledWith(fabric.id); + }); + + it("sets fabric and loaded once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.fabric).toBe(fabric); + expect($scope.loaded).toBe(true); + }); + + it("title is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(fabric.name); + }); + + it("default fabric title is not special", function() { + fabric.id = 0; + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(fabric.name); + }); + + it("updates $scope.rows with VLANs containing subnet(s)", function() { + var spaces = [{ id: 0, name: "space-0" }]; + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: fabric.id }]; + var subnets = [ + { id: 0, name: "subnet1", vlan: 1, space: 0, cidr: "10.20.0.0/16" } + ]; + fabric.vlan_ids = [1]; + fabric.default_vlan_id = 1; + spaces[0].subnet_ids = [0]; + SpacesManager._items.push(spaces[0]); + VLANsManager._items.push(vlans[0]); + SubnetsManager._items.push(subnets[0]); + makeControllerResolveSetActiveItem(); + $scope.$apply(); + $rootScope.$digest(); + var rows = $scope.rows; + expect(rows[0].vlan).toBe(vlans[0]); + expect(rows[0].subnet_name).toEqual("10.20.0.0/16 (subnet1)"); + expect(rows[0].space_name).toEqual("space-0"); + }); + + it("updates $scope.rows with VLANs containing no subnet(s)", function() { + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: fabric.id }]; + fabric.vlan_ids = [1]; + fabric.default_vlan_id = 1; + VLANsManager._items.push(vlans[0]); + makeControllerResolveSetActiveItem(); + $rootScope.$digest(); + var rows = $scope.rows; + expect(rows[0].vlan).toBe(vlans[0]); + expect(rows[0].subnet_name).toBe(null); + expect(rows[0].space_name).toBe(null); + }); + + describe("editSubnetSummary", function() { + it("enters edit mode for summary", function() { + makeController(); + $scope.editSummary = false; + $scope.enterEditSummary(); + expect($scope.editSummary).toBe(true); + }); + }); + + describe("exitEditSubnetSummary", function() { + it("enters edit mode for summary", function() { + makeController(); + $scope.editSummary = true; + $scope.exitEditSummary(); + expect($scope.editSummary).toBe(false); + }); + }); + + describe("canBeDeleted", function() { + it("returns false if fabric is null", function() { + makeControllerResolveSetActiveItem(); + $scope.fabric = null; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns false if fabric is default fabric", function() { + makeControllerResolveSetActiveItem(); + $scope.fabric.id = 0; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns true if fabric is not default fabric", function() { + makeControllerResolveSetActiveItem(); + $scope.fabric.id = 1; + expect($scope.canBeDeleted()).toBe(true); + }); + }); + + describe("deleteButton", function() { + it("confirms delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + expect($scope.confirmingDelete).toBe(true); + }); + + it("clears error", function() { + makeControllerResolveSetActiveItem(); + $scope.error = makeName("error"); + $scope.deleteButton(); + expect($scope.error).toBeNull(); + }); + }); + + describe("cancelDeleteButton", function() { + it("cancels delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + $scope.cancelDeleteButton(); + expect($scope.confirmingDelete).toBe(false); + }); + }); + + describe("deleteFabric", function() { + it("calls deleteFabric", function() { + $location = {}; + $location.path = jasmine.createSpy("path"); + $location.search = jasmine.createSpy("search"); + makeController(); + var deleteFabric = spyOn(FabricsManager, "deleteFabric"); + var defer = $q.defer(); + deleteFabric.and.returnValue(defer.promise); + $scope.deleteConfirmButton(); + defer.resolve(); + $rootScope.$apply(); + expect(deleteFabric).toHaveBeenCalled(); + expect($location.path).toHaveBeenCalledWith("/networks"); + expect($location.search).toHaveBeenCalledWith("by", "fabric"); }); - - it("calls loadManagers with correct managers" + - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - FabricsManager, VLANsManager, SubnetsManager, - SpacesManager, ControllersManager, UsersManager]); - }); - - it("raises error if fabric identifier is invalid", function() { - spyOn(FabricsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ErrorService, "raiseError").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.fabric_id = 'xyzzy'; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.fabric).toBe(null); - expect($scope.loaded).toBe(false); - expect(FabricsManager.setActiveItem).not.toHaveBeenCalled(); - expect(ErrorService.raiseError).toHaveBeenCalled(); - }); - - it("doesn't call setActiveItem if fabric is loaded", function() { - spyOn(FabricsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - FabricsManager._activeItem = fabric; - $routeParams.fabric_id = fabric.id; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.fabric).toBe(fabric); - expect($scope.loaded).toBe(true); - expect(FabricsManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if fabric is not active", function() { - spyOn(FabricsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.fabric_id = fabric.id; - - defer.resolve(); - $rootScope.$digest(); - - expect(FabricsManager.setActiveItem).toHaveBeenCalledWith( - fabric.id); - }); - - it("sets fabric and loaded once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.fabric).toBe(fabric); - expect($scope.loaded).toBe(true); - }); - - it("title is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(fabric.name); - }); - - it("default fabric title is not special", function() { - fabric.id = 0; - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(fabric.name); - }); - - it("updates $scope.rows with VLANs containing subnet(s)", function() { - var spaces = [{ id: 0, name: "space-0" }]; - var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: fabric.id }]; - var subnets = [ - { id: 0, name:"subnet1", vlan: 1, space: 0, cidr: "10.20.0.0/16" } - ]; - fabric.vlan_ids = [1]; - fabric.default_vlan_id = 1; - spaces[0].subnet_ids = [0]; - SpacesManager._items.push(spaces[0]); - VLANsManager._items.push(vlans[0]); - SubnetsManager._items.push(subnets[0]); - makeControllerResolveSetActiveItem(); - $scope.$apply(); - $rootScope.$digest(); - var rows = $scope.rows; - expect(rows[0].vlan).toBe(vlans[0]); - expect(rows[0].subnet_name).toEqual("10.20.0.0/16 (subnet1)"); - expect(rows[0].space_name).toEqual("space-0"); - }); - - it("updates $scope.rows with VLANs containing no subnet(s)", function() { - var vlans = [ { id: 1, name: "vlan4", vid: 4, fabric: fabric.id } ]; - fabric.vlan_ids = [1]; - fabric.default_vlan_id = 1; - VLANsManager._items.push(vlans[0]); - makeControllerResolveSetActiveItem(); - $rootScope.$digest(); - var rows = $scope.rows; - expect(rows[0].vlan).toBe(vlans[0]); - expect(rows[0].subnet_name).toBe(null); - expect(rows[0].space_name).toBe(null); - }); - - describe("editSubnetSummary", function() { - - it("enters edit mode for summary", function() { - makeController(); - $scope.editSummary = false; - $scope.enterEditSummary(); - expect($scope.editSummary).toBe(true); - }); - }); - - describe("exitEditSubnetSummary", function() { - - it("enters edit mode for summary", function() { - makeController(); - $scope.editSummary = true; - $scope.exitEditSummary(); - expect($scope.editSummary).toBe(false); - }); - }); - - describe("canBeDeleted", function() { - - it("returns false if fabric is null", function() { - makeControllerResolveSetActiveItem(); - $scope.fabric = null; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns false if fabric is default fabric", function() { - makeControllerResolveSetActiveItem(); - $scope.fabric.id = 0; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns true if fabric is not default fabric", function() { - makeControllerResolveSetActiveItem(); - $scope.fabric.id = 1; - expect($scope.canBeDeleted()).toBe(true); - }); - }); - - describe("deleteButton", function() { - - it("confirms delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - expect($scope.confirmingDelete).toBe(true); - }); - - it("clears error", function() { - makeControllerResolveSetActiveItem(); - $scope.error = makeName("error"); - $scope.deleteButton(); - expect($scope.error).toBeNull(); - }); - }); - - describe("cancelDeleteButton", function() { - - it("cancels delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - $scope.cancelDeleteButton(); - expect($scope.confirmingDelete).toBe(false); - }); - }); - - describe("deleteFabric", function() { - - it("calls deleteFabric", function() { - $location = {}; - $location.path = jasmine.createSpy('path'); - $location.search = jasmine.createSpy('search'); - makeController(); - var deleteFabric = spyOn(FabricsManager, "deleteFabric"); - var defer = $q.defer(); - deleteFabric.and.returnValue(defer.promise); - $scope.deleteConfirmButton(); - defer.resolve(); - $rootScope.$apply(); - expect(deleteFabric).toHaveBeenCalled(); - expect($location.path).toHaveBeenCalledWith("/networks"); - expect($location.search).toHaveBeenCalledWith("by", "fabric"); - }); - }); - + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_images.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_images.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_images.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_images.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,99 +5,99 @@ */ describe("ImagesController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load any injected managers and services. - var BootResourcesManager, ConfigsManager, UsersManager; - var ManagerHelperService; - beforeEach(inject(function($injector) { - BootResourcesManager = $injector.get("BootResourcesManager"); - ConfigsManager = $injector.get("ConfigsManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Makes the NodesListController - function makeController(loadManagerDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("ImagesController", { - $scope: $scope, - $rootScope: $rootScope, - BootResourcesManager: BootResourcesManager, - ConfigsManager: ConfigsManager, - ManagerHelperService: ManagerHelperService - }); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load any injected managers and services. + var BootResourcesManager, ConfigsManager, UsersManager; + var ManagerHelperService; + beforeEach(inject(function($injector) { + BootResourcesManager = $injector.get("BootResourcesManager"); + ConfigsManager = $injector.get("ConfigsManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Makes the NodesListController + function makeController(loadManagerDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("images"); - }); - - it("calls loadManagers with correct managers", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ConfigsManager, UsersManager]); - }); - - it("sets initial $scope", function() { - makeController(); - expect($scope.loading).toBe(true); - expect($scope.bootResources).toBe(BootResourcesManager.getData()); - expect($scope.configManager).toBe(ConfigsManager); - expect($scope.autoImport).toBeNull(); - }); - - it("clears loading and sets title", function() { - makeController(); - BootResourcesManager._data.resources = []; - $scope.$digest(); - expect($scope.loading).toBe(false); - expect($scope.title).toBe("Images"); - }); - - it("sets autoImport object", function() { - var defer = $q.defer(); - makeController(defer); - var autoImport = { - name: "boot_images_auto_import", - value: true - }; - ConfigsManager._items = [autoImport]; - defer.resolve(); - $scope.$digest(); - expect($scope.autoImport).toBe(autoImport); - }); - - describe("isSuperUser", function() { - - it("returns isSuperUser from UsersManager", function() { - makeController(); - var sentinel = {}; - spyOn(UsersManager, "isSuperUser").and.returnValue(sentinel); - expect($scope.isSuperUser()).toBe(sentinel); - }); + // Create the controller. + var controller = $controller("ImagesController", { + $scope: $scope, + $rootScope: $rootScope, + BootResourcesManager: BootResourcesManager, + ConfigsManager: ConfigsManager, + ManagerHelperService: ManagerHelperService + }); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("images"); + }); + + it("calls loadManagers with correct managers", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ConfigsManager, + UsersManager + ]); + }); + + it("sets initial $scope", function() { + makeController(); + expect($scope.loading).toBe(true); + expect($scope.bootResources).toBe(BootResourcesManager.getData()); + expect($scope.configManager).toBe(ConfigsManager); + expect($scope.autoImport).toBeNull(); + }); + + it("clears loading and sets title", function() { + makeController(); + BootResourcesManager._data.resources = []; + $scope.$digest(); + expect($scope.loading).toBe(false); + expect($scope.title).toBe("Images"); + }); + + it("sets autoImport object", function() { + var defer = $q.defer(); + makeController(defer); + var autoImport = { + name: "boot_images_auto_import", + value: true + }; + ConfigsManager._items = [autoImport]; + defer.resolve(); + $scope.$digest(); + expect($scope.autoImport).toBe(autoImport); + }); + + describe("isSuperUser", function() { + it("returns isSuperUser from UsersManager", function() { + makeController(); + var sentinel = {}; + spyOn(UsersManager, "isSuperUser").and.returnValue(sentinel); + expect($scope.isSuperUser()).toBe(sentinel); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_intro.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_intro.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_intro.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_intro.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,299 +4,289 @@ * Unit tests for IntroController. */ -// Global maas config. -MAAS_config = {}; - describe("IntroController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $window; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $window = { + location: { + reload: jasmine.createSpy("reload") + } + }; + })); + + // Load any injected managers and services. + var ConfigsManager, PackageRepositoriesManager, BootResourcesManager; + var ManagerHelperService; + beforeEach(inject(function($injector) { + ConfigsManager = $injector.get("ConfigsManager"); + PackageRepositoriesManager = $injector.get("PackageRepositoriesManager"); + BootResourcesManager = $injector.get("BootResourcesManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Before the test mark it as not complete, after each test mark + // it as complete. + beforeEach(function() { + window.MAAS_config.completed_intro = false; + }); + afterEach(function() { + window.MAAS_config.completed_intro = true; + }); + + // Makes the IntroController + function makeController(loadManagerDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); + } - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $window; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $window = { - location: { - reload: jasmine.createSpy("reload") - } - }; - })); - - // Load any injected managers and services. - var ConfigsManager, PackageRepositoriesManager, BootResourcesManager; - var ManagerHelperService; - beforeEach(inject(function($injector) { - ConfigsManager = $injector.get("ConfigsManager"); - PackageRepositoriesManager = $injector.get( - "PackageRepositoriesManager"); - BootResourcesManager = $injector.get("BootResourcesManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Before the test mark it as not complete, after each test mark - // it as complete. - beforeEach(function() { - window.MAAS_config.completed_intro = false; - }); - afterEach(function() { - window.MAAS_config.completed_intro = true; - }); - - // Makes the IntroController - function makeController(loadManagerDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); + // Create the controller. + var controller = $controller("IntroController", { + $scope: $scope, + $rootScope: $rootScope, + $window: $window, + $location: $location, + ConfigsManager: ConfigsManager, + PackageRepositoriesManager: PackageRepositoriesManager, + BootResourcesManager: BootResourcesManager, + ManagerHelperService: ManagerHelperService + }); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Welcome"); + expect($rootScope.page).toBe("intro"); + }); + + it("calls loadManagers with correct managers", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ConfigsManager, + PackageRepositoriesManager + ]); + }); + + it("sets initial $scope", function() { + makeController(); + expect($scope.loading).toBe(true); + expect($scope.configManager).toBe(ConfigsManager); + expect($scope.repoManager).toBe(PackageRepositoriesManager); + expect($scope.bootResources).toBe(BootResourcesManager.getData()); + expect($scope.hasImages).toBe(false); + expect($scope.maasName).toBeNull(); + expect($scope.upstreamDNS).toBeNull(); + expect($scope.mainArchive).toBeNull(); + expect($scope.portsArchive).toBeNull(); + expect($scope.httpProxy).toBeNull(); + }); + + it("clears loading", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $scope.$digest(); + expect($scope.loading).toBe(false); + }); + + it("calls $location.path if already completed", function() { + window.MAAS_config.completed_intro = true; + spyOn($location, "path"); + makeController(); + expect($location.path).toHaveBeenCalledWith("/"); + }); + + it("sets required objects on resolve", function() { + var defer = $q.defer(); + makeController(defer); + var maasName = { name: "maas_name" }; + var upstreamDNS = { name: "upstream_dns" }; + var httpProxy = { name: "http_proxy" }; + var mainArchive = { + default: true, + name: "main_archive" + }; + var portsArchive = { + default: true, + name: "ports_archive" + }; + ConfigsManager._items = [maasName, upstreamDNS, httpProxy, mainArchive]; + PackageRepositoriesManager._items = [mainArchive, portsArchive]; + + defer.resolve(); + $scope.$digest(); + expect($scope.maasName).toBe(maasName); + expect($scope.upstreamDNS).toBe(upstreamDNS); + expect($scope.httpProxy).toBe(httpProxy); + expect($scope.mainArchive).toBe(mainArchive); + expect($scope.portsArchive).toBe(portsArchive); + }); + + describe("$rootScope.skip", function() { + it("calls updateItem and reloads", function() { + makeController(); + var defer = $q.defer(); + spyOn(ConfigsManager, "updateItem").and.returnValue(defer.promise); + $rootScope.skip(); + + expect(ConfigsManager.updateItem).toHaveBeenCalledWith({ + name: "completed_intro", + value: true + }); + defer.resolve(); + $scope.$digest(); + expect($window.location.reload).toHaveBeenCalled(); + }); + }); + + describe("welcomeInError", function() { + it("returns false without form", function() { + makeController(); + $scope.maasName = {}; + expect($scope.welcomeInError()).toBe(false); + }); + + it("returns hasErrors from form", function() { + makeController(); + var sentinel = {}; + var hasErrors = jasmine.createSpy("hasErrors"); + hasErrors.and.returnValue(sentinel); + $scope.maasName = { + $maasForm: { + hasErrors: hasErrors } + }; + expect($scope.welcomeInError()).toBe(sentinel); + }); + }); - // Create the controller. - var controller = $controller("IntroController", { - $scope: $scope, - $rootScope: $rootScope, - $window: $window, - $location: $location, - ConfigsManager: ConfigsManager, - PackageRepositoriesManager: PackageRepositoriesManager, - BootResourcesManager: BootResourcesManager, - ManagerHelperService: ManagerHelperService - }); - - return controller; - } + describe("networkInError", function() { + it("returns false when no forms", function() { + makeController(); + $scope.upstreamDNS = {}; + $scope.mainArchive = {}; + $scope.portsArchive = {}; + $scope.httpProxy = {}; + expect($scope.networkInError()).toBe(false); + }); + + it("returns false when none have errors", function() { + makeController(); + var hasErrors = jasmine.createSpy("hasErrors"); + hasErrors.and.returnValue(false); + var obj = { + $maasForm: { + hasErrors: hasErrors + } + }; + $scope.upstreamDNS = obj; + $scope.mainArchive = obj; + $scope.portsArchive = obj; + $scope.httpProxy = obj; + expect($scope.networkInError()).toBe(false); + }); - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Welcome"); - expect($rootScope.page).toBe("intro"); - }); - - it("calls loadManagers with correct managers", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ConfigsManager, PackageRepositoriesManager]); - }); - - it("sets initial $scope", function() { - makeController(); - expect($scope.loading).toBe(true); - expect($scope.configManager).toBe(ConfigsManager); - expect($scope.repoManager).toBe(PackageRepositoriesManager); - expect($scope.bootResources).toBe(BootResourcesManager.getData()); - expect($scope.hasImages).toBe(false); - expect($scope.maasName).toBeNull(); - expect($scope.upstreamDNS).toBeNull(); - expect($scope.mainArchive).toBeNull(); - expect($scope.portsArchive).toBeNull(); - expect($scope.httpProxy).toBeNull(); - }); - - it("clears loading", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $scope.$digest(); - expect($scope.loading).toBe(false); - }); - - it("calls $location.path if already completed", function() { - window.MAAS_config.completed_intro = true; - spyOn($location, 'path'); - makeController(); - expect($location.path).toHaveBeenCalledWith('/'); - }); - - it("sets required objects on resolve", function() { - var defer = $q.defer(); - makeController(defer); - var maasName = { name: 'maas_name' }; - var upstreamDNS = { name: 'upstream_dns' }; - var httpProxy = { name: 'http_proxy' }; - var mainArchive = { - 'default': true, - name: 'main_archive' - }; - var portsArchive = { - 'default': true, - name: 'ports_archive' - }; - ConfigsManager._items = [ - maasName, upstreamDNS, httpProxy, mainArchive]; - PackageRepositoriesManager._items = [mainArchive, portsArchive]; - - defer.resolve(); - $scope.$digest(); - expect($scope.maasName).toBe(maasName); - expect($scope.upstreamDNS).toBe(upstreamDNS); - expect($scope.httpProxy).toBe(httpProxy); - expect($scope.mainArchive).toBe(mainArchive); - expect($scope.portsArchive).toBe(portsArchive); - }); - - describe("$rootScope.skip", function() { - - it("calls updateItem and reloads", function() { - makeController(); - var defer = $q.defer(); - spyOn(ConfigsManager, "updateItem").and.returnValue(defer.promise); - $rootScope.skip(); - - expect(ConfigsManager.updateItem).toHaveBeenCalledWith({ - 'name': 'completed_intro', - 'value': true - }); - defer.resolve(); - $scope.$digest(); - expect($window.location.reload).toHaveBeenCalled(); - }); - }); - - describe("welcomeInError", function() { - - it("returns false without form", function() { - makeController(); - $scope.maasName = {}; - expect($scope.welcomeInError()).toBe(false); - }); - - it("returns hasErrors from form", function() { - makeController(); - var sentinel = {}; - var hasErrors = jasmine.createSpy("hasErrors"); - hasErrors.and.returnValue(sentinel); - $scope.maasName = { - $maasForm: { - hasErrors: hasErrors - } - }; - expect($scope.welcomeInError()).toBe(sentinel); - }); - }); - - describe("networkInError", function() { - - it("returns false when no forms", function() { - makeController(); - $scope.upstreamDNS = {}; - $scope.mainArchive = {}; - $scope.portsArchive = {}; - $scope.httpProxy = {}; - expect($scope.networkInError()).toBe(false); - }); - - it("returns false when none have errors", function() { - makeController(); - var hasErrors = jasmine.createSpy("hasErrors"); - hasErrors.and.returnValue(false); - var obj = { - $maasForm: { - hasErrors: hasErrors - } - }; - $scope.upstreamDNS = obj; - $scope.mainArchive = obj; - $scope.portsArchive = obj; - $scope.httpProxy = obj; - expect($scope.networkInError()).toBe(false); - }); - - it("returns true when one has error", function() { - makeController(); - var hasErrorsFalse = jasmine.createSpy("hasErrors"); - hasErrorsFalse.and.returnValue(false); - var objFalse = { - $maasForm: { - hasErrors: hasErrorsFalse - } - }; - var hasErrorsTrue = jasmine.createSpy("hasErrors"); - hasErrorsTrue.and.returnValue(true); - var objTrue = { - $maasForm: { - hasErrors: hasErrorsTrue - } - }; - $scope.upstreamDNS = objTrue; - $scope.mainArchive = objFalse; - $scope.portsArchive = objFalse; - $scope.httpProxy = objFalse; - expect($scope.networkInError()).toBe(true); - }); - }); - - describe("canContinue", function() { - - it("returns false when welcome has error", function() { - makeController(); - spyOn($scope, "welcomeInError").and.returnValue(true); - expect($scope.canContinue()).toBe(false); - }); - - it("returns false when network has error", function() { - makeController(); - spyOn($scope, "welcomeInError").and.returnValue(false); - spyOn($scope, "networkInError").and.returnValue(true); - expect($scope.canContinue()).toBe(false); - }); - - it("returns false when no images", function() { - makeController(); - spyOn($scope, "welcomeInError").and.returnValue(false); - spyOn($scope, "networkInError").and.returnValue(false); - $scope.hasImages = false; - expect($scope.canContinue()).toBe(false); - }); - - it("returns true", function() { - makeController(); - spyOn($scope, "welcomeInError").and.returnValue(false); - spyOn($scope, "networkInError").and.returnValue(false); - $scope.hasImages = true; - expect($scope.canContinue()).toBe(true); - }); - }); - - describe("clickContinue", function() { - - it("does nothing if cannot continue", function() { - makeController(); - spyOn($scope, "canContinue").and.returnValue(false); - spyOn(ConfigsManager, "updateItem"); - $scope.clickContinue(); - expect(ConfigsManager.updateItem).not.toHaveBeenCalled(); - }); - - it("forces ignores canContinue", function() { - makeController(); - spyOn($scope, "canContinue").and.returnValue(false); - spyOn(ConfigsManager, "updateItem").and.returnValue( - $q.defer().promise); - $scope.clickContinue(true); - expect(ConfigsManager.updateItem).toHaveBeenCalledWith({ - 'name': 'completed_intro', - 'value': true - }); - }); - - it("calls updateItem and reloads", function() { - makeController(); - var defer = $q.defer(); - spyOn(ConfigsManager, "updateItem").and.returnValue(defer.promise); - $scope.clickContinue(true); - - expect(ConfigsManager.updateItem).toHaveBeenCalledWith({ - 'name': 'completed_intro', - 'value': true - }); - defer.resolve(); - $scope.$digest(); - expect($window.location.reload).toHaveBeenCalled(); - }); + it("returns true when one has error", function() { + makeController(); + var hasErrorsFalse = jasmine.createSpy("hasErrors"); + hasErrorsFalse.and.returnValue(false); + var objFalse = { + $maasForm: { + hasErrors: hasErrorsFalse + } + }; + var hasErrorsTrue = jasmine.createSpy("hasErrors"); + hasErrorsTrue.and.returnValue(true); + var objTrue = { + $maasForm: { + hasErrors: hasErrorsTrue + } + }; + $scope.upstreamDNS = objTrue; + $scope.mainArchive = objFalse; + $scope.portsArchive = objFalse; + $scope.httpProxy = objFalse; + expect($scope.networkInError()).toBe(true); + }); + }); + + describe("canContinue", function() { + it("returns false when welcome has error", function() { + makeController(); + spyOn($scope, "welcomeInError").and.returnValue(true); + expect($scope.canContinue()).toBe(false); + }); + + it("returns false when network has error", function() { + makeController(); + spyOn($scope, "welcomeInError").and.returnValue(false); + spyOn($scope, "networkInError").and.returnValue(true); + expect($scope.canContinue()).toBe(false); + }); + + it("returns false when no images", function() { + makeController(); + spyOn($scope, "welcomeInError").and.returnValue(false); + spyOn($scope, "networkInError").and.returnValue(false); + $scope.hasImages = false; + expect($scope.canContinue()).toBe(false); + }); + + it("returns true", function() { + makeController(); + spyOn($scope, "welcomeInError").and.returnValue(false); + spyOn($scope, "networkInError").and.returnValue(false); + $scope.hasImages = true; + expect($scope.canContinue()).toBe(true); + }); + }); + + describe("clickContinue", function() { + it("does nothing if cannot continue", function() { + makeController(); + spyOn($scope, "canContinue").and.returnValue(false); + spyOn(ConfigsManager, "updateItem"); + $scope.clickContinue(); + expect(ConfigsManager.updateItem).not.toHaveBeenCalled(); + }); + + it("forces ignores canContinue", function() { + makeController(); + spyOn($scope, "canContinue").and.returnValue(false); + spyOn(ConfigsManager, "updateItem").and.returnValue($q.defer().promise); + $scope.clickContinue(true); + expect(ConfigsManager.updateItem).toHaveBeenCalledWith({ + name: "completed_intro", + value: true + }); + }); + + it("calls updateItem and reloads", function() { + makeController(); + var defer = $q.defer(); + spyOn(ConfigsManager, "updateItem").and.returnValue(defer.promise); + $scope.clickContinue(true); + + expect(ConfigsManager.updateItem).toHaveBeenCalledWith({ + name: "completed_intro", + value: true + }); + defer.resolve(); + $scope.$digest(); + expect($window.location.reload).toHaveBeenCalled(); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_intro_user.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_intro_user.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_intro_user.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_intro_user.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,176 +4,170 @@ * Unit tests for IntroUserController. */ -// Global maas config. -MAAS_config = {}; - describe("IntroUserController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $window; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $window = { - location: { - reload: jasmine.createSpy("reload") - } - }; - })); - - // Load any injected managers and services. - var UsersManager, ManagerHelperService; - beforeEach(inject(function($injector) { - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Before the test mark it as not complete, after each test mark - // it as complete. - beforeEach(function() { - window.MAAS_config.user_completed_intro = false; - }); - afterEach(function() { - window.MAAS_config.user_completed_intro = true; - }); - - // Makes the IntroUserController - function makeController(loadManagerDefer) { - var loadManager = spyOn(ManagerHelperService, "loadManager"); - if(angular.isObject(loadManagerDefer)) { - loadManager.and.returnValue(loadManagerDefer.promise); - } else { - loadManager.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("IntroUserController", { - $scope: $scope, - $rootScope: $rootScope, - $window: $window, - $location: $location, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService - }); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $window; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $window = { + location: { + reload: jasmine.createSpy("reload") + } + }; + })); + + // Load any injected managers and services. + var UsersManager, ManagerHelperService; + beforeEach(inject(function($injector) { + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Before the test mark it as not complete, after each test mark + // it as complete. + beforeEach(function() { + window.MAAS_config.user_completed_intro = false; + }); + afterEach(function() { + window.MAAS_config.user_completed_intro = true; + }); + + // Makes the IntroUserController + function makeController(loadManagerDefer) { + var loadManager = spyOn(ManagerHelperService, "loadManager"); + if (angular.isObject(loadManagerDefer)) { + loadManager.and.returnValue(loadManagerDefer.promise); + } else { + loadManager.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Welcome"); - expect($rootScope.page).toBe("intro"); - }); - - it("calls loadManager with correct managers", function() { - makeController(); - expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( - $scope, UsersManager); - }); - - it("sets initial $scope", function() { - makeController(); - expect($scope.loading).toBe(true); - expect($scope.user).toBeNull(); - }); - - it("clears loading", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $scope.$digest(); - expect($scope.loading).toBe(false); - }); - - it("calls $location.path if already completed", function() { - window.MAAS_config.user_completed_intro = true; - spyOn($location, 'path'); - makeController(); - expect($location.path).toHaveBeenCalledWith('/'); - }); - - it("sets user on resolve", function() { - var defer = $q.defer(); - makeController(defer); - var user = {}; - spyOn(UsersManager, "getAuthUser").and.returnValue(user); - - defer.resolve(); - $scope.$digest(); - expect($scope.user).toBe(user); - }); - - describe("$rootScope.skip", function() { - - it("calls markIntroComplete and reloads", function() { - makeController(); - var defer = $q.defer(); - spyOn(UsersManager, "markIntroComplete").and.returnValue( - defer.promise); - $rootScope.skip(); - - expect(UsersManager.markIntroComplete).toHaveBeenCalled(); - defer.resolve(); - $scope.$digest(); - expect($window.location.reload).toHaveBeenCalled(); - }); - }); - - describe("canContinue", function() { - - it("returns false when no sshkeys", function() { - makeController(); - $scope.user = { - sshkeys_count: 0 - }; - expect($scope.canContinue()).toBe(false); - }); - - it("returns true when sshkeys", function() { - makeController(); - $scope.user = { - sshkeys_count: 1 - }; - expect($scope.canContinue()).toBe(true); - }); - }); - - describe("clickContinue", function() { - - it("does nothing if cannot continue", function() { - makeController(); - spyOn($scope, "canContinue").and.returnValue(false); - spyOn(UsersManager, "markIntroComplete"); - $scope.clickContinue(); - expect(UsersManager.markIntroComplete).not.toHaveBeenCalled(); - }); - - it("forces ignores canContinue", function() { - makeController(); - spyOn($scope, "canContinue").and.returnValue(false); - spyOn(UsersManager, "markIntroComplete").and.returnValue( - $q.defer().promise); - $scope.clickContinue(true); - expect(UsersManager.markIntroComplete).toHaveBeenCalled(); - }); - - it("calls updateItem and reloads", function() { - makeController(); - var defer = $q.defer(); - spyOn(UsersManager, "markIntroComplete").and.returnValue( - defer.promise); - $scope.clickContinue(true); - - expect(UsersManager.markIntroComplete).toHaveBeenCalled(); - defer.resolve(); - $scope.$digest(); - expect($window.location.reload).toHaveBeenCalled(); - }); + // Create the controller. + var controller = $controller("IntroUserController", { + $scope: $scope, + $rootScope: $rootScope, + $window: $window, + $location: $location, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService + }); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Welcome"); + expect($rootScope.page).toBe("intro"); + }); + + it("calls loadManager with correct managers", function() { + makeController(); + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( + $scope, + UsersManager + ); + }); + + it("sets initial $scope", function() { + makeController(); + expect($scope.loading).toBe(true); + expect($scope.user).toBeNull(); + }); + + it("clears loading", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $scope.$digest(); + expect($scope.loading).toBe(false); + }); + + it("calls $location.path if already completed", function() { + window.MAAS_config.user_completed_intro = true; + spyOn($location, "path"); + makeController(); + expect($location.path).toHaveBeenCalledWith("/"); + }); + + it("sets user on resolve", function() { + var defer = $q.defer(); + makeController(defer); + var user = {}; + spyOn(UsersManager, "getAuthUser").and.returnValue(user); + + defer.resolve(); + $scope.$digest(); + expect($scope.user).toBe(user); + }); + + describe("$rootScope.skip", function() { + it("calls markIntroComplete and reloads", function() { + makeController(); + var defer = $q.defer(); + spyOn(UsersManager, "markIntroComplete").and.returnValue(defer.promise); + $rootScope.skip(); + + expect(UsersManager.markIntroComplete).toHaveBeenCalled(); + defer.resolve(); + $scope.$digest(); + expect($window.location.reload).toHaveBeenCalled(); + }); + }); + + describe("canContinue", function() { + it("returns false when no sshkeys", function() { + makeController(); + $scope.user = { + sshkeys_count: 0 + }; + expect($scope.canContinue()).toBe(false); + }); + + it("returns true when sshkeys", function() { + makeController(); + $scope.user = { + sshkeys_count: 1 + }; + expect($scope.canContinue()).toBe(true); + }); + }); + + describe("clickContinue", function() { + it("does nothing if cannot continue", function() { + makeController(); + spyOn($scope, "canContinue").and.returnValue(false); + spyOn(UsersManager, "markIntroComplete"); + $scope.clickContinue(); + expect(UsersManager.markIntroComplete).not.toHaveBeenCalled(); + }); + + it("forces ignores canContinue", function() { + makeController(); + spyOn($scope, "canContinue").and.returnValue(false); + spyOn(UsersManager, "markIntroComplete").and.returnValue( + $q.defer().promise + ); + $scope.clickContinue(true); + expect(UsersManager.markIntroComplete).toHaveBeenCalled(); + }); + + it("calls updateItem and reloads", function() { + makeController(); + var defer = $q.defer(); + spyOn(UsersManager, "markIntroComplete").and.returnValue(defer.promise); + $scope.clickContinue(true); + + expect(UsersManager.markIntroComplete).toHaveBeenCalled(); + defer.resolve(); + $scope.$digest(); + expect($window.location.reload).toHaveBeenCalled(); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_networks_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_networks_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_networks_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_networks_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,433 +4,446 @@ * Unit tests for SubentsListController. */ +import { makeInteger } from "testing/utils"; + describe("NetworksListController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q, $routeParams, $location; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load the managers and services. + var SubnetsManager, FabricsManager, SpacesManager, VLANsManager; + var UsersManager, ManagerHelperService; + beforeEach(inject(function($injector) { + SubnetsManager = $injector.get("SubnetsManager"); + FabricsManager = $injector.get("FabricsManager"); + SpacesManager = $injector.get("SpacesManager"); + VLANsManager = $injector.get("VLANsManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Makes the SubnetsListController + function makeController(loadManagersDefer, defaultConnectDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); + } - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q, $routeParams, $location; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load the managers and services. - var SubnetsManager, FabricsManager, SpacesManager, VLANsManager; - var UsersManager, ManagerHelperService; - beforeEach(inject(function($injector) { - SubnetsManager = $injector.get("SubnetsManager"); - FabricsManager = $injector.get("FabricsManager"); - SpacesManager = $injector.get("SpacesManager"); - VLANsManager = $injector.get("VLANsManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Makes the SubnetsListController - function makeController(loadManagersDefer, defaultConnectDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } + // Create the controller. + var controller = $controller("NetworksListController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + SubnetsManager: SubnetsManager, + FabricsManager: FabricsManager, + SpacesManager: SpacesManager, + VLANsManager: VLANsManager, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService + }); - // Create the controller. - var controller = $controller("NetworksListController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - SubnetsManager: SubnetsManager, - FabricsManager: FabricsManager, - SpacesManager: SpacesManager, - VLANsManager: VLANsManager, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService - }); + return controller; + } - return controller; + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Subnets"); + expect($rootScope.page).toBe("networks"); + }); + + it("sets initial values on $scope", function() { + // tab-independent variables. + makeController(); + expect($scope.subnets).toBe(SubnetsManager.getItems()); + expect($scope.fabrics).toBe(FabricsManager.getItems()); + expect($scope.spaces).toBe(SpacesManager.getItems()); + expect($scope.vlans).toBe(VLANsManager.getItems()); + expect($scope.loading).toBe(true); + }); + + it("calls loadManagers with expected managers", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + SubnetsManager, + FabricsManager, + SpacesManager, + VLANsManager, + UsersManager + ]); + }); + + it("sets loading to false with loadManagers resolves", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.loading).toBe(false); + }); + + it("populates actions when loadManagers resolves", function() { + UsersManager._authUser = { is_superuser: true }; + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.actionOptions.length).toBe(4); + }); + + it( + "creates empty actions array when loadManagers resolves " + + "if not superuser", + function() { + UsersManager._authUser = { is_superuser: false }; + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.actionOptions.length).toBe(0); } + ); - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Subnets"); - expect($rootScope.page).toBe("networks"); - }); - - it("sets initial values on $scope", function() { - // tab-independent variables. - makeController(); - expect($scope.subnets).toBe(SubnetsManager.getItems()); - expect($scope.fabrics).toBe(FabricsManager.getItems()); - expect($scope.spaces).toBe(SpacesManager.getItems()); - expect($scope.vlans).toBe(VLANsManager.getItems()); - expect($scope.loading).toBe(true); - }); - - it("calls loadManagers with expected managers", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - SubnetsManager, FabricsManager, SpacesManager, - VLANsManager, UsersManager - ]); - }); - - it("sets loading to false with loadManagers resolves", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.loading).toBe(false); - }); - - it("populates actions when loadManagers resolves", function() { - UsersManager._authUser = { is_superuser: true }; - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.actionOptions.length).toBe(4); - }); - - it("creates empty actions array when loadManagers resolves " + - "if not superuser", function() { - UsersManager._authUser = { is_superuser: false }; - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.actionOptions.length).toBe(0); - }); - - describe("watchers and resolved managers", function() { - - function setupController(fabrics, spaces, vlans, subnets) { - var defer = $q.defer(); - var controller = makeController(defer); - $scope.fabrics = fabrics; - FabricsManager._items = fabrics; - $scope.spaces = spaces; - SpacesManager._items = spaces; - $scope.vlans = vlans; - VLANsManager._items = vlans; - $scope.subnets = subnets; - SubnetsManager._items = subnets; - defer.resolve(); - $rootScope.$digest(); - return controller; - } + describe("watchers and resolved managers", function() { + function setupController(fabrics, spaces, vlans, subnets) { + var defer = $q.defer(); + var controller = makeController(defer); + $scope.fabrics = fabrics; + FabricsManager._items = fabrics; + $scope.spaces = spaces; + SpacesManager._items = spaces; + $scope.vlans = vlans; + VLANsManager._items = vlans; + $scope.subnets = subnets; + SubnetsManager._items = subnets; + defer.resolve(); + $rootScope.$digest(); + return controller; + } - function replaceArray(dest, source) { - Array.prototype.splice.apply( - dest, [0, source.length].concat(source)); - } + function replaceArray(dest, source) { + Array.prototype.splice.apply(dest, [0, source.length].concat(source)); + } + + function doUpdates(controller, fabrics, spaces, vlans, subnets) { + replaceArray(FabricsManager._items, fabrics); + replaceArray(SpacesManager._items, spaces); + replaceArray(VLANsManager._items, vlans); + replaceArray(SubnetsManager._items, subnets); + $rootScope.$digest(); + } + + it("selects fabric groupBy by default", function() { + setupController([], [], [], []); + expect($scope.groupBy).toBe("fabric"); + }); + + it("selects space groupBy with search string", function() { + $location.search("by", "space"); + setupController([], [], [], []); + expect($scope.groupBy).toBe("space"); + }); + + it("updates groupBy when location changes", function() { + setupController([], [], [], []); + $location.search("by", "space"); + $rootScope.$broadcast("$routeUpdate"); + expect($scope.groupBy).toBe("space"); + }); + + it("updates location when groupBy changes", function() { + setupController([], [], [], []); + expect($location.search()).toEqual({ by: "fabric" }); + $scope.groupBy = "space"; + $scope.updateGroupBy(); + expect($location.search()).toEqual({ by: "space" }); + }); + + it("initial update populates fabrics", function() { + $location.search("by", "fabric"); + var fabrics = [{ id: 0, name: "fabric 0" }]; + var spaces = [{ id: 0, name: "space 0" }]; + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: 0 }]; + var subnets = [ + { id: 0, name: "subnet 0", vlan: 1, space: 0, cidr: "10.20.0.0/16" } + ]; + setupController(fabrics, spaces, vlans, subnets); + var rows = $scope.group.fabrics.rows; + expect(rows.length).toBe(1); + expect($scope.group.spaces.rows).toBe(undefined); + expect(rows[0].subnet).toBe(subnets[0]); + expect(rows[0].subnet_name).toBe("10.20.0.0/16 (subnet 0)"); + expect(rows[0].space).toBe(spaces[0]); + expect(rows[0].fabric).toBe(fabrics[0]); + expect(rows[0].fabric_name).toBe("fabric 0"); + expect(rows[0].vlan).toBe(vlans[0]); + expect(rows[0].vlan_name).toBe("4 (vlan4)"); + }); - function doUpdates(controller, fabrics, spaces, vlans, subnets) { - replaceArray(FabricsManager._items, fabrics); - replaceArray(SpacesManager._items, spaces); - replaceArray(VLANsManager._items, vlans); - replaceArray(SubnetsManager._items, subnets); - $rootScope.$digest(); + it("initial update populates spaces", function() { + $location.search("by", "space"); + var fabrics = [{ id: 0, name: "fabric 0" }]; + var spaces = [{ id: 0, name: "space 0" }]; + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: 0 }]; + var subnets = [ + { id: 0, name: "subnet 0", vlan: 1, space: 0, cidr: "10.20.0.0/16" } + ]; + setupController(fabrics, spaces, vlans, subnets); + var rows = $scope.group.spaces.rows; + expect(rows.length).toBe(1); + expect($scope.group.fabrics.rows).toBe(undefined); + expect(rows[0].subnet).toBe(subnets[0]); + expect(rows[0].subnet_name).toBe("10.20.0.0/16 (subnet 0)"); + expect(rows[0].space).toBe(spaces[0]); + expect(rows[0].space_name).toBe("space 0"); + expect(rows[0].fabric).toBe(fabrics[0]); + expect(rows[0].vlan).toBe(vlans[0]); + expect(rows[0].vlan_name).toBe("4 (vlan4)"); + }); + + it("adding fabric updates lists", function() { + var fabrics = [{ id: 0, name: "fabric-0" }]; + var spaces = [{ id: 0, name: "space-0" }]; + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: 0 }]; + var subnets = [ + { id: 0, name: "subnet-0", vlan: 1, space: 0, cidr: "10.20.0.0/16" } + ]; + + var controller = setupController(fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(1); + fabrics.push({ id: 1, name: "fabric 1" }); + vlans.push({ id: 2, vid: 0, fabric: 1 }); + doUpdates(controller, fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(2); + $scope.groupBy = "space"; + $scope.updateGroupBy(); + // We can't show a new fabric+vlan that doesn't have a subnet+space + // on the "spaces" group by, so we need more data first. + expect($scope.group.spaces.rows.length).toBe(1); + subnets.push({ + id: 1, + name: "subnet 1", + vlan: 2, + space: 0, + cidr: "10.21.0.0/16" + }); + spaces.push({ id: 1, name: "space-1" }); + doUpdates(controller, fabrics, spaces, vlans, subnets); + // We expect an extra row here for the space which isn't associated + // with any subnets. + expect($scope.group.spaces.rows.length).toBe(3); + }); + + it("adding space updates lists", function() { + var fabrics = [{ id: 0, name: "fabric 0" }]; + var spaces = [{ id: 0, name: "space 0" }]; + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: 0 }]; + var subnets = [ + { id: 0, name: "subnet 0", vlan: 1, space: 0, cidr: "10.20.0.0/16" } + ]; + var controller = setupController(fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(1); + spaces.push({ id: 1, name: "space 1" }); + subnets.push({ + id: 1, + name: "subnet 1", + vlan: 1, + space: 1, + cidr: "10.20.0.0/16" + }); + doUpdates(controller, fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(2); + $scope.groupBy = "space"; + $scope.updateGroupBy(); + // Second space should have a blank name + expect($scope.group.spaces.rows.length).toBe(2); + // Move 2nd subnet into first space and check that the name is no + // longer shown. + subnets[1].space = 0; + $scope.updateGroupBy(); + expect($scope.group.spaces.rows[1].space_name).toBe(""); + }); + + it("adding vlan updates lists appropriately", function() { + var fabrics = [{ id: 0, name: "default" }]; + var spaces = [{ id: 0, name: "space-0" }]; + var vlans = [ + { + id: 1, + name: "vlan4", + space: 0, + vid: 4, + fabric: 0 + } + ]; + var subnets = [ + { + id: 0, + name: "subnet 0", + vlan: 1, + cidr: "10.20.0.0/16", + space: 0 } + ]; + var controller = setupController(fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(1); + vlans.push({ + id: 2, + name: "vlan2", + vid: 2, + fabric: 0, + space: null + }); + subnets.push({ + id: 1, + name: "subnet 1", + vlan: 2, + cidr: "10.21.0.0/16", + space: null + }); + doUpdates(controller, fabrics, spaces, vlans, subnets); + // Fabric should have blank name + expect($scope.group.fabrics.rows[1].fabric_name).toBe(""); + expect($scope.group.fabrics.rows.length).toBe(2); + $scope.groupBy = "space"; + $scope.updateGroupBy(); + // Orphaned VLANs are called out separately. + expect($scope.group.spaces.rows.length).toBe(1); + expect($scope.group.spaces.orphanVLANs.length).toBe(1); + }); + + it("adding subnet updates lists", function() { + var fabrics = [{ id: 0, name: "fabric 0" }]; + var spaces = [{ id: 0, name: "space 0" }]; + var vlans = [{ id: 1, name: "vlan4", vid: 4, fabric: 0 }]; + var subnets = [ + { id: 0, name: "subnet 0", vlan: 1, space: 0, cidr: "10.20.0.0/16" } + ]; + var controller = setupController(fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(1); + subnets.push({ + id: 1, + name: "subnet 1", + vlan: 1, + space: 0, + cidr: "10.99.34.0/24" + }); + doUpdates(controller, fabrics, spaces, vlans, subnets); + expect($scope.group.fabrics.rows.length).toBe(2); + // Test that redundant fabric and VLAN names are suppressed + expect($scope.group.fabrics.rows[1].fabric_name).toBe(""); + expect($scope.group.fabrics.rows[1].vlan_name).toBe(""); + $scope.groupBy = "space"; + $scope.updateGroupBy(); + expect($scope.group.spaces.rows.length).toBe(2); + }); + }); + + describe("actionChanged", function() { + it("initializes newObject for fabric", function() { + makeController(); + $scope.actionOption = { + name: "add_fabric", + objectName: "fabric" + }; + $scope.actionChanged(); + expect($scope.newObject).toEqual({}); + }); + + it("initializes newObject for vlan", function() { + makeController(); + var fabric = { + id: makeInteger(0, 100) + }; + $scope.fabrics = [fabric]; + $scope.actionOption = { + name: "add_vlan", + objectName: "vlan" + }; + $scope.actionChanged(); + expect($scope.newObject).toEqual({ + fabric: fabric.id + }); + }); + + it("initializes newObject for space", function() { + makeController(); + $scope.actionOption = { + name: "add_space", + objectName: "space" + }; + $scope.actionChanged(); + expect($scope.newObject).toEqual({}); + }); + + it("initializes newObject for subnet", function() { + makeController(); + var space = { + id: makeInteger(0, 100) + }; + var fabric = { + id: makeInteger(0, 100) + }; + var vlan = { + id: makeInteger(0, 100), + fabric: fabric.id + }; + fabric.vlan_ids = [vlan.id]; + $scope.fabrics = [fabric]; + $scope.vlans = [vlan]; + $scope.spaces = [space]; + $scope.actionOption = { + name: "add_subnet", + objectName: "subnet" + }; + $scope.actionChanged(); + expect($scope.newObject).toEqual({ + vlan: vlan.id + }); + }); + }); + + describe("cancelAction", function() { + it("clears actionOption and newObject", function() { + makeController(); + $scope.actionOption = {}; + $scope.newObject = {}; + $scope.cancelAction(); + expect($scope.actionOption).toBeNull(); + expect($scope.newObject).toBeNull(); + }); + }); - it("selects fabric groupBy by default", function() { - setupController([], [], [], []); - expect($scope.groupBy).toBe("fabric"); - }); - - it("selects space groupBy with search string", function() { - $location.search('by', 'space'); - setupController([], [], [], []); - expect($scope.groupBy).toBe("space"); - }); - - it("updates groupBy when location changes", function() { - setupController([], [], [], []); - $location.search('by', 'space'); - $rootScope.$broadcast('$routeUpdate'); - expect($scope.groupBy).toBe("space"); - }); - - it("updates location when groupBy changes", function() { - setupController([], [], [], []); - expect($location.search()).toEqual({by: 'fabric'}); - $scope.groupBy = "space"; - $scope.updateGroupBy(); - expect($location.search()).toEqual({by: 'space'}); - }); - - it("initial update populates fabrics", function() { - $location.search('by', 'fabric'); - var fabrics = [ { id: 0, name: "fabric 0" } ]; - var spaces = [ { id: 0, name: "space 0" } ]; - var vlans = [ { id: 1, name: "vlan4", vid: 4, fabric: 0 } ]; - var subnets = [ - { id:0, name:"subnet 0", vlan:1, space:0, cidr:"10.20.0.0/16" } - ]; - setupController(fabrics, spaces, vlans, subnets); - var rows = $scope.group.fabrics.rows; - expect(rows.length).toBe(1); - expect($scope.group.spaces.rows).toBe(undefined); - expect(rows[0].subnet).toBe(subnets[0]); - expect(rows[0].subnet_name).toBe("10.20.0.0/16 (subnet 0)"); - expect(rows[0].space).toBe(spaces[0]); - expect(rows[0].fabric).toBe(fabrics[0]); - expect(rows[0].fabric_name).toBe("fabric 0"); - expect(rows[0].vlan).toBe(vlans[0]); - expect(rows[0].vlan_name).toBe("4 (vlan4)"); - }); - - it("initial update populates spaces", function() { - $location.search('by', 'space'); - var fabrics = [ { id: 0, name: "fabric 0" } ]; - var spaces = [ { id: 0, name: "space 0" } ]; - var vlans = [ { id: 1, name: "vlan4", vid: 4, fabric: 0 } ]; - var subnets = [ - { id:0, name:"subnet 0", vlan:1, space:0, cidr:"10.20.0.0/16" } - ]; - setupController(fabrics, spaces, vlans, subnets); - var rows = $scope.group.spaces.rows; - expect(rows.length).toBe(1); - expect($scope.group.fabrics.rows).toBe(undefined); - expect(rows[0].subnet).toBe(subnets[0]); - expect(rows[0].subnet_name).toBe("10.20.0.0/16 (subnet 0)"); - expect(rows[0].space).toBe(spaces[0]); - expect(rows[0].space_name).toBe("space 0"); - expect(rows[0].fabric).toBe(fabrics[0]); - expect(rows[0].vlan).toBe(vlans[0]); - expect(rows[0].vlan_name).toBe("4 (vlan4)"); - }); - - it("adding fabric updates lists", function() { - var fabrics = [ { id: 0, name: "fabric-0" } ]; - var spaces = [ { id: 0, name: "space-0" } ]; - var vlans = [ { id: 1, name: "vlan4", vid: 4, fabric: 0 } ]; - var subnets = [ - { id:0, name:"subnet-0", vlan:1, space:0, cidr:"10.20.0.0/16" } - ]; - - var controller = setupController(fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(1); - fabrics.push({id: 1, name: "fabric 1"}); - vlans.push({id: 2, vid:0, fabric: 1}); - doUpdates(controller, fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(2); - $scope.groupBy = "space"; - $scope.updateGroupBy(); - // We can't show a new fabric+vlan that doesn't have a subnet+space - // on the "spaces" group by, so we need more data first. - expect($scope.group.spaces.rows.length).toBe(1); - subnets.push({ - id:1, - name:"subnet 1", - vlan: 2, - space: 0, - cidr:"10.21.0.0/16" - }); - spaces.push({id: 1, name: "space-1"}); - doUpdates(controller, fabrics, spaces, vlans, subnets); - // We expect an extra row here for the space which isn't associated - // with any subnets. - expect($scope.group.spaces.rows.length).toBe(3); - }); - - it("adding space updates lists", function() { - var fabrics = [ { id: 0, name: "fabric 0" } ]; - var spaces = [ { id: 0, name: "space 0" } ]; - var vlans = [ { id: 1, name: "vlan4", vid: 4, fabric: 0 } ]; - var subnets = [ - {id:0, name:"subnet 0", vlan:1, space:0, cidr:"10.20.0.0/16"} - ]; - var controller = setupController(fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(1); - spaces.push({id: 1, name: "space 1"}); - subnets.push( - {id:1, name:"subnet 1", vlan:1, space:1, cidr:"10.20.0.0/16"}); - doUpdates(controller, fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(2); - $scope.groupBy = "space"; - $scope.updateGroupBy(); - // Second space should have a blank name - expect($scope.group.spaces.rows.length).toBe(2); - // Move 2nd subnet into first space and check that the name is no - // longer shown. - subnets[1].space = 0; - $scope.updateGroupBy(); - expect($scope.group.spaces.rows[1].space_name).toBe(""); - }); - - it("adding vlan updates lists appropriately", function() { - var fabrics = [ { id: 0, name: "default" } ]; - var spaces = [ { id: 0, name: "space-0" } ]; - var vlans = [{ - id: 1, - name: "vlan4", - space: 0, - vid: 4, - fabric: 0 - }]; - var subnets = [{ - id: 0, - name: "subnet 0", - vlan: 1, - cidr: "10.20.0.0/16", - space: 0 - }]; - var controller = setupController(fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(1); - vlans.push({ - id: 2, - name: "vlan2", - vid: 2, - fabric: 0, - space: null - }); - subnets.push({ - id: 1, - name: "subnet 1", - vlan: 2, - cidr: "10.21.0.0/16", - space: null - }); - doUpdates(controller, fabrics, spaces, vlans, subnets); - // Fabric should have blank name - expect($scope.group.fabrics.rows[1].fabric_name).toBe(""); - expect($scope.group.fabrics.rows.length).toBe(2); - $scope.groupBy = "space"; - $scope.updateGroupBy(); - // Orphaned VLANs are called out separately. - expect($scope.group.spaces.rows.length).toBe(1); - expect($scope.group.spaces.orphanVLANs.length).toBe(1); - }); - - it("adding subnet updates lists", function() { - var fabrics = [ { id: 0, name: "fabric 0" } ]; - var spaces = [ { id: 0, name: "space 0" } ]; - var vlans = [ { id: 1, name: "vlan4", vid: 4, fabric: 0 } ]; - var subnets = [ - { id:0, name:"subnet 0", vlan:1, space:0, cidr:"10.20.0.0/16" } - ]; - var controller = setupController(fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(1); - subnets.push( - {id: 1, name: "subnet 1", vlan: 1, space: 0, - cidr: "10.99.34.0/24"} - ); - doUpdates(controller, fabrics, spaces, vlans, subnets); - expect($scope.group.fabrics.rows.length).toBe(2); - // Test that redundant fabric and VLAN names are suppressed - expect($scope.group.fabrics.rows[1].fabric_name).toBe(""); - expect($scope.group.fabrics.rows[1].vlan_name).toBe(""); - $scope.groupBy = "space"; - $scope.updateGroupBy(); - expect($scope.group.spaces.rows.length).toBe(2); - }); - }); - - describe("actionChanged", function() { - - it("initializes newObject for fabric", function() { - makeController(); - $scope.actionOption = { - name: "add_fabric", - objectName: "fabric" - }; - $scope.actionChanged(); - expect($scope.newObject).toEqual({}); - }); - - it("initializes newObject for vlan", function() { - makeController(); - var fabric = { - id: makeInteger(0, 100) - }; - $scope.fabrics = [fabric]; - $scope.actionOption = { - name: "add_vlan", - objectName: "vlan" - }; - $scope.actionChanged(); - expect($scope.newObject).toEqual({ - fabric: fabric.id - }); - }); - - it("initializes newObject for space", function() { - makeController(); - $scope.actionOption = { - name: "add_space", - objectName: "space" - }; - $scope.actionChanged(); - expect($scope.newObject).toEqual({}); - }); - - it("initializes newObject for subnet", function() { - makeController(); - var space = { - id: makeInteger(0, 100) - }; - var fabric = { - id: makeInteger(0, 100) - }; - var vlan = { - id: makeInteger(0, 100), - fabric: fabric.id - }; - fabric.vlan_ids = [vlan.id]; - $scope.fabrics = [fabric]; - $scope.vlans = [vlan]; - $scope.spaces = [space]; - $scope.actionOption = { - name: "add_subnet", - objectName: "subnet" - }; - $scope.actionChanged(); - expect($scope.newObject).toEqual({ - vlan: vlan.id - }); - }); - }); - - describe("cancelAction", function() { - - it("clears actionOption and newObject", function() { - makeController(); - $scope.actionOption = {}; - $scope.newObject = {}; - $scope.cancelAction(); - expect($scope.actionOption).toBeNull(); - expect($scope.newObject).toBeNull(); - }); - }); - - describe("actionSubnetPreSave", function() { - - it("sets fabric to fabric ID for selected VLAN", function() { - makeController(); - var fabric = { - id: makeInteger(0, 100) - }; - var vlan = { - id: makeInteger(0, 100), - fabric: fabric.id - }; - VLANsManager._items = [vlan]; - var updated = $scope.actionSubnetPreSave({ - vlan: vlan.id - }); - expect(updated).toEqual({ - vlan: vlan.id, - fabric: fabric.id - }); - }); + describe("actionSubnetPreSave", function() { + it("sets fabric to fabric ID for selected VLAN", function() { + makeController(); + var fabric = { + id: makeInteger(0, 100) + }; + var vlan = { + id: makeInteger(0, 100), + fabric: fabric.id + }; + VLANsManager._items = [vlan]; + var updated = $scope.actionSubnetPreSave({ + vlan: vlan.id + }); + expect(updated).toEqual({ + vlan: vlan.id, + fabric: fabric.id + }); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,2423 +4,2550 @@ * Unit tests for NodeDetailsController. */ +import { makeInteger, makeName } from "testing/utils"; + // Make a fake user. var userId = 0; function makeUser() { - return { - id: userId++, - username: makeName("username"), - first_name: makeName("first_name"), - last_name: makeName("last_name"), - email: makeName("email"), - is_superuser: false, - sshkeys_count: 0 - }; + return { + id: userId++, + username: makeName("username"), + first_name: makeName("first_name"), + last_name: makeName("last_name"), + email: makeName("email"), + is_superuser: false, + sshkeys_count: 0 + }; } - describe("NodeDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the NodeDetails controller and - // mock the websocket connection. - var MachinesManager, ControllersManager, ServicesManager; - var DevicesManager, GeneralManager, UsersManager, DomainsManager; - var TagsManager, RegionConnection, ManagerHelperService, ErrorService; - var ScriptsManager, ResourcePoolsManager, VLANsManager; - var webSocket; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get("MachinesManager"); - DevicesManager = $injector.get("DevicesManager"); - ControllersManager = $injector.get("ControllersManager"); - ZonesManager = $injector.get("ZonesManager"); - ResourcePoolsManager = $injector.get("ResourcePoolsManager"); - GeneralManager = $injector.get("GeneralManager"); - UsersManager = $injector.get("UsersManager"); - TagsManager = $injector.get("TagsManager"); - DomainsManager = $injector.get("DomainsManager"); - RegionConnection = $injector.get("RegionConnection"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ServicesManager = $injector.get("ServicesManager"); - ErrorService = $injector.get("ErrorService"); - ScriptsManager = $injector.get("ScriptsManager"); - VLANsManager = $injector.get("VLANsManager"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Make a fake zone. - function makeZone() { - var zone = { - id: makeInteger(0, 10000), - name: makeName("zone") - }; - ZonesManager._items.push(zone); - return zone; - } + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $log; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $log = $injector.get("$log"); + })); + + // Load the required dependencies for the NodeDetails controller and + // mock the websocket connection. + var MachinesManager, ControllersManager, ServicesManager; + var DevicesManager, GeneralManager, UsersManager, DomainsManager; + var TagsManager, RegionConnection, ManagerHelperService, ErrorService; + var ScriptsManager, ResourcePoolsManager, VLANsManager, ZonesManager; + var webSocket; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + DevicesManager = $injector.get("DevicesManager"); + ControllersManager = $injector.get("ControllersManager"); + ZonesManager = $injector.get("ZonesManager"); + ResourcePoolsManager = $injector.get("ResourcePoolsManager"); + GeneralManager = $injector.get("GeneralManager"); + UsersManager = $injector.get("UsersManager"); + TagsManager = $injector.get("TagsManager"); + DomainsManager = $injector.get("DomainsManager"); + RegionConnection = $injector.get("RegionConnection"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ServicesManager = $injector.get("ServicesManager"); + ErrorService = $injector.get("ErrorService"); + ScriptsManager = $injector.get("ScriptsManager"); + VLANsManager = $injector.get("VLANsManager"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Make a fake zone. + function makeZone() { + var zone = { + id: makeInteger(0, 10000), + name: makeName("zone") + }; + ZonesManager._items.push(zone); + return zone; + } + + // Make a fake resource pool. + function makeResourcePool() { + var pool = { + id: makeInteger(0, 10000), + name: makeName("pool") + }; + ResourcePoolsManager._items.push(pool); + return pool; + } + + // Make a fake node. + function makeNode() { + var zone = makeZone(); + var pool = makeResourcePool(); + var node = { + system_id: makeName("system_id"), + hostname: makeName("hostname"), + fqdn: makeName("fqdn"), + actions: [], + architecture: "amd64/generic", + zone: angular.copy(zone), + pool: angular.copy(pool), + node_type: 0, + power_type: "", + power_parameters: null, + summary_xml: null, + summary_yaml: null, + commissioning_results: [], + testing_results: [], + installation_results: [], + events: [], + interfaces: [], + extra_macs: [], + cpu_count: makeInteger(0, 64) + }; + MachinesManager._items.push(node); + return node; + } - // Make a fake resource pool. - function makeResourcePool() { - var pool = { - id: makeInteger(0, 10000), - name: makeName("pool") - }; - ResourcePoolsManager._items.push(pool); - return pool; - } + // Make a fake event. + function makeEvent() { + return { + type: { + description: makeName("type") + }, + description: makeName("description") + }; + } + // Create the node that will be used and set the routeParams. + var node, $routeParams; + beforeEach(function() { + node = makeNode(); + $routeParams = { + system_id: node.system_id + }; + }); - // Make a fake node. - function makeNode() { - var zone = makeZone(); - var pool = makeResourcePool(); - var node = { - system_id: makeName("system_id"), - hostname: makeName("hostname"), - fqdn: makeName("fqdn"), - actions: [], - architecture: "amd64/generic", - zone: angular.copy(zone), - pool: angular.copy(pool), - node_type: 0, - power_type: "", - power_parameters: null, - summary_xml: null, - summary_yaml: null, - commissioning_results: [], - testing_results: [], - installation_results: [], - events: [], - interfaces: [], - extra_macs: [], - cpu_count: makeInteger(0, 64) - }; - MachinesManager._items.push(node); - return node; + // Makes the NodeDetailsController + function makeController(loadManagersDefer, loadItemsDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - // Make a fake event. - function makeEvent() { - return { - type: { - description: makeName("type") - }, - description: makeName("description") - }; + var loadItems = spyOn(GeneralManager, "loadItems"); + if (angular.isObject(loadItemsDefer)) { + loadItems.and.returnValue(loadItemsDefer.promise); + } else { + loadItems.and.returnValue($q.defer().promise); } - // Create the node that will be used and set the routeParams. - var node, $routeParams; - beforeEach(function() { - node = makeNode(); - $routeParams = { - system_id: node.system_id - }; - }); - - // Makes the NodeDetailsController - function makeController(loadManagersDefer, loadItemsDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - var loadItems = spyOn(GeneralManager, "loadItems"); - if(angular.isObject(loadItemsDefer)) { - loadItems.and.returnValue(loadItemsDefer.promise); - } else { - loadItems.and.returnValue($q.defer().promise); - } - - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - // Set the authenticated user, and by default make them superuser. - UsersManager._authUser = { - is_superuser: true - }; - - // Create the controller. - var controller = $controller("NodeDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - MachinesManager: MachinesManager, - DevicesManager: DevicesManager, - ControllersManager: ControllersManager, - ZonesManager: ZonesManager, - GeneralManager: GeneralManager, - UsersManager: UsersManager, - TagsManager: TagsManager, - DomainsManager: DomainsManager, - ManagerHelperService: ManagerHelperService, - ServicesManager: ServicesManager, - ErrorService: ErrorService, - ScriptsManager: ScriptsManager, - ResourcePoolsManager: ResourcePoolsManager, - }); - - // Since the osSelection directive is not used in this test the - // osSelection item on the model needs to have $reset function added - // because it will be called throughout many of the tests. - $scope.osSelection.$reset = jasmine.createSpy("$reset"); + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + // Set the authenticated user, and by default make them superuser. + UsersManager._authUser = { + is_superuser: true + }; - return controller; + // Create the controller. + var controller = $controller("NodeDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + MachinesManager: MachinesManager, + DevicesManager: DevicesManager, + ControllersManager: ControllersManager, + ZonesManager: ZonesManager, + GeneralManager: GeneralManager, + UsersManager: UsersManager, + TagsManager: TagsManager, + DomainsManager: DomainsManager, + ManagerHelperService: ManagerHelperService, + ServicesManager: ServicesManager, + ErrorService: ErrorService, + ScriptsManager: ScriptsManager, + ResourcePoolsManager: ResourcePoolsManager + }); + + // Since the osSelection directive is not used in this test the + // osSelection item on the model needs to have $reset function added + // because it will be called throughout many of the tests. + $scope.osSelection.$reset = jasmine.createSpy("$reset"); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + var controller = makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(node); + $rootScope.$digest(); + + return controller; + } + + it("sets title to loading", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + }); + + it("sets the initial $scope values", function() { + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.action.option).toBeNull(); + expect($scope.action.allOptions).toBeNull(); + expect($scope.action.availableOptions).toEqual([]); + expect($scope.action.error).toBeNull(); + expect($scope.action.showing_confirmation).toBe(false); + expect($scope.action.confirmation_message).toEqual(""); + expect($scope.action.confirmation_details).toEqual([]); + expect($scope.osinfo).toBe(GeneralManager.getData("osinfo")); + expect($scope.power_types).toBe(GeneralManager.getData("power_types")); + expect($scope.osSelection.osystem).toBeNull(); + expect($scope.osSelection.release).toBeNull(); + expect($scope.commissionOptions).toEqual({ + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }); + expect($scope.releaseOptions).toEqual({}); + expect($scope.checkingPower).toBe(false); + expect($scope.devices).toEqual([]); + expect($scope.services).toEqual({}); + }); + + it("sets initial values for summary section", function() { + makeController(); + expect($scope.summary).toEqual({ + editing: false, + architecture: { + selected: null, + options: GeneralManager.getData("architectures") + }, + min_hwe_kernel: { + selected: null, + options: GeneralManager.getData("min_hwe_kernels") + }, + pool: { + selected: null, + options: ResourcePoolsManager.getItems() + }, + zone: { + selected: null, + options: ZonesManager.getItems() + }, + tags: [] + }); + expect($scope.summary.architecture.options).toBe( + GeneralManager.getData("architectures") + ); + expect($scope.summary.min_hwe_kernel.options).toBe( + GeneralManager.getData("min_hwe_kernels") + ); + expect($scope.summary.zone.options).toBe(ZonesManager.getItems()); + expect($scope.summary.pool.options).toBe(ResourcePoolsManager.getItems()); + }); + + it("sets initial values for power section", function() { + makeController(); + expect($scope.power).toEqual({ + editing: false, + type: null, + bmc_node_count: 0, + parameters: {}, + in_pod: false + }); + }); + + it("sets initial values for events section", function() { + makeController(); + expect($scope.events).toEqual({ + limit: 10 + }); + }); + + it("sets initial area to routeParams value", function() { + $routeParams.area = makeName("area"); + makeController(); + expect($scope.section.area).toEqual($routeParams.area); + }); + + it("calls loadManagers for machine", function() { + $location.path("/machine"); + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + GeneralManager, + UsersManager, + TagsManager, + DomainsManager, + ServicesManager, + ResourcePoolsManager, + MachinesManager, + ScriptsManager + ]); + }); + + it("calls loadManagers for device", function() { + $location.path("/device"); + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + GeneralManager, + UsersManager, + TagsManager, + DomainsManager, + ServicesManager, + ResourcePoolsManager, + DevicesManager + ]); + }); + + it("calls loadManagers for controller", function() { + $location.path("/controller"); + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + GeneralManager, + UsersManager, + TagsManager, + DomainsManager, + ServicesManager, + ResourcePoolsManager, + ControllersManager, + ScriptsManager, + VLANsManager + ]); + }); + + it("doesnt call setActiveItem if node is loaded", function() { + spyOn(MachinesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if node is not active", function() { + spyOn(MachinesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + + expect(MachinesManager.setActiveItem).toHaveBeenCalledWith(node.system_id); + }); + + it("sets node and loaded once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + }); + + it("sets machine values on load", function() { + spyOn(MachinesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.nodesManager).toBe(MachinesManager); + expect($scope.isController).toBe(false); + expect($scope.type_name).toBe("machine"); + expect($scope.type_name_title).toBe("Machine"); + }); + + it("sets controller values on load", function() { + $location.path("/controller"); + spyOn(MachinesManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ControllersManager, "setActiveItem").and.returnValue( + $q.defer().promise + ); + var defer = $q.defer(); + makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.nodesManager).toBe(ControllersManager); + expect($scope.isController).toBe(true); + expect($scope.type_name).toBe("controller"); + expect($scope.type_name_title).toBe("Controller"); + }); + + it("updateServices sets $scope.services when node is loaded", function() { + spyOn(ControllersManager, "getServices").and.returnValue([ + { status: "running", name: "rackd" } + ]); + spyOn(ControllersManager, "setActiveItem").and.returnValue( + $q.defer().promise + ); + + var defer = $q.defer(); + $location.path("/controller"); + makeController(defer); + ControllersManager._activeItem = node; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + + expect(ControllersManager.getServices).toHaveBeenCalledWith(node); + expect($scope.services).not.toBeNull(); + expect(Object.keys($scope.services).length).toBe(1); + expect($scope.services.rackd.status).toBe("running"); + }); + + it("loads node actions", function() { + spyOn(ControllersManager, "setActiveItem").and.returnValue( + $q.defer().promise + ); + var called = false; + spyOn(GeneralManager, "isDataLoaded").and.callFake(function() { + var tmp = called; + called = true; + return tmp; + }); + var loadManagersDefer = $q.defer(); + var loadItemsDefer = $q.defer(); + $location.path("/controller"); + makeController(loadManagersDefer, loadItemsDefer); + var myNode = angular.copy(node); + // Make node a rack controller. + myNode.node_type = 2; + ControllersManager._activeItem = myNode; + loadManagersDefer.resolve(); + loadItemsDefer.resolve(); + $rootScope.$digest(); + expect(GeneralManager.isDataLoaded.calls.count()).toBe(2); + expect(GeneralManager.isDataLoaded).toHaveBeenCalledWith( + "rack_controller_actions" + ); + expect(GeneralManager.loadItems).toHaveBeenCalledWith([ + "rack_controller_actions" + ]); + }); + + it("title is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(node.fqdn); + }); + + it("summary section placed in edit mode if architecture blank", function() { + node.architecture = ""; + node.permissions = ["edit"]; + GeneralManager._data.power_types.data = [{}]; + GeneralManager._data.architectures.data = ["amd64/generic"]; + + makeControllerResolveSetActiveItem(); + expect($scope.summary.editing).toBe(true); + }); + + it(`summary section not placed in edit mode + if no usable architectures`, function() { + node.architecture = ""; + GeneralManager._data.power_types.data = [{}]; + + makeControllerResolveSetActiveItem(); + expect($scope.summary.editing).toBe(false); + }); + + it(`summary section not placed in edit mode + if architecture present`, function() { + GeneralManager._data.architectures.data = [node.architecture]; + + makeControllerResolveSetActiveItem(); + expect($scope.summary.editing).toBe(false); + }); + + it("summary section is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.summary.zone.selected).toBe( + ZonesManager.getItemFromList(node.zone.id) + ); + expect($scope.summary.architecture.selected).toBe(node.architecture); + expect($scope.summary.tags).toEqual(node.tags); + }); + + it("power section no edit if power_type blank for controller", function() { + GeneralManager._data.power_types.data = [{}]; + node.node_type = 4; + makeControllerResolveSetActiveItem(); + expect($scope.power.editing).toBe(false); + }); + + it("power section edit mode if power_type blank for a machine", function() { + GeneralManager._data.power_types.data = [{}]; + node.permissions = ["edit"]; + makeControllerResolveSetActiveItem(); + expect($scope.power.editing).toBe(true); + }); + + it("power section not placed in edit mode if no power_types", function() { + makeControllerResolveSetActiveItem(); + expect($scope.power.editing).toBe(false); + }); + + it("power section not placed in edit mode if power_type", function() { + node.power_type = makeName("power"); + GeneralManager._data.power_types.data = [{}]; + + makeControllerResolveSetActiveItem(); + expect($scope.power.editing).toBe(false); + }); + + it("starts watching once setActiveItem resolves", function() { + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + makeController(defer); + + spyOn($scope, "$watch"); + spyOn($scope, "$watchCollection"); + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(node); + $rootScope.$digest(); + + var watches = []; + var i, + calls = $scope.$watch.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watches.push(calls[i][0]); } - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(node); - $rootScope.$digest(); - - return controller; + var watchCollections = []; + calls = $scope.$watchCollection.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watchCollections.push(calls[i][0]); } - it("sets title to loading", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - }); - - it("sets the initial $scope values", function() { - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.action.option).toBeNull(); - expect($scope.action.allOptions).toBeNull(); - expect($scope.action.availableOptions).toEqual([]); - expect($scope.action.error).toBeNull(); - expect($scope.action.showing_confirmation).toBe(false); - expect($scope.action.confirmation_message).toEqual(""); - expect($scope.action.confirmation_details).toEqual([]); - expect($scope.osinfo).toBe(GeneralManager.getData("osinfo")); - expect($scope.power_types).toBe(GeneralManager.getData("power_types")); - expect($scope.osSelection.osystem).toBeNull(); - expect($scope.osSelection.release).toBeNull(); - expect($scope.commissionOptions).toEqual({ - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }); - expect($scope.releaseOptions).toEqual({}); - expect($scope.checkingPower).toBe(false); - expect($scope.devices).toEqual([]); - expect($scope.services).toEqual({}); - }); - - it("sets initial values for summary section", function() { - makeController(); - expect($scope.summary).toEqual({ - editing: false, - architecture: { - selected: null, - options: GeneralManager.getData("architectures") - }, - min_hwe_kernel: { - selected: null, - options: GeneralManager.getData("min_hwe_kernels") - }, - pool: { - selected: null, - options: ResourcePoolsManager.getItems() - }, - zone: { - selected: null, - options: ZonesManager.getItems() - }, - tags: [] - }); - expect($scope.summary.architecture.options).toBe( - GeneralManager.getData("architectures")); - expect($scope.summary.min_hwe_kernel.options).toBe( - GeneralManager.getData("min_hwe_kernels")); - expect($scope.summary.zone.options).toBe( - ZonesManager.getItems()); - expect($scope.summary.pool.options).toBe( - ResourcePoolsManager.getItems()); - }); - - it("sets initial values for power section", function() { - makeController(); - expect($scope.power).toEqual({ - editing: false, - type: null, - bmc_node_count: 0, - parameters: {}, - in_pod: false - }); - }); - - it("sets initial values for events section", function() { - makeController(); - expect($scope.events).toEqual({ - limit: 10 - }); - }); - - it("sets initial area to routeParams value", function() { - $routeParams.area = makeName("area"); - makeController(); - expect($scope.section.area).toEqual($routeParams.area); - }); - - it("calls loadManagers for machine", function() { - $location.path("/machine"); - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - ZonesManager, GeneralManager, UsersManager, TagsManager, - DomainsManager, ServicesManager, ResourcePoolsManager, - MachinesManager, ScriptsManager]); - }); - - it("calls loadManagers for device", function() { - $location.path("/device"); - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - ZonesManager, GeneralManager, UsersManager, TagsManager, - DomainsManager, ServicesManager, ResourcePoolsManager, - DevicesManager]); - }); - - it("calls loadManagers for controller", function() { - $location.path("/controller"); - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - ZonesManager, GeneralManager, UsersManager, TagsManager, - DomainsManager, ServicesManager, ResourcePoolsManager, - ControllersManager, ScriptsManager, VLANsManager]); - }); - - it("doesnt call setActiveItem if node is loaded", function() { - spyOn(MachinesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if node is not active", function() { - spyOn(MachinesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - - expect(MachinesManager.setActiveItem).toHaveBeenCalledWith( - node.system_id); - }); - - it("sets node and loaded once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - }); - - it("sets machine values on load", function () { - spyOn(MachinesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.nodesManager).toBe(MachinesManager); - expect($scope.isController).toBe(false); - expect($scope.type_name).toBe('machine'); - expect($scope.type_name_title).toBe('Machine'); - }); - - it("sets controller values on load", function () { - $location.path('/controller'); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ControllersManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.nodesManager).toBe(ControllersManager); - expect($scope.isController).toBe(true); - expect($scope.type_name).toBe('controller'); - expect($scope.type_name_title).toBe('Controller'); - }); - - it("updateServices sets $scope.services when node is loaded", function() { - spyOn(ControllersManager, "getServices").and.returnValue([ - { "status": "running", "name": "rackd" } - ]); - spyOn(ControllersManager, "setActiveItem").and.returnValue( - $q.defer().promise); - - var defer = $q.defer(); - $location.path('/controller'); - makeController(defer); - ControllersManager._activeItem = node; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - - expect(ControllersManager.getServices).toHaveBeenCalledWith(node); - expect($scope.services).not.toBeNull(); - expect(Object.keys($scope.services).length).toBe(1); - expect($scope.services.rackd.status).toBe('running'); - }); - - it("loads node actions", function() { - spyOn(ControllersManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var called = false; - spyOn(GeneralManager, "isDataLoaded").and.callFake(function() { - var tmp = called; - called = true; - return tmp; - }); - var loadManagersDefer = $q.defer(); - var loadItemsDefer = $q.defer(); - $location.path("/controller"); - makeController(loadManagersDefer, loadItemsDefer); - var myNode = angular.copy(node); - // Make node a rack controller. - myNode.node_type = 2; - ControllersManager._activeItem = myNode; - loadManagersDefer.resolve(); - loadItemsDefer.resolve(); - $rootScope.$digest(); - expect(GeneralManager.isDataLoaded.calls.count()).toBe(2); - expect(GeneralManager.isDataLoaded).toHaveBeenCalledWith( - "rack_controller_actions"); - expect(GeneralManager.loadItems).toHaveBeenCalledWith( - ["rack_controller_actions"]); - }); - - it("title is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(node.fqdn); - }); - - it("summary section placed in edit mode if architecture blank", - function() { - node.architecture = ""; - node.permissions = ['edit']; - GeneralManager._data.power_types.data = [{}]; - GeneralManager._data.architectures.data = ["amd64/generic"]; - - makeControllerResolveSetActiveItem(); - expect($scope.summary.editing).toBe(true); - }); - - it("summary section not placed in edit mode if no usable architectures", - function() { - node.architecture = ""; - GeneralManager._data.power_types.data = [{}]; - - makeControllerResolveSetActiveItem(); - expect($scope.summary.editing).toBe(false); - }); - - it("summary section not placed in edit mode if architecture present", - function() { - GeneralManager._data.architectures.data = [node.architecture]; - - makeControllerResolveSetActiveItem(); - expect($scope.summary.editing).toBe(false); - }); - - it("summary section is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.summary.zone.selected).toBe( - ZonesManager.getItemFromList(node.zone.id)); - expect($scope.summary.architecture.selected).toBe(node.architecture); - expect($scope.summary.tags).toEqual(node.tags); - }); - - it("power section no edit if power_type blank for controller", function() { - GeneralManager._data.power_types.data = [{}]; - node.node_type = 4; - makeControllerResolveSetActiveItem(); - expect($scope.power.editing).toBe(false); - }); - - it("power section edit mode if power_type blank for a machine", function() { - GeneralManager._data.power_types.data = [{}]; - node.permissions = ['edit']; - makeControllerResolveSetActiveItem(); - expect($scope.power.editing).toBe(true); - }); - - it("power section not placed in edit mode if no power_types", function() { - makeControllerResolveSetActiveItem(); - expect($scope.power.editing).toBe(false); - }); - - it("power section not placed in edit mode if power_type", function() { - node.power_type = makeName("power"); - GeneralManager._data.power_types.data = [{}]; - - makeControllerResolveSetActiveItem(); - expect($scope.power.editing).toBe(false); - }); - - it("starts watching once setActiveItem resolves", function() { - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - makeController(defer); - - spyOn($scope, "$watch"); - spyOn($scope, "$watchCollection"); - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(node); - $rootScope.$digest(); - - var watches = []; - var i, calls = $scope.$watch.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watches.push(calls[i][0]); - } - - var watchCollections = []; - calls = $scope.$watchCollection.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watchCollections.push(calls[i][0]); - } - - expect(watches).toEqual([ - "node.fqdn", - "node.devices", - "node.actions", - "node.architecture", - "node.min_hwe_kernel", - "node.zone.id", - "node.pool.id", - "node.power_type", - "node.power_parameters", - "node.service_ids" - ]); - expect(watchCollections).toEqual([ - $scope.summary.architecture.options, - $scope.summary.min_hwe_kernel.options, - $scope.summary.zone.options, - $scope.summary.pool.options, - "power_types" - ]); - }); - - it("updates $scope.devices", function() { - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - makeController(defer); - - node.devices = [ - { - fqdn: "device1.maas", - interfaces: [] - }, - { - fqdn: "device2.maas", - interfaces: [ - { - mac_address: "00:11:22:33:44:55", - links: [] - } - ] - }, - { - fqdn: "device3.maas", - interfaces: [ - { - mac_address: "00:11:22:33:44:66", - links: [] - }, - { - mac_address: "00:11:22:33:44:77", - links: [ - { - ip_address: "192.168.122.1" - }, - { - ip_address: "192.168.122.2" - }, - { - ip_address: "192.168.122.3" - } - ] - } - ] - } - ]; - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(node); - $rootScope.$digest(); - - expect($scope.devices).toEqual([ - { - name: "device1.maas" - }, - { - name: "device2.maas", - mac_address: "00:11:22:33:44:55" - }, - { - name: "device3.maas", - mac_address: "00:11:22:33:44:66" - }, - { - name: "", - mac_address: "00:11:22:33:44:77", + expect(watches).toEqual([ + "node.fqdn", + "node.devices", + "node.actions", + "node.architecture", + "node.min_hwe_kernel", + "node.zone.id", + "node.pool.id", + "node.power_type", + "node.power_parameters", + "node.service_ids" + ]); + expect(watchCollections).toEqual([ + $scope.summary.architecture.options, + $scope.summary.min_hwe_kernel.options, + $scope.summary.zone.options, + $scope.summary.pool.options, + "power_types" + ]); + }); + + it("updates $scope.devices", function() { + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + makeController(defer); + + node.devices = [ + { + fqdn: "device1.maas", + interfaces: [] + }, + { + fqdn: "device2.maas", + interfaces: [ + { + mac_address: "00:11:22:33:44:55", + links: [] + } + ] + }, + { + fqdn: "device3.maas", + interfaces: [ + { + mac_address: "00:11:22:33:44:66", + links: [] + }, + { + mac_address: "00:11:22:33:44:77", + links: [ + { ip_address: "192.168.122.1" - }, - { - name: "", - mac_address: "", + }, + { ip_address: "192.168.122.2" - }, - { - name: "", - mac_address: "", + }, + { ip_address: "192.168.122.3" - } - ]); - }); - - it("reloads osinfo on route update", function() { - makeController(); - $scope.$emit("$routeUpdate"); - expect(GeneralManager.loadItems).toHaveBeenCalled(); - }); - - it("updates $scope.actions", function() { - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var loadManagersDefer = $q.defer(); - var loadItemsDefer = $q.defer(); - makeController(loadManagersDefer, loadItemsDefer); - node.node_type = 0; - node.actions = ['test', 'release', 'delete']; - var all_actions = [ - {'name': 'deploy'}, - {'name': 'commission'}, - {'name': 'test'}, - {'name': 'release'}, - {'name': 'delete'} - ]; - loadManagersDefer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(node); - $rootScope.$digest(); - // loadItems normally sets loaded to true and sets data to the items - // retrieved from the region. The spy prevents that from happening - // which is needed for GeneralManager.isLoaded to work. - GeneralManager._data.machine_actions.loaded = true; - GeneralManager._data.machine_actions.data = all_actions; - loadItemsDefer.resolve(all_actions); - $rootScope.$digest(); - expect($scope.action.allOptions).toEqual(all_actions); - expect($scope.action.availableOptions).toEqual([ - {'name': 'test'}, {'name': 'release'}, {'name': 'delete'}]); - }); - - describe("tagsAutocomplete", function() { - it("calls TagsManager.autocomplete with query", function() { - makeController(); - spyOn(TagsManager, "autocomplete"); - var query = makeName("query"); - $scope.tagsAutocomplete(query); - expect(TagsManager.autocomplete).toHaveBeenCalledWith(query); - }); - }); - - describe("isSuperUser", function() { - - it("returns false if no authUser", function() { - makeController(); - UsersManager._authUser = null; - expect($scope.isSuperUser()).toBe(false); - }); - - it("returns false if authUser.is_superuser is false", function() { - makeController(); - UsersManager._authUser.is_superuser = false; - expect($scope.isSuperUser()).toBe(false); - }); - - it("returns true if authUser.is_superuser is true", function() { - makeController(); - UsersManager._authUser.is_superuser = true; - expect($scope.isSuperUser()).toBe(true); - }); - }); - - describe("getPowerStateClass", function() { - - it("returns blank if no node", function() { - makeController(); - expect($scope.getPowerStateClass()).toBe(""); - }); - - it("returns check if checkingPower is true", function() { - makeController(); - $scope.node = node; - $scope.checkingPower = true; - expect($scope.getPowerStateClass()).toBe("checking"); - }); - - it("returns power_state from node ", function() { - makeController(); - var state = makeName("state"); - $scope.node = node; - node.power_state = state; - expect($scope.getPowerStateClass()).toBe(state); - }); - }); - - describe("getPowerStateText", function() { - - it("returns blank if no node", function() { - makeController(); - expect($scope.getPowerStateText()).toBe(""); - }); - - it("returns 'Checking' if checkingPower is true", function() { - makeController(); - $scope.node = node; - $scope.checkingPower = true; - node.power_state = "unknown"; - expect($scope.getPowerStateText()).toBe("Checking power"); - }); - - it("returns blank if power_state is unknown", function() { - makeController(); - $scope.node = node; - node.power_state = "unknown"; - expect($scope.getPowerStateText()).toBe(""); - }); - - it("returns power_state prefixed with Power ", function() { - makeController(); - var state = makeName("state"); - $scope.node = node; - node.power_state = state; - expect($scope.getPowerStateText()).toBe("Power " + state); - }); - }); - - describe("canCheckPowerState", function() { - - it("returns false if no node", function() { - makeController(); - expect($scope.canCheckPowerState()).toBe(false); - }); - - it("returns false if power_state is unknown", function() { - makeController(); - $scope.node = node; - node.power_state = "unknown"; - expect($scope.canCheckPowerState()).toBe(false); - }); - - it("returns false if checkingPower is true", function() { - makeController(); - $scope.node = node; - $scope.checkingPower = true; - expect($scope.canCheckPowerState()).toBe(false); - }); - - it("returns true if not checkingPower and power_state not unknown", - function() { - makeController(); - $scope.node = node; - expect($scope.canCheckPowerState()).toBe(true); - }); - }); - - describe("checkPowerState", function() { - - it("sets checkingPower to true", function() { - makeController(); - spyOn(MachinesManager, "checkPowerState").and.returnValue( - $q.defer().promise); - $scope.checkPowerState(); - expect($scope.checkingPower).toBe(true); - }); - - it("sets checkingPower to false once checkPowerState resolves", - function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "checkPowerState").and.returnValue( - defer.promise); - $scope.checkPowerState(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.checkingPower).toBe(false); - }); - }); - - describe("isUbuntuOS", function() { - it("returns true when ubuntu", function() { - makeController(); - $scope.node = node; - node.osystem = 'ubuntu'; - node.distro_series = makeName("distro_series"); - expect($scope.isUbuntuOS()).toBe(true); - }); - - it("returns false when otheros", function() { - makeController(); - $scope.node = node; - node.osystem = makeName("osystem"); - node.distro_series = makeName("distro_series"); - expect($scope.isUbuntuOS()).toBe(false); - }); - }); - - describe("isUbuntuCoreOS", function() { - it("returns true when ubuntu-core", function() { - makeController(); - $scope.node = node; - node.osystem = 'ubuntu-core'; - node.distro_series = makeName("distro_series"); - expect($scope.isUbuntuCoreOS()).toBe(true); - }); - - it("returns false when otheros", function() { - makeController(); - $scope.node = node; - node.osystem = makeName("osystem"); - node.distro_series = makeName("distro_series"); - expect($scope.isUbuntuCoreOS()).toBe(false); - }); - }); - - describe("isCentOS", function() { - it("returns true when CentOS", function() { - makeController(); - $scope.node = node; - node.osystem = 'centos'; - node.distro_series = makeName("distro_series"); - expect($scope.isCentOS()).toBe(true); - }); - - it("returns true when RHEL", function() { - makeController(); - $scope.node = node; - node.osystem = 'rhel'; - node.distro_series = makeName("distro_series"); - expect($scope.isCentOS()).toBe(true); - }); - - it("returns false when otheros", function() { - makeController(); - $scope.node = node; - node.osystem = makeName("osystem"); - node.distro_series = makeName("distro_series"); - expect($scope.isCentOS()).toBe(false); - }); - }); - - describe("isCustomOS", function() { - it("returns true when custom OS", function() { - makeController(); - $scope.node = node; - node.osystem = 'custom'; - node.distro_series = makeName("distro_series"); - expect($scope.isCustomOS()).toBe(true); - }); - - it("returns false when otheros", function() { - makeController(); - $scope.node = node; - node.osystem = makeName("osystem"); - node.distro_series = makeName("distro_series"); - expect($scope.isCustomOS()).toBe(false); - }); - }); - - describe("isActionError", function() { - - it("returns true if actionError", function() { - makeController(); - $scope.action.error = makeName("error"); - expect($scope.isActionError()).toBe(true); - }); - - it("returns false if not actionError", function() { - makeController(); - $scope.action.error = null; - expect($scope.isActionError()).toBe(false); - }); - }); - - describe("isDeployError", function() { - - it("returns false if already actionError", function() { - makeController(); - $scope.action.error = makeName("error"); - expect($scope.isDeployError()).toBe(false); - }); - - it("returns true if deploy action and missing osinfo", function() { - makeController(); - $scope.action.option = { - name: "deploy" - }; - expect($scope.isDeployError()).toBe(true); - }); - - it("returns true if deploy action and no osystems", function() { - makeController(); - $scope.action.option = { - name: "deploy" - }; - $scope.osinfo = { - osystems: [] - }; - expect($scope.isDeployError()).toBe(true); - }); - - it("returns false if actionOption null", function() { - makeController(); - expect($scope.isDeployError()).toBe(false); - }); - - it("returns false if not deploy action", function() { - makeController(); - $scope.action.option = { - name: "release" - }; - expect($scope.isDeployError()).toBe(false); - }); - - it("returns false if osystems present", function() { - makeController(); - $scope.action.option = { - name: "deploy" - }; - $scope.osinfo = { - osystems: [makeName("os")] - }; - expect($scope.isDeployError()).toBe(false); - }); - }); - - - describe("isSSHKeyError", function() { - - it("returns true if deploy action and missing ssh keys", function() { - makeController(); - $scope.action.option = { - name: "deploy" - }; - var firstUser = makeUser(); - firstUser.sshkeys_count = 0; - UsersManager._authUser = firstUser; - expect($scope.isSSHKeyError()).toBe(true); - }); - - it("returns false if actionOption null", function() { - makeController(); - var firstUser = makeUser(); - firstUser.sshkeys_count = 1; - UsersManager._authUser = firstUser; - expect($scope.isSSHKeyError()).toBe(false); - }); - - it("returns false if not deploy action", function() { - makeController(); - $scope.action.option = { - name: "release" - }; - var firstUser = makeUser(); - firstUser.sshkeys_count = 1; - UsersManager._authUser = firstUser; - expect($scope.isSSHKeyError()).toBe(false); - }); - - it("returns false if ssh keys present", function() { - makeController(); - $scope.action.option = { - name: "deploy" - }; - var firstUser = makeUser(); - firstUser.sshkeys_count = 1; - UsersManager._authUser = firstUser; - expect($scope.isSSHKeyError()).toBe(false); - }); - }); - - describe("actionOptionChanged", function() { - - it("clears actionError", function() { - makeController(); - $scope.action.error = makeName("error"); - $scope.action.optionChanged(); - expect($scope.action.error).toBeNull(); - }); - }); - - describe("actionCancel", function() { - - it("sets actionOption to null", function() { - makeController(); - $scope.action.option = {}; - $scope.actionCancel(); - expect($scope.action.option).toBeNull(); - }); - - it("clears actionError", function() { - makeController(); - $scope.action.error = makeName("error"); - $scope.actionCancel(); - expect($scope.action.error).toBeNull(); - }); - - it("resets showing_confirmation", function() { - makeController(); - $scope.action.showing_confirmation = true; - $scope.action.confirmation_message = makeName("message"); - $scope.action.confirmation_details = [ - makeName("detail"), makeName("detail"), makeName("detail")]; - $scope.actionCancel(); - expect($scope.action.showing_confirmation).toBe(false); - expect($scope.action.confirmation_message).toEqual(""); - expect($scope.action.confirmation_details).toEqual([]); - }); - }); - - describe("actionGo", function() { - - it("calls performAction with node and actionOption name", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "power_off" - }; - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "power_off", {}); - }); - - it("calls performAction with osystem and distro_series", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.osSelection.osystem = "ubuntu"; - $scope.osSelection.release = "ubuntu/trusty"; - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "deploy", { - osystem: "ubuntu", - distro_series: "trusty", - install_kvm: false - }); - }); - - it("calls performAction with install_kvm", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.osSelection.osystem = "debian"; - $scope.osSelection.release = "etch"; - $scope.deployOptions.installKVM = true; - $scope.actionGo(); - // When deploying KVM, coerce the distro to ubuntu/bionic. - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "deploy", { - osystem: "ubuntu", - distro_series: "bionic", - install_kvm: true - }); - }); - - it("calls performAction with hwe kernel", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.osSelection.osystem = "ubuntu"; - $scope.osSelection.release = "ubuntu/xenial"; - $scope.osSelection.hwe_kernel = "hwe-16.04-edge"; - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "deploy", { - osystem: "ubuntu", - distro_series: "xenial", - hwe_kernel: "hwe-16.04-edge", - install_kvm: false - }); - }); - - it("calls performAction with ga kernel", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.osSelection.osystem = "ubuntu"; - $scope.osSelection.release = "ubuntu/xenial"; - $scope.osSelection.hwe_kernel = "ga-16.04"; - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "deploy", { - osystem: "ubuntu", - distro_series: "xenial", - hwe_kernel: "ga-16.04", - install_kvm: false - }); - }); - - it("calls performAction with commissionOptions", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "commission" - }; - var commissioning_script_ids = [ - makeInteger(0, 100), makeInteger(0, 100)]; - var testing_script_ids = [ - makeInteger(0, 100), makeInteger(0, 100)]; - $scope.commissionOptions.enableSSH = true; - $scope.commissionOptions.skipBMCConfig = false; - $scope.commissionOptions.skipNetworking = false; - $scope.commissionOptions.skipStorage = false; - $scope.commissionOptions.updateFirmware = true; - $scope.commissionOptions.configureHBA = true; - $scope.commissioningSelection = []; - angular.forEach(commissioning_script_ids, function(script_id) { - $scope.commissioningSelection.push({ - id: script_id, - name: makeName("script_name") - }); - }); - $scope.testSelection = []; - angular.forEach(testing_script_ids, function(script_id) { - $scope.testSelection.push({ - id: script_id, - name: makeName("script_name") - }); - }); - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "commission", { - enable_ssh: true, - skip_bmc_config: false, - skip_networking: false, - skip_storage: false, - commissioning_scripts: - commissioning_script_ids.concat([ - 'update_firmware', 'configure_hba']), - testing_scripts: testing_script_ids - }); - }); - - it("calls performAction with testOptions", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "test" - }; - var testing_script_ids = [ - makeInteger(0, 100), makeInteger(0, 100)]; - $scope.commissionOptions.enableSSH = true; - $scope.testSelection = []; - angular.forEach(testing_script_ids, function(script_id) { - $scope.testSelection.push({ - id: script_id, - name: makeName("script_name") - }); - }); - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "test", { - enable_ssh: true, - testing_scripts: testing_script_ids - }); - }); - - it("sets showing_confirmation with testOptions", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - node.status_code = 6; - $scope.node = node; - $scope.action.option = { - name: "test" - }; - $scope.actionGo(); - expect($scope.action.showing_confirmation).toBe(true); - expect(MachinesManager.performAction).not.toHaveBeenCalled(); - }); - - it("calls performAction with releaseOptions", function() { - makeController(); - spyOn(MachinesManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.action.option = { - name: "release" - }; - var secureErase = makeName("secureErase"); - var quickErase = makeName("quickErase"); - $scope.releaseOptions.erase = true; - $scope.releaseOptions.secureErase = secureErase; - $scope.releaseOptions.quickErase = quickErase; - $scope.actionGo(); - expect(MachinesManager.performAction).toHaveBeenCalledWith( - node, "release", { - erase: true, - secure_erase: secureErase, - quick_erase: quickErase - }); - }); - - it("sets showing_confirmation with deleteOptions", function() { - // Regression test for LP:1793478 - makeController(); - spyOn(ControllersManager, "performAction").and.returnValue( - $q.defer().promise); - $scope.node = node; - $scope.type_name = "controller"; - $scope.vlans = [{ - "id": 0, - "primary_rack": node.system_id, - "name": "Default VLAN" - }]; - $scope.action.option = { - name: "delete" - }; - $scope.actionGo(); - expect($scope.action.showing_confirmation).toBe(true); - expect($scope.action.confirmation_message).not.toEqual(""); - expect($scope.action.confirmation_details).not.toEqual([]); - expect(ControllersManager.performAction).not.toHaveBeenCalled(); - }); - - it("clears actionOption on resolve", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "performAction").and.returnValue( - defer.promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.actionGo(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.action.option).toBeNull(); - }); - - it("clears osSelection on resolve", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "performAction").and.returnValue( - defer.promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.osSelection.osystem = "ubuntu"; - $scope.osSelection.release = "ubuntu/trusty"; - $scope.actionGo(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.osSelection.$reset).toHaveBeenCalled(); - }); - - it("clears commissionOptions on resolve", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "performAction").and.returnValue( - defer.promise); - $scope.node = node; - $scope.action.option = { - name: "commission" - }; - $scope.commissionOptions.enableSSH = true; - $scope.commissionOptions.skipBMCConfig = true; - $scope.commissionOptions.skipNetworking = true; - $scope.commissionOptions.skipStorage = true; - $scope.commissionOptions.updateFirmware = true; - $scope.commissionOptions.configureHBA = true; - $scope.commissioningSelection = [{ - id: makeInteger(0, 100), - name: makeName("script_name") - }]; - $scope.testSelection = [{ - id: makeInteger(0, 100), - name: makeName("script_name") - }]; - $scope.actionGo(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.commissionOptions).toEqual({ - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }); - expect($scope.commissioningSelection).toEqual([]); - expect($scope.testSelection).toEqual([]); - }); - - it("clears actionError on resolve", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "performAction").and.returnValue( - defer.promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - $scope.action.error = makeName("error"); - $scope.actionGo(); - defer.resolve(); - $rootScope.$digest(); - expect($scope.action.error).toBeNull(); - }); - - it("changes path to node listing on delete", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "performAction").and.returnValue( - defer.promise); - spyOn($location, "path"); - $scope.node = node; - $scope.action.option = { - name: "delete" - }; - $scope.actionGo(); - defer.resolve(); - $rootScope.$digest(); - expect($location.path).toHaveBeenCalledWith("/machines"); - }); - - it("sets actionError when rejected", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "performAction").and.returnValue( - defer.promise); - $scope.node = node; - $scope.action.option = { - name: "deploy" - }; - var error = makeName("error"); - $scope.actionGo(); - defer.reject(error); - $rootScope.$digest(); - expect($scope.action.error).toBe(error); - }); - }); - - describe("hasUsableArchitectures", function() { - - it("returns true if architecture available", function() { - makeController(); - $scope.summary.architecture.options = ["amd64/generic"]; - expect($scope.hasUsableArchitectures()).toBe(true); - }); - - it("returns false if no architecture available", function() { - makeController(); - $scope.summary.architecture.options = []; - expect($scope.hasUsableArchitectures()).toBe(false); - }); - }); - - describe("getArchitecturePlaceholder", function() { - - it("returns choose if architecture available", function() { - makeController(); - $scope.summary.architecture.options = ["amd64/generic"]; - expect($scope.getArchitecturePlaceholder()).toBe( - "Choose an architecture"); - }); - - it("returns error if no architecture available", function() { - makeController(); - $scope.summary.architecture.options = []; - expect($scope.getArchitecturePlaceholder()).toBe( - "-- No usable architectures --"); - }); - }); - - describe("hasInvalidArchitecture", function() { - - it("returns false if node is null", function() { - makeController(); - $scope.node = null; - $scope.summary.architecture.options = ["amd64/generic"]; - expect($scope.hasInvalidArchitecture()).toBe(false); - }); - - it("returns true if node.architecture is blank", function() { - makeController(); - $scope.node = { - architecture: "" - }; - $scope.summary.architecture.options = ["amd64/generic"]; - expect($scope.hasInvalidArchitecture()).toBe(true); - }); - - it("returns true if node.architecture not in options", function() { - makeController(); - $scope.node = { - architecture: "i386/generic" - }; - $scope.summary.architecture.options = ["amd64/generic"]; - expect($scope.hasInvalidArchitecture()).toBe(true); - }); - - it("returns false if node.architecture in options", function() { - makeController(); - $scope.node = { - architecture: "amd64/generic" - }; - $scope.summary.architecture.options = ["amd64/generic"]; - expect($scope.hasInvalidArchitecture()).toBe(false); - }); - }); - - describe("invalidArchitecture", function() { - - it("returns true if selected architecture empty", function() { - makeController(); - $scope.summary.architecture.selected = ""; - expect($scope.invalidArchitecture()).toBe(true); - }); - - it("returns true if selected architecture not in options", function() { - makeController(); - $scope.summary.architecture.options = [makeName("arch")]; - $scope.summary.architecture.selected = makeName("arch"); - expect($scope.invalidArchitecture()).toBe(true); - }); - - it("returns false if selected architecture in options", function() { - makeController(); - var arch = makeName("arch"); - $scope.summary.architecture.options = [arch]; - $scope.summary.architecture.selected = arch; - expect($scope.invalidArchitecture()).toBe(false); - }); - }); - - describe("isRackControllerConnected", function() { - - it("returns false no power_types", function() { - makeController(); - $scope.power_types = []; - expect($scope.isRackControllerConnected()).toBe(false); - }); - - it("returns true if power_types", function() { - makeController(); - $scope.power_types = [{}]; - expect($scope.isRackControllerConnected()).toBe(true); - }); - }); - - describe("hasPermission", function() { - - it("returns false no permissions field", function() { - makeController(); - $scope.node = {}; - expect($scope.hasPermission('edit')).toBe(false); - }); - - it("returns false no permission in field", function() { - makeController(); - $scope.node = { - permissions: ['delete'] - }; - expect($scope.hasPermission('edit')).toBe(false); - }); - - it("returns true permissions", function() { - makeController(); - $scope.node = { - permissions: ['edit'] - }; - expect($scope.hasPermission('edit')).toBe(true); - }); - }); - - describe("canEdit", function() { - - it("returns false if no edit permission", function() { - makeController(); - $scope.isDevice = false; - spyOn($scope, "hasPermission").and.returnValue(false); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canEdit()).toBe(false); - }); - - it("returns true if edit permission but device", function() { - makeController(); - $scope.isDevice = true; - spyOn($scope, "hasPermission").and.returnValue(true); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(false); - expect($scope.canEdit()).toBe(true); - }); - - it("returns false if rack disconnected", function() { - makeController(); - $scope.isDevice = false; - spyOn($scope, "hasPermission").and.returnValue(true); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(false); - expect($scope.canEdit()).toBe(false); - }); - - it("returns false if machine is locked", - function() { - makeController(); - $scope.isDevice = false; - spyOn($scope, "hasPermission").and.returnValue(true); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - $scope.node = makeNode(); - $scope.node.locked = true; - expect($scope.canEdit()).toBe(false); - }); - }); - - describe("editHeaderDomain", function() { - - it("doesnt set editing false and editing_domain true if cannot edit", - function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(true); - $scope.header.editing = true; - $scope.header.editing_domain = false; - $scope.editHeaderDomain(); - expect($scope.header.editing).toBe(true); - expect($scope.header.editing_domain).toBe(false); - }); - - it("sets editing to false and editing_domain to true if able", - function() { - makeController(); - $scope.node = node; - spyOn($scope, "canEdit").and.returnValue(false); - $scope.header.editing = true; - $scope.header.editing_domain = false; - $scope.editHeaderDomain(); - expect($scope.header.editing).toBe(false); - expect($scope.header.editing_domain).toBe(true); - }); - - it("sets header.hostname.value to node hostname", function() { - makeController(); - $scope.node = node; - spyOn($scope, "canEdit").and.returnValue(false); - $scope.editHeaderDomain(); - expect($scope.header.hostname.value).toBe(node.hostname); - }); - - it("doesnt reset header.hostname.value on multiple calls", function() { - makeController(); - $scope.node = node; - spyOn($scope, "canEdit").and.returnValue(false); - $scope.editHeaderDomain(); - var updatedName = makeName("name"); - $scope.header.hostname.value = updatedName; - $scope.editHeaderDomain(); - expect($scope.header.hostname.value).toBe(updatedName); - }); - }); - - describe("editHeader", function() { - - it("doesnt set editing true and editing_domain false if cannot edit", - function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(false); - $scope.header.editing = false; - $scope.header.editing_domain = true; - $scope.editHeader(); - expect($scope.header.editing).toBe(false); - expect($scope.header.editing_domain).toBe(true); - }); - - it("sets editing to true and editing_domain to false if able", - function() { - makeController(); - $scope.node = node; - spyOn($scope, "canEdit").and.returnValue(true); - $scope.header.editing = false; - $scope.header.editing_domain = true; - $scope.editHeader(); - expect($scope.header.editing).toBe(true); - expect($scope.header.editing_domain).toBe(false); - }); - - it("sets header.hostname.value to node hostname", function() { - makeController(); - $scope.node = node; - spyOn($scope, "canEdit").and.returnValue(true); - $scope.editHeader(); - expect($scope.header.hostname.value).toBe(node.hostname); - }); - - it("doesnt reset header.hostname.value on multiple calls", function() { - makeController(); - $scope.node = node; - spyOn($scope, "canEdit").and.returnValue(true); - $scope.editHeader(); - var updatedName = makeName("name"); - $scope.header.hostname.value = updatedName; - $scope.editHeader(); - expect($scope.header.hostname.value).toBe(updatedName); - }); - }); - - describe("editHeaderInvalid", function() { - - it("returns false if not editing and not editing_domain", function() { - makeController(); - $scope.header.editing = false; - $scope.header.editing_domain = false; - $scope.header.hostname.value = "abc_invalid.local"; - expect($scope.editHeaderInvalid()).toBe(false); - }); - - it("returns true for bad values", function() { - makeController(); - $scope.header.editing = true; - $scope.header.editing_domain = false; - var values = [ - { - input: "aB0-z", - output: false - }, - { - input: "abc_alpha", - output: true - }, - { - input: "ab^&c", - output: true - }, - { - input: "abc.local", - output: true - } - ]; - angular.forEach(values, function(value) { - $scope.header.hostname.value = value.input; - expect($scope.editHeaderInvalid()).toBe(value.output); - }); - }); - }); - - describe("cancelEditHeader", function() { - - it("sets editing and editing_domain to false for nameHeader section", - function() { - makeController(); - $scope.node = node; - $scope.header.editing = true; - $scope.header.editing_domain = true; - $scope.cancelEditHeader(); - expect($scope.header.editing).toBe(false); - expect($scope.header.editing_domain).toBe(false); - }); - - it("sets header.hostname.value back to fqdn", function() { - makeController(); - $scope.node = node; - $scope.header.editing = true; - $scope.header.hostname.value = makeName("name"); - $scope.cancelEditHeader(); - expect($scope.header.hostname.value).toBe(node.fqdn); - }); - }); - - describe("saveEditHeader", function() { - - it("does nothing if value is invalid", function() { - makeController(); - $scope.node = node; - spyOn($scope, "editHeaderInvalid").and.returnValue(true); - var sentinel = {}; - $scope.header.editing = sentinel; - $scope.header.editing_domain = sentinel; - $scope.saveEditHeader(); - expect($scope.header.editing).toBe(sentinel); - expect($scope.header.editing_domain).toBe(sentinel); - }); - - it("sets editing to false", function() { - makeController(); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - spyOn($scope, "editHeaderInvalid").and.returnValue(false); - - $scope.node = node; - $scope.header.editing = true; - $scope.header.editing_domain = true; - $scope.header.hostname.value = makeName("name"); - $scope.saveEditHeader(); - - expect($scope.header.editing).toBe(false); - expect($scope.header.editing_domain).toBe(false); - }); - - it("calls updateItem with copy of node", function() { - makeController(); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - spyOn($scope, "editHeaderInvalid").and.returnValue(false); - - $scope.node = node; - $scope.header.editing = true; - $scope.header.hostname.value = makeName("name"); - $scope.saveEditHeader(); - - var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithNode).not.toBe(node); - }); - - it("calls updateItem with new hostname on node", function() { - makeController(); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - spyOn($scope, "editHeaderInvalid").and.returnValue(false); - - var newName = makeName("name"); - $scope.node = node; - $scope.header.editing = true; - $scope.header.hostname.value = newName; - $scope.saveEditHeader(); - - var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithNode.hostname).toBe(newName); - }); - - it("calls updateName once updateItem resolves", function() { - makeController(); - var defer = $q.defer(); - spyOn(MachinesManager, "updateItem").and.returnValue( - defer.promise); - spyOn($scope, "editHeaderInvalid").and.returnValue(false); - - $scope.node = node; - $scope.header.editing = true; - $scope.header.hostname.value = makeName("name"); - $scope.saveEditHeader(); - - defer.resolve(node); - $rootScope.$digest(); - - // Since updateName is private in the controller, check - // that the header.hostname.value is set to the nodes fqdn. - expect($scope.header.hostname.value).toBe(node.fqdn); - }); - }); - - describe("editSummary", function() { - - it("doesnt sets editing to true if cannot edit", function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(false); - $scope.summary.editing = false; - $scope.editSummary(); - expect($scope.summary.editing).toBe(false); - }); - - it("sets editing to true for summary section", function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(true); - $scope.summary.editing = false; - $scope.editSummary(); - expect($scope.summary.editing).toBe(true); - }); - }); - - describe("cancelEditSummary", function() { - - it("sets editing to false for summary section", function() { - makeController(); - $scope.node = node; - $scope.summary.architecture.options = [node.architecture]; - $scope.summary.editing = true; - $scope.cancelEditSummary(); - expect($scope.summary.editing).toBe(false); - }); - - it("doesnt set editing to false if invalid architecture", function() { - makeController(); - $scope.node = node; - $scope.summary.editing = true; - $scope.cancelEditSummary(); - expect($scope.summary.editing).toBe(true); - }); - - it("does set editing to true if device", function() { - makeController(); - $scope.isDevice = true; - $scope.node = node; - $scope.summary.editing = true; - $scope.cancelEditSummary(); - expect($scope.summary.editing).toBe(false); - }); - - it("does set editing to true if controller", function() { - makeController(); - $scope.isController = true; - $scope.node = node; - $scope.summary.editing = true; - $scope.cancelEditSummary(); - expect($scope.summary.editing).toBe(false); - }); - - it("calls updateSummary", function() { - makeController(); - $scope.node = node; - $scope.summary.architecture.options = [node.architecture]; - $scope.summary.editing = true; - $scope.cancelEditSummary(); - }); - }); - - describe("saveEditSummary", function() { - - // Configures the summary area in the scope to have a zone, and - // architecture. - function configureSummary() { - $scope.summary.editing = true; - $scope.summary.zone.selected = makeZone(); - $scope.summary.pool.selected = makeResourcePool(); - $scope.summary.description = "This is a description" - $scope.summary.architecture.selected = makeName("architecture"); - $scope.summary.tags = [ - { text: makeName("tag") }, - { text: makeName("tag") } - ]; + } + ] + } + ] + } + ]; + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(node); + $rootScope.$digest(); + + expect($scope.devices).toEqual([ + { + name: "device1.maas" + }, + { + name: "device2.maas", + mac_address: "00:11:22:33:44:55" + }, + { + name: "device3.maas", + mac_address: "00:11:22:33:44:66" + }, + { + name: "", + mac_address: "00:11:22:33:44:77", + ip_address: "192.168.122.1" + }, + { + name: "", + mac_address: "", + ip_address: "192.168.122.2" + }, + { + name: "", + mac_address: "", + ip_address: "192.168.122.3" + } + ]); + }); + + it("reloads osinfo on route update", function() { + makeController(); + $scope.$emit("$routeUpdate"); + expect(GeneralManager.loadItems).toHaveBeenCalled(); + }); + + it("updates $scope.actions", function() { + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var loadManagersDefer = $q.defer(); + var loadItemsDefer = $q.defer(); + makeController(loadManagersDefer, loadItemsDefer); + node.node_type = 0; + node.actions = ["test", "release", "delete"]; + var all_actions = [ + { name: "deploy" }, + { name: "commission" }, + { name: "test" }, + { name: "release" }, + { name: "delete" } + ]; + loadManagersDefer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(node); + $rootScope.$digest(); + // loadItems normally sets loaded to true and sets data to the items + // retrieved from the region. The spy prevents that from happening + // which is needed for GeneralManager.isLoaded to work. + GeneralManager._data.machine_actions.loaded = true; + GeneralManager._data.machine_actions.data = all_actions; + loadItemsDefer.resolve(all_actions); + $rootScope.$digest(); + expect($scope.action.allOptions).toEqual(all_actions); + expect($scope.action.availableOptions).toEqual([ + { name: "test" }, + { name: "release" }, + { name: "delete" } + ]); + }); + + describe("tagsAutocomplete", function() { + it("calls TagsManager.autocomplete with query", function() { + makeController(); + spyOn(TagsManager, "autocomplete"); + var query = makeName("query"); + $scope.tagsAutocomplete(query); + expect(TagsManager.autocomplete).toHaveBeenCalledWith(query); + }); + }); + + describe("isSuperUser", function() { + it("returns false if no authUser", function() { + makeController(); + UsersManager._authUser = null; + expect($scope.isSuperUser()).toBe(false); + }); + + it("returns false if authUser.is_superuser is false", function() { + makeController(); + UsersManager._authUser.is_superuser = false; + expect($scope.isSuperUser()).toBe(false); + }); + + it("returns true if authUser.is_superuser is true", function() { + makeController(); + UsersManager._authUser.is_superuser = true; + expect($scope.isSuperUser()).toBe(true); + }); + }); + + describe("getPowerStateClass", function() { + it("returns blank if no node", function() { + makeController(); + expect($scope.getPowerStateClass()).toBe(""); + }); + + it("returns check if checkingPower is true", function() { + makeController(); + $scope.node = node; + $scope.checkingPower = true; + expect($scope.getPowerStateClass()).toBe("checking"); + }); + + it("returns power_state from node ", function() { + makeController(); + var state = makeName("state"); + $scope.node = node; + node.power_state = state; + expect($scope.getPowerStateClass()).toBe(state); + }); + }); + + describe("getPowerStateText", function() { + it("returns blank if no node", function() { + makeController(); + expect($scope.getPowerStateText()).toBe(""); + }); + + it("returns 'Checking' if checkingPower is true", function() { + makeController(); + $scope.node = node; + $scope.checkingPower = true; + node.power_state = "unknown"; + expect($scope.getPowerStateText()).toBe("Checking power"); + }); + + it("returns blank if power_state is unknown", function() { + makeController(); + $scope.node = node; + node.power_state = "unknown"; + expect($scope.getPowerStateText()).toBe(""); + }); + + it("returns power_state prefixed with Power ", function() { + makeController(); + var state = makeName("state"); + $scope.node = node; + node.power_state = state; + expect($scope.getPowerStateText()).toBe("Power " + state); + }); + }); + + describe("canCheckPowerState", function() { + it("returns false if no node", function() { + makeController(); + expect($scope.canCheckPowerState()).toBe(false); + }); + + it("returns false if power_state is unknown", function() { + makeController(); + $scope.node = node; + node.power_state = "unknown"; + expect($scope.canCheckPowerState()).toBe(false); + }); + + it("returns false if checkingPower is true", function() { + makeController(); + $scope.node = node; + $scope.checkingPower = true; + expect($scope.canCheckPowerState()).toBe(false); + }); + + it(`returns true if not checkingPower and + power_state not unknown`, function() { + makeController(); + $scope.node = node; + expect($scope.canCheckPowerState()).toBe(true); + }); + }); + + describe("checkPowerState", function() { + it("sets checkingPower to true", function() { + makeController(); + spyOn(MachinesManager, "checkPowerState").and.returnValue( + $q.defer().promise + ); + $scope.checkPowerState(); + expect($scope.checkingPower).toBe(true); + }); + + it("sets checkingPower to false once checkPowerState resolves", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "checkPowerState").and.returnValue(defer.promise); + $scope.checkPowerState(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.checkingPower).toBe(false); + }); + }); + + describe("isUbuntuOS", function() { + it("returns true when ubuntu", function() { + makeController(); + $scope.node = node; + node.osystem = "ubuntu"; + node.distro_series = makeName("distro_series"); + expect($scope.isUbuntuOS()).toBe(true); + }); + + it("returns false when otheros", function() { + makeController(); + $scope.node = node; + node.osystem = makeName("osystem"); + node.distro_series = makeName("distro_series"); + expect($scope.isUbuntuOS()).toBe(false); + }); + }); + + describe("isUbuntuCoreOS", function() { + it("returns true when ubuntu-core", function() { + makeController(); + $scope.node = node; + node.osystem = "ubuntu-core"; + node.distro_series = makeName("distro_series"); + expect($scope.isUbuntuCoreOS()).toBe(true); + }); + + it("returns false when otheros", function() { + makeController(); + $scope.node = node; + node.osystem = makeName("osystem"); + node.distro_series = makeName("distro_series"); + expect($scope.isUbuntuCoreOS()).toBe(false); + }); + }); + + describe("isCentOS", function() { + it("returns true when CentOS", function() { + makeController(); + $scope.node = node; + node.osystem = "centos"; + node.distro_series = makeName("distro_series"); + expect($scope.isCentOS()).toBe(true); + }); + + it("returns true when RHEL", function() { + makeController(); + $scope.node = node; + node.osystem = "rhel"; + node.distro_series = makeName("distro_series"); + expect($scope.isCentOS()).toBe(true); + }); + + it("returns false when otheros", function() { + makeController(); + $scope.node = node; + node.osystem = makeName("osystem"); + node.distro_series = makeName("distro_series"); + expect($scope.isCentOS()).toBe(false); + }); + }); + + describe("isCustomOS", function() { + it("returns true when custom OS", function() { + makeController(); + $scope.node = node; + node.osystem = "custom"; + node.distro_series = makeName("distro_series"); + expect($scope.isCustomOS()).toBe(true); + }); + + it("returns false when otheros", function() { + makeController(); + $scope.node = node; + node.osystem = makeName("osystem"); + node.distro_series = makeName("distro_series"); + expect($scope.isCustomOS()).toBe(false); + }); + }); + + describe("isActionError", function() { + it("returns true if actionError", function() { + makeController(); + $scope.action.error = makeName("error"); + expect($scope.isActionError()).toBe(true); + }); + + it("returns false if not actionError", function() { + makeController(); + $scope.action.error = null; + expect($scope.isActionError()).toBe(false); + }); + }); + + describe("isDeployError", function() { + it("returns false if already actionError", function() { + makeController(); + $scope.action.error = makeName("error"); + expect($scope.isDeployError()).toBe(false); + }); + + it("returns true if deploy action and missing osinfo", function() { + makeController(); + $scope.action.option = { + name: "deploy" + }; + expect($scope.isDeployError()).toBe(true); + }); + + it("returns true if deploy action and no osystems", function() { + makeController(); + $scope.action.option = { + name: "deploy" + }; + $scope.osinfo = { + osystems: [] + }; + expect($scope.isDeployError()).toBe(true); + }); + + it("returns false if actionOption null", function() { + makeController(); + expect($scope.isDeployError()).toBe(false); + }); + + it("returns false if not deploy action", function() { + makeController(); + $scope.action.option = { + name: "release" + }; + expect($scope.isDeployError()).toBe(false); + }); + + it("returns false if osystems present", function() { + makeController(); + $scope.action.option = { + name: "deploy" + }; + $scope.osinfo = { + osystems: [makeName("os")] + }; + expect($scope.isDeployError()).toBe(false); + }); + }); + + describe("isSSHKeyWarning", function() { + it("returns true if deploy action and missing ssh keys", function() { + makeController(); + $scope.action.option = { + name: "deploy" + }; + var firstUser = makeUser(); + firstUser.sshkeys_count = 0; + UsersManager._authUser = firstUser; + expect($scope.isSSHKeyWarning()).toBe(true); + }); + + it("returns false if actionOption null", function() { + makeController(); + var firstUser = makeUser(); + firstUser.sshkeys_count = 1; + UsersManager._authUser = firstUser; + expect($scope.isSSHKeyWarning()).toBe(false); + }); + + it("returns false if not deploy action", function() { + makeController(); + $scope.action.option = { + name: "release" + }; + var firstUser = makeUser(); + firstUser.sshkeys_count = 1; + UsersManager._authUser = firstUser; + expect($scope.isSSHKeyWarning()).toBe(false); + }); + + it("returns false if ssh keys present", function() { + makeController(); + $scope.action.option = { + name: "deploy" + }; + var firstUser = makeUser(); + firstUser.sshkeys_count = 1; + UsersManager._authUser = firstUser; + expect($scope.isSSHKeyWarning()).toBe(false); + }); + }); + + describe("actionOptionChanged", function() { + it("clears actionError", function() { + makeController(); + $scope.action.error = makeName("error"); + $scope.action.optionChanged(); + expect($scope.action.error).toBeNull(); + }); + }); + + describe("actionCancel", function() { + it("sets actionOption to null", function() { + makeController(); + $scope.action.option = {}; + $scope.actionCancel(); + expect($scope.action.option).toBeNull(); + }); + + it("clears actionError", function() { + makeController(); + $scope.action.error = makeName("error"); + $scope.actionCancel(); + expect($scope.action.error).toBeNull(); + }); + + it("resets showing_confirmation", function() { + makeController(); + $scope.action.showing_confirmation = true; + $scope.action.confirmation_message = makeName("message"); + $scope.action.confirmation_details = [ + makeName("detail"), + makeName("detail"), + makeName("detail") + ]; + $scope.actionCancel(); + expect($scope.action.showing_confirmation).toBe(false); + expect($scope.action.confirmation_message).toEqual(""); + expect($scope.action.confirmation_details).toEqual([]); + }); + }); + + describe("actionGo", function() { + it("calls performAction with node and actionOption name", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "power_off" + }; + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "power_off", + {} + ); + }); + + it("calls performAction with osystem and distro_series", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.osSelection.osystem = "ubuntu"; + $scope.osSelection.release = "ubuntu/trusty"; + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "deploy", + { + osystem: "ubuntu", + distro_series: "trusty", + install_kvm: false } - - it("does nothing if invalidArchitecture", function() { - makeController(); - spyOn($scope, "invalidArchitecture").and.returnValue(true); - $scope.node = node; - var editing = {}; - $scope.summary.editing = editing; - $scope.saveEditSummary(); - - // Editing remains the same then the method exited early. - expect($scope.summary.editing).toBe(editing); - }); - - it("sets editing to false", function() { - makeController(); - spyOn($scope, "invalidArchitecture").and.returnValue(false); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - - $scope.node = node; - $scope.summary.editing = true; - $scope.saveEditSummary(); - - expect($scope.summary.editing).toBe(false); - }); - - it("calls updateItem with copy of node", function() { - makeController(); - spyOn($scope, "invalidArchitecture").and.returnValue(false); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - - $scope.node = node; - $scope.summary.editing = true; - $scope.saveEditSummary(); - - var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithNode).not.toBe(node); - }); - - it("calls updateItem with new copied values on node", function() { - makeController(); - spyOn($scope, "invalidArchitecture").and.returnValue(false); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - - $scope.node = node; - configureSummary(); - var newZone = $scope.summary.zone.selected; - var newPool = $scope.summary.pool.selected; - var newDescription = $scope.summary.description; - var newArchitecture = $scope.summary.architecture.selected; - var newTags = []; - angular.forEach($scope.summary.tags, function(tag) { - newTags.push(tag.text); - }); - $scope.saveEditSummary(); - - var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithNode.zone).toEqual(newZone); - expect(calledWithNode.zone).not.toBe(newZone); - expect(calledWithNode.pool).not.toBe(newPool); - expect(calledWithNode.description).toBe(newDescription); - expect(calledWithNode.architecture).toBe(newArchitecture); - expect(calledWithNode.tags).toEqual(newTags); - }); - - it("logs error if not disconnected error", function() { - makeController(); - spyOn($scope, "invalidArchitecture").and.returnValue(false); - - var defer = $q.defer(); - spyOn(MachinesManager, "updateItem").and.returnValue( - defer.promise); - - $scope.node = node; - configureSummary(); - $scope.saveEditSummary(); - - spyOn(console, "log"); - var error = makeName("error"); - defer.reject(error); - $rootScope.$digest(); - - expect(console.log).toHaveBeenCalledWith(error); - }); + ); }); - describe("invalidPowerType", function() { - - it("returns true if missing power type", function() { - makeController(); - $scope.power.type = null; - expect($scope.invalidPowerType()).toBe(true); - }); - - it("returns false if selected power type", function() { - makeController(); - $scope.power.type = { - name: makeName("power") - }; - expect($scope.invalidPowerType()).toBe(false); - }); - }); - - describe("editPower", function() { - - it("doesnt sets editing to true if cannot edit", function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(false); - $scope.power.editing = false; - $scope.editPower(); - expect($scope.power.editing).toBe(false); - }); - - it("sets editing to true for power section", function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(true); - $scope.power.editing = false; - $scope.editPower(); - expect($scope.power.editing).toBe(true); - }); - - }); - - describe("cancelEditPower", function() { - - it("sets editing to false for power section", function() { - makeController(); - node.power_type = makeName("power"); - $scope.node = node; - $scope.power.editing = true; - $scope.cancelEditPower(); - expect($scope.power.editing).toBe(false); - }); - - it("doesnt sets editing to false when no power_type", function() { - makeController(); - $scope.node = node; - $scope.power.editing = true; - $scope.cancelEditPower(); - expect($scope.power.editing).toBe(true); - }); - - it("sets editing false with no power_type for controller", function() { - makeController(); - node.node_type = 4; - $scope.node = node; - $scope.power.editing = true; - $scope.cancelEditPower(); - expect($scope.power.editing).toBe(false); - }); - - it("sets in_pod to true for node in pod", function() { - makeController(); - node.power_type = makeName("power"); - node.pod = makeName("pod"); - $scope.node = node; - $scope.power.editing = true; - $scope.cancelEditPower(); - expect($scope.power.in_pod).toBe(true); - }); - - }); - - describe("saveEditPower", function() { - - it("does nothing if no selected power_type", function() { - makeController(); - $scope.node = node; - var editing = {}; - $scope.power.editing = editing; - $scope.power.type = null; - $scope.saveEditPower(); - // Editing should still be true, because the function exitted - // early. - expect($scope.power.editing).toBe(editing); - }); - - it("sets editing to false", function() { - makeController(); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - - $scope.node = node; - $scope.power.editing = true; - $scope.power.type = { - name: makeName("power") - }; - $scope.saveEditPower(); - - expect($scope.power.editing).toBe(false); - }); - - it("calls updateItem with copy of node", function() { - makeController(); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - - $scope.node = node; - $scope.power.editing = true; - $scope.power.type = { - name: makeName("power") - }; - $scope.saveEditPower(); - - var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithNode).not.toBe(node); - }); - - it("calls updateItem with new copied values on node", function() { - makeController(); - spyOn(MachinesManager, "updateItem").and.returnValue( - $q.defer().promise); - - var newPowerType = { - name: makeName("power") - }; - var newPowerParameters = { - foo: makeName("bar") - }; - - $scope.node = node; - $scope.power.editing = true; - $scope.power.type = newPowerType; - $scope.power.parameters = newPowerParameters; - $scope.saveEditPower(); - - var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithNode.power_type).toBe(newPowerType.name); - expect(calledWithNode.power_parameters).toEqual( - newPowerParameters); - expect(calledWithNode.power_parameters).not.toBe( - newPowerParameters); - }); - - it("calls handleSaveError once updateItem is rejected", function() { - makeController(); - - var defer = $q.defer(); - spyOn(MachinesManager, "updateItem").and.returnValue( - defer.promise); - - $scope.node = node; - $scope.power.editing = true; - $scope.power.type = { - name: makeName("power") - }; - $scope.power.parameters = { - foo: makeName("bar") - }; - $scope.saveEditPower(); - - spyOn(console, "log"); - var error = makeName("error"); - defer.reject(error); - $rootScope.$digest(); - - // If the error message was logged to the console then - // handleSaveError was called. - expect(console.log).toHaveBeenCalledWith(error); - }); + it("calls performAction with install_kvm", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.osSelection.osystem = "debian"; + $scope.osSelection.release = "etch"; + $scope.deployOptions.installKVM = true; + $scope.actionGo(); + // When deploying KVM, coerce the distro to ubuntu/bionic. + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "deploy", + { + osystem: "ubuntu", + distro_series: "bionic", + install_kvm: true + } + ); }); - describe("allowShowMoreEvents", function() { - - it("returns false if node is null", function() { - makeController(); - $scope.node = null; - expect($scope.allowShowMoreEvents()).toBe(false); - }); - - it("returns false if node.events is not array", function() { - makeController(); - $scope.node = node; - $scope.node.events = undefined; - expect($scope.allowShowMoreEvents()).toBe(false); - }); - - it("returns false if node has no events", function() { - makeController(); - $scope.node = node; - expect($scope.allowShowMoreEvents()).toBe(false); - }); - - it("returns false if node events less then the limit", function() { - makeController(); - $scope.node = node; - $scope.node.events = [ - makeEvent(), - makeEvent() - ]; - $scope.events.limit = 10; - expect($scope.allowShowMoreEvents()).toBe(false); - }); - - it("returns false if events limit greater than 50", function() { - makeController(); - $scope.node = node; - var i; - for(i = 0; i < 50; i++) { - $scope.node.events.push(makeEvent()); - } - $scope.events.limit = 50; - expect($scope.allowShowMoreEvents()).toBe(false); - }); - - it("returns true if more events than limit", function() { - makeController(); - $scope.node = node; - var i; - for(i = 0; i < 20; i++) { - $scope.node.events.push(makeEvent()); - } - $scope.events.limit = 10; - expect($scope.allowShowMoreEvents()).toBe(true); - }); + it("calls performAction with hwe kernel", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.osSelection.osystem = "ubuntu"; + $scope.osSelection.release = "ubuntu/xenial"; + $scope.osSelection.hwe_kernel = "hwe-16.04-edge"; + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "deploy", + { + osystem: "ubuntu", + distro_series: "xenial", + hwe_kernel: "hwe-16.04-edge", + install_kvm: false + } + ); }); - describe("showMoreEvents", function() { - - it("increments events limit by 10", function() { - makeController(); - $scope.showMoreEvents(); - expect($scope.events.limit).toBe(20); - $scope.showMoreEvents(); - expect($scope.events.limit).toBe(30); - }); + it("calls performAction with ga kernel", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.osSelection.osystem = "ubuntu"; + $scope.osSelection.release = "ubuntu/xenial"; + $scope.osSelection.hwe_kernel = "ga-16.04"; + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "deploy", + { + osystem: "ubuntu", + distro_series: "xenial", + hwe_kernel: "ga-16.04", + install_kvm: false + } + ); }); - describe("getEventText", function() { - - it("returns just event type description without dash", function() { - makeController(); - var evt = makeEvent(); - delete evt.description; - expect($scope.getEventText(evt)).toBe(evt.type.description); - }); - - it("returns event type description with event description", - function() { - makeController(); - var evt = makeEvent(); - expect($scope.getEventText(evt)).toBe( - evt.type.description + " - " + evt.description); - }); - }); - - describe("getPowerEventError", function() { - - it("returns event if there is a power event error", function() { - makeController(); - var evt = makeEvent(); - evt.type.level = "warning"; - evt.type.description = "Failed to query node's BMC"; - $scope.node = node; - $scope.node.events = [ - makeEvent(), - evt - ]; - expect($scope.getPowerEventError()).toBe(evt); - }); - - it("returns nothing if there is no power event error", function() { - makeController(); - var evt_info = makeEvent(); - var evt_error = makeEvent(); - evt_info.type.level = "info"; - evt_info.type.description = "Queried node's BMC"; - evt_error.type.level = "warning"; - evt_error.type.description = "Failed to query node's BMC"; - $scope.node = node; - $scope.node.events = [ - makeEvent(), - evt_info, - evt_error - ]; - expect($scope.getPowerEventError()).toBe(); - }); + it("calls performAction with commissionOptions", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "commission" + }; + var commissioning_script_ids = [makeInteger(0, 100), makeInteger(0, 100)]; + var testing_script_ids = [makeInteger(0, 100), makeInteger(0, 100)]; + $scope.commissionOptions.enableSSH = true; + $scope.commissionOptions.skipBMCConfig = false; + $scope.commissionOptions.skipNetworking = false; + $scope.commissionOptions.skipStorage = false; + $scope.commissionOptions.updateFirmware = true; + $scope.commissionOptions.configureHBA = true; + $scope.commissioningSelection = []; + angular.forEach(commissioning_script_ids, function(script_id) { + $scope.commissioningSelection.push({ + id: script_id, + name: makeName("script_name") + }); + }); + $scope.testSelection = []; + angular.forEach(testing_script_ids, function(script_id) { + $scope.testSelection.push({ + id: script_id, + name: makeName("script_name") + }); + }); + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "commission", + { + enable_ssh: true, + skip_bmc_config: false, + skip_networking: false, + skip_storage: false, + commissioning_scripts: commissioning_script_ids.concat([ + "update_firmware", + "configure_hba" + ]), + testing_scripts: testing_script_ids + } + ); }); - describe("hasPowerEventError", function() { - - it("returns true if last event is an error", function() { - makeController(); - var evt = makeEvent(); - evt.type.level = "warning"; - evt.type.description = "Failed to query node's BMC"; - $scope.node = node; - $scope.node.events = [evt]; - expect($scope.hasPowerEventError()).toBe(true); - }); - - it("returns false if last event is not an error", function() { - makeController(); - $scope.node = node; - $scope.node.events = [makeEvent()]; - expect($scope.hasPowerEventError()).toBe(false); - }); + it("calls performAction with testOptions", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "test" + }; + var testing_script_ids = [makeInteger(0, 100), makeInteger(0, 100)]; + $scope.commissionOptions.enableSSH = true; + $scope.testSelection = []; + angular.forEach(testing_script_ids, function(script_id) { + $scope.testSelection.push({ + id: script_id, + name: makeName("script_name") + }); + }); + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith(node, "test", { + enable_ssh: true, + testing_scripts: testing_script_ids + }); + }); + + it("sets showing_confirmation with testOptions", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + node.status_code = 6; + $scope.node = node; + $scope.action.option = { + name: "test" + }; + $scope.actionGo(); + expect($scope.action.showing_confirmation).toBe(true); + expect(MachinesManager.performAction).not.toHaveBeenCalled(); + }); + + it("calls performAction with releaseOptions", function() { + makeController(); + spyOn(MachinesManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.action.option = { + name: "release" + }; + var secureErase = makeName("secureErase"); + var quickErase = makeName("quickErase"); + $scope.releaseOptions.erase = true; + $scope.releaseOptions.secureErase = secureErase; + $scope.releaseOptions.quickErase = quickErase; + $scope.actionGo(); + expect(MachinesManager.performAction).toHaveBeenCalledWith( + node, + "release", + { + erase: true, + secure_erase: secureErase, + quick_erase: quickErase + } + ); }); - describe("getPowerEventErrorText", function() { - - it("returns just empty string", function() { - makeController(); - $scope.node = node; - $scope.node.events = [makeEvent()]; - expect($scope.getPowerEventErrorText()).toBe(""); - }); - - it("returns event description", function() { - makeController(); - var evt = makeEvent(); - evt.type.level = "warning"; - evt.type.description = "Failed to query node's BMC"; - $scope.node = node; - $scope.node.events = [evt]; - expect($scope.getPowerEventErrorText()).toBe(evt.description); - }); - }); - - describe("getServiceClass", function() { - - it("returns 'none' if null", function() { - makeController(); - expect($scope.getServiceClass(null)).toBe("none"); - }); - - it("returns 'success' when running", function() { - makeController(); - expect($scope.getServiceClass({ - status: "running" - })).toBe("success"); - }); - - it("returns 'power-error' when dead", function() { - makeController(); - expect($scope.getServiceClass({ - status: "dead" - })).toBe("error"); - }); - - it("returns 'warning' when degraded", function() { - makeController(); - expect($scope.getServiceClass({ - status: "degraded" - })).toBe("warning"); - }); + it("sets showing_confirmation with deleteOptions", function() { + // Regression test for LP:1793478 + makeController(); + spyOn(ControllersManager, "performAction").and.returnValue( + $q.defer().promise + ); + $scope.node = node; + $scope.type_name = "controller"; + $scope.vlans = [ + { + id: 0, + primary_rack: node.system_id, + name: "Default VLAN" + } + ]; + $scope.action.option = { + name: "delete" + }; + $scope.actionGo(); + expect($scope.action.showing_confirmation).toBe(true); + expect($scope.action.confirmation_message).not.toEqual(""); + expect($scope.action.confirmation_details).not.toEqual([]); + expect(ControllersManager.performAction).not.toHaveBeenCalled(); + }); + + it("clears actionOption on resolve", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.actionGo(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.action.option).toBeNull(); + }); + + it("clears osSelection on resolve", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.osSelection.osystem = "ubuntu"; + $scope.osSelection.release = "ubuntu/trusty"; + $scope.actionGo(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.osSelection.$reset).toHaveBeenCalled(); + }); + + it("clears commissionOptions on resolve", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + $scope.node = node; + $scope.action.option = { + name: "commission" + }; + $scope.commissionOptions.enableSSH = true; + $scope.commissionOptions.skipBMCConfig = true; + $scope.commissionOptions.skipNetworking = true; + $scope.commissionOptions.skipStorage = true; + $scope.commissionOptions.updateFirmware = true; + $scope.commissionOptions.configureHBA = true; + $scope.commissioningSelection = [ + { + id: makeInteger(0, 100), + name: makeName("script_name") + } + ]; + $scope.testSelection = [ + { + id: makeInteger(0, 100), + name: makeName("script_name") + } + ]; + $scope.actionGo(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.commissionOptions).toEqual({ + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }); + expect($scope.commissioningSelection).toEqual([]); + expect($scope.testSelection).toEqual([]); + }); + + it("clears actionError on resolve", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + $scope.action.error = makeName("error"); + $scope.actionGo(); + defer.resolve(); + $rootScope.$digest(); + expect($scope.action.error).toBeNull(); + }); + + it("changes path to node listing on delete", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + spyOn($location, "path"); + $scope.node = node; + $scope.action.option = { + name: "delete" + }; + $scope.actionGo(); + defer.resolve(); + $rootScope.$digest(); + expect($location.path).toHaveBeenCalledWith("/machines"); + }); + + it("sets actionError when rejected", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + $scope.node = node; + $scope.action.option = { + name: "deploy" + }; + var error = makeName("error"); + $scope.actionGo(); + defer.reject(error); + $rootScope.$digest(); + expect($scope.action.error).toBe(error); + }); + }); + + describe("hasUsableArchitectures", function() { + it("returns true if architecture available", function() { + makeController(); + $scope.summary.architecture.options = ["amd64/generic"]; + expect($scope.hasUsableArchitectures()).toBe(true); + }); + + it("returns false if no architecture available", function() { + makeController(); + $scope.summary.architecture.options = []; + expect($scope.hasUsableArchitectures()).toBe(false); + }); + }); + + describe("getArchitecturePlaceholder", function() { + it("returns choose if architecture available", function() { + makeController(); + $scope.summary.architecture.options = ["amd64/generic"]; + expect($scope.getArchitecturePlaceholder()).toBe( + "Choose an architecture" + ); + }); + + it("returns error if no architecture available", function() { + makeController(); + $scope.summary.architecture.options = []; + expect($scope.getArchitecturePlaceholder()).toBe( + "-- No usable architectures --" + ); + }); + }); + + describe("hasInvalidArchitecture", function() { + it("returns false if node is null", function() { + makeController(); + $scope.node = null; + $scope.summary.architecture.options = ["amd64/generic"]; + expect($scope.hasInvalidArchitecture()).toBe(false); + }); + + it("returns true if node.architecture is blank", function() { + makeController(); + $scope.node = { + architecture: "" + }; + $scope.summary.architecture.options = ["amd64/generic"]; + expect($scope.hasInvalidArchitecture()).toBe(true); + }); + + it("returns true if node.architecture not in options", function() { + makeController(); + $scope.node = { + architecture: "i386/generic" + }; + $scope.summary.architecture.options = ["amd64/generic"]; + expect($scope.hasInvalidArchitecture()).toBe(true); + }); + + it("returns false if node.architecture in options", function() { + makeController(); + $scope.node = { + architecture: "amd64/generic" + }; + $scope.summary.architecture.options = ["amd64/generic"]; + expect($scope.hasInvalidArchitecture()).toBe(false); + }); + }); + + describe("invalidArchitecture", function() { + it("returns true if selected architecture empty", function() { + makeController(); + $scope.summary.architecture.selected = ""; + expect($scope.invalidArchitecture()).toBe(true); + }); + + it("returns true if selected architecture not in options", function() { + makeController(); + $scope.summary.architecture.options = [makeName("arch")]; + $scope.summary.architecture.selected = makeName("arch"); + expect($scope.invalidArchitecture()).toBe(true); + }); + + it("returns false if selected architecture in options", function() { + makeController(); + var arch = makeName("arch"); + $scope.summary.architecture.options = [arch]; + $scope.summary.architecture.selected = arch; + expect($scope.invalidArchitecture()).toBe(false); + }); + }); + + describe("isRackControllerConnected", function() { + it("returns false no power_types", function() { + makeController(); + $scope.power_types = []; + expect($scope.isRackControllerConnected()).toBe(false); + }); + + it("returns true if power_types", function() { + makeController(); + $scope.power_types = [{}]; + expect($scope.isRackControllerConnected()).toBe(true); + }); + }); + + describe("hasPermission", function() { + it("returns false no permissions field", function() { + makeController(); + $scope.node = {}; + expect($scope.hasPermission("edit")).toBe(false); + }); + + it("returns false no permission in field", function() { + makeController(); + $scope.node = { + permissions: ["delete"] + }; + expect($scope.hasPermission("edit")).toBe(false); + }); + + it("returns true permissions", function() { + makeController(); + $scope.node = { + permissions: ["edit"] + }; + expect($scope.hasPermission("edit")).toBe(true); + }); + }); + + describe("canEdit", function() { + it("returns false if no edit permission", function() { + makeController(); + $scope.isDevice = false; + spyOn($scope, "hasPermission").and.returnValue(false); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canEdit()).toBe(false); + }); + + it("returns true if edit permission but device", function() { + makeController(); + $scope.isDevice = true; + spyOn($scope, "hasPermission").and.returnValue(true); + spyOn($scope, "isRackControllerConnected").and.returnValue(false); + expect($scope.canEdit()).toBe(true); + }); + + it("returns false if rack disconnected", function() { + makeController(); + $scope.isDevice = false; + spyOn($scope, "hasPermission").and.returnValue(true); + spyOn($scope, "isRackControllerConnected").and.returnValue(false); + expect($scope.canEdit()).toBe(false); + }); + + it("returns false if machine is locked", function() { + makeController(); + $scope.isDevice = false; + spyOn($scope, "hasPermission").and.returnValue(true); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + $scope.node = makeNode(); + $scope.node.locked = true; + expect($scope.canEdit()).toBe(false); + }); + }); + + describe("editHeaderDomain", function() { + it(`doesn't set editing false and + editing_domain true if cannot edit`, function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(true); + $scope.header.editing = true; + $scope.header.editing_domain = false; + $scope.editHeaderDomain(); + expect($scope.header.editing).toBe(true); + expect($scope.header.editing_domain).toBe(false); + }); + + it("sets editing to false and editing_domain to true if able", function() { + makeController(); + $scope.node = node; + spyOn($scope, "canEdit").and.returnValue(false); + $scope.header.editing = true; + $scope.header.editing_domain = false; + $scope.editHeaderDomain(); + expect($scope.header.editing).toBe(false); + expect($scope.header.editing_domain).toBe(true); + }); + + it("sets header.hostname.value to node hostname", function() { + makeController(); + $scope.node = node; + spyOn($scope, "canEdit").and.returnValue(false); + $scope.editHeaderDomain(); + expect($scope.header.hostname.value).toBe(node.hostname); + }); + + it("doesnt reset header.hostname.value on multiple calls", function() { + makeController(); + $scope.node = node; + spyOn($scope, "canEdit").and.returnValue(false); + $scope.editHeaderDomain(); + var updatedName = makeName("name"); + $scope.header.hostname.value = updatedName; + $scope.editHeaderDomain(); + expect($scope.header.hostname.value).toBe(updatedName); + }); + }); + + describe("editHeader", function() { + it(`doesn't set editing true and editing_domain + false if cannot edit`, function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(false); + $scope.header.editing = false; + $scope.header.editing_domain = true; + $scope.editHeader(); + expect($scope.header.editing).toBe(false); + expect($scope.header.editing_domain).toBe(true); + }); + + it("sets editing to true and editing_domain to false if able", function() { + makeController(); + $scope.node = node; + spyOn($scope, "canEdit").and.returnValue(true); + $scope.header.editing = false; + $scope.header.editing_domain = true; + $scope.editHeader(); + expect($scope.header.editing).toBe(true); + expect($scope.header.editing_domain).toBe(false); + }); + + it("sets header.hostname.value to node hostname", function() { + makeController(); + $scope.node = node; + spyOn($scope, "canEdit").and.returnValue(true); + $scope.editHeader(); + expect($scope.header.hostname.value).toBe(node.hostname); + }); + + it("doesnt reset header.hostname.value on multiple calls", function() { + makeController(); + $scope.node = node; + spyOn($scope, "canEdit").and.returnValue(true); + $scope.editHeader(); + var updatedName = makeName("name"); + $scope.header.hostname.value = updatedName; + $scope.editHeader(); + expect($scope.header.hostname.value).toBe(updatedName); + }); + }); + + describe("editHeaderInvalid", function() { + it("returns false if not editing and not editing_domain", function() { + makeController(); + $scope.header.editing = false; + $scope.header.editing_domain = false; + $scope.header.hostname.value = "abc_invalid.local"; + expect($scope.editHeaderInvalid()).toBe(false); + }); + + it("returns true for bad values", function() { + makeController(); + $scope.header.editing = true; + $scope.header.editing_domain = false; + var values = [ + { + input: "aB0-z", + output: false + }, + { + input: "abc_alpha", + output: true + }, + { + input: "ab^&c", + output: true + }, + { + input: "abc.local", + output: true + } + ]; + angular.forEach(values, function(value) { + $scope.header.hostname.value = value.input; + expect($scope.editHeaderInvalid()).toBe(value.output); + }); + }); + }); + + describe("cancelEditHeader", function() { + it(`sets editing and editing_domain to false + for nameHeader section`, function() { + makeController(); + $scope.node = node; + $scope.header.editing = true; + $scope.header.editing_domain = true; + $scope.cancelEditHeader(); + expect($scope.header.editing).toBe(false); + expect($scope.header.editing_domain).toBe(false); + }); + + it("sets header.hostname.value back to fqdn", function() { + makeController(); + $scope.node = node; + $scope.header.editing = true; + $scope.header.hostname.value = makeName("name"); + $scope.cancelEditHeader(); + expect($scope.header.hostname.value).toBe(node.fqdn); + }); + }); + + describe("saveEditHeader", function() { + it("does nothing if value is invalid", function() { + makeController(); + $scope.node = node; + spyOn($scope, "editHeaderInvalid").and.returnValue(true); + var sentinel = {}; + $scope.header.editing = sentinel; + $scope.header.editing_domain = sentinel; + $scope.saveEditHeader(); + expect($scope.header.editing).toBe(sentinel); + expect($scope.header.editing_domain).toBe(sentinel); + }); + + it("sets editing to false", function() { + makeController(); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + spyOn($scope, "editHeaderInvalid").and.returnValue(false); + + $scope.node = node; + $scope.header.editing = true; + $scope.header.editing_domain = true; + $scope.header.hostname.value = makeName("name"); + $scope.saveEditHeader(); + + expect($scope.header.editing).toBe(false); + expect($scope.header.editing_domain).toBe(false); + }); + + it("calls updateItem with copy of node", function() { + makeController(); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + spyOn($scope, "editHeaderInvalid").and.returnValue(false); + + $scope.node = node; + $scope.header.editing = true; + $scope.header.hostname.value = makeName("name"); + $scope.saveEditHeader(); + + var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithNode).not.toBe(node); + }); + + it("calls updateItem with new hostname on node", function() { + makeController(); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + spyOn($scope, "editHeaderInvalid").and.returnValue(false); + + var newName = makeName("name"); + $scope.node = node; + $scope.header.editing = true; + $scope.header.hostname.value = newName; + $scope.saveEditHeader(); + + var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithNode.hostname).toBe(newName); + }); + + it("calls updateName once updateItem resolves", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "updateItem").and.returnValue(defer.promise); + spyOn($scope, "editHeaderInvalid").and.returnValue(false); + + $scope.node = node; + $scope.header.editing = true; + $scope.header.hostname.value = makeName("name"); + $scope.saveEditHeader(); + + defer.resolve(node); + $rootScope.$digest(); + + // Since updateName is private in the controller, check + // that the header.hostname.value is set to the nodes fqdn. + expect($scope.header.hostname.value).toBe(node.fqdn); + }); + }); + + describe("editSummary", function() { + it("doesnt sets editing to true if cannot edit", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(false); + $scope.summary.editing = false; + $scope.editSummary(); + expect($scope.summary.editing).toBe(false); + }); + + it("sets editing to true for summary section", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(true); + $scope.summary.editing = false; + $scope.editSummary(); + expect($scope.summary.editing).toBe(true); + }); + }); + + describe("cancelEditSummary", function() { + it("sets editing to false for summary section", function() { + makeController(); + $scope.node = node; + $scope.summary.architecture.options = [node.architecture]; + $scope.summary.editing = true; + $scope.cancelEditSummary(); + expect($scope.summary.editing).toBe(false); + }); + + it("doesnt set editing to false if invalid architecture", function() { + makeController(); + $scope.node = node; + $scope.summary.editing = true; + $scope.cancelEditSummary(); + expect($scope.summary.editing).toBe(true); + }); + + it("does set editing to true if device", function() { + makeController(); + $scope.isDevice = true; + $scope.node = node; + $scope.summary.editing = true; + $scope.cancelEditSummary(); + expect($scope.summary.editing).toBe(false); + }); + + it("does set editing to true if controller", function() { + makeController(); + $scope.isController = true; + $scope.node = node; + $scope.summary.editing = true; + $scope.cancelEditSummary(); + expect($scope.summary.editing).toBe(false); + }); + + it("calls updateSummary", function() { + makeController(); + $scope.node = node; + $scope.summary.architecture.options = [node.architecture]; + $scope.summary.editing = true; + $scope.cancelEditSummary(); + }); + }); + + describe("saveEditSummary", function() { + // Configures the summary area in the scope to have a zone, and + // architecture. + function configureSummary() { + $scope.summary.editing = true; + $scope.summary.zone.selected = makeZone(); + $scope.summary.pool.selected = makeResourcePool(); + $scope.summary.description = "This is a description"; + $scope.summary.architecture.selected = makeName("architecture"); + $scope.summary.tags = [ + { text: makeName("tag") }, + { text: makeName("tag") } + ]; + } - it("returns 'none' for anything else", function() { - makeController(); - expect($scope.getServiceClass({ - status: makeName("status") - })).toBe("none"); - }); + it("does nothing if invalidArchitecture", function() { + makeController(); + spyOn($scope, "invalidArchitecture").and.returnValue(true); + $scope.node = node; + var editing = {}; + $scope.summary.editing = editing; + $scope.saveEditSummary(); + + // Editing remains the same then the method exited early. + expect($scope.summary.editing).toBe(editing); + }); + + it("sets editing to false", function() { + makeController(); + spyOn($scope, "invalidArchitecture").and.returnValue(false); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + + $scope.node = node; + $scope.summary.editing = true; + $scope.saveEditSummary(); + + expect($scope.summary.editing).toBe(false); + }); + + it("calls updateItem with copy of node", function() { + makeController(); + spyOn($scope, "invalidArchitecture").and.returnValue(false); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + + $scope.node = node; + $scope.summary.editing = true; + $scope.saveEditSummary(); + + var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithNode).not.toBe(node); + }); + + it("calls updateItem with new copied values on node", function() { + makeController(); + spyOn($scope, "invalidArchitecture").and.returnValue(false); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + + $scope.node = node; + configureSummary(); + var newZone = $scope.summary.zone.selected; + var newPool = $scope.summary.pool.selected; + var newDescription = $scope.summary.description; + var newArchitecture = $scope.summary.architecture.selected; + var newTags = []; + angular.forEach($scope.summary.tags, function(tag) { + newTags.push(tag.text); + }); + $scope.saveEditSummary(); + + var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithNode.zone).toEqual(newZone); + expect(calledWithNode.zone).not.toBe(newZone); + expect(calledWithNode.pool).not.toBe(newPool); + expect(calledWithNode.description).toBe(newDescription); + expect(calledWithNode.architecture).toBe(newArchitecture); + expect(calledWithNode.tags).toEqual(newTags); + }); + + it("logs error if not disconnected error", function() { + makeController(); + spyOn($scope, "invalidArchitecture").and.returnValue(false); + + var defer = $q.defer(); + spyOn(MachinesManager, "updateItem").and.returnValue(defer.promise); + + $scope.node = node; + configureSummary(); + $scope.saveEditSummary(); + + spyOn($log, "error"); + var error = makeName("error"); + defer.reject(error); + $rootScope.$digest(); + + expect($log.error).toHaveBeenCalledWith(error); + }); + }); + + describe("invalidPowerType", function() { + it("returns true if missing power type", function() { + makeController(); + $scope.power.type = null; + expect($scope.invalidPowerType()).toBe(true); + }); + + it("returns false if selected power type", function() { + makeController(); + $scope.power.type = { + name: makeName("power") + }; + expect($scope.invalidPowerType()).toBe(false); + }); + }); + + describe("editPower", function() { + it("doesnt sets editing to true if cannot edit", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(false); + $scope.power.editing = false; + $scope.editPower(); + expect($scope.power.editing).toBe(false); + }); + + it("sets editing to true for power section", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(true); + $scope.power.editing = false; + $scope.editPower(); + expect($scope.power.editing).toBe(true); + }); + }); + + describe("cancelEditPower", function() { + it("sets editing to false for power section", function() { + makeController(); + node.power_type = makeName("power"); + $scope.node = node; + $scope.power.editing = true; + $scope.cancelEditPower(); + expect($scope.power.editing).toBe(false); + }); + + it("doesnt sets editing to false when no power_type", function() { + makeController(); + $scope.node = node; + $scope.power.editing = true; + $scope.cancelEditPower(); + expect($scope.power.editing).toBe(true); + }); + + it("sets editing false with no power_type for controller", function() { + makeController(); + node.node_type = 4; + $scope.node = node; + $scope.power.editing = true; + $scope.cancelEditPower(); + expect($scope.power.editing).toBe(false); + }); + + it("sets in_pod to true for node in pod", function() { + makeController(); + node.power_type = makeName("power"); + node.pod = makeName("pod"); + $scope.node = node; + $scope.power.editing = true; + $scope.cancelEditPower(); + expect($scope.power.in_pod).toBe(true); + }); + }); + + describe("saveEditPower", function() { + it("does nothing if no selected power_type", function() { + makeController(); + $scope.node = node; + var editing = {}; + $scope.power.editing = editing; + $scope.power.type = null; + $scope.saveEditPower(); + // Editing should still be true, because the function exitted + // early. + expect($scope.power.editing).toBe(editing); + }); + + it("sets editing to false", function() { + makeController(); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + + $scope.node = node; + $scope.power.editing = true; + $scope.power.type = { + name: makeName("power") + }; + $scope.saveEditPower(); + + expect($scope.power.editing).toBe(false); + }); + + it("calls updateItem with copy of node", function() { + makeController(); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + + $scope.node = node; + $scope.power.editing = true; + $scope.power.type = { + name: makeName("power") + }; + $scope.saveEditPower(); + + var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithNode).not.toBe(node); + }); + + it("calls updateItem with new copied values on node", function() { + makeController(); + spyOn(MachinesManager, "updateItem").and.returnValue($q.defer().promise); + + var newPowerType = { + name: makeName("power") + }; + var newPowerParameters = { + foo: makeName("bar") + }; + + $scope.node = node; + $scope.power.editing = true; + $scope.power.type = newPowerType; + $scope.power.parameters = newPowerParameters; + $scope.saveEditPower(); + + var calledWithNode = MachinesManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithNode.power_type).toBe(newPowerType.name); + expect(calledWithNode.power_parameters).toEqual(newPowerParameters); + expect(calledWithNode.power_parameters).not.toBe(newPowerParameters); + }); + + it("calls handleSaveError once updateItem is rejected", function() { + makeController(); + + var defer = $q.defer(); + spyOn(MachinesManager, "updateItem").and.returnValue(defer.promise); + + $scope.node = node; + $scope.power.editing = true; + $scope.power.type = { + name: makeName("power") + }; + $scope.power.parameters = { + foo: makeName("bar") + }; + $scope.saveEditPower(); + + spyOn($log, "error"); + var error = makeName("error"); + defer.reject(error); + $rootScope.$digest(); + + // If the error message was logged to the console then + // handleSaveError was called. + expect($log.error).toHaveBeenCalledWith(error); + }); + }); + + describe("allowShowMoreEvents", function() { + it("returns false if node is null", function() { + makeController(); + $scope.node = null; + expect($scope.allowShowMoreEvents()).toBe(false); + }); + + it("returns false if node.events is not array", function() { + makeController(); + $scope.node = node; + $scope.node.events = undefined; + expect($scope.allowShowMoreEvents()).toBe(false); + }); + + it("returns false if node has no events", function() { + makeController(); + $scope.node = node; + expect($scope.allowShowMoreEvents()).toBe(false); + }); + + it("returns false if node events less then the limit", function() { + makeController(); + $scope.node = node; + $scope.node.events = [makeEvent(), makeEvent()]; + $scope.events.limit = 10; + expect($scope.allowShowMoreEvents()).toBe(false); + }); + + it("returns false if events limit greater than 50", function() { + makeController(); + $scope.node = node; + var i; + for (i = 0; i < 50; i++) { + $scope.node.events.push(makeEvent()); + } + $scope.events.limit = 50; + expect($scope.allowShowMoreEvents()).toBe(false); + }); + + it("returns true if more events than limit", function() { + makeController(); + $scope.node = node; + var i; + for (i = 0; i < 20; i++) { + $scope.node.events.push(makeEvent()); + } + $scope.events.limit = 10; + expect($scope.allowShowMoreEvents()).toBe(true); + }); + }); + + describe("showMoreEvents", function() { + it("increments events limit by 10", function() { + makeController(); + $scope.showMoreEvents(); + expect($scope.events.limit).toBe(20); + $scope.showMoreEvents(); + expect($scope.events.limit).toBe(30); + }); + }); + + describe("getEventText", function() { + it("returns just event type description without dash", function() { + makeController(); + var evt = makeEvent(); + delete evt.description; + expect($scope.getEventText(evt)).toBe(evt.type.description); + }); + + it("returns event type description with event description", function() { + makeController(); + var evt = makeEvent(); + expect($scope.getEventText(evt)).toBe( + evt.type.description + " - " + evt.description + ); + }); + }); + + describe("getPowerEventError", function() { + it("returns event if there is a power event error", function() { + makeController(); + var evt = makeEvent(); + evt.type.level = "warning"; + evt.type.description = "Failed to query node's BMC"; + $scope.node = node; + $scope.node.events = [makeEvent(), evt]; + expect($scope.getPowerEventError()).toBe(evt); + }); + + it("returns nothing if there is no power event error", function() { + makeController(); + var evt_info = makeEvent(); + var evt_error = makeEvent(); + evt_info.type.level = "info"; + evt_info.type.description = "Queried node's BMC"; + evt_error.type.level = "warning"; + evt_error.type.description = "Failed to query node's BMC"; + $scope.node = node; + $scope.node.events = [makeEvent(), evt_info, evt_error]; + expect($scope.getPowerEventError()).toBe(); + }); + }); + + describe("hasPowerEventError", function() { + it("returns true if last event is an error", function() { + makeController(); + var evt = makeEvent(); + evt.type.level = "warning"; + evt.type.description = "Failed to query node's BMC"; + $scope.node = node; + $scope.node.events = [evt]; + expect($scope.hasPowerEventError()).toBe(true); + }); + + it("returns false if last event is not an error", function() { + makeController(); + $scope.node = node; + $scope.node.events = [makeEvent()]; + expect($scope.hasPowerEventError()).toBe(false); + }); + }); + + describe("getPowerEventErrorText", function() { + it("returns just empty string", function() { + makeController(); + $scope.node = node; + $scope.node.events = [makeEvent()]; + expect($scope.getPowerEventErrorText()).toBe(""); + }); + + it("returns event description", function() { + makeController(); + var evt = makeEvent(); + evt.type.level = "warning"; + evt.type.description = "Failed to query node's BMC"; + $scope.node = node; + $scope.node.events = [evt]; + expect($scope.getPowerEventErrorText()).toBe(evt.description); + }); + }); + + describe("getServiceClass", function() { + it("returns 'none' if null", function() { + makeController(); + expect($scope.getServiceClass(null)).toBe("none"); + }); + + it("returns 'success' when running", function() { + makeController(); + expect( + $scope.getServiceClass({ + status: "running" + }) + ).toBe("success"); + }); + + it("returns 'power-error' when dead", function() { + makeController(); + expect( + $scope.getServiceClass({ + status: "dead" + }) + ).toBe("error"); + }); + + it("returns 'warning' when degraded", function() { + makeController(); + expect( + $scope.getServiceClass({ + status: "degraded" + }) + ).toBe("warning"); + }); + + it("returns 'none' for anything else", function() { + makeController(); + expect( + $scope.getServiceClass({ + status: makeName("status") + }) + ).toBe("none"); + }); + }); + + describe("hasCustomCommissioningScripts", function() { + it("returns true with custom commissioning scripts", function() { + makeController(); + ScriptsManager._items.push({ script_type: 0 }); + expect($scope.hasCustomCommissioningScripts()).toBe(true); + }); + + it("returns false without custom commissioning scripts", function() { + makeController(); + expect($scope.hasCustomCommissioningScripts()).toBe(false); + }); + }); + + describe("showFailedTestWarning", function() { + it("returns false when device", function() { + makeController(); + $scope.node = { + node_type: 1 + }; + expect($scope.showFailedTestWarning()).toBe(false); + }); + + it("returns false when new, commissioning, or testing", function() { + makeController(); + $scope.node = node; + angular.forEach([0, 1, 2, 21, 22], function(status) { + node.status_code = status; + expect($scope.showFailedTestWarning()).toBe(false); + }); + }); + + it("returns false when tests havn't been run or passed", function() { + makeController(); + // READY + node.status_code = 4; + $scope.node = node; + angular.forEach([-1, 2], function(status) { + node.testing_status = status; + expect($scope.showFailedTestWarning()).toBe(false); + }); + }); + + it("returns true otherwise", function() { + makeController(); + var i, j; + $scope.node = node; + // i < 3 or i > 20 is tested above. + for (i = 3; i <= 20; i++) { + for (j = 3; j <= 8; j++) { + node.status_code = i; + node.testing_status = j; + expect($scope.showFailedTestWarning()).toBe(true); + } + } }); + }); - describe("hasCustomCommissioningScripts", function() { - it("returns true with custom commissioning scripts", function() { - makeController(); - ScriptsManager._items.push({script_type: 0}); - expect($scope.hasCustomCommissioningScripts()).toBe(true); - }); + describe("getCPUSubtext", function() { + it("returns only cores when unknown speed", function() { + makeController(); + $scope.node = node; + expect($scope.getCPUSubtext()).toEqual(node.cpu_count + " cores"); + }); + + it("returns speed in mhz", function() { + makeController(); + node.cpu_speed = makeInteger(100, 999); + $scope.node = node; + expect($scope.getCPUSubtext()).toEqual( + node.cpu_count + " cores @ " + node.cpu_speed + " Mhz" + ); + }); + + it("returns speed in ghz", function() { + makeController(); + node.cpu_speed = makeInteger(1000, 10000); + $scope.node = node; + expect($scope.getCPUSubtext()).toEqual( + node.cpu_count + " cores @ " + node.cpu_speed / 1000 + " Ghz" + ); + }); + }); + + describe("openSection", function() { + it("sets section.area to passed argument", function() { + makeController(); + $scope.node = node; + $scope.openSection("controllers"); + expect($scope.section.area).toBe("controllers"); + }); + }); + + describe("dismissHighAvailabilityNotification", function() { + it("sets hideHighAvailabilityNotification to true", function() { + makeController(); + $scope.vlan = { id: 5001 }; + $scope.hideHighAvailabilityNotification = false; + $scope.dismissHighAvailabilityNotification(); + expect($scope.hideHighAvailabilityNotification).toBe(true); + }); + }); + + describe("showHighAvailabilityNotification", function() { + it("returns true if hide notification flag not set", function() { + makeController(); + $scope.hideHighAvailabilityNotification = false; + $scope.node = { + dhcp_on: true + }; + $scope.vlan = { + rack_sids: ["asd3d", "sd3sd"], + secondary_rack: "" + }; + expect($scope.showHighAvailabilityNotification()).toBe(true); + }); + + it("returns false if hide notification flag is set", function() { + makeController(); + $scope.hideHighAvailabilityNotification = true; + $scope.node = { + dhcp_on: true + }; + $scope.vlan = { + rack_sids: ["asd3d", "sd3sd"], + secondary_rack: "" + }; + expect($scope.showHighAvailabilityNotification()).toBe(false); + }); + + it("returns false if dhcp not enabled", function() { + makeController(); + $scope.hideHighAvailabilityNotification = false; + $scope.node = { + dhcp_on: false + }; + $scope.vlan = { + rack_sids: ["asd3d", "sd3sd"], + secondary_rack: "" + }; + expect($scope.showHighAvailabilityNotification()).toBe(false); + }); + + it("returns false if one or less rack_sid", function() { + makeController(); + $scope.hideHighAvailabilityNotification = false; + $scope.node = { + dhcp_on: true + }; + $scope.vlan = { + rack_sids: ["asd3d"], + secondary_rack: "" + }; + expect($scope.showHighAvailabilityNotification()).toBe(false); + }); + + it("returns false if has secondary rack", function() { + makeController(); + $scope.hideHighAvailabilityNotification = false; + $scope.node = { + dhcp_on: false + }; + $scope.vlan = { + rack_sids: ["asd3d", "sd3sd"], + secondary_rack: "sdf3" + }; + expect($scope.showHighAvailabilityNotification()).toBe(false); + }); + }); + + describe( + "getHardwareTestErrorText if 'Unable to run destructive" + + " test while deployed!'", + function() { + it("returns correct string", function() { + makeController(); + expect( + $scope.getHardwareTestErrorText( + "Unable to run destructive test while deployed!" + ) + ).toBe( + "The selected hardware tests contain one or more destructive tests." + + " Destructive tests cannot run on deployed machines." + ); + }); + + it( + "return passed error string if not 'Unable to run destructive test" + + " while deployed!'", + function() { + makeController(); + var errorString = "There was an error"; + expect($scope.getHardwareTestErrorText(errorString)).toBe( + errorString + ); + } + ); + } + ); - it("returns false without custom commissioning scripts", function() { - makeController(); - expect($scope.hasCustomCommissioningScripts()).toBe(false); - }); + describe("powerParametersValid", function() { + it("returns false if no power_parameters", function() { + makeController(); + expect($scope.powerParametersValid()).toBe(false); }); - describe("showFailedTestWarning", function() { - - it("returns false when device", function() { - makeController(); - $scope.node = { - node_type: 1 - }; - expect($scope.showFailedTestWarning()).toBe(false); - }); - - it("returns false when new, commissioning, or testing", function() { - makeController(); - $scope.node = node; - angular.forEach([0, 1, 2, 21, 22], function(status) { - node.status_code = status; - expect($scope.showFailedTestWarning()).toBe(false); - }); - }); - - it("returns false when tests havn't been run or passed", function() { - makeController(); - // READY - node.status_code = 4; - $scope.node = node; - angular.forEach([-1, 2], function(status) { - node.testing_status = status; - expect($scope.showFailedTestWarning()).toBe(false); - }); - }); - - it("returns true otherwise", function() { - makeController(); - var i, j; - $scope.node = node; - // i < 3 or i > 20 is tested above. - for(i = 3; i <= 20; i++) { - for(j = 3; j <= 8; j++) { - node.status_code = i; - node.testing_status = j; - expect($scope.showFailedTestWarning()).toBe(true); - } - } - }); + it("returns false if power_parameters are empty", function() { + makeController(); + expect($scope.powerParametersValid({})).toBe(false); }); - describe("getCPUSubtext", function() { - - it("returns only cores when unknown speed", function() { - makeController(); - $scope.node = node; - expect($scope.getCPUSubtext()).toEqual( - node.cpu_count + " cores"); - }); - - it("returns speed in mhz", function() { - makeController(); - node.cpu_speed = makeInteger(100, 999); - $scope.node = node; - expect($scope.getCPUSubtext()).toEqual( - node.cpu_count + " cores @ " + node.cpu_speed + " Mhz"); - }); - - it("returns speed in ghz", function() { - makeController(); - node.cpu_speed = makeInteger(1000, 10000); - $scope.node = node; - expect($scope.getCPUSubtext()).toEqual( - node.cpu_count + " cores @ " + (node.cpu_speed / 1000) + - " Ghz"); - }); + it("returns true if power_parameters have values", function() { + makeController(); + expect( + $scope.powerParametersValid({ + power_address: "qemu+ssh://ubuntu@172.16.3.247/system", + power_id: 26 + }) + ).toBe(true); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details_networking.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,4856 +4,4885 @@ * Unit tests for NodeNetworkingController. */ -describe("filterByUnusedForInterface", function() { +import { makeInteger, makeName } from "testing/utils"; - // Load the MAAS module. - beforeEach(module("MAAS")); +describe("filterByUnusedForInterface", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the filterByUnusedForInterface. - var filterByUnusedForInterface; - beforeEach(inject(function($filter) { - filterByUnusedForInterface = $filter("filterByUnusedForInterface"); - })); - - it("returns empty if undefined nic", function() { - var i, vlan, vlans = []; - for(i = 0; i < 3; i++) { - vlan = { - fabric: 0 - }; - vlans.push(vlan); - } - expect(filterByUnusedForInterface(vlans)).toEqual([]); - }); - - it("returns only free vlans", function() { - var i, vlan, used_vlans = [], free_vlans = [], all_vlans = []; - for(i = 0; i < 3; i++) { - vlan = { - id: i, - fabric: 0 - }; - used_vlans.push(vlan); - all_vlans.push(vlan); - } - for(i = 3; i < 6; i++) { - vlan = { - id: i, - fabric: 0 - }; - free_vlans.push(vlan); - all_vlans.push(vlan); - } + // Load the filterByUnusedForInterface. + var filterByUnusedForInterface; + beforeEach(inject(function($filter) { + filterByUnusedForInterface = $filter("filterByUnusedForInterface"); + })); + + it("returns empty if undefined nic", function() { + var i, + vlan, + vlans = []; + for (i = 0; i < 3; i++) { + vlan = { + fabric: 0 + }; + vlans.push(vlan); + } + expect(filterByUnusedForInterface(vlans)).toEqual([]); + }); - var nic = { - id: 0 - }; - var originalInterfaces = { - 0: { - type: "vlan", - parents: [0], - vlan_id: used_vlans[0].id - }, - 1: { - type: "vlan", - parents: [0], - vlan_id: used_vlans[1].id - }, - 2: { - type: "vlan", - parents: [0], - vlan_id: used_vlans[2].id - }, - 3: { - type: "physical", - vlan_id: free_vlans[0].id - } - }; + it("returns only free vlans", function() { + var i, + vlan, + used_vlans = [], + free_vlans = [], + all_vlans = []; + for (i = 0; i < 3; i++) { + vlan = { + id: i, + fabric: 0 + }; + used_vlans.push(vlan); + all_vlans.push(vlan); + } + for (i = 3; i < 6; i++) { + vlan = { + id: i, + fabric: 0 + }; + free_vlans.push(vlan); + all_vlans.push(vlan); + } - expect( - filterByUnusedForInterface( - all_vlans, nic, originalInterfaces)).toEqual(free_vlans); - }); + var nic = { + id: 0 + }; + var originalInterfaces = { + 0: { + type: "vlan", + parents: [0], + vlan_id: used_vlans[0].id + }, + 1: { + type: "vlan", + parents: [0], + vlan_id: used_vlans[1].id + }, + 2: { + type: "vlan", + parents: [0], + vlan_id: used_vlans[2].id + }, + 3: { + type: "physical", + vlan_id: free_vlans[0].id + } + }; + + expect( + filterByUnusedForInterface(all_vlans, nic, originalInterfaces) + ).toEqual(free_vlans); + }); }); - describe("removeInterfaceParents", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Load the removeInterfaceParents. - var removeInterfaceParents; - beforeEach(inject(function($filter) { - removeInterfaceParents = $filter("removeInterfaceParents"); - })); - - it("returns empty if undefined bondInterface", function() { - var i, nic, interfaces = []; - for(i = 0; i < 3; i++) { - nic = { - id: i, - link_id: i - }; - interfaces.push(nic); - } - expect( - removeInterfaceParents(interfaces)).toEqual(interfaces); - }); - - it("removes parents from interfaces", function() { - var vlan = { - id: makeInteger(0, 100) - }; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var nic2 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var interfaces = [nic1, nic2]; - var bondInterface = { - parents: interfaces - }; - expect( - removeInterfaceParents( - interfaces, bondInterface, false)).toEqual([]); - }); + // Load the removeInterfaceParents. + var removeInterfaceParents; + beforeEach(inject(function($filter) { + removeInterfaceParents = $filter("removeInterfaceParents"); + })); + + it("returns empty if undefined bondInterface", function() { + var i, + nic, + interfaces = []; + for (i = 0; i < 3; i++) { + nic = { + id: i, + link_id: i + }; + interfaces.push(nic); + } + expect(removeInterfaceParents(interfaces)).toEqual(interfaces); + }); - it("does not remove parents from interfaces when skipping", function() { - var vlan = { - id: makeInteger(0, 100) - }; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var nic2 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var interfaces = [nic1, nic2]; - var bondInterface = { - parents: interfaces - }; - expect( - removeInterfaceParents( - interfaces, bondInterface, true)).toEqual(interfaces); - }); + it("removes parents from interfaces", function() { + var vlan = { + id: makeInteger(0, 100) + }; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var nic2 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var interfaces = [nic1, nic2]; + var bondInterface = { + parents: interfaces + }; + expect(removeInterfaceParents(interfaces, bondInterface, false)).toEqual( + [] + ); + }); + + it("does not remove parents from interfaces when skipping", function() { + var vlan = { + id: makeInteger(0, 100) + }; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var nic2 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var interfaces = [nic1, nic2]; + var bondInterface = { + parents: interfaces + }; + expect(removeInterfaceParents(interfaces, bondInterface, true)).toEqual( + interfaces + ); + }); }); - describe("filterVLANNotOnFabric", function() { - // Load the MAAS module. - beforeEach(module("MAAS")); + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the filter - var filterVLANNotOnFabric; - beforeEach(inject(function ($filter) { - filterVLANNotOnFabric = $filter("filterVLANNotOnFabric"); - })); - - it("returns VLANs if undefined type", function() { - var vlans = [{ id: 3 }, { id: 4 }]; - expect(filterVLANNotOnFabric(vlans, null)).toEqual(vlans); - }); - - it("removes VLANs from a different fabric", function() { - var vlans = [{ id: 3 }, { id: 4 }]; - var vlanIDs = [3]; - expect(filterVLANNotOnFabric(vlans, vlanIDs)).toEqual([{ id: 3 }]); - }); - - it("returns all VLANs in fabric", function() { - var vlans = [{ id: 3 }, { id: 4 }]; - var vlanIDs = [3, 4]; - expect(filterVLANNotOnFabric(vlans, vlanIDs)).toEqual(vlans); - }); + // Load the filter + var filterVLANNotOnFabric; + beforeEach(inject(function($filter) { + filterVLANNotOnFabric = $filter("filterVLANNotOnFabric"); + })); + + it("returns VLANs if undefined type", function() { + var vlans = [{ id: 3 }, { id: 4 }]; + expect(filterVLANNotOnFabric(vlans, null)).toEqual(vlans); + }); + + it("removes VLANs from a different fabric", function() { + var vlans = [{ id: 3 }, { id: 4 }]; + var vlanIDs = [3]; + expect(filterVLANNotOnFabric(vlans, vlanIDs)).toEqual([{ id: 3 }]); + }); + + it("returns all VLANs in fabric", function() { + var vlans = [{ id: 3 }, { id: 4 }]; + var vlanIDs = [3, 4]; + expect(filterVLANNotOnFabric(vlans, vlanIDs)).toEqual(vlans); + }); }); +describe("filterEditInterface", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); + + // Load the filter + var filterEditInterface; + beforeEach(inject(function($filter) { + filterEditInterface = $filter("filterEditInterface"); + })); + + it("returns interfaces if undefined type", function() { + var interfaces = [{ id: 36, name: "eth-AMPnu0" }]; + expect(filterEditInterface(interfaces, "foo")).toEqual(interfaces); + }); + + it("removes editInterface", function() { + var editInterface = { + id: 36, + name: "eth-AMPnu0", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + }; + + var interfaces = [ + { + id: 37, + name: "eth-AMPnu1", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + } + ]; -describe("filterEditInterface", function () { - - // Load the MAAS module. - beforeEach(module("MAAS")); + interfaces.push(editInterface); - // Load the filter - var filterEditInterface; - beforeEach(inject(function ($filter) { - filterEditInterface = $filter("filterEditInterface"); - })); - - it("returns interfaces if undefined type", function () { - var interfaces = [{ id: 36, name: "eth-AMPnu0" }]; - expect(filterEditInterface(interfaces, "foo")).toEqual(interfaces); - }); - - it("removes editInterface", function () { - var editInterface = { - id: 36, - name: "eth-AMPnu0", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - }; + expect(filterEditInterface(interfaces, editInterface)).toEqual([ + { + id: 37, + name: "eth-AMPnu1", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + } + ]); + }); + + it("removes item on different fabric", function() { + var interfaces = [ + { id: 36, name: "eth=AMPnu0", fabric: { name: "fabric-1" } } + ]; + var editInterface = { fabric: { name: "fabric-2" } }; - var interfaces = [ - { - id: 37, - name: "eth-AMPnu1", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - } - ]; + expect(filterEditInterface(interfaces, editInterface)).toEqual([]); + }); +}); - interfaces.push(editInterface); +describe("removeDefaultVLANIfVLAN", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - expect( - filterEditInterface(interfaces, editInterface) - ).toEqual([ - { - id: 37, - name: "eth-AMPnu1", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - } - ]); - }); + // Load the removeDefaultVLANIfVLAN. + var removeDefaultVLANIfVLAN; + beforeEach(inject(function($filter) { + removeDefaultVLANIfVLAN = $filter("removeDefaultVLANIfVLAN"); + })); + + it("returns vlans if undefined type", function() { + var i, + vlan, + vlans = []; + for (i = 0; i < 3; i++) { + vlan = { + id: i, + vid: i, + fabric: 0 + }; + vlans.push(vlan); + } + expect(removeDefaultVLANIfVLAN(vlans)).toEqual(vlans); + }); - it("removes item on different fabric", function() { - var interfaces = [ - { id: 36, name: "eth=AMPnu0", fabric: { name: "fabric-1" }} - ]; - var editInterface = { fabric: { name: "fabric-2" }}; + it("removes default vlans from vlans", function() { + var i, + vlan, + vlans = []; + for (i = 0; i < 3; i++) { + vlan = { + id: i, + vid: i, + fabric: 0 + }; + vlans.push(vlan); + } - expect(filterEditInterface(interfaces, editInterface)) - .toEqual([]); - }); + expect(removeDefaultVLANIfVLAN(vlans, "vlan")).toEqual([ + vlans[1], + vlans[2] + ]); + }); }); +describe("filterSelectedInterfaces", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); -describe("removeDefaultVLANIfVLAN", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Load the removeDefaultVLANIfVLAN. - var removeDefaultVLANIfVLAN; - beforeEach(inject(function($filter) { - removeDefaultVLANIfVLAN = $filter("removeDefaultVLANIfVLAN"); - })); - - it("returns vlans if undefined type", function() { - var i, vlan, vlans = []; - for(i = 0; i < 3; i++) { - vlan = { - id: i, - vid: i, - fabric: 0 - }; - vlans.push(vlan); - } - expect(removeDefaultVLANIfVLAN(vlans)).toEqual(vlans); - }); - - it("removes default vlans from vlans", function() { - var i, vlan, vlans = []; - for(i = 0; i < 3; i++) { - vlan = { - id: i, - vid: i, - fabric: 0 - }; - vlans.push(vlan); - } - - expect( - removeDefaultVLANIfVLAN( - vlans, "vlan")).toEqual([vlans[1], vlans[2]]); - }); + // Load the filterSelectedInterfaces. + var filterSelectedInterfaces; + beforeEach(inject(function($filter) { + filterSelectedInterfaces = $filter("filterSelectedInterfaces"); + })); + + it("removes selected items from interfaces", function() { + var interfaces = [ + { + id: 36, + name: "eth-AMPnu0", + link_id: -1, + fabric: { + name: "fabric-1" + } + } + ]; + var selectedInterfaces = ["36/-1"]; + var newBondInterface = { fabric: { name: "fabric-1" } }; + + expect( + filterSelectedInterfaces(interfaces, selectedInterfaces, newBondInterface) + ).toEqual([]); + }); + + it("does not remove interface if not selected", function() { + var interfaces = [ + { + id: 36, + name: "eth-AMPnu0", + link_id: -1, + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + } + ]; + var selectedInterfaces = ["37/-1"]; + var newBondInterface = { fabric: { name: "fabric-1" }, vlan: { id: 2 } }; + + expect( + filterSelectedInterfaces(interfaces, selectedInterfaces, newBondInterface) + ).toEqual(interfaces); + }); + + it("removes interface if not on same fabric as new bond", function() { + var interfaces = [ + { + id: 36, + name: "eth-AMPnu0", + link_id: -1, + fabric: { + name: "fabric-1" + } + } + ]; + var selectedInterfaces = ["37/-1"]; + var newBondInterface = { fabric: { name: "fabric-2" } }; + + expect( + filterSelectedInterfaces(interfaces, selectedInterfaces, newBondInterface) + ).toEqual([]); + }); }); +describe("filterLinkModes", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); -describe("filterSelectedInterfaces", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); + // Load the filterLinkModes. + var filterLinkModes; + beforeEach(inject(function($filter) { + filterLinkModes = $filter("filterLinkModes"); + })); + + // Load the modes before each test. + var modes; + beforeEach(function() { + modes = [ + { + mode: "auto", + text: "Auto assign" + }, + { + mode: "static", + text: "Static assign" + }, + { + mode: "dhcp", + text: "DHCP" + }, + { + mode: "link_up", + text: "Unconfigured" + } + ]; + }); + + it("only link_up when no subnet", function() { + var nic = { + subnet: null + }; + expect(filterLinkModes(modes, nic)).toEqual([ + { + mode: "link_up", + text: "Unconfigured" + } + ]); + }); + + it("honors getValue()", function() { + var nic = { + getValue: function() { + return null; + } + }; + expect(filterLinkModes(modes, nic)).toEqual([ + { + mode: "link_up", + text: "Unconfigured" + } + ]); + }); + + it("all modes if only one link", function() { + var nic = { + subnet: {}, + links: [{}] + }; + expect(filterLinkModes(modes, nic)).toEqual([ + { + mode: "auto", + text: "Auto assign" + }, + { + mode: "static", + text: "Static assign" + }, + { + mode: "dhcp", + text: "DHCP" + }, + { + mode: "link_up", + text: "Unconfigured" + } + ]); + }); + + it("auto, static, and dhcp modes if more than one link", function() { + var nic = { + subnet: {}, + links: [{}, {}] + }; + expect(filterLinkModes(modes, nic)).toEqual([ + { + mode: "auto", + text: "Auto assign" + }, + { + mode: "static", + text: "Static assign" + }, + { + mode: "dhcp", + text: "DHCP" + } + ]); + }); + + it("auto and static modes if interface is alias", function() { + var nic = { + type: "alias", + subnet: {} + }; + expect(filterLinkModes(modes, nic)).toEqual([ + { + mode: "auto", + text: "Auto assign" + }, + { + mode: "static", + text: "Static assign" + } + ]); + }); +}); - // Load the filterSelectedInterfaces. - var filterSelectedInterfaces; - beforeEach(inject(function($filter) { - filterSelectedInterfaces = $filter("filterSelectedInterfaces"); - })); +describe("NodeNetworkingController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - it("removes selected items from interfaces", function() { - var interfaces = [{ id: 36, name: "eth-AMPnu0", link_id: -1, fabric: { - name: "fabric-1" - }}]; - var selectedInterfaces = ["36/-1"]; - var newBondInterface = { fabric: { name: "fabric-1" }}; - - expect(filterSelectedInterfaces( - interfaces, - selectedInterfaces, - newBondInterface - )).toEqual([]); - }); - - it("does not remove interface if not selected", function() { - var interfaces = [ - { - id: 36, - name: "eth-AMPnu0", - link_id: -1, - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - } - ]; - var selectedInterfaces = ["37/-1"]; - var newBondInterface = { fabric: { name: "fabric-1" }, vlan: { id: 2}}; + // Grab the needed angular pieces. + var $controller, $rootScope, $parentScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $parentScope = $rootScope.$new(); + $scope = $parentScope.$new(); + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the NodeNetworkingController. + var FabricsManager, VLANsManager, SubnetsManager; + var MachinesManager, DevicesManager, GeneralManager, ManagerHelperService; + beforeEach(inject(function($injector) { + FabricsManager = $injector.get("FabricsManager"); + VLANsManager = $injector.get("VLANsManager"); + SubnetsManager = $injector.get("SubnetsManager"); + MachinesManager = $injector.get("MachinesManager"); + DevicesManager = $injector.get("DevicesManager"); + GeneralManager = $injector.get("GeneralManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + var node; + beforeEach(function() { + node = { + interfaces: [] + }; + $parentScope.node = node; + $parentScope.isController = false; + $parentScope.controllerLoaded = jasmine.createSpy("controllerLoaded"); + }); + + // Makes the NodeStorageController. + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); + } - expect(filterSelectedInterfaces( - interfaces, - selectedInterfaces, - newBondInterface - )).toEqual(interfaces); - }); + var loadItems = spyOn(GeneralManager, "loadItems"); + loadItems.and.returnValue($q.defer().promise); - it("removes interface if not on same fabric as new bond", function() { - var interfaces = [{ id: 36, name: "eth-AMPnu0", link_id: -1, fabric: { - name: "fabric-1" - }}]; - var selectedInterfaces = ["37/-1"]; - var newBondInterface = { fabric: { name: "fabric-2" }}; - - expect(filterSelectedInterfaces( - interfaces, - selectedInterfaces, - newBondInterface - )).toEqual([]); - }); -}); + $parentScope.nodesManager = MachinesManager; + // Create the controller. + var controller = $controller("NodeNetworkingController", { + $scope: $scope, + FabricsManager: FabricsManager, + VLANsManager: VLANsManager, + SubnetsManager: SubnetsManager, + MachinesManager: MachinesManager, + DevicesManager: DevicesManager, + GeneralManager: GeneralManager, + ManagerHelperService: ManagerHelperService + }); + return controller; + } + + it("sets initial values", function() { + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.nodeHasLoaded).toBe(false); + expect($scope.managersHaveLoaded).toBe(false); + expect($scope.tableInfo.column).toBe("name"); + expect($scope.fabrics).toBe(FabricsManager.getItems()); + expect($scope.vlans).toBe(VLANsManager.getItems()); + expect($scope.subnets).toBe(SubnetsManager.getItems()); + expect($scope.interfaces).toEqual([]); + expect($scope.interfaceLinksMap).toEqual({}); + expect($scope.originalInterfaces).toEqual({}); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.selectedMode).toBeNull(); + expect($scope.newInterface).toEqual({}); + expect($scope.newBondInterface).toEqual({}); + expect($scope.newBridgeInterface).toEqual({}); + expect($scope.editInterface).toBeNull(); + expect($scope.bondOptions).toBe(GeneralManager.getData("bond_options")); + }); + + it("sets loaded once node loaded then managers loaded", function() { + var defer = $q.defer(); + makeController(defer); + + // All should false. + expect($scope.loaded).toBe(false); + expect($scope.nodeHasLoaded).toBe(false); + expect($scope.managersHaveLoaded).toBe(false); + + // Only nodeHasLoaded should be true. + $scope.nodeLoaded(); + expect($scope.loaded).toBe(false); + expect($scope.nodeHasLoaded).toBe(true); + expect($scope.managersHaveLoaded).toBe(false); + + // All three should be true. + defer.resolve(); + $rootScope.$digest(); + expect($scope.loaded).toBe(true); + expect($scope.nodeHasLoaded).toBe(true); + expect($scope.managersHaveLoaded).toBe(true); + }); + + it("sets loaded once managers loaded then node loaded", function() { + var defer = $q.defer(); + makeController(defer); + + // All should false. + expect($scope.loaded).toBe(false); + expect($scope.nodeHasLoaded).toBe(false); + expect($scope.managersHaveLoaded).toBe(false); + + // Only managersHaveLoaded should be true. + defer.resolve(); + $rootScope.$digest(); + expect($scope.loaded).toBe(false); + expect($scope.nodeHasLoaded).toBe(false); + expect($scope.managersHaveLoaded).toBe(true); + + // All three should be true. + $scope.nodeLoaded(); + expect($scope.loaded).toBe(true); + expect($scope.nodeHasLoaded).toBe(true); + expect($scope.managersHaveLoaded).toBe(true); + }); + + it("loads bond_options if not yet loaded", function() { + var defer = $q.defer(); + makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + + expect(GeneralManager.loadItems).toHaveBeenCalledWith(["bond_options"]); + }); + + it("watches interfaces and subnets once nodeLoaded called", function() { + makeController(); + spyOn($scope, "$watch"); + spyOn($scope, "$watchCollection"); + $scope.nodeLoaded(); + + var watches = []; + var i, + calls = $scope.$watch.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watches.push(calls[i][0]); + } + var watchCollections = []; + calls = $scope.$watchCollection.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watchCollections.push(calls[i][0]); + } -describe("filterLinkModes", function() { + expect(watches).toEqual(["node.interfaces"]); + expect(watchCollections).toEqual([]); + }); + + it("watches interfaces and subnets once nodeLoaded called", function() { + makeController(); + spyOn($scope, "$watch"); + spyOn($scope, "$watchCollection"); + $parentScope.isController = true; + $scope.nodeLoaded(); + + var watches = []; + var i, + calls = $scope.$watch.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watches.push(calls[i][0]); + } + var watchCollections = []; + calls = $scope.$watchCollection.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watchCollections.push(calls[i][0]); + } - // Load the MAAS module. - beforeEach(module("MAAS")); + expect(watches).toEqual(["node.interfaces", "subnets"]); + expect(watchCollections).toEqual([]); + }); + + it("edit device subnet correctly when subnet is set", function() { + makeController(); + $parentScope.isDevice = true; + $scope.subnets = [{ id: 0, vlan: 0 }, { id: 1, vlan: 0 }]; + var nic = { + id: 1, + name: "eth0", + ip_assignment: "static", + tags: [], + params: { + bridge_fd: 15, + bridge_stp: false + }, + subnet: $scope.subnets[1] + }; + $scope.edit(nic); + expect($scope.editInterface.defaultSubnet.id).toEqual(1); + }); + + it("edit device subnet correctly when subnet is not set", function() { + makeController(); + $parentScope.isDevice = true; + $scope.subnets = [{ id: 0, vlan: 0 }, { id: 1, vlan: 0 }]; + var nic = { + id: 1, + name: "eth0", + ip_assignment: "static", + tags: [], + params: { + bridge_fd: 15, + bridge_stp: false + }, + subnet: null + }; + $scope.edit(nic); + expect($scope.editInterface.defaultSubnet.id).toEqual(0); + }); + + describe("updateInterfaces", function() { + // updateInterfaces is a private method in the controller but we test + // it by calling nodeLoaded which will setup the watcher which call + // updateInterfaces and set $scope.interfaces. + function updateInterfaces(controller) { + if (!angular.isObject(controller)) { + controller = makeController(); + } + $scope.nodeLoaded(); + $scope.$digest(); + } - // Load the filterLinkModes. - var filterLinkModes; - beforeEach(inject(function($filter) { - filterLinkModes = $filter("filterLinkModes"); - })); - - // Load the modes before each test. - var modes; - beforeEach(function() { - modes = [ - { - mode: "auto", - text: "Auto assign" - }, - { - mode: "static", - text: "Static assign" - }, - { - mode: "dhcp", - text: "DHCP" - }, - { - mode: "link_up", - text: "Unconfigured" - } - ]; + it("returns empty list when node.interfaces empty", function() { + node.interfaces = []; + updateInterfaces(); + expect($scope.interfaces).toEqual([]); + }); + + it("adds interfaces to originalInterfaces map", function() { + var nic1 = { + id: 1, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [] + }; + var nic2 = { + id: 2, + name: "eth1", + type: "physical", + parents: [], + children: [], + links: [] + }; + node.interfaces = [nic1, nic2]; + updateInterfaces(); + expect($scope.originalInterfaces).toEqual({ + 1: nic1, + 2: nic2 + }); + }); + + it("removes bond parents and places them as members", function() { + var parent1 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var parent2 = { + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var bond = { + id: 2, + name: "bond0", + type: "bond", + parents: [0, 1], + children: [], + links: [] + }; + node.interfaces = [parent1, parent2, bond]; + updateInterfaces(); + expect($scope.interfaces).toEqual([ + { + id: 2, + name: "bond0", + type: "bond", + parents: [0, 1], + children: [], + links: [], + members: [parent1, parent2], + vlan: null, + link_id: -1, + subnet: null, + mode: "link_up", + ip_address: "" + } + ]); }); - it("only link_up when no subnet", function() { - var nic = { - subnet : null - }; - expect(filterLinkModes(modes, nic)).toEqual([ - { - "mode": "link_up", - "text": "Unconfigured" - } - ]); + it("removes bridge parents and places them as members", function() { + var parent1 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var parent2 = { + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var bridge = { + id: 2, + name: "br0", + type: "bridge", + parents: [0, 1], + children: [], + links: [] + }; + node.interfaces = [parent1, parent2, bridge]; + updateInterfaces(); + expect($scope.interfaces).toEqual([ + { + id: 2, + name: "br0", + type: "bridge", + parents: [0, 1], + children: [], + links: [], + members: [parent1, parent2], + vlan: null, + link_id: -1, + subnet: null, + mode: "link_up", + ip_address: "" + } + ]); }); - it("honors getValue()", function() { - var nic = { - getValue: function() { return null; } - }; - expect(filterLinkModes(modes, nic)).toEqual([ - { - "mode": "link_up", - "text": "Unconfigured" - } - ]); + it("clears editInterface if parent is now in a bond", function() { + var parent1 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var parent2 = { + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var bond = { + id: 2, + name: "bond0", + type: "bond", + parents: [0, 1], + children: [], + links: [] + }; + node.interfaces = [parent1, parent2, bond]; + $scope.editInterface = { + id: 0, + link_id: -1 + }; + updateInterfaces(); + expect($scope.editInterface).toBeNull(); + }); + + it("clears editInterface if parent is now in a bridge", function() { + var parent1 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var parent2 = { + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [2], + links: [] + }; + var bridge = { + id: 2, + name: "br0", + type: "bridge", + parents: [0, 1], + children: [], + links: [] + }; + node.interfaces = [parent1, parent2, bridge]; + $scope.editInterface = { + id: 0, + link_id: -1 + }; + updateInterfaces(); + expect($scope.editInterface).toBeNull(); + }); + + it("sets vlan and fabric on interface", function() { + var fabric = { + id: 0 + }; + var vlan = { + id: 0, + fabric: 0 + }; + var nic = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + FabricsManager._items = [fabric]; + VLANsManager._items = [vlan]; + node.interfaces = [nic]; + updateInterfaces(); + expect($scope.interfaces[0].vlan).toBe(vlan); + expect($scope.interfaces[0].fabric).toBe(fabric); + }); + + it("sets default to link_up if not links", function() { + var nic = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [] + }; + node.interfaces = [nic]; + updateInterfaces(); + expect($scope.interfaces).toEqual([ + { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan: null, + link_id: -1, + subnet: null, + mode: "link_up", + ip_address: "" + } + ]); }); - it("all modes if only one link", function() { - var nic = { - subnet : {}, - links: [{}] - }; - expect(filterLinkModes(modes, nic)).toEqual([ - { - "mode": "auto", - "text": "Auto assign" - }, - { - "mode": "static", - "text": "Static assign" - }, - { - "mode": "dhcp", - "text": "DHCP" - }, - { - "mode": "link_up", - "text": "Unconfigured" - } - ]); + it("duplicates links as alias interfaces", function() { + var subnet0 = { id: 0 }, + subnet1 = { id: 1 }, + subnet2 = { id: 2 }; + SubnetsManager._items = [subnet0, subnet1, subnet2]; + var links = [ + { + id: 0, + subnet_id: 0, + mode: "dhcp", + ip_address: "" + }, + { + id: 1, + subnet_id: 1, + mode: "auto", + ip_address: "" + }, + { + id: 2, + subnet_id: 2, + mode: "static", + ip_address: "192.168.122.10" + } + ]; + var nic = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: links + }; + node.interfaces = [nic]; + updateInterfaces(); + expect($scope.interfaces).toEqual([ + { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: links, + vlan: null, + fabric: undefined, + link_id: 0, + subnet: subnet0, + mode: "dhcp", + ip_address: "" + }, + { + id: 0, + name: "eth0:1", + type: "alias", + parents: [], + children: [], + links: links, + vlan: null, + fabric: undefined, + link_id: 1, + subnet: subnet1, + mode: "auto", + ip_address: "" + }, + { + id: 0, + name: "eth0:2", + type: "alias", + parents: [], + children: [], + links: links, + vlan: null, + fabric: undefined, + link_id: 2, + subnet: subnet2, + mode: "static", + ip_address: "192.168.122.10" + } + ]); }); - it("auto, static, and dhcp modes if more than one link", function() { - var nic = { - subnet : {}, - links: [{}, {}] - }; - expect(filterLinkModes(modes, nic)).toEqual([ - { - "mode": "auto", - "text": "Auto assign" - }, - { - "mode": "static", - "text": "Static assign" - }, - { - "mode": "dhcp", - "text": "DHCP" - } - ]); + it("renders empty vlanTable for non-controllers", function() { + var fabric0 = { + id: 0 + }; + var vlan0 = { + id: 0, + fabric: 0 + }; + var subnet0 = { id: 0 }; + var nic0 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + SubnetsManager._items = [subnet0]; + FabricsManager._items = [fabric0]; + VLANsManager._items = [vlan0]; + node.interfaces = [nic0]; + + // Should be blank for non-controller. + updateInterfaces(); + expect($scope.vlanTable).toEqual([]); + }); + + it("renders vlanTable OK when no subnets", function() { + var fabric0 = { + id: 0, + name: "fabric0" + }; + var vlan0 = { + id: 0, + fabric: 0, + name: "vlan0" + }; + var nic0 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + SubnetsManager._items = []; + FabricsManager._items = [fabric0]; + VLANsManager._items = [vlan0]; + node.interfaces = [nic0]; + + $parentScope.isController = true; + updateInterfaces(); + expect($scope.vlanTable).toEqual([ + { + fabric: fabric0, + vlan: vlan0, + subnets: [], + primary_rack: null, + secondary_rack: null, + sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) + } + ]); }); - it("auto and static modes if interface is alias", function() { - var nic = { - type: "alias", - subnet : {} - }; - expect(filterLinkModes(modes, nic)).toEqual([ - { - "mode": "auto", - "text": "Auto assign" - }, - { - "mode": "static", - "text": "Static assign" - } - ]); + it("renders single entry vlanTable", function() { + var subnet0 = { id: 0, vlan: 0 }; + var fabric0 = { + id: 0 + }; + var vlan0 = { + id: 0, + fabric: 0 + }; + var nic0 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + SubnetsManager._items = [subnet0]; + FabricsManager._items = [fabric0]; + VLANsManager._items = [vlan0]; + node.interfaces = [nic0]; + + // Should not blank for a controller. + $parentScope.isController = true; + updateInterfaces(); + expect($scope.vlanTable).toEqual([ + { + fabric: fabric0, + vlan: vlan0, + subnets: [subnet0], + primary_rack: null, + secondary_rack: null, + sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) + } + ]); }); -}); - - -describe("NodeNetworkingController", function() { - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $parentScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $parentScope = $rootScope.$new(); - $scope = $parentScope.$new(); - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the NodeNetworkingController. - var FabricsManager, VLANsManager, SubnetsManager; - var MachinesManager, DevicesManager, GeneralManager, ManagerHelperService; - beforeEach(inject(function($injector) { - FabricsManager = $injector.get("FabricsManager"); - VLANsManager = $injector.get("VLANsManager"); - SubnetsManager = $injector.get("SubnetsManager"); - MachinesManager = $injector.get("MachinesManager"); - DevicesManager = $injector.get("DevicesManager"); - GeneralManager = $injector.get("GeneralManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - var node; - beforeEach(function() { - node = { - interfaces: [] - }; - $parentScope.node = node; - $parentScope.isController = false; - $parentScope.controllerLoaded = jasmine.createSpy("controllerLoaded"); - }); - - // Makes the NodeStorageController. - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - var loadItems = spyOn(GeneralManager, "loadItems"); - loadItems.and.returnValue($q.defer().promise); - - $parentScope.nodesManager = MachinesManager; - - // Create the controller. - var controller = $controller("NodeNetworkingController", { - $scope: $scope, - FabricsManager: FabricsManager, - VLANsManager: VLANsManager, - SubnetsManager: SubnetsManager, - MachinesManager: MachinesManager, - DevicesManager: DevicesManager, - GeneralManager: GeneralManager, - ManagerHelperService: ManagerHelperService - }); - return controller; - } - - it("sets initial values", function() { - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.nodeHasLoaded).toBe(false); - expect($scope.managersHaveLoaded).toBe(false); - expect($scope.tableInfo.column).toBe('name'); - expect($scope.fabrics).toBe(FabricsManager.getItems()); - expect($scope.vlans).toBe(VLANsManager.getItems()); - expect($scope.subnets).toBe(SubnetsManager.getItems()); - expect($scope.interfaces).toEqual([]); - expect($scope.interfaceLinksMap).toEqual({}); - expect($scope.originalInterfaces).toEqual({}); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.selectedMode).toBeNull(); - expect($scope.newInterface).toEqual({}); - expect($scope.newBondInterface).toEqual({}); - expect($scope.newBridgeInterface).toEqual({}); - expect($scope.editInterface).toBeNull(); - expect($scope.bondOptions).toBe( - GeneralManager.getData("bond_options")); - }); - - it("sets loaded once node loaded then managers loaded", function() { - var defer = $q.defer(); - makeController(defer); - - // All should false. - expect($scope.loaded).toBe(false); - expect($scope.nodeHasLoaded).toBe(false); - expect($scope.managersHaveLoaded).toBe(false); - - // Only nodeHasLoaded should be true. - $scope.nodeLoaded(); - expect($scope.loaded).toBe(false); - expect($scope.nodeHasLoaded).toBe(true); - expect($scope.managersHaveLoaded).toBe(false); - - // All three should be true. - defer.resolve(); - $rootScope.$digest(); - expect($scope.loaded).toBe(true); - expect($scope.nodeHasLoaded).toBe(true); - expect($scope.managersHaveLoaded).toBe(true); - }); - - it("sets loaded once managers loaded then node loaded", function() { - var defer = $q.defer(); - makeController(defer); - - // All should false. - expect($scope.loaded).toBe(false); - expect($scope.nodeHasLoaded).toBe(false); - expect($scope.managersHaveLoaded).toBe(false); - - // Only managersHaveLoaded should be true. - defer.resolve(); - $rootScope.$digest(); - expect($scope.loaded).toBe(false); - expect($scope.nodeHasLoaded).toBe(false); - expect($scope.managersHaveLoaded).toBe(true); - - // All three should be true. - $scope.nodeLoaded(); - expect($scope.loaded).toBe(true); - expect($scope.nodeHasLoaded).toBe(true); - expect($scope.managersHaveLoaded).toBe(true); - }); - - it("loads bond_options if not yet loaded", function() { - var defer = $q.defer(); - makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - expect(GeneralManager.loadItems).toHaveBeenCalledWith( - ["bond_options"]); + var makeInterestingNetwork = function() { + var net = {}; + net.space0 = { id: 0 }; + net.subnet0 = { id: 0, vlan: 0, cidr: "10.0.0.0/16" }; + net.subnet1 = { id: 1, vlan: 0, cidr: "10.10.0.0/16" }; + net.subnet2 = { id: 2, vlan: 1, cidr: "10.20.0.0/16" }; + net.fabric0 = { + id: 0 + }; + net.fabric1 = { + id: 1 + }; + net.vlan0 = { + id: 0, + fabric: 0 + }; + net.vlan1 = { + id: 1, + fabric: 1 + }; + net.nic0 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + net.nic1 = { + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 1 + }; + //SpacesManager._items = [net.space0]; + SubnetsManager._items = [net.subnet0, net.subnet1, net.subnet2]; + FabricsManager._items = [net.fabric0, net.fabric1]; + VLANsManager._items = [net.vlan0, net.vlan1]; + node.interfaces = [net.nic0, net.nic1]; + return net; + }; + + it("renders multi-entry vlanTable", function() { + var net = makeInterestingNetwork(); + // Should not blank for a controller. + $parentScope.isController = true; + updateInterfaces(); + expect($scope.vlanTable).toEqual([ + { + fabric: net.fabric0, + vlan: net.vlan0, + subnets: [net.subnet0, net.subnet1], + primary_rack: null, + secondary_rack: null, + sort_key: net.fabric0.name + "|" + $scope.getVLANText(net.vlan0) + }, + { + fabric: net.fabric1, + vlan: net.vlan1, + subnets: [net.subnet2], + primary_rack: null, + secondary_rack: null, + sort_key: net.fabric0.name + "|" + $scope.getVLANText(net.vlan1) + } + ]); }); - it("watches interfaces and subnets once nodeLoaded called", function() { - makeController(); - spyOn($scope, "$watch"); - spyOn($scope, "$watchCollection"); - $scope.nodeLoaded(); - - var watches = []; - var i, calls = $scope.$watch.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watches.push(calls[i][0]); + it("no duplicate vlans", function() { + // Regression for https://bugs.launchpad.net/maas/+bug/1559332. + // Same vlan on two nics shouldn't result in two vlans in table. + var subnet0 = { id: 0, vlan: 0 }; + var fabric0 = { + id: 0 + }; + var vlan0 = { + id: 0, + fabric: 0 + }; + var nic0 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + var nic1 = { + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + SubnetsManager._items = [subnet0]; + FabricsManager._items = [fabric0]; + VLANsManager._items = [vlan0]; + node.interfaces = [nic0, nic1]; + + // Should not blank for a controller. + $parentScope.isController = true; + updateInterfaces(); + expect($scope.vlanTable).toEqual([ + { + vlan: vlan0, + fabric: fabric0, + subnets: [subnet0], + primary_rack: null, + secondary_rack: null, + sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) } - var watchCollections = []; - calls = $scope.$watchCollection.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watchCollections.push(calls[i][0]); + ]); + }); + + // Regression for https://bugs.launchpad.net/maas/+bug/1576267. + it("updates vlanTable when add subnet", function() { + var net = makeInterestingNetwork(); + + // Should not blank for a controller. + $parentScope.isController = true; + updateInterfaces(); + expect($scope.vlanTable).toEqual([ + { + fabric: net.fabric0, + vlan: net.vlan0, + subnets: [net.subnet0, net.subnet1], + primary_rack: null, + secondary_rack: null, + sort_key: net.fabric0.name + "|" + $scope.getVLANText(net.vlan0) + }, + { + fabric: net.fabric1, + vlan: net.vlan1, + subnets: [net.subnet2], + primary_rack: null, + secondary_rack: null, + sort_key: net.fabric0.name + "|" + $scope.getVLANText(net.vlan1) } + ]); - expect(watches).toEqual(["node.interfaces"]); - expect(watchCollections).toEqual([]); + // Add subnet and make sure it shows up in vlanTable. + var subnet = { + id: 3, + name: "subnet3", + vlan: 0, + space: 0, + cidr: "10.30.0.0/16" + }; + SubnetsManager._items.push(subnet); + $scope.$digest(); + expect($scope.vlanTable).toEqual([ + { + fabric: net.fabric0, + vlan: net.vlan0, + subnets: [net.subnet0, net.subnet1, subnet], + primary_rack: null, + secondary_rack: null, + sort_key: net.fabric0.name + "|" + $scope.getVLANText(net.vlan0) + }, + { + fabric: net.fabric1, + vlan: net.vlan1, + subnets: [net.subnet2], + primary_rack: null, + secondary_rack: null, + sort_key: net.fabric0.name + "|" + $scope.getVLANText(net.vlan1) + } + ]); }); - it("watches interfaces and subnets once nodeLoaded called", function() { - makeController(); - spyOn($scope, "$watch"); - spyOn($scope, "$watchCollection"); - $parentScope.isController = true; - $scope.nodeLoaded(); - - var watches = []; - var i, calls = $scope.$watch.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watches.push(calls[i][0]); + // Regression for https://bugs.launchpad.net/maas/+bug/1576267. + it("updates empty vlanTable when add subnet", function() { + var fabric0 = { + id: 0, + name: "fabric0" + }; + var vlan0 = { + id: 0, + fabric: 0, + name: "vlan0" + }; + var nic0 = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [], + vlan_id: 0 + }; + SubnetsManager._items = []; + FabricsManager._items = [fabric0]; + VLANsManager._items = [vlan0]; + node.interfaces = [nic0]; + + $parentScope.isController = true; + updateInterfaces(); + expect($scope.vlanTable).toEqual([ + { + fabric: fabric0, + vlan: vlan0, + subnets: [], + primary_rack: null, + secondary_rack: null, + sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) } - var watchCollections = []; - calls = $scope.$watchCollection.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watchCollections.push(calls[i][0]); + ]); + // Add subnet and make sure it shows up in vlanTable. + var subnet = { + id: 3, + name: "subnet3", + vlan: 0, + space: 0, + cidr: "10.30.0.0/16" + }; + SubnetsManager._items.push(subnet); + $scope.$digest(); + expect($scope.vlanTable).toEqual([ + { + fabric: fabric0, + vlan: vlan0, + subnets: [subnet], + primary_rack: null, + secondary_rack: null, + sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) } - - expect(watches).toEqual(["node.interfaces", "subnets"]); - expect(watchCollections).toEqual([]); + ]); }); - it("edit device subnet correctly when subnet is set", function() { - makeController(); - $parentScope.isDevice = true; - $scope.subnets = [{ id: 0, vlan: 0 }, { id: 1, vlan: 0}]; - var nic = { + it("creates interfaceLinksMap", function() { + var links = [ + { + id: 0, + subnet_id: 0, + mode: "dhcp", + ip_address: "" + }, + { + id: 1, + subnet_id: 1, + mode: "auto", + ip_address: "" + }, + { + id: 2, + subnet_id: 2, + mode: "static", + ip_address: "192.168.122.10" + } + ]; + var nic = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: links + }; + node.interfaces = [nic]; + updateInterfaces(); + expect($scope.interfaceLinksMap[0][0].link_id).toBe(0); + expect($scope.interfaceLinksMap[0][1].link_id).toBe(1); + expect($scope.interfaceLinksMap[0][2].link_id).toBe(2); + }); + + it("clears editInterface if interface no longer exists", function() { + node.interfaces = []; + $scope.editInterface = { + id: 0, + link_id: -1 + }; + updateInterfaces(); + expect($scope.editInterface).toBeNull(); + }); + + it("clears editInterface if link no longer exists", function() { + var nic = { + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: [] + }; + node.interfaces = [nic]; + $scope.editInterface = { + id: 0, + link_id: 0 + }; + updateInterfaces(); + expect($scope.editInterface).toBeNull(); + }); + + describe("newInterface", function() { + // Setup the initial data for newInterface to be set. + function setupNewInterface(controller, newInterface) { + var links = [ + { + id: 0, + subnet_id: 0, + mode: "dhcp", + ip_address: "" + }, + { id: 1, - name: "eth0", - ip_assignment: 'static', - tags: [], - params: { - bridge_fd: 15, - bridge_stp: false - }, - subnet: $scope.subnets[1] - }; - $scope.edit(nic); - expect($scope.editInterface.defaultSubnet.id).toEqual(1); - }); - - it("edit device subnet correctly when subnet is not set", function() { - makeController(); - $parentScope.isDevice = true; - $scope.subnets = [{ id: 0, vlan: 0 }, { id: 1, vlan: 0}]; + subnet_id: 1, + mode: "auto", + ip_address: "" + }, + { + id: 2, + subnet_id: 2, + mode: "static", + ip_address: "192.168.122.10" + } + ]; var nic = { - id: 1, - name: "eth0", - ip_assignment: 'static', - tags: [], - params: { - bridge_fd: 15, - bridge_stp: false - }, - subnet: null - }; - $scope.edit(nic); - expect($scope.editInterface.defaultSubnet.id).toEqual(0); - }); - - describe("updateInterfaces", function() { - - // updateInterfaces is a private method in the controller but we test - // it by calling nodeLoaded which will setup the watcher which call - // updateInterfaces and set $scope.interfaces. - function updateInterfaces(controller) { - if(!angular.isObject(controller)) { - controller = makeController(); - } - $scope.nodeLoaded(); - $scope.$digest(); - } - - it("returns empty list when node.interfaces empty", function() { - node.interfaces = []; - updateInterfaces(); - expect($scope.interfaces).toEqual([]); - }); - - it("adds interfaces to originalInterfaces map", function() { - var nic1 = { - id: 1, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [] - }; - var nic2 = { - id: 2, - name: "eth1", - type: "physical", - parents: [], - children: [], - links: [] - }; - node.interfaces = [nic1, nic2]; - updateInterfaces(); - expect($scope.originalInterfaces).toEqual({ - 1: nic1, - 2: nic2 - }); - }); - - it("removes bond parents and places them as members", function() { - var parent1 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var parent2 = { - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var bond = { - id: 2, - name: "bond0", - type: "bond", - parents: [0, 1], - children: [], - links: [] - }; - node.interfaces = [parent1, parent2, bond]; - updateInterfaces(); - expect($scope.interfaces).toEqual([{ - id: 2, - name: "bond0", - type: "bond", - parents: [0, 1], - children: [], - links: [], - members: [parent1, parent2], - vlan: null, - link_id: -1, - subnet: null, - mode: "link_up", - ip_address: "" - }]); - }); - - it("removes bridge parents and places them as members", function() { - var parent1 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var parent2 = { - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var bridge = { - id: 2, - name: "br0", - type: "bridge", - parents: [0, 1], - children: [], - links: [] - }; - node.interfaces = [parent1, parent2, bridge]; - updateInterfaces(); - expect($scope.interfaces).toEqual([{ - id: 2, - name: "br0", - type: "bridge", - parents: [0, 1], - children: [], - links: [], - members: [parent1, parent2], - vlan: null, - link_id: -1, - subnet: null, - mode: "link_up", - ip_address: "" - }]); - }); - - it("clears editInterface if parent is now in a bond", function() { - var parent1 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var parent2 = { - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var bond = { - id: 2, - name: "bond0", - type: "bond", - parents: [0, 1], - children: [], - links: [] - }; - node.interfaces = [parent1, parent2, bond]; - $scope.editInterface = { - id: 0, - link_id: -1 - }; - updateInterfaces(); - expect($scope.editInterface).toBeNull(); - }); - - it("clears editInterface if parent is now in a bridge", function() { - var parent1 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var parent2 = { - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [2], - links: [] - }; - var bridge = { - id: 2, - name: "br0", - type: "bridge", - parents: [0, 1], - children: [], - links: [] - }; - node.interfaces = [parent1, parent2, bridge]; - $scope.editInterface = { - id: 0, - link_id: -1 - }; - updateInterfaces(); - expect($scope.editInterface).toBeNull(); - }); - - it("sets vlan and fabric on interface", function() { - var fabric = { - id: 0 - }; - var vlan = { - id: 0, - fabric: 0 - }; - var nic = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - FabricsManager._items = [fabric]; - VLANsManager._items = [vlan]; - node.interfaces = [nic]; - updateInterfaces(); - expect($scope.interfaces[0].vlan).toBe(vlan); - expect($scope.interfaces[0].fabric).toBe(fabric); - }); - - it("sets default to link_up if not links", function() { - var nic = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [] - }; - node.interfaces = [nic]; - updateInterfaces(); - expect($scope.interfaces).toEqual([{ - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan: null, - link_id: -1, - subnet: null, - mode: "link_up", - ip_address: "" - }]); - }); - - it("duplicates links as alias interfaces", function() { - var subnet0 = { id: 0 }, subnet1 = { id: 1 }, subnet2 = { id: 2 }; - SubnetsManager._items = [subnet0, subnet1, subnet2]; - var links = [ - { - id: 0, - subnet_id: 0, - mode: "dhcp", - ip_address: "" - }, - { - id: 1, - subnet_id: 1, - mode: "auto", - ip_address: "" - }, - { - id: 2, - subnet_id: 2, - mode: "static", - ip_address: "192.168.122.10" - } - ]; - var nic = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: links - }; - node.interfaces = [nic]; - updateInterfaces(); - expect($scope.interfaces).toEqual([ - { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: links, - vlan: null, - fabric: undefined, - link_id: 0, - subnet: subnet0, - mode: "dhcp", - ip_address: "" - }, - { - id: 0, - name: "eth0:1", - type: "alias", - parents: [], - children: [], - links: links, - vlan: null, - fabric: undefined, - link_id: 1, - subnet: subnet1, - mode: "auto", - ip_address: "" - }, - { - id: 0, - name: "eth0:2", - type: "alias", - parents: [], - children: [], - links: links, - vlan: null, - fabric: undefined, - link_id: 2, - subnet: subnet2, - mode: "static", - ip_address: "192.168.122.10" - } - ]); - }); - - it("renders empty vlanTable for non-controllers", function() { - var fabric0 = { - id: 0 - }; - var vlan0 = { - id: 0, - fabric: 0 - }; - var subnet0 = { id: 0 }; - var nic0 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - SubnetsManager._items = [subnet0]; - FabricsManager._items = [fabric0]; - VLANsManager._items = [vlan0]; - node.interfaces = [nic0]; - - // Should be blank for non-controller. - updateInterfaces(); - expect($scope.vlanTable).toEqual([]); - }); - - it("renders vlanTable OK when no subnets", function() { - var fabric0 = { - id: 0, - name: 'fabric0' - }; - var vlan0 = { - id: 0, - fabric: 0, - name: 'vlan0' - }; - var nic0 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - SubnetsManager._items = []; - FabricsManager._items = [fabric0]; - VLANsManager._items = [vlan0]; - node.interfaces = [nic0]; - - $parentScope.isController = true; - updateInterfaces(); - expect($scope.vlanTable).toEqual([ - { - fabric: fabric0, - vlan: vlan0, - subnets: [], - primary_rack: null, - secondary_rack: null, - sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) - } - ]); - }); - - it("renders single entry vlanTable", function() { - var subnet0 = { id: 0, vlan: 0 }; - var fabric0 = { - id: 0 - }; - var vlan0 = { - id: 0, - fabric: 0 - }; - var nic0 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - SubnetsManager._items = [subnet0]; - FabricsManager._items = [fabric0]; - VLANsManager._items = [vlan0]; - node.interfaces = [nic0]; - - // Should not blank for a controller. - $parentScope.isController = true; - updateInterfaces(); - expect($scope.vlanTable).toEqual([ - { - fabric: fabric0, - vlan: vlan0, - subnets: [subnet0], - primary_rack: null, - secondary_rack: null, - sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) - } - ]); - }); - - var makeInterestingNetwork = function() { - var net = {}; - net.space0 = { id: 0 }; - net.subnet0 = { id: 0, vlan:0, cidr: "10.0.0.0/16" }; - net.subnet1 = { id: 1, vlan:0, cidr: "10.10.0.0/16" }; - net.subnet2 = { id: 2, vlan:1, cidr: "10.20.0.0/16" }; - net.fabric0 = { - id: 0 - }; - net.fabric1 = { - id: 1 - }; - net.vlan0 = { - id: 0, - fabric: 0 - }; - net.vlan1 = { - id: 1, - fabric: 1 - }; - net.nic0 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - net.nic1 = { - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 1 - }; - //SpacesManager._items = [net.space0]; - SubnetsManager._items = [net.subnet0, net.subnet1, net.subnet2]; - FabricsManager._items = [net.fabric0, net.fabric1]; - VLANsManager._items = [net.vlan0, net.vlan1]; - node.interfaces = [net.nic0, net.nic1]; - return net; + id: 0, + name: "eth0", + type: "physical", + parents: [], + children: [], + links: links }; + node.interfaces = [nic]; + updateInterfaces(controller); - it("renders multi-entry vlanTable", function() { - var net = makeInterestingNetwork(); - // Should not blank for a controller. - $parentScope.isController = true; - updateInterfaces(); - expect($scope.vlanTable).toEqual([ - { - fabric: net.fabric0, - vlan: net.vlan0, - subnets: [net.subnet0, net.subnet1], - primary_rack: null, - secondary_rack: null, - sort_key: net.fabric0.name + "|" + - $scope.getVLANText(net.vlan0) - }, - { - fabric: net.fabric1, - vlan: net.vlan1, - subnets: [net.subnet2], - primary_rack: null, - secondary_rack: null, - sort_key: net.fabric0.name + "|" + - $scope.getVLANText(net.vlan1) - } - ]); - }); - - it("no duplicate vlans", function() { - // Regression for https://bugs.launchpad.net/maas/+bug/1559332. - // Same vlan on two nics shouldn't result in two vlans in table. - var subnet0 = { id: 0, vlan:0 }; - var fabric0 = { - id: 0 - }; - var vlan0 = { - id: 0, - fabric: 0 - }; - var nic0 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - var nic1 = { - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - SubnetsManager._items = [subnet0]; - FabricsManager._items = [fabric0]; - VLANsManager._items = [vlan0]; - node.interfaces = [nic0, nic1]; - - // Should not blank for a controller. - $parentScope.isController = true; - updateInterfaces(); - expect($scope.vlanTable).toEqual([ - { - vlan: vlan0, - fabric: fabric0, - subnets: [subnet0], - primary_rack: null, - secondary_rack: null, - sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) - } - ]); - }); + var parent = $scope.interfaceLinksMap[0][0]; + newInterface.parent = parent; + $scope.newInterface = newInterface; + } + + // Cause the updateInterfaces to be called again to perform + // the logic on newInterface. + function reloadNewInterface(controller) { + // Add another nic to interfaces so that updateInterfaces + // really performs an action. + node.interfaces.push({ + id: 1, + name: "eth1", + type: "physical", + parents: [], + children: [], + links: [] + }); + updateInterfaces(controller); + } + + it("updates newInterface.parent object", function() { + var controller = makeController(); + var newInterface = { + type: "alias" + }; + setupNewInterface(controller, newInterface); + var parent = newInterface.parent; + reloadNewInterface(controller); + + // Should be the same value but a different object. + expect(newInterface.parent).toEqual(parent); + expect(newInterface.parent).not.toBe(parent); + }); + + it("changes newInterface.type from alias to VLAN", function() { + var controller = makeController(); + var newInterface = { + type: "alias" + }; + setupNewInterface(controller, newInterface); - // Regression for https://bugs.launchpad.net/maas/+bug/1576267. - it("updates vlanTable when add subnet", function() { - var net = makeInterestingNetwork(); - - // Should not blank for a controller. - $parentScope.isController = true; - updateInterfaces(); - expect($scope.vlanTable).toEqual([ - { - fabric: net.fabric0, - vlan: net.vlan0, - subnets: [net.subnet0, net.subnet1], - primary_rack: null, - secondary_rack: null, - sort_key: net.fabric0.name + "|" + - $scope.getVLANText(net.vlan0) - }, - { - fabric: net.fabric1, - vlan: net.vlan1, - subnets: [net.subnet2], - primary_rack: null, - secondary_rack: null, - sort_key: net.fabric0.name + "|" + - $scope.getVLANText(net.vlan1) - } - ]); - - // Add subnet and make sure it shows up in vlanTable. - var subnet = { - id: 3, name:"subnet3", vlan: 0, space: 0, cidr: "10.30.0.0/16" - }; - SubnetsManager._items.push(subnet); - $scope.$digest(); - expect($scope.vlanTable).toEqual([ - { - fabric: net.fabric0, - vlan: net.vlan0, - subnets: [net.subnet0, net.subnet1, subnet], - primary_rack: null, - secondary_rack: null, - sort_key: net.fabric0.name + "|" + - $scope.getVLANText(net.vlan0) - }, - { - fabric: net.fabric1, - vlan: net.vlan1, - subnets: [net.subnet2], - primary_rack: null, - secondary_rack: null, - sort_key: net.fabric0.name + "|" + - $scope.getVLANText(net.vlan1) - } - ]); - }); + spyOn($scope, "canAddAlias").and.returnValue(false); + spyOn($scope, "canAddVLAN").and.returnValue(true); + spyOn($scope, "addTypeChanged"); + reloadNewInterface(controller); + expect(newInterface.type).toBe("vlan"); + expect($scope.addTypeChanged).toHaveBeenCalled(); + }); + + it("changes newInterface.type from VLAN to alias", function() { + var controller = makeController(); + var newInterface = { + type: "vlan" + }; + setupNewInterface(controller, newInterface); - // Regression for https://bugs.launchpad.net/maas/+bug/1576267. - it("updates empty vlanTable when add subnet", function() { - var fabric0 = { - id: 0, - name: 'fabric0' - }; - var vlan0 = { - id: 0, - fabric: 0, - name: 'vlan0' - }; - var nic0 = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [], - vlan_id: 0 - }; - SubnetsManager._items = []; - FabricsManager._items = [fabric0]; - VLANsManager._items = [vlan0]; - node.interfaces = [nic0]; - - $parentScope.isController = true; - updateInterfaces(); - expect($scope.vlanTable).toEqual([ - { - fabric: fabric0, - vlan: vlan0, - subnets: [], - primary_rack: null, - secondary_rack: null, - sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) - } - ]); - // Add subnet and make sure it shows up in vlanTable. - var subnet = { - id: 3, name:"subnet3", vlan: 0, space: 0, cidr: "10.30.0.0/16" - }; - SubnetsManager._items.push(subnet); - $scope.$digest(); - expect($scope.vlanTable).toEqual([ - { - fabric: fabric0, - vlan: vlan0, - subnets: [subnet], - primary_rack: null, - secondary_rack: null, - sort_key: fabric0.name + "|" + $scope.getVLANText(vlan0) - } - ]); - }); + spyOn($scope, "canAddAlias").and.returnValue(true); + spyOn($scope, "canAddVLAN").and.returnValue(false); + spyOn($scope, "addTypeChanged"); + reloadNewInterface(controller); + expect(newInterface.type).toBe("alias"); + expect($scope.addTypeChanged).toHaveBeenCalled(); + }); + + it("clears newInterface if cannot add VLAN or alias", function() { + var controller = makeController(); + var newInterface = { + type: "vlan" + }; + setupNewInterface(controller, newInterface); - it("creates interfaceLinksMap", function() { - var links = [ - { - id: 0, - subnet_id: 0, - mode: "dhcp", - ip_address: "" - }, - { - id: 1, - subnet_id: 1, - mode: "auto", - ip_address: "" - }, - { - id: 2, - subnet_id: 2, - mode: "static", - ip_address: "192.168.122.10" - } - ]; - var nic = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: links - }; - node.interfaces = [nic]; - updateInterfaces(); - expect($scope.interfaceLinksMap[0][0].link_id).toBe(0); - expect($scope.interfaceLinksMap[0][1].link_id).toBe(1); - expect($scope.interfaceLinksMap[0][2].link_id).toBe(2); - }); + spyOn($scope, "canAddAlias").and.returnValue(false); + spyOn($scope, "canAddVLAN").and.returnValue(false); + reloadNewInterface(controller); + expect($scope.newInterface).toEqual({}); + }); - it("clears editInterface if interface no longer exists", function() { - node.interfaces = []; - $scope.editInterface = { - id: 0, - link_id: -1 - }; - updateInterfaces(); - expect($scope.editInterface).toBeNull(); - }); + it("clears newInterface if parent removed", function() { + var controller = makeController(); + var newInterface = { + type: "vlan" + }; + setupNewInterface(controller, newInterface); - it("clears editInterface if link no longer exists", function() { - var nic = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: [] - }; - node.interfaces = [nic]; - $scope.editInterface = { - id: 0, - link_id: 0 - }; - updateInterfaces(); - expect($scope.editInterface).toBeNull(); - }); + spyOn($scope, "canAddAlias").and.returnValue(false); + spyOn($scope, "canAddVLAN").and.returnValue(false); + $scope.selectedMode = "add"; + reloadNewInterface(controller); + expect($scope.selectedMode).toBeNull(); + }); - describe("newInterface", function() { + it("leaves single selection mode if newInterface is cleared", function() { + var controller = makeController(); + var newInterface = { + type: "vlan" + }; + setupNewInterface(controller, newInterface); - // Setup the initial data for newInterface to be set. - function setupNewInterface(controller, newInterface) { - var links = [ - { - id: 0, - subnet_id: 0, - mode: "dhcp", - ip_address: "" - }, - { - id: 1, - subnet_id: 1, - mode: "auto", - ip_address: "" - }, - { - id: 2, - subnet_id: 2, - mode: "static", - ip_address: "192.168.122.10" - } - ]; - var nic = { - id: 0, - name: "eth0", - type: "physical", - parents: [], - children: [], - links: links - }; - node.interfaces = [nic]; - updateInterfaces(controller); - - var parent = $scope.interfaceLinksMap[0][0]; - newInterface.parent = parent; - $scope.newInterface = newInterface; - } - - // Cause the updateInterfaces to be called again to perform - // the logic on newInterface. - function reloadNewInterface(controller) { - // Add another nic to interfaces so that updateInterfaces - // really performs an action. - node.interfaces.push({ - id: 1, - name: "eth1", - type: "physical", - parents: [], - children: [], - links: [] - }); - updateInterfaces(controller); - } - - it("updates newInterface.parent object", function() { - var controller = makeController(); - var newInterface = { - type: "alias" - }; - setupNewInterface(controller, newInterface); - var parent = newInterface.parent; - reloadNewInterface(controller); - - // Should be the same value but a different object. - expect(newInterface.parent).toEqual(parent); - expect(newInterface.parent).not.toBe(parent); - }); - - it("changes newInterface.type from alias to VLAN", function() { - var controller = makeController(); - var newInterface = { - type: "alias" - }; - setupNewInterface(controller, newInterface); - - spyOn($scope, "canAddAlias").and.returnValue(false); - spyOn($scope, "canAddVLAN").and.returnValue(true); - spyOn($scope, "addTypeChanged"); - reloadNewInterface(controller); - expect(newInterface.type).toBe("vlan"); - expect($scope.addTypeChanged).toHaveBeenCalled(); - }); - - it("changes newInterface.type from VLAN to alias", function() { - var controller = makeController(); - var newInterface = { - type: "vlan" - }; - setupNewInterface(controller, newInterface); - - spyOn($scope, "canAddAlias").and.returnValue(true); - spyOn($scope, "canAddVLAN").and.returnValue(false); - spyOn($scope, "addTypeChanged"); - reloadNewInterface(controller); - expect(newInterface.type).toBe("alias"); - expect($scope.addTypeChanged).toHaveBeenCalled(); - }); - - it("clears newInterface if cannot add VLAN or alias", function() { - var controller = makeController(); - var newInterface = { - type: "vlan" - }; - setupNewInterface(controller, newInterface); - - spyOn($scope, "canAddAlias").and.returnValue(false); - spyOn($scope, "canAddVLAN").and.returnValue(false); - reloadNewInterface(controller); - expect($scope.newInterface).toEqual({}); - }); - - it("clears newInterface if parent removed", - function() { - var controller = makeController(); - var newInterface = { - type: "vlan" - }; - setupNewInterface(controller, newInterface); - - spyOn($scope, "canAddAlias").and.returnValue(false); - spyOn($scope, "canAddVLAN").and.returnValue(false); - $scope.selectedMode = "add"; - reloadNewInterface(controller); - expect($scope.selectedMode).toBeNull(); - }); - - it("leaves single selection mode if newInterface is cleared", - function() { - var controller = makeController(); - var newInterface = { - type: "vlan" - }; - setupNewInterface(controller, newInterface); - - spyOn($scope, "canAddAlias").and.returnValue(false); - spyOn($scope, "canAddVLAN").and.returnValue(false); - $scope.selectedMode = "add"; - node.interfaces = []; - updateInterfaces(controller); - expect($scope.newInterface).toEqual({}); - expect($scope.selectedMode).toBeNull(); - }); - }); + spyOn($scope, "canAddAlias").and.returnValue(false); + spyOn($scope, "canAddVLAN").and.returnValue(false); + $scope.selectedMode = "add"; + node.interfaces = []; + updateInterfaces(controller); + expect($scope.newInterface).toEqual({}); + expect($scope.selectedMode).toBeNull(); + }); }); + }); - describe("isBootInterface", function() { - - it("returns true if is_boot is true", function() { - makeController(); - var nic = { - type: "physical", - is_boot: true - }; - expect($scope.isBootInterface(nic)).toBe(true); - }); - - it("returns true if is_boot is true and alias", function() { - makeController(); - var nic = { - type: "alias", - is_boot: true - }; - expect($scope.isBootInterface(nic)).toBe(false); - }); - - it("returns false if is_boot is false", function() { - makeController(); - var nic = { - type: "physical", - is_boot: false - }; - expect($scope.isBootInterface(nic)).toBe(false); - }); - - it("returns false if bond has no members with is_boot", function() { - makeController(); - var nic = { - type: "bond", - is_boot: false, - members: [ - { - is_boot: false - }, - { - is_boot: false - } - ] - }; - expect($scope.isBootInterface(nic)).toBe(false); - }); - - it("returns true if bond has member with is_boot", function() { - makeController(); - var nic = { - type: "bond", - is_boot: false, - members: [ - { - is_boot: false - }, - { - is_boot: true - } - ] - }; - expect($scope.isBootInterface(nic)).toBe(true); - }); + describe("isBootInterface", function() { + it("returns true if is_boot is true", function() { + makeController(); + var nic = { + type: "physical", + is_boot: true + }; + expect($scope.isBootInterface(nic)).toBe(true); + }); + + it("returns true if is_boot is true and alias", function() { + makeController(); + var nic = { + type: "alias", + is_boot: true + }; + expect($scope.isBootInterface(nic)).toBe(false); + }); + + it("returns false if is_boot is false", function() { + makeController(); + var nic = { + type: "physical", + is_boot: false + }; + expect($scope.isBootInterface(nic)).toBe(false); + }); + + it("returns false if bond has no members with is_boot", function() { + makeController(); + var nic = { + type: "bond", + is_boot: false, + members: [ + { + is_boot: false + }, + { + is_boot: false + } + ] + }; + expect($scope.isBootInterface(nic)).toBe(false); }); - describe("getInterfaceTypeText", function() { - var INTERFACE_TYPE_TEXTS = { - "physical": "Physical", - "bond": "Bond", - "vlan": "VLAN", - "alias": "Alias", - "missing_type": "missing_type" - }; + it("returns true if bond has member with is_boot", function() { + makeController(); + var nic = { + type: "bond", + is_boot: false, + members: [ + { + is_boot: false + }, + { + is_boot: true + } + ] + }; + expect($scope.isBootInterface(nic)).toBe(true); + }); + }); + + describe("getInterfaceTypeText", function() { + var INTERFACE_TYPE_TEXTS = { + physical: "Physical", + bond: "Bond", + vlan: "VLAN", + alias: "Alias", + missing_type: "missing_type" + }; - angular.forEach(INTERFACE_TYPE_TEXTS, function(value, type) { - it("returns correct value for '" + type + "'", function() { - makeController(); - var nic = { - type: type - }; - expect($scope.getInterfaceTypeText(nic)).toBe(value); - }); - }); + angular.forEach(INTERFACE_TYPE_TEXTS, function(value, type) { + it("returns correct value for '" + type + "'", function() { + makeController(); + var nic = { + type: type + }; + expect($scope.getInterfaceTypeText(nic)).toBe(value); + }); }); + }); + + describe("getLinkModeText", function() { + var LINK_MODE_TEXTS = { + auto: "Auto assign", + static: "Static assign", + dhcp: "DHCP", + link_up: "Unconfigured", + missing_type: "missing_type" + }; - describe("getLinkModeText", function() { - var LINK_MODE_TEXTS = { - "auto": "Auto assign", - "static": "Static assign", - "dhcp": "DHCP", - "link_up": "Unconfigured", - "missing_type": "missing_type" + angular.forEach(LINK_MODE_TEXTS, function(value, mode) { + it("returns correct value for '" + mode + "'", function() { + makeController(); + var nic = { + mode: mode }; + expect($scope.getLinkModeText(nic)).toBe(value); + }); + }); + }); - angular.forEach(LINK_MODE_TEXTS, function(value, mode) { - it("returns correct value for '" + mode + "'", function() { - makeController(); - var nic = { - mode: mode - }; - expect($scope.getLinkModeText(nic)).toBe(value); - }); - }); + describe("getVLANText", function() { + it("returns empty if vlan undefined", function() { + makeController(); + expect($scope.getVLANText()).toBe(""); + }); + + it("returns just vid", function() { + makeController(); + var vlan = { + vid: 5 + }; + expect($scope.getVLANText(vlan)).toBe(5); + }); + + it("returns vid + name", function() { + makeController(); + var name = makeName("vlan"); + var vlan = { + vid: 5, + name: name + }; + expect($scope.getVLANText(vlan)).toBe("5 (" + name + ")"); + }); + }); + + describe("getSubnetText", function() { + it("returns 'Unconfigured' for null", function() { + makeController(); + expect($scope.getSubnetText(null)).toBe("Unconfigured"); + }); + + it("returns just cidr if no name", function() { + makeController(); + var cidr = makeName("cidr"); + var subnet = { + cidr: cidr + }; + expect($scope.getSubnetText(subnet)).toBe(cidr); + }); + + it("returns just cidr if name same as cidr", function() { + makeController(); + var cidr = makeName("cidr"); + var subnet = { + cidr: cidr, + name: cidr + }; + expect($scope.getSubnetText(subnet)).toBe(cidr); + }); + + it("returns cidr + name", function() { + makeController(); + var cidr = makeName("cidr"); + var name = makeName("name"); + var subnet = { + cidr: cidr, + name: name + }; + expect($scope.getSubnetText(subnet)).toBe(cidr + " (" + name + ")"); + }); + }); + + describe("getSubnet", function() { + it("calls SubnetsManager.getItemFromList", function() { + makeController(); + var subnetId = makeInteger(0, 100); + var subnet = {}; + spyOn(SubnetsManager, "getItemFromList").and.returnValue(subnet); + + expect($scope.getSubnet(subnetId)).toBe(subnet); + expect(SubnetsManager.getItemFromList).toHaveBeenCalledWith(subnetId); + }); + }); + + describe("saveInterface", function() { + it("calls MachinesManager.updateInterface if name changed", function() { + makeController(); + var id = makeInteger(0, 100); + var name = makeName("nic"); + var vlan = { id: makeInteger(0, 100) }; + var original_nic = { + id: id, + name: name, + vlan_id: vlan.id + }; + var nic = { + id: id, + name: makeName("newName"), + vlan: vlan, + tags: [] + }; + $scope.originalInterfaces[id] = original_nic; + $scope.interfaces = [nic]; + + spyOn(MachinesManager, "updateInterface").and.returnValue( + $q.defer().promise + ); + $scope.saveInterface(nic); + expect(MachinesManager.updateInterface).toHaveBeenCalledWith(node, id, { + name: nic.name, + mac_address: undefined, + vlan: vlan.id, + mode: undefined, + fabric: null, + subnet: null, + tags: [] + }); + }); + + it("calls MachinesManager.updateInterface if vlan changed", function() { + makeController(); + var id = makeInteger(0, 100); + var name = makeName("nic"); + var vlan = { id: makeInteger(0, 100) }; + var original_nic = { + id: id, + name: name, + vlan_id: makeInteger(200, 300) + }; + var nic = { + id: id, + name: name, + vlan: vlan, + tags: [] + }; + $scope.originalInterfaces[id] = original_nic; + $scope.interfaces = [nic]; + + spyOn(MachinesManager, "updateInterface").and.returnValue( + $q.defer().promise + ); + $scope.saveInterface(nic); + expect(MachinesManager.updateInterface).toHaveBeenCalledWith(node, id, { + name: name, + mac_address: undefined, + vlan: vlan.id, + mode: undefined, + fabric: null, + subnet: null, + tags: [] + }); + }); + + it("calls MachinesManager.updateInterface if vlan set", function() { + makeController(); + var id = makeInteger(0, 100); + var name = makeName("nic"); + var vlan = { id: makeInteger(0, 100) }; + var original_nic = { + id: id, + name: name, + vlan_id: null + }; + var nic = { + id: id, + name: name, + vlan: vlan, + tags: [] + }; + $scope.originalInterfaces[id] = original_nic; + $scope.interfaces = [nic]; + + spyOn(MachinesManager, "updateInterface").and.returnValue( + $q.defer().promise + ); + $scope.saveInterface(nic); + expect(MachinesManager.updateInterface).toHaveBeenCalledWith(node, id, { + name: name, + mac_address: undefined, + vlan: vlan.id, + mode: undefined, + fabric: null, + subnet: null, + tags: [] + }); + }); + + it("calls MachinesManager.updateInterface if vlan unset", function() { + makeController(); + var id = makeInteger(0, 100); + var name = makeName("nic"); + var original_nic = { + id: id, + name: name, + vlan_id: makeInteger(200, 300) + }; + var nic = { + id: id, + name: name, + vlan: null, + tags: [] + }; + $scope.originalInterfaces[id] = original_nic; + $scope.interfaces = [nic]; + + spyOn(MachinesManager, "updateInterface").and.returnValue( + $q.defer().promise + ); + $scope.saveInterface(nic); + expect(MachinesManager.updateInterface).toHaveBeenCalledWith(node, id, { + name: name, + mac_address: undefined, + mode: undefined, + fabric: null, + subnet: null, + vlan: null, + tags: [] + }); + }); + }); + + describe("isInterfaceNameInvalid", function() { + it("returns true if name is empty", function() { + makeController(); + var nic = { + name: "" + }; + expect($scope.isInterfaceNameInvalid(nic)).toBe(true); + }); + + it("returns true if name is missing", function() { + makeController(); + var nic = {}; + expect($scope.isInterfaceNameInvalid(nic)).toBe(true); + }); + + it("returns true if nic is null", function() { + makeController(); + var nic = null; + expect($scope.isInterfaceNameInvalid(nic)).toBe(true); + }); + + it("returns true if name is same as another interface", function() { + makeController(); + var name = makeName("nic"); + var nic = { + id: 0, + name: name + }; + var otherNic = { + id: 1, + name: name + }; + $scope.node.interfaces = [nic, otherNic]; + expect($scope.isInterfaceNameInvalid(nic)).toBe(true); + }); + + it("returns false if name is same name as self", function() { + makeController(); + var name = makeName("nic"); + var nic = { + id: 0, + name: name + }; + $scope.node.interfaces = [nic]; + expect($scope.isInterfaceNameInvalid(nic)).toBe(false); + }); + + it("returns false if name is different", function() { + makeController(); + var name = makeName("nic"); + var newName = makeName("newNic"); + var nic = { + id: 0, + name: newName + }; + var otherNic = { + id: 1, + name: name + }; + $scope.node.interfaces = [otherNic]; + expect($scope.isInterfaceNameInvalid(nic)).toBe(false); + }); + }); + + describe("fabricChanged", function() { + it("sets vlan on interface", function() { + makeController(); + var fabric = { + id: 0, + default_vlan_id: 0, + vlan_ids: [0] + }; + var vlan = { + id: 0, + fabric: fabric.id + }; + FabricsManager._items = [fabric]; + VLANsManager._items = [vlan]; + var nic = { + vlan: null, + fabric: fabric + }; + spyOn($scope, "saveInterface"); + $scope.fabricChanged(nic); + expect(nic.vlan).toBe(vlan); + }); + + it("sets vlan to null", function() { + makeController(); + var nic = { + vlan: {}, + fabric: null + }; + spyOn($scope, "saveInterface"); + $scope.fabricChanged(nic); + expect(nic.vlan).toBeNull(); + }); + }); + + describe("isLinkModeDisabled", function() { + it("enabled when subnet", function() { + makeController(); + var nic = { + subnet: {} + }; + expect($scope.isLinkModeDisabled(nic)).toBe(false); + }); + + it("disabled when not subnet", function() { + makeController(); + var nic = { + subnet: null + }; + expect($scope.isLinkModeDisabled(nic)).toBe(true); + }); + + it("enabled when subnet with getValue", function() { + makeController(); + var nic = { + getValue: function() { + return {}; + } + }; + expect($scope.isLinkModeDisabled(nic)).toBe(false); }); - describe("getVLANText", function() { + it("disabled when not subnet with getValue", function() { + makeController(); + var nic = { + getValue: function() { + return null; + } + }; + expect($scope.isLinkModeDisabled(nic)).toBe(true); + }); + }); - it("returns empty if vlan undefined", function() { - makeController(); - expect($scope.getVLANText()).toBe(""); - }); + describe("saveInterfaceLink", function() { + it("calls MachinesManager.linkSubnet with params", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + mode: "static", + subnet: { id: makeInteger(0, 100) }, + link_id: makeInteger(0, 100), + ip_address: "192.168.122.1" + }; + spyOn(MachinesManager, "linkSubnet").and.returnValue($q.defer().promise); + $scope.saveInterfaceLink(nic); + expect(MachinesManager.linkSubnet).toHaveBeenCalledWith(node, nic.id, { + mode: "static", + subnet: nic.subnet.id, + link_id: nic.link_id, + ip_address: nic.ip_address + }); + }); + }); + + describe("subnetChanged", function() { + it("sets mode to link_up if set to no subnet", function() { + makeController(); + var nic = { + subnet: null + }; + spyOn($scope, "saveInterfaceLink"); + $scope.subnetChanged(nic); + expect(nic.mode).toBe("link_up"); + }); + + it("doesnt set mode to link_up if set if subnet", function() { + makeController(); + var nic = { + mode: "static", + subnet: { + statistics: { + first_address: "172.16.3.1" + } + } + }; + spyOn($scope, "saveInterfaceLink"); + $scope.subnetChanged(nic); + expect(nic.mode).toBe("static"); + }); + + it("clears ip_address", function() { + makeController(); + var nic = { + subnet: null, + ip_address: makeName("ip") + }; + spyOn($scope, "saveInterfaceLink"); + $scope.subnetChanged(nic); + expect(nic.ip_address).toBe(""); + }); + }); + + describe("subnetChangedForm", function() { + it("sets mode to link_up if set to no subnet", function() { + makeController(); + var nic = { + getValue: function(name) { + return this["_" + name]; + }, + updateValue: function(name, val) { + this["_" + name] = val; + }, + _subnet: null + }; + spyOn($scope, "saveInterfaceLink"); + $scope.subnetChangedForm("subnet", null, nic); + expect(nic._mode).toBe("link_up"); + }); + + it("doesnt set mode to link_up if set if subnet", function() { + makeController(); + var nic = { + getValue: function(name) { + return this["_" + name]; + }, + updateValue: function(name, val) { + this["_" + name] = val; + }, + _mode: "static", + _subnet: {} + }; + spyOn($scope, "saveInterfaceLink"); + $scope.subnetChangedForm("subnet", {}, nic); + expect(nic._mode).toBe("static"); + }); + + it("clears ip_address", function() { + makeController(); + var nic = { + getValue: function(name) { + return this["_" + name]; + }, + updateValue: function(name, val) { + this["_" + name] = val; + }, + _subnet: null, + _ip_address: makeName("ip") + }; + spyOn($scope, "saveInterfaceLink"); + $scope.subnetChangedForm("subnet", null, nic); + expect(nic._ip_address).toBe(""); + }); + }); + + describe("isIPAddressInvalid", function() { + it("true if empty IP address", function() { + makeController(); + var nic = { + ip_address: "", + mode: "static" + }; + expect($scope.isIPAddressInvalid(nic)).toBe(true); + }); + + it("true if not valid IP address", function() { + makeController(); + var nic = { + ip_address: "192.168.260.5", + mode: "static" + }; + expect($scope.isIPAddressInvalid(nic)).toBe(true); + }); + + it("true if IP address not in subnet", function() { + makeController(); + var nic = { + ip_address: "192.168.123.10", + mode: "static", + subnet: { + cidr: "192.168.122.0/24" + } + }; + expect($scope.isIPAddressInvalid(nic)).toBe(true); + }); - it("returns just vid", function() { - makeController(); - var vlan = { - vid: 5 - }; - expect($scope.getVLANText(vlan)).toBe(5); - }); + it("false if IP address in subnet", function() { + makeController(); + var nic = { + ip_address: "192.168.122.10", + mode: "static", + subnet: { + cidr: "192.168.122.0/24" + } + }; + expect($scope.isIPAddressInvalid(nic)).toBe(false); + }); + }); - it("returns vid + name", function() { - makeController(); - var name = makeName("vlan"); - var vlan = { - vid: 5, - name: name - }; - expect($scope.getVLANText(vlan)).toBe("5 (" + name + ")"); - }); + describe("getUniqueKey", function() { + it("returns id + / + link_id", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + expect($scope.getUniqueKey(nic)).toBe(nic.id + "/" + nic.link_id); + }); + }); + + describe("toggleInterfaceSelect", function() { + it("selects interface and enters single mode", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + var key = $scope.getUniqueKey(nic); + $scope.toggleInterfaceSelect(nic); + expect($scope.selectedInterfaces).toEqual([key]); + expect($scope.selectedMode).toBe("single"); + }); + + it("deselects interface and enters none mode", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + $scope.toggleInterfaceSelect(nic); + $scope.toggleInterfaceSelect(nic); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.selectedMode).toBeNull(); + }); + + it("selecting multiple enters multi mode", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + var nic2 = { + id: makeInteger(100, 200), + link_id: makeInteger(0, 100) + }; + var key1 = $scope.getUniqueKey(nic1); + var key2 = $scope.getUniqueKey(nic2); + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + expect($scope.selectedInterfaces).toEqual([key1, key2]); + expect($scope.selectedMode).toBe("multi"); + }); + }); + + describe("isInterfaceSelected", function() { + it("returns true when selected", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + var key = $scope.getUniqueKey(nic); + $scope.selectedInterfaces = [key]; + expect($scope.isInterfaceSelected(nic)).toBe(true); + }); + + it("returns false when not selected", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + $scope.selectedInterfaces = []; + expect($scope.isInterfaceSelected(nic)).toBe(false); + }); + }); + + describe("cannotEditInterface", function() { + it("returns true when only one selected", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + var key = $scope.getUniqueKey(nic); + $scope.selectedInterfaces = [key]; + expect($scope.cannotEditInterface(nic)).toBe(false); + }); + + it("returns false when multiple selected", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + var nic2 = { + id: makeInteger(100, 200), + link_id: makeInteger(0, 100) + }; + var key1 = $scope.getUniqueKey(nic1); + var key2 = $scope.getUniqueKey(nic2); + $scope.selectedInterfaces = [key1, key2]; + expect($scope.cannotEditInterface(nic1)).toBe(false); + }); + + it("returns false when not selected", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + $scope.selectedInterfaces = []; + expect($scope.cannotEditInterface(nic)).toBe(false); + }); + }); + + describe("isShowingDeleteConfirm", function() { + it("returns true in delete mode", function() { + makeController(); + $scope.selectedMode = "delete"; + expect($scope.isShowingDeleteConfirm()).toBe(true); + }); + + it("returns false not in delete mode", function() { + makeController(); + $scope.selectedMode = "single"; + expect($scope.isShowingDeleteConfirm()).toBe(false); + }); + }); + + describe("isShowingAdd", function() { + it("returns true in add mode", function() { + makeController(); + $scope.selectedMode = "add"; + expect($scope.isShowingAdd()).toBe(true); + }); + + it("returns false not in add mode", function() { + makeController(); + $scope.selectedMode = "delete"; + expect($scope.isShowingAdd()).toBe(false); + }); + }); + + describe("canAddAliasOrVLAN", function() { + it("returns false if isController", function() { + makeController(); + $parentScope.isController = true; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); + spyOn($scope, "canAddAlias").and.returnValue(true); + spyOn($scope, "canAddVLAN").and.returnValue(true); + expect($scope.canAddAliasOrVLAN({})).toBe(false); + }); + + it("returns false if no node editing", function() { + makeController(); + $parentScope.isController = false; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(true); + spyOn($scope, "canAddAlias").and.returnValue(true); + spyOn($scope, "canAddVLAN").and.returnValue(true); + expect($scope.canAddAliasOrVLAN({})).toBe(false); + }); + + it("returns true if can edit alias", function() { + makeController(); + $parentScope.isController = false; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); + spyOn($scope, "canAddAlias").and.returnValue(true); + spyOn($scope, "canAddVLAN").and.returnValue(false); + expect($scope.canAddAliasOrVLAN({})).toBe(true); + }); + + it("returns true if can edit VLAN", function() { + makeController(); + $parentScope.isController = false; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); + spyOn($scope, "canAddAlias").and.returnValue(false); + spyOn($scope, "canAddVLAN").and.returnValue(true); + expect($scope.canAddAliasOrVLAN({})).toBe(true); + }); + }); + + describe("canAddAlias", function() { + it("returns false if nic undefined", function() { + makeController(); + expect($scope.canAddAlias()).toBe(false); + }); + + it("returns false if nic type is alias", function() { + makeController(); + var nic = { + type: "alias" + }; + expect($scope.canAddAlias(nic)).toBe(false); + }); + + it("returns false if nic has no links", function() { + makeController(); + var nic = { + type: "physical", + links: [] + }; + expect($scope.canAddAlias(nic)).toBe(false); + }); + + it("returns false if nic has link_up", function() { + makeController(); + var nic = { + type: "physical", + links: [ + { + mode: "link_up" + } + ] + }; + expect($scope.canAddAlias(nic)).toBe(false); }); - describe("getSubnetText", function() { + it("returns true if nic has dhcp", function() { + makeController(); + var nic = { + type: "physical", + links: [ + { + mode: "dhcp" + } + ] + }; + expect($scope.canAddAlias(nic)).toBe(true); + }); - it("returns 'Unconfigured' for null", function() { - makeController(); - expect($scope.getSubnetText(null)).toBe("Unconfigured"); - }); + it("returns true if nic has static", function() { + makeController(); + var nic = { + type: "physical", + links: [ + { + mode: "static" + } + ] + }; + expect($scope.canAddAlias(nic)).toBe(true); + }); - it("returns just cidr if no name", function() { - makeController(); - var cidr = makeName("cidr"); - var subnet = { - cidr: cidr - }; - expect($scope.getSubnetText(subnet)).toBe(cidr); - }); + it("returns true if nic has auto", function() { + makeController(); + var nic = { + type: "physical", + links: [ + { + mode: "auto" + } + ] + }; + expect($scope.canAddAlias(nic)).toBe(true); + }); + }); + + describe("canAddVLAN", function() { + it("returns false if nic undefined", function() { + makeController(); + expect($scope.canAddVLAN()).toBe(false); + }); + + it("returns false if nic type is alias", function() { + makeController(); + var nic = { + type: "alias" + }; + expect($scope.canAddVLAN(nic)).toBe(false); + }); + + it("returns false if nic type is vlan", function() { + makeController(); + var nic = { + type: "vlan" + }; + expect($scope.canAddVLAN(nic)).toBe(false); + }); + + it("returns false if no unused vlans", function() { + makeController(); + var fabric = { + id: 0 + }; + var vlans = [ + { + id: 0, + fabric: 0 + }, + { + id: 1, + fabric: 0 + }, + { + id: 2, + fabric: 0 + } + ]; + var originalInterfaces = [ + { + id: 0, + type: "physical", + parents: [], + children: [1, 2, 3], + vlan_id: 0 + }, + { + id: 1, + type: "vlan", + parents: [0], + children: [], + vlan_id: 0 + }, + { + id: 2, + type: "vlan", + parents: [0], + children: [], + vlan_id: 1 + }, + { + id: 3, + type: "vlan", + parents: [0], + children: [], + vlan_id: 2 + } + ]; + var nic = { + id: 0, + type: "physical", + fabric: fabric + }; + $scope.originalInterfaces = originalInterfaces; + $scope.vlans = vlans; + expect($scope.canAddVLAN(nic)).toBe(false); + }); + + it("returns true if unused vlans", function() { + makeController(); + var fabric = { + id: 0 + }; + var vlans = [ + { + id: 0, + fabric: 0 + }, + { + id: 1, + fabric: 0 + }, + { + id: 2, + fabric: 0 + } + ]; + var originalInterfaces = [ + { + id: 0, + type: "physical", + parents: [], + children: [1, 2, 3], + vlan_id: 0 + }, + { + id: 1, + type: "vlan", + parents: [0], + children: [], + vlan_id: 0 + }, + { + id: 2, + type: "vlan", + parents: [0], + children: [], + vlan_id: 1 + } + ]; + var nic = { + id: 0, + type: "physical", + fabric: fabric + }; + $scope.originalInterfaces = originalInterfaces; + $scope.vlans = vlans; + expect($scope.canAddVLAN(nic)).toBe(true); + }); + }); + + describe("canAddAnotherVLAN", function() { + it("returns false if canAddVLAN returns false", function() { + makeController(); + spyOn($scope, "canAddVLAN").and.returnValue(false); + expect($scope.canAddAnotherVLAN()).toBe(false); + }); + + it("returns false if only 1 unused vlans", function() { + makeController(); + var fabric = { + id: 0 + }; + var vlans = [ + { + id: 0, + fabric: 0 + }, + { + id: 1, + fabric: 0 + }, + { + id: 2, + fabric: 0 + } + ]; + var originalInterfaces = [ + { + id: 0, + type: "physical", + parents: [], + children: [1, 2, 3], + vlan_id: 0 + }, + { + id: 1, + type: "vlan", + parents: [0], + children: [], + vlan_id: 0 + }, + { + id: 2, + type: "vlan", + parents: [0], + children: [], + vlan_id: 1 + } + ]; + var nic = { + id: 0, + type: "physical", + fabric: fabric + }; + $scope.originalInterfaces = originalInterfaces; + $scope.vlans = vlans; + expect($scope.canAddAnotherVLAN(nic)).toBe(false); + }); + + it("returns true if more than 1 unused vlans", function() { + makeController(); + var fabric = { + id: 0 + }; + var vlans = [ + { + id: 0, + fabric: 0 + }, + { + id: 1, + fabric: 0 + }, + { + id: 2, + fabric: 0 + } + ]; + var originalInterfaces = [ + { + id: 0, + type: "physical", + parents: [], + children: [1, 2, 3], + vlan_id: 0 + }, + { + id: 1, + type: "vlan", + parents: [0], + children: [], + vlan_id: 0 + } + ]; + var nic = { + id: 0, + type: "physical", + fabric: fabric + }; + $scope.originalInterfaces = originalInterfaces; + $scope.vlans = vlans; + expect($scope.canAddAnotherVLAN(nic)).toBe(true); + }); + }); + + describe("getRemoveTypeText", function() { + it("returns interface for physical interface", function() { + makeController(); + var nic = { + type: "physical" + }; + expect($scope.getRemoveTypeText(nic)).toBe("interface"); + }); + + it("returns VLAN for VLAN interface", function() { + makeController(); + var nic = { + type: "vlan" + }; + expect($scope.getRemoveTypeText(nic)).toBe("VLAN"); + }); + + it("returns type for other types", function() { + makeController(); + var type = makeName("type"); + var nic = { + type: type + }; + expect($scope.getRemoveTypeText(nic)).toBe(type); + }); + }); + + describe("canBeRemoved", function() { + it("false if isController", function() { + makeController(); + $parentScope.isController = true; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); + expect($scope.canBeRemoved()).toBe(false); + }); + + it("false if no node editing", function() { + makeController(); + $parentScope.isController = false; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(true); + expect($scope.canBeRemoved()).toBe(false); + }); + + it("true if node can be edited", function() { + makeController(); + $parentScope.isController = false; + spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); + expect($scope.canBeRemoved()).toBe(true); + }); + }); + + describe("remove", function() { + it("sets selectedMode to delete", function() { + makeController(); + $scope.remove(); + expect($scope.selectedMode).toBe("delete"); + }); + }); + + describe("quickRemove", function() { + it("selects interface and sets selectedMode to delete", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + $scope.quickRemove(nic); + expect($scope.isInterfaceSelected(nic)).toBe(true); + expect($scope.selectedMode).toBe("delete"); + }); + }); + + describe("cancel", function() { + it("clears newInterface and sets selectedMode to single", function() { + makeController(); + var newInterface = {}; + $scope.newInterface = newInterface; + $scope.selectedMode = "delete"; + $scope.cancel(); + expect($scope.newInterface).not.toBe(newInterface); + expect($scope.selectedMode).toBe("single"); + }); + + it("clears newInterface and create resets to none", function() { + makeController(); + var newInterface = {}; + $scope.newInterface = newInterface; + $scope.selectedMode = "create-physical"; + $scope.cancel(); + expect($scope.newInterface).not.toBe(newInterface); + expect($scope.selectedMode).toBeNull(); + }); + + it("clears newBondInterface and sets selectedMode to multi", function() { + makeController(); + var newBondInterface = {}; + $scope.newBondInterface = newBondInterface; + $scope.selectedMode = "create-bond"; + $scope.cancel(); + expect($scope.newBondInterface).not.toBe(newBondInterface); + expect($scope.selectedMode).toBe("multi"); + }); + }); + + describe("confirmRemove", function() { + it("sets selectedMode to none", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + type: "physical", + link_id: makeInteger(0, 100) + }; + $scope.toggleInterfaceSelect(nic); + $scope.selectedMode = "delete"; + + spyOn(MachinesManager, "deleteInterface"); + $scope.confirmRemove(nic); + + expect($scope.selectedMode).toBeNull(); + expect($scope.selectedInterfaces).toEqual([]); + }); + + it("calls MachinesManager.deleteInterface", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + type: "physical", + link_id: makeInteger(0, 100) + }; + $scope.toggleInterfaceSelect(nic); + $scope.selectedMode = "delete"; + + spyOn(MachinesManager, "deleteInterface"); + $scope.confirmRemove(nic); + + expect(MachinesManager.deleteInterface).toHaveBeenCalledWith( + node, + nic.id + ); + }); + + it("calls MachinesManager.unlinkSubnet", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + type: "alias", + link_id: makeInteger(0, 100) + }; + $scope.toggleInterfaceSelect(nic); + $scope.selectedMode = "delete"; + + spyOn(MachinesManager, "unlinkSubnet"); + $scope.confirmRemove(nic); + + expect(MachinesManager.unlinkSubnet).toHaveBeenCalledWith( + node, + nic.id, + nic.link_id + ); + }); + + it("removes nic from interfaces", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + type: "alias", + link_id: makeInteger(0, 100) + }; + $scope.interfaces = [nic]; + $scope.toggleInterfaceSelect(nic); + $scope.selectedMode = "delete"; + + spyOn(MachinesManager, "unlinkSubnet"); + $scope.confirmRemove(nic); + + expect($scope.interfaces).toEqual([]); + }); + }); + + describe("add", function() { + it("sets up newInterface for alias", function() { + makeController(); + var vlan = { id: 0 }; + var subnet = { id: 0, vlan: 0 }; + $scope.subnets = [subnet]; + var nic = { + id: makeInteger(0, 100), + type: "physical", + link_id: makeInteger(0, 100), + vlan: vlan + }; + + $scope.add("alias", nic); + expect($scope.newInterface).toEqual({ + type: "alias", + vlan: vlan, + subnet: subnet, + mode: "auto", + parent: nic, + tags: [] + }); + expect($scope.newInterface.vlan).toBe(vlan); + expect($scope.newInterface.subnet).toBe(subnet); + expect($scope.newInterface.parent).toBe(nic); + expect($scope.selectedMode).toBe("add"); + }); + + it("sets up newInterface for vlan", function() { + makeController(); + var fabric = { + id: 0 + }; + var vlans = [ + { + id: 0, + fabric: 0 + }, + { + id: 1, + fabric: 0 + }, + { + id: 2, + fabric: 0 + } + ]; + var originalInterfaces = [ + { + id: 0, + type: "physical", + parents: [], + children: [1], + vlan_id: 0 + }, + { + id: 1, + type: "vlan", + parents: [0], + children: [], + vlan_id: 0 + } + ]; + var nic = { + id: 0, + type: "physical", + link_id: -1, + fabric: fabric, + vlan: vlans[0] + }; + $scope.originalInterfaces = originalInterfaces; + $scope.vlans = vlans; + $scope.newInterface = { + vlan: vlans[1] + }; + + $scope.add("vlan", nic); + expect($scope.newInterface).toEqual({ + type: "vlan", + vlan: vlans[2], + subnet: null, + mode: "link_up", + parent: nic, + tags: [] + }); + expect($scope.newInterface.vlan).toBe(vlans[2]); + expect($scope.newInterface.parent).toBe(nic); + expect($scope.selectedMode).toBe("add"); + }); + }); + + describe("quickAdd", function() { + it("selects nic and calls add with alias", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + + $scope.selectedInterfaces = [{}, {}, {}]; + spyOn($scope, "canAddAlias").and.returnValue(true); + spyOn($scope, "add"); + + $scope.quickAdd(nic); + expect($scope.selectedInterfaces).toEqual([$scope.getUniqueKey(nic)]); + expect($scope.add).toHaveBeenCalledWith("alias", nic); + }); + + it("selects nic and calls add with vlan", function() { + makeController(); + var nic = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100) + }; + + $scope.selectedInterfaces = [{}, {}, {}]; + spyOn($scope, "canAddAlias").and.returnValue(false); + spyOn($scope, "add"); + + $scope.quickAdd(nic); + expect($scope.selectedInterfaces).toEqual([$scope.getUniqueKey(nic)]); + expect($scope.add).toHaveBeenCalledWith("vlan", nic); + }); + }); + + describe("getAddName", function() { + it("returns alias name based on links length", function() { + makeController(); + var name = makeName("eth"); + var parent = { + id: makeInteger(0, 100), + name: name, + link_id: makeInteger(0, 100), + links: [{}, {}, {}] + }; + $scope.newInterface = { + type: "alias", + parent: parent + }; + + expect($scope.getAddName()).toBe(name + ":3"); + }); + + it("returns VLAN name based on VLAN vid", function() { + makeController(); + var name = makeName("eth"); + var vid = makeInteger(0, 100); + var parent = { + id: makeInteger(0, 100), + name: name, + link_id: makeInteger(0, 100) + }; + $scope.newInterface = { + type: "vlan", + parent: parent, + vlan: { + vid: vid + } + }; - it("returns just cidr if name same as cidr", function() { - makeController(); - var cidr = makeName("cidr"); - var subnet = { - cidr: cidr, - name: cidr - }; - expect($scope.getSubnetText(subnet)).toBe(cidr); - }); + expect($scope.getAddName()).toBe(name + "." + vid); + }); + }); - it("returns cidr + name", function() { - makeController(); - var cidr = makeName("cidr"); - var name = makeName("name"); - var subnet = { - cidr: cidr, - name: name - }; - expect($scope.getSubnetText(subnet)).toBe( - cidr + " (" + name + ")"); - }); + describe("addTypeChanged", function() { + it("reset properties based on the new type alias", function() { + makeController(); + var vlan = { id: 0 }; + var subnet = { id: 0, vlan: 0 }; + $scope.subnets = [subnet]; + var parent = { + id: makeInteger(0, 100), + name: name, + link_id: makeInteger(0, 100), + vlan: vlan + }; + $scope.newInterface = { + type: "alias", + parent: parent + }; + $scope.addTypeChanged(); + + expect($scope.newInterface.vlan).toBe(vlan); + expect($scope.newInterface.subnet).toBe(subnet); + expect($scope.newInterface.mode).toBe("auto"); + }); + + it("reset properties based on the new type VLAN", function() { + makeController(); + var fabric = { + id: 0 + }; + var vlans = [ + { + id: 0, + fabric: 0 + }, + { + id: 1, + fabric: 0 + }, + { + id: 2, + fabric: 0 + } + ]; + var originalInterfaces = [ + { + id: 0, + type: "physical", + parents: [], + children: [1], + vlan_id: 0 + }, + { + id: 1, + type: "vlan", + parents: [0], + children: [], + vlan_id: 0 + } + ]; + var parent = { + id: 0, + type: "physical", + link_id: -1, + fabric: fabric, + vlan: vlans[0] + }; + + $scope.originalInterfaces = originalInterfaces; + $scope.vlans = vlans; + $scope.newInterface = { + type: "vlan", + parent: parent + }; + $scope.addTypeChanged(); + + expect($scope.newInterface.vlan).toBe(vlans[1]); + expect($scope.newInterface.subnet).toBeNull(); + expect($scope.newInterface.mode).toBe("link_up"); + }); + }); + + describe("vlanChanged", function() { + it("clears subnets on newInterface", function() { + makeController(); + $scope.newInterface = { + subnet: {} + }; + $scope.vlanChanged($scope.newInterface); + expect($scope.newInterface.subnet).toBeNull(); + }); + }); + + describe("vlanChangedForm", function() { + it("clears subnets on newInterface", function() { + makeController(); + $scope.newInterface = { + getValue: function(name) { + return this["_" + name]; + }, + updateValue: function(name, val) { + this["_" + name] = val; + }, + _subnet: {} + }; + $scope.vlanChangedForm("vlan", {}, $scope.newInterface); + expect($scope.newInterface._subnet).toBeNull(); + }); + }); + + describe("subnetChanged", function() { + it("sets mode to link_up if no subnet", function() { + makeController(); + $scope.newInterface = { + mode: "auto" + }; + $scope.subnetChanged($scope.newInterface); + expect($scope.newInterface.mode).toBe("link_up"); + }); + + it("leaves mode to alone when subnet", function() { + makeController(); + $scope.newInterface = { + mode: "auto", + subnet: {} + }; + $scope.subnetChanged($scope.newInterface); + expect($scope.newInterface.mode).toBe("auto"); + }); + }); + + describe("addInterface", function() { + it("calls saveInterfaceLink with correct params", function() { + makeController(); + var parent = { + id: makeInteger(0, 100) + }; + var subnet = {}; + $scope.newInterface = { + type: "alias", + mode: "auto", + subnet: subnet, + parent: parent + }; + $scope.selectedInterfaces = [{}]; + $scope.selectedMode = "add"; + spyOn($scope, "saveInterfaceLink"); + $scope.addInterface(); + expect($scope.saveInterfaceLink).toHaveBeenCalledWith({ + id: parent.id, + mode: "auto", + subnet: subnet, + ip_address: undefined + }); + expect($scope.selectedMode).toBeNull(); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.newInterface).toEqual({}); + }); + + it("calls createVLANInterface with correct params", function() { + makeController(); + var parent = { + id: makeInteger(0, 100) + }; + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + $scope.newInterface = { + type: "vlan", + mode: "auto", + parent: parent, + tags: [], + vlan: vlan, + subnet: subnet, + ip_address: undefined + }; + $scope.selectedInterfaces = [{}]; + $scope.selectedMode = "add"; + spyOn(MachinesManager, "createVLANInterface").and.returnValue( + $q.defer().promise + ); + + $scope.addInterface(); + expect(MachinesManager.createVLANInterface).toHaveBeenCalledWith(node, { + parent: parent.id, + tags: [], + vlan: vlan.id, + mode: "auto", + subnet: subnet.id, + ip_address: undefined + }); + expect($scope.selectedMode).toBeNull(); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.newInterface).toEqual({}); + }); + + it("calls add again with type", function() { + makeController(); + var parent = { + id: makeInteger(0, 100) + }; + $scope.newInterface = { + type: "alias", + mode: "auto", + subnet: {}, + parent: parent + }; + var selection = [{}]; + $scope.selectedInterfaces = selection; + $scope.selectedMode = "add"; + spyOn($scope, "saveInterfaceLink"); + spyOn($scope, "add"); + $scope.addInterface("alias"); + + expect($scope.add).toHaveBeenCalledWith("alias", parent); + expect($scope.selectedMode).toBe("add"); + expect($scope.selectedInterfaces).toBe(selection); + }); + }); + + describe("isDisabled", function() { + it("returns false when in none, single, or multi mode", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + // Node needs to be Ready or Broken for the mode to be considered. + $scope.node = { status: "Ready" }; + $scope.selectedMode = null; + expect($scope.isDisabled()).toBe(false); + $scope.selectedMode = "single"; + expect($scope.isDisabled()).toBe(false); + $scope.selectedMode = "multi"; + expect($scope.isDisabled()).toBe(false); + }); + + it("returns true when in delete, add, or create modes", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + // Node needs to be Ready or Broken for the mode to be considered. + $scope.node = { status: "Ready" }; + $scope.selectedMode = "create-bond"; + expect($scope.isDisabled()).toBe(true); + $scope.selectedMode = "add"; + expect($scope.isDisabled()).toBe(true); + $scope.selectedMode = "delete"; + expect($scope.isDisabled()).toBe(true); + }); + + it(`returns true when the node state + is not 'Ready' or 'Broken'`, function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node = { status: "Ready" }; + expect($scope.isDisabled()).toBe(false); + $scope.node = { status: "Broken" }; + expect($scope.isDisabled()).toBe(false); + [ + "New", + "Commissioning", + "Failed commissioning", + "Missing", + "Reserved", + "Allocated", + "Deploying", + "Deployed", + "Retired", + "Failed deployment", + "Releasing", + "Releasing failed", + "Disk erasing", + "Failed disk erasing" + ].forEach(function(s) { + $scope.node = { state: s }; + expect($scope.isDisabled()).toBe(true); + }); + }); + + it("returns true if the user is not a superuser", function() { + makeController(); + $scope.canEdit = function() { + return false; + }; + $scope.node = { status: "Ready" }; + expect($scope.isDisabled()).toBe(true); + $scope.node = { status: "Broken" }; + expect($scope.isDisabled()).toBe(true); + }); + }); + + describe("isLimitedEditingAllowed", function() { + it("returns false when not superuser", function() { + makeController(); + $scope.canEdit = function() { + return false; + }; + expect($scope.isLimitedEditingAllowed()).toBe(false); + }); + + it("returns false when isController", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $parentScope.isController = true; + expect($scope.isLimitedEditingAllowed()).toBe(false); + }); + + it("returns true when deployed and not vlan", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $parentScope.isController = false; + $scope.node = { + status: "Deployed" + }; + var nic = { + type: "physical" + }; + expect($scope.isLimitedEditingAllowed(nic)).toBe(true); + }); + }); + + describe("isAllNetworkingDisabled", function() { + it( + "returns true if the user is not a superuser " + + "and the node is not a device", + function() { + makeController(); + $parentScope.isDevice = false; + $scope.canEdit = function() { + return false; + }; + expect($scope.isAllNetworkingDisabled()).toBe(true); + } + ); + + it( + "returns false if the user is not a superuser " + + "and the node is not a device", + function() { + makeController(); + $parentScope.isDevice = true; + $scope.canEdit = function() { + return false; + }; + expect($scope.isAllNetworkingDisabled()).toBe(false); + } + ); + + it( + "returns false when a non-controller node state " + + "is 'New', 'Ready', 'Allocated' or 'Broken' and we are a superuser", + function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node = { status: "New" }; + expect($scope.isAllNetworkingDisabled()).toBe(false); + $scope.node = { status: "Ready" }; + expect($scope.isAllNetworkingDisabled()).toBe(false); + $scope.node = { status: "Allocated" }; + expect($scope.isAllNetworkingDisabled()).toBe(false); + $scope.node = { status: "Broken" }; + expect($scope.isAllNetworkingDisabled()).toBe(false); + [ + "Commissioning", + "Failed commissioning", + "Missing", + "Reserved", + "Allocated", + "Deploying", + "Deployed", + "Retired", + "Failed deployment", + "Releasing", + "Releasing failed", + "Disk erasing", + "Failed disk erasing" + ].forEach(function(s) { + $scope.node = { state: s }; + expect($scope.isAllNetworkingDisabled()).toBe(true); + }); + } + ); + + it(`returns false for controllers, in any state, + even if superuser`, function() { + makeController(); + $parentScope.isController = true; + $scope.canEdit = function() { + return true; + }; + [ + "Ready", + "Broken", + "New", + "Commissioning", + "Failed commissioning", + "Missing", + "Reserved", + "Allocated", + "Deploying", + "Deployed", + "Retired", + "Failed deployment", + "Releasing", + "Releasing failed", + "Disk erasing", + "Failed disk erasing" + ].forEach(function(s) { + $scope.node = { state: s }; + expect($scope.isAllNetworkingDisabled()).toBe(false); + }); + }); + }); + + describe("canCreateBond", function() { + it("returns false if not in multi mode", function() { + makeController(); + var modes = [null, "add", "delete", "single", "delete"]; + angular.forEach(modes, function(mode) { + $scope.selectedMode = mode; + expect($scope.canCreateBond()).toBe(false); + }); + }); + + it("returns false if selected interface is bond", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "bond" + }; + var nic2 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "bond" + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + expect($scope.canCreateBond()).toBe(false); + }); + + it("returns false if selected interface is alias", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "alias" + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "alias" + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + expect($scope.canCreateBond()).toBe(false); + }); + + it("returns false if not same selected vlan", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: {} + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: {} + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + expect($scope.canCreateBond()).toBe(false); + }); + + it("returns true if same selected vlan", function() { + makeController(); + var vlan = {}; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + expect($scope.canCreateBond()).toBe(true); + }); + }); + + describe("isShowingCreateBond", function() { + it("returns true in create-bond mode", function() { + makeController(); + $scope.selectedMode = "create-bond"; + expect($scope.isShowingCreateBond()).toBe(true); + }); + + it("returns false in multi mode", function() { + makeController(); + $scope.selectedMode = "multi"; + expect($scope.isShowingCreateBond()).toBe(false); + }); + }); + + describe("showCreateBond", function() { + it("sets mode to create-bond", function() { + makeController(); + $scope.selectedMode = "multi"; + spyOn($scope, "canCreateBond").and.returnValue(true); + $scope.showCreateBond(); + expect($scope.selectedMode).toBe("create-bond"); + }); + + it("creates the newBondInterface", function() { + makeController(); + var vlan = {}; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + $scope.showCreateBond(); + expect($scope.newBondInterface).toEqual({ + name: "bond0", + parents: [nic1, nic2], + primary: nic1, + tags: [], + mac_address: "", + bond_mode: "active-backup", + fabric: "", + vlan: {}, + subnet: "", + lacpRate: "fast", + xmitHashPolicy: "layer2", + bond_updelay: 0, + bond_downdelay: 0, + bond_miimon: 100 + }); + }); + }); + + describe("toggleInterfaces", function() { + it("sets isShowingInterfaces to false if showing", function() { + makeController(); + $scope.isShowingInterfaces = true; + $scope.toggleInterfaces(); + expect($scope.isShowingInterfaces).toBe(false); + }); + + it("sets isShowingInterfaces to true if not showing", function() { + makeController(); + $scope.isShowingInterfaces = false; + $scope.toggleInterfaces(); + expect($scope.isShowingInterfaces).toBe(true); + }); + }); + + describe("isCorrectInterfaceType", function() { + var bondInterface = { + id: 35, + type: "physical" + }; + + var parents = [ + { + id: 34, + type: "physical" + } + ]; + + it("returns true if not parent & type is same as parent", function() { + makeController(); + expect($scope.isCorrectInterfaceType(bondInterface, parents)).toBe(true); + }); + + it("returns false if parent", function() { + makeController(); + + bondInterface.id = 34; + + expect($scope.isCorrectInterfaceType(bondInterface, parents)).toBe(false); + }); + + it("returns false is type is not same as parent", function() { + makeController(); + + bondInterface.id = 33; + bondInterface.type = "vlan"; + + expect($scope.isCorrectInterfaceType(bondInterface, parents)).toBe(false); + }); + }); + + describe("hasBootInterface", function() { + it("returns false if bond has no members with is_boot", function() { + makeController(); + $scope.newBondInterface = { + parents: [ + { + is_boot: false + }, + { + is_boot: false + } + ] + }; + expect($scope.hasBootInterface($scope.newBondInterface)).toBe(false); }); - describe("getSubnet", function() { + it("returns true if bond has member with is_boot", function() { + makeController(); + $scope.newBondInterface = { + parents: [ + { + is_boot: false + }, + { + is_boot: true + } + ] + }; + expect($scope.hasBootInterface($scope.newBondInterface)).toBe(true); + }); + }); + + describe("getInterfacePlaceholderMACAddress", function() { + it("returns empty string if primary not set", function() { + makeController(); + expect($scope.getInterfacePlaceholderMACAddress({})).toBe(""); + }); + + it("returns the MAC address of the primary interface", function() { + makeController(); + var mac_address = makeName("mac"); + $scope.newBondInterface.primary = { + mac_address: mac_address + }; + expect( + $scope.getInterfacePlaceholderMACAddress($scope.newBondInterface) + ).toBe(mac_address); + }); + }); + + describe("isMACAddressInvalid", function() { + it(`returns false when the mac_address blank + and not invalidEmpty`, function() { + makeController(); + expect($scope.isMACAddressInvalid("")).toBe(false); + }); + + it(`returns true when the mac_address + is blank and invalidEmpty`, function() { + makeController(); + expect($scope.isMACAddressInvalid("", true)).toBe(true); + }); + + it("returns false if valid mac_address", function() { + makeController(); + expect($scope.isMACAddressInvalid("00:11:22:33:44:55")).toBe(false); + }); + + it("returns true if invalid mac_address", function() { + makeController(); + expect($scope.isMACAddressInvalid("00:11:22:33:44")).toBe(true); + }); + }); + + describe("showLACPRate", function() { + it("returns true if in 802.3ad mode", function() { + makeController(); + $scope.newBondInterface.bond_mode = "802.3ad"; + expect($scope.showLACPRate()).toBe(true); + }); + + it("returns false if not in 802.3ad mode", function() { + makeController(); + $scope.newBondInterface.bond_mode = makeName("otherMode"); + expect($scope.showLACPRate()).toBe(false); + }); + }); + + describe("modeAndPolicyCompliant", function() { + it("returns true if policy is layer3+4", function() { + makeController(); + $scope.newBondInterface.bond_mode = "802.3ad"; + $scope.newBondInterface.xmitHashPolicy = "layer3+4"; + expect($scope.showLACPRate()).toBe(true); + }); + + it("returns true if policy is encap3+4", function() { + makeController(); + $scope.newBondInterface.bond_mode = "802.3ad"; + $scope.newBondInterface.xmitHashPolicy = "encap3+4"; + expect($scope.showLACPRate()).toBe(true); + }); + + it("returns false if 802.3ad compliant", function() { + makeController(); + $scope.editInterface = { + id: 0, + link_id: -1 + }; + $scope.newBondInterface.bond_mode = "802.3ad"; + $scope.newBondInterface.xmitHashPolicy = "layer2+3"; + expect($scope.showLACPRate()).toBe(false); + }); + }); + + describe("showXMITHashPolicy", function() { + it("returns true if in balance-xor mode", function() { + makeController(); + $scope.newBondInterface.bond_mode = "balance-xor"; + expect($scope.showXMITHashPolicy()).toBe(true); + }); + + it("returns true if in 802.3ad mode", function() { + makeController(); + $scope.newBondInterface.bond_mode = "802.3ad"; + expect($scope.showXMITHashPolicy()).toBe(true); + }); + + it("returns true if in balance-tlb mode", function() { + makeController(); + $scope.newBondInterface.bond_mode = "balance-tlb"; + expect($scope.showXMITHashPolicy()).toBe(true); + }); + + it("returns false if not in other modes", function() { + makeController(); + $scope.newBondInterface.bond_mode = makeName("otherMode"); + expect($scope.showXMITHashPolicy()).toBe(false); + }); + }); + + describe("cannotAddBond", function() { + it("returns true when isInterfaceNameInvalid is true", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(true); + expect($scope.cannotAddBond()).toBe(true); + }); + + it("returns true when isMACAddressInvalid is true", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); + spyOn($scope, "isMACAddressInvalid").and.returnValue(true); + expect($scope.cannotAddBond()).toBe(true); + }); + + it("returns false when both are false", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); + spyOn($scope, "isMACAddressInvalid").and.returnValue(false); + expect($scope.cannotAddBond()).toBe(false); + }); + }); + + describe("addBond", function() { + it("does nothing if cannotAddBond returns true", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + $scope.showCreateBond(); + + spyOn(MachinesManager, "createBondInterface").and.returnValue( + $q.defer().promise + ); + spyOn($scope, "cannotAddBond").and.returnValue(true); + $scope.newBondInterface.name = "bond0"; + $scope.newBondInterface.mac_address = "00:11:22:33:44:55"; + + $scope.addBond(); + expect(MachinesManager.createBondInterface).not.toHaveBeenCalled(); + }); + + it("calls createBondInterface and removes selection", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + $scope.showCreateBond(); + + spyOn(MachinesManager, "createBondInterface").and.returnValue( + $q.defer().promise + ); + spyOn($scope, "cannotAddBond").and.returnValue(false); + $scope.newBondInterface.name = "bond0"; + $scope.newBondInterface.mac_address = "00:11:22:33:44:55"; + $scope.newBondInterface.vlan = vlan; + $scope.newBondInterface.subnet = subnet; + $scope.newBondInterface.mode = "static"; + $scope.newBondInterface.ip_address = "192.168.1.100"; + $scope.addBond(); + + expect(MachinesManager.createBondInterface).toHaveBeenCalledWith(node, { + name: "bond0", + mac_address: "00:11:22:33:44:55", + tags: [], + parents: [nic1.id, nic2.id], + bond_mode: "active-backup", + bond_lacp_rate: "fast", + bond_xmit_hash_policy: "layer2", + vlan: vlan.id, + subnet: subnet.id, + mode: "static", + ip_address: "192.168.1.100", + bond_miimon: 100, + bond_updelay: 0, + bond_downdelay: 0 + }); + expect($scope.newBondInterface).toEqual({}); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.selectedMode).toBeNull(); + }); + + it("calls createBondInterface even when disconnected", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: null + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: null + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + $scope.showCreateBond(); + + spyOn(MachinesManager, "createBondInterface").and.returnValue( + $q.defer().promise + ); + spyOn($scope, "cannotAddBond").and.returnValue(false); + $scope.newBondInterface.name = "bond0"; + $scope.newBondInterface.mac_address = "00:11:22:33:44:55"; + $scope.addBond(); + + expect(MachinesManager.createBondInterface).toHaveBeenCalledWith(node, { + name: "bond0", + mac_address: "00:11:22:33:44:55", + tags: [], + parents: [nic1.id, nic2.id], + bond_mode: "active-backup", + bond_lacp_rate: "fast", + bond_xmit_hash_policy: "layer2", + vlan: undefined, + subnet: null, + mode: undefined, + ip_address: undefined, + bond_miimon: 100, + bond_updelay: 0, + bond_downdelay: 0 + }); + expect($scope.newBondInterface).toEqual({}); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.selectedMode).toBeNull(); + }); + }); + + describe("canCreateBridge", function() { + it("returns false if not in single mode", function() { + makeController(); + var modes = [null, "add", "delete", "multi", "delete"]; + angular.forEach(modes, function(mode) { + $scope.selectedMode = mode; + expect($scope.canCreateBridge()).toBe(false); + }); + }); + + it("returns false if selected interface is bridge", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "bridge" + }; + $scope.interfaces = [nic1]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.toggleInterfaceSelect(nic1); + expect($scope.canCreateBridge()).toBe(false); + }); + + it("returns false if selected interface is alias", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "alias" + }; + $scope.interfaces = [nic1]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.toggleInterfaceSelect(nic1); + expect($scope.canCreateBridge()).toBe(false); + }); + + it("returns false if muliple selected", function() { + makeController(); + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: {} + }; + var nic2 = { + id: makeInteger(101, 200), + link_id: makeInteger(0, 100), + type: "physical", + vlan: {} + }; + $scope.interfaces = [nic1, nic2]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.interfaceLinksMap[nic2.id] = {}; + $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; + $scope.toggleInterfaceSelect(nic1); + $scope.toggleInterfaceSelect(nic2); + expect($scope.canCreateBridge()).toBe(false); + }); + + it("returns true if selected", function() { + makeController(); + var vlan = {}; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.toggleInterfaceSelect(nic1); + expect($scope.canCreateBridge()).toBe(true); + }); + }); + + describe("isShowingCreateBridge", function() { + it("returns true in create-bridge mode", function() { + makeController(); + $scope.selectedMode = "create-bridge"; + expect($scope.isShowingCreateBridge()).toBe(true); + }); + + it("returns false in single mode", function() { + makeController(); + $scope.selectedMode = "single"; + expect($scope.isShowingCreateBridge()).toBe(false); + }); + }); + + describe("isShowingEdit", function() { + it("returns true in edit mode", function() { + makeController(); + $scope.selectedMode = "edit"; + expect($scope.isShowingEdit()).toBe(true); + }); + + it("returns false in single mode", function() { + makeController(); + $scope.selectedMode = "single"; + expect($scope.isShowingEdit()).toBe(false); + }); + }); + + describe("showCreateBridge", function() { + it("sets mode to create-bridge", function() { + makeController(); + $scope.selectedMode = "single"; + spyOn($scope, "canCreateBridge").and.returnValue(true); + $scope.showCreateBridge(); + expect($scope.selectedMode).toBe("create-bridge"); + }); + + it("creates the newBridgeInterface", function() { + makeController(); + var vlan = {}; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.toggleInterfaceSelect(nic1); + $scope.showCreateBridge(); + expect($scope.newBridgeInterface).toEqual({ + name: "br0", + parents: [nic1], + primary: nic1, + tags: [], + mac_address: "", + vlan: {}, + fabric: "", + bridge_stp: false, + bridge_fd: 15 + }); + }); + }); + + describe("cannotAddBridge", function() { + it("returns true when isInterfaceNameInvalid is true", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(true); + expect($scope.cannotAddBridge()).toBe(true); + }); + + it("returns true when isMACAddressInvalid is true", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); + spyOn($scope, "isMACAddressInvalid").and.returnValue(true); + expect($scope.cannotAddBridge()).toBe(true); + }); + + it("returns false when both are false", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); + spyOn($scope, "isMACAddressInvalid").and.returnValue(false); + expect($scope.cannotAddBridge()).toBe(false); + }); + }); + + describe("addBridge", function() { + it("does nothing if cannotAddBridge returns true", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan + }; + $scope.interfaces = [nic1]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.toggleInterfaceSelect(nic1); + $scope.showCreateBridge(); + + spyOn(MachinesManager, "createBridgeInterface").and.returnValue( + $q.defer().promise + ); + spyOn($scope, "cannotAddBridge").and.returnValue(true); + $scope.newBridgeInterface.name = "br0"; + $scope.newBridgeInterface.mac_address = "00:11:22:33:44:55"; + + $scope.addBridge(); + expect(MachinesManager.createBridgeInterface).not.toHaveBeenCalled(); + }); + + it("calls createBridgeInterface and removes selection", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + var fabric = { + id: makeInteger(0, 100) + }; + var nic1 = { + id: makeInteger(0, 100), + link_id: makeInteger(0, 100), + type: "physical", + vlan: vlan, + fabric: fabric + }; + + $scope.interfaces = [nic1]; + $scope.interfaceLinksMap = {}; + $scope.interfaceLinksMap[nic1.id] = {}; + $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; + $scope.toggleInterfaceSelect(nic1); + $scope.showCreateBridge(); + + spyOn(MachinesManager, "createBridgeInterface").and.returnValue( + $q.defer().promise + ); + spyOn($scope, "cannotAddBridge").and.returnValue(false); + $scope.newBridgeInterface.name = "br0"; + $scope.newBridgeInterface.mac_address = "00:11:22:33:44:55"; + $scope.newBridgeInterface.vlan = vlan; + $scope.newBridgeInterface.subnet = subnet; + $scope.newBridgeInterface.mode = "static"; + $scope.newBridgeInterface.ip_address = "192.168.1.100"; + $scope.addBridge(); + + expect(MachinesManager.createBridgeInterface).toHaveBeenCalledWith(node, { + name: "br0", + mac_address: "00:11:22:33:44:55", + tags: [], + parents: [nic1.id], + bridge_stp: false, + bridge_fd: 15, + vlan: vlan.id, + subnet: subnet.id, + mode: "static", + ip_address: "192.168.1.100" + }); + expect($scope.interfaces).toEqual([]); + expect($scope.newBridgeInterface).toEqual({}); + expect($scope.selectedInterfaces).toEqual([]); + expect($scope.selectedMode).toBeNull(); + }); + }); + + describe("isShowingCreatePhysical", function() { + it("returns true in create-physical mode", function() { + makeController(); + $scope.selectedMode = "create-physical"; + expect($scope.isShowingCreatePhysical()).toBe(true); + }); + + it("returns false in single mode", function() { + makeController(); + $scope.selectedMode = "single"; + expect($scope.isShowingCreatePhysical()).toBe(false); + }); + }); + + describe("showCreatePhysical", function() { + it("sets mode to create-physical", function() { + makeController(); + var vlan = { id: 0, fabric: 0 }; + var fabric = { + id: 0, + name: makeName("fabric"), + default_vlan_id: 0, + vlan_ids: [0] + }; + VLANsManager._items = [vlan]; + $scope.fabrics = [fabric]; + $scope.selectedMode = null; + $scope.showCreatePhysical(); + expect($scope.selectedMode).toBe("create-physical"); + }); + + it("creates the newInterface", function() { + makeController(); + var vlan = { id: 0, fabric: 0 }; + var fabric = { + id: 0, + name: makeName("fabric"), + default_vlan_id: 0, + vlan_ids: [0] + }; + VLANsManager._items = [vlan]; + $scope.fabrics = [fabric]; + $scope.selectedMode = null; + $scope.showCreatePhysical(); + expect($scope.newInterface).toEqual({ + name: "eth0", + mac_address: "", + macError: false, + tags: [], + errorMsg: null, + fabric: fabric, + vlan: vlan, + subnet: null, + mode: "link_up" + }); + }); + }); + + describe("fabricChanged", function() { + it("sets newInterface.vlan with new fabric", function() { + makeController(); + var vlan = { id: 0, fabric: 0 }; + var fabric = { + id: 0, + name: makeName("fabric"), + default_vlan_id: 0, + vlan_ids: [0] + }; + VLANsManager._items = [vlan]; + $scope.newInterface.fabric = fabric; + $scope.newInterface.subnet = {}; + $scope.newInterface.mode = "auto"; + $scope.fabricChanged($scope.newInterface); + expect($scope.newInterface.vlan).toBe(vlan); + expect($scope.newInterface.subnet).toBeNull(); + expect($scope.newInterface.mode).toBe("link_up"); + }); + }); + + describe("fabricChangedForm", function() { + it("sets newInterface.vlan with new fabric", function() { + makeController(); + var vlan = { id: 0, fabric: 0 }; + var fabric = { + id: 0, + name: makeName("fabric"), + default_vlan_id: 0, + vlan_ids: [0] + }; + VLANsManager._items = [vlan]; + $scope.newInterface._fabric = fabric; + $scope.newInterface._subnet = {}; + $scope.newInterface._mode = "auto"; + $scope.newInterface.getValue = function(name) { + return this["_" + name]; + }; + $scope.newInterface.updateValue = function(name, val) { + this["_" + name] = val; + }; + $scope.fabricChangedForm("fabric", fabric, $scope.newInterface); + expect($scope.newInterface._vlan).toBe(vlan); + expect($scope.newInterface._subnet).toBeNull(); + expect($scope.newInterface._mode).toBe("link_up"); + }); + }); + + describe("subnetChanged", function() { + it("sets mode to link_up when no subnet", function() { + makeController(); + $scope.newInterface.subnet = null; + $scope.newInterface.mode = "auto"; + $scope.subnetChanged($scope.newInterface); + expect($scope.newInterface.mode).toBe("link_up"); + }); + + it("leaves mode to original when subnet", function() { + makeController(); + $scope.newInterface.subnet = {}; + $scope.newInterface.mode = "auto"; + $scope.subnetChanged($scope.newInterface); + expect($scope.newInterface.mode).toBe("auto"); + }); + }); + + describe("subnetChangedForm", function() { + it("sets mode to link_up when no subnet", function() { + makeController(); + $scope.newInterface = { + getValue: function(name) { + return this["_" + name]; + }, + updateValue: function(name, val) { + this["_" + name] = val; + }, + _subnet: null, + _mode: "auto" + }; + $scope.subnetChangedForm("subnet", null, $scope.newInterface); + expect($scope.newInterface._mode).toBe("link_up"); + }); + + it("leaves mode to original when subnet", function() { + makeController(); + $scope.newInterface = { + getValue: function(name) { + return this["_" + name]; + }, + updateValue: function(name, val) { + this["_" + name] = val; + }, + _subnet: {}, + _mode: "auto" + }; + $scope.subnetChangedForm("subnet", {}, $scope.newInterface); + expect($scope.newInterface._mode).toBe("auto"); + }); + }); + + describe("cannotAddPhysicalInterface", function() { + it("returns true when isInterfaceNameInvalid is true", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(true); + expect($scope.cannotAddPhysicalInterface()).toBe(true); + }); + + it("returns true when isMACAddressInvalid is true", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); + spyOn($scope, "isMACAddressInvalid").and.returnValue(true); + expect($scope.cannotAddPhysicalInterface()).toBe(true); + }); + + it("returns false when both are false", function() { + makeController(); + spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); + spyOn($scope, "isMACAddressInvalid").and.returnValue(false); + expect($scope.cannotAddPhysicalInterface()).toBe(false); + }); + }); + + describe("addPhysicalInterface", function() { + it("does nothing if cannotAddInterface returns true", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + $scope.newInterface = { + name: "eth0", + mac_address: "00:11:22:33:44:55", + tags: [], + vlan: vlan, + subnet: subnet, + mode: "auto" + }; + + spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( + $q.defer().promise + ); + spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(true); + $scope.addPhysicalInterface(); + + expect(MachinesManager.createPhysicalInterface).not.toHaveBeenCalled(); + }); + + it("calls createPhysicalInterface and removes selection", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + $scope.newInterface = { + name: "eth0", + mac_address: "00:11:22:33:44:55", + tags: [], + vlan: vlan, + subnet: subnet, + mode: "auto" + }; + $scope.selectedMode = "create-physical"; + + var defer = $q.defer(); + spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( + defer.promise + ); + spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(false); + $scope.addPhysicalInterface(); + defer.resolve(); + $scope.$digest(); + + expect(MachinesManager.createPhysicalInterface).toHaveBeenCalledWith( + node, + { + name: "eth0", + mac_address: "00:11:22:33:44:55", + tags: [], + vlan: vlan.id, + subnet: subnet.id, + mode: "auto", + ip_address: undefined + } + ); + expect($scope.newInterface).toEqual({}); + expect($scope.selectedMode).toBeNull(); + }); + + it("clears error on call", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + $scope.newInterface = { + name: "eth0", + mac_address: "00:11:22:33:44:55", + tags: [], + vlan: vlan, + subnet: subnet, + mode: "auto", + macError: true, + errorMsg: "error" + }; + + var defer = $q.defer(); + spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( + defer.promise + ); + spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(false); + $scope.addPhysicalInterface(); + + expect($scope.newInterface.macError).toBe(false); + expect($scope.newInterface.errorMsg).toBeNull(); + }); + + it("handles mac_address error", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var subnet = { + id: makeInteger(0, 100) + }; + $scope.newInterface = { + name: "eth0", + mac_address: "00:11:22:33:44:55", + tags: [], + vlan: vlan, + subnet: subnet, + mode: "auto" + }; + + var defer = $q.defer(); + spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( + defer.promise + ); + spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(false); + $scope.addPhysicalInterface(); + + var error = { + mac_address: ["MACAddress is already in use"] + }; + defer.reject(angular.toJson(error)); + $scope.$digest(); + + expect($scope.newInterface.macError).toBe(true); + expect($scope.newInterface.errorMsg).toBe("MACAddress is already in use"); + }); + }); + + describe("isBond", function() { + it("returns true if has type of bond", function() { + makeController(); + var item = { type: "bond" }; + expect($scope.isBond(item)).toBe(true); + }); + + it("returns false if doesn't have type of bond", function() { + makeController(); + var item = { type: "physical" }; + expect($scope.isBond(item)).toBe(false); + }); + }); + + describe("showEditButton", function() { + it("returns true if all conditions are met", function() { + makeController(); + + var editInterface = { + id: "foo", + type: "bond", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + }, + members: [] + }; + + var interfaces = [ + { + id: "foo", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + }, + { + id: "bar", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + }, + { + id: "baz", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + } + ]; - it("calls SubnetsManager.getItemFromList", function() { - makeController(); - var subnetId = makeInteger(0, 100); - var subnet = {}; - spyOn(SubnetsManager, "getItemFromList").and.returnValue(subnet); - - expect($scope.getSubnet(subnetId)).toBe(subnet); - expect(SubnetsManager.getItemFromList).toHaveBeenCalledWith( - subnetId); - }); + expect($scope.showEditButton(editInterface, interfaces)).toBe(true); }); - describe("saveInterface", function() { - - it("calls MachinesManager.updateInterface if name changed", function() { - makeController(); - var id = makeInteger(0, 100); - var name = makeName("nic"); - var vlan = { id: makeInteger(0, 100) }; - var original_nic = { - id: id, - name: name, - vlan_id: vlan.id - }; - var nic = { - id: id, - name: makeName("newName"), - vlan: vlan, - tags: [] - }; - $scope.originalInterfaces[id] = original_nic; - $scope.interfaces = [nic]; - - spyOn(MachinesManager, "updateInterface").and.returnValue( - $q.defer().promise); - $scope.saveInterface(nic); - expect(MachinesManager.updateInterface).toHaveBeenCalledWith( - node, id, { - "name": nic.name, - "mac_address": undefined, - "vlan": vlan.id, - "mode": undefined, - "fabric": null, - "subnet": null, - "tags": [] - }); - }); + it("returns false if editInterface is not bond", function() { + makeController(); + var editInterface = { + id: "foo", + type: "physical", + fabric: { + name: "fabric-1" + } + }; + var interfaces = [ + { id: "foo", fabric: { name: "fabric-1" } }, + { id: "bar", fabric: { name: "fabric-1" } }, + { id: "baz", fabric: { name: "fabric-1" } } + ]; + expect($scope.showEditButton(editInterface, interfaces)).toBe(false); + }); + + it("returns false if no interfaces", function() { + makeController(); + var editInterface = { + id: "foo", + type: "bond", + fabric: { + name: "fabric-1" + }, + members: [] + }; + var interfaces = []; + expect($scope.showEditButton(editInterface, interfaces)).toBe(false); + }); + }); + + describe("showCreateEditButton", function() { + it("returns true if interfaces exist", function() { + makeController(); + $scope.selectedInterfaces = []; + $scope.newBondInterface = { + id: "bar", + fabric: { + name: "fabric-1" + }, + vlan: { + id: 2 + } + }; + $scope.interfaces = [ + { id: "foo", fabric: { name: "fabric-1" }, vlan: { id: 2 } } + ]; + expect($scope.showCreateEditButton()).toBe(true); + }); - it("calls MachinesManager.updateInterface if vlan changed", function() { - makeController(); - var id = makeInteger(0, 100); - var name = makeName("nic"); - var vlan = { id: makeInteger(0, 100) }; - var original_nic = { - id: id, - name: name, - vlan_id: makeInteger(200, 300) - }; - var nic = { - id: id, - name: name, - vlan: vlan, - tags: [] - }; - $scope.originalInterfaces[id] = original_nic; - $scope.interfaces = [nic]; - - spyOn(MachinesManager, "updateInterface").and.returnValue( - $q.defer().promise); - $scope.saveInterface(nic); - expect(MachinesManager.updateInterface).toHaveBeenCalledWith( - node, id, { - "name": name, - "mac_address": undefined, - "vlan": vlan.id, - "mode": undefined, - "fabric": null, - "subnet": null, - "tags": [] - }); - }); - - it("calls MachinesManager.updateInterface if vlan set", function() { - makeController(); - var id = makeInteger(0, 100); - var name = makeName("nic"); - var vlan = { id: makeInteger(0, 100) }; - var original_nic = { - id: id, - name: name, - vlan_id: null - }; - var nic = { - id: id, - name: name, - vlan: vlan, - tags: [] - }; - $scope.originalInterfaces[id] = original_nic; - $scope.interfaces = [nic]; - - spyOn(MachinesManager, "updateInterface").and.returnValue( - $q.defer().promise); - $scope.saveInterface(nic); - expect(MachinesManager.updateInterface).toHaveBeenCalledWith( - node, id, { - "name": name, - "mac_address": undefined, - "vlan": vlan.id, - "mode": undefined, - "fabric": null, - "subnet": null, - "tags": [] - }); - }); - - it("calls MachinesManager.updateInterface if vlan unset", function() { - makeController(); - var id = makeInteger(0, 100); - var name = makeName("nic"); - var original_nic = { - id: id, - name: name, - vlan_id: makeInteger(200, 300) - }; - var nic = { - id: id, - name: name, - vlan: null, - tags: [] - }; - $scope.originalInterfaces[id] = original_nic; - $scope.interfaces = [nic]; - - spyOn(MachinesManager, "updateInterface").and.returnValue( - $q.defer().promise); - $scope.saveInterface(nic); - expect(MachinesManager.updateInterface).toHaveBeenCalledWith( - node, id, { - "name": name, - "mac_address": undefined, - "mode": undefined, - "fabric": null, - "subnet": null, - "vlan": null, - "tags": [] - }); - }); - }); - - describe("isInterfaceNameInvalid", function() { - - it("returns true if name is empty", function() { - makeController(); - var nic = { - name: "" - }; - expect($scope.isInterfaceNameInvalid(nic)).toBe(true); - }); - - it("returns true if name is missing", function() { - makeController(); - var nic = {}; - expect($scope.isInterfaceNameInvalid(nic)).toBe(true); - }); - - it("returns true if nic is null", function() { - makeController(); - var nic = null; - expect($scope.isInterfaceNameInvalid(nic)).toBe(true); - }); - - it("returns true if name is same as another interface", function() { - makeController(); - var name = makeName("nic"); - var nic = { - id: 0, - name: name - }; - var otherNic = { - id: 1, - name: name - }; - $scope.node.interfaces = [nic, otherNic]; - expect($scope.isInterfaceNameInvalid(nic)).toBe(true); - }); - - it("returns false if name is same name as self", function() { - makeController(); - var name = makeName("nic"); - var nic = { - id: 0, - name: name - }; - $scope.node.interfaces = [nic]; - expect($scope.isInterfaceNameInvalid(nic)).toBe(false); - }); - - it("returns false if name is different", function() { - makeController(); - var name = makeName("nic"); - var newName = makeName("newNic"); - var nic = { - id: 0, - name: newName - }; - var otherNic = { - id: 1, - name: name - }; - $scope.node.interfaces = [otherNic]; - expect($scope.isInterfaceNameInvalid(nic)).toBe(false); - }); - }); - - describe("fabricChanged", function() { - - it("sets vlan on interface", function() { - makeController(); - var fabric = { - id: 0, - default_vlan_id: 0, - vlan_ids: [0] - }; - var vlan = { - id: 0, - fabric: fabric.id - }; - FabricsManager._items = [fabric]; - VLANsManager._items = [vlan]; - var nic = { - vlan: null, - fabric: fabric - }; - spyOn($scope, "saveInterface"); - $scope.fabricChanged(nic); - expect(nic.vlan).toBe(vlan); - }); - - it("sets vlan to null", function() { - makeController(); - var nic = { - vlan: {}, - fabric: null - }; - spyOn($scope, "saveInterface"); - $scope.fabricChanged(nic); - expect(nic.vlan).toBeNull(); - }); - }); - - describe("isLinkModeDisabled", function() { - - it("enabled when subnet", function() { - makeController(); - var nic = { - subnet : {} - }; - expect($scope.isLinkModeDisabled(nic)).toBe(false); - }); - - it("disabled when not subnet", function() { - makeController(); - var nic = { - subnet : null - }; - expect($scope.isLinkModeDisabled(nic)).toBe(true); - }); - - it("enabled when subnet with getValue", function() { - makeController(); - var nic = { - getValue : function() { return {};} - }; - expect($scope.isLinkModeDisabled(nic)).toBe(false); - }); - - it("disabled when not subnet with getValue", function() { - makeController(); - var nic = { - getValue : function() { return null;} - }; - expect($scope.isLinkModeDisabled(nic)).toBe(true); - }); - }); - - describe("saveInterfaceLink", function() { - - it("calls MachinesManager.linkSubnet with params", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - mode: "static", - subnet: { id: makeInteger(0, 100) }, - link_id: makeInteger(0, 100), - ip_address: "192.168.122.1" - }; - spyOn(MachinesManager, "linkSubnet").and.returnValue( - $q.defer().promise); - $scope.saveInterfaceLink(nic); - expect(MachinesManager.linkSubnet).toHaveBeenCalledWith( - node, nic.id, { - "mode": "static", - "subnet": nic.subnet.id, - "link_id": nic.link_id, - "ip_address": nic.ip_address - }); - }); - }); - - describe("subnetChanged", function() { - - it("sets mode to link_up if set to no subnet", function() { - makeController(); - var nic = { - subnet: null - }; - spyOn($scope, "saveInterfaceLink"); - $scope.subnetChanged(nic); - expect(nic.mode).toBe("link_up"); - }); - - it("doesnt set mode to link_up if set if subnet", function() { - makeController(); - var nic = { - mode: "static", - subnet: { - statistics: { - first_address: "172.16.3.1" - } - } - }; - spyOn($scope, "saveInterfaceLink"); - $scope.subnetChanged(nic); - expect(nic.mode).toBe("static"); - }); - - it("clears ip_address", function() { - makeController(); - var nic = { - subnet: null, - ip_address: makeName("ip") - }; - spyOn($scope, "saveInterfaceLink"); - $scope.subnetChanged(nic); - expect(nic.ip_address).toBe(""); - }); - }); - - describe("subnetChangedForm", function() { - - it("sets mode to link_up if set to no subnet", function() { - makeController(); - var nic = { - getValue: function(name) { return this["_" + name];}, - updateValue: function(name, val) { this["_" + name] = val; }, - _subnet: null - }; - spyOn($scope, "saveInterfaceLink"); - $scope.subnetChangedForm('subnet', null, nic); - expect(nic._mode).toBe("link_up"); - }); - - it("doesnt set mode to link_up if set if subnet", function() { - makeController(); - var nic = { - getValue: function(name) { return this["_" + name];}, - updateValue: function(name, val) { this["_" + name] = val; }, - _mode: "static", - _subnet: {} - }; - spyOn($scope, "saveInterfaceLink"); - $scope.subnetChangedForm('subnet', {}, nic); - expect(nic._mode).toBe("static"); - }); - - it("clears ip_address", function() { - makeController(); - var nic = { - getValue: function(name) { return this["_" + name];}, - updateValue: function(name, val) { this["_" + name] = val; }, - _subnet: null, - _ip_address: makeName("ip") - }; - spyOn($scope, "saveInterfaceLink"); - $scope.subnetChangedForm('subnet', null, nic); - expect(nic._ip_address).toBe(""); - }); - }); - - describe("isIPAddressInvalid", function() { - - it("true if empty IP address", function() { - makeController(); - var nic = { - ip_address: "", - mode: "static" - }; - expect($scope.isIPAddressInvalid(nic)).toBe(true); - }); - - it("true if not valid IP address", function() { - makeController(); - var nic = { - ip_address: "192.168.260.5", - mode: "static" - }; - expect($scope.isIPAddressInvalid(nic)).toBe(true); - }); - - it("true if IP address not in subnet", function() { - makeController(); - var nic = { - ip_address: "192.168.123.10", - mode: "static", - subnet: { - cidr: "192.168.122.0/24" - } - }; - expect($scope.isIPAddressInvalid(nic)).toBe(true); - }); - - it("false if IP address in subnet", function() { - makeController(); - var nic = { - ip_address: "192.168.122.10", - mode: "static", - subnet: { - cidr: "192.168.122.0/24" - } - }; - expect($scope.isIPAddressInvalid(nic)).toBe(false); - }); - }); - - describe("getUniqueKey", function() { - - it("returns id + / + link_id", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - expect($scope.getUniqueKey(nic)).toBe(nic.id + "/" + nic.link_id); - }); - }); - - describe("toggleInterfaceSelect", function() { - - it("selects interface and enters single mode", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - var key = $scope.getUniqueKey(nic); - $scope.toggleInterfaceSelect(nic); - expect($scope.selectedInterfaces).toEqual([key]); - expect($scope.selectedMode).toBe("single"); - }); - - it("deselects interface and enters none mode", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - $scope.toggleInterfaceSelect(nic); - $scope.toggleInterfaceSelect(nic); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.selectedMode).toBeNull(); - }); - - it("selecting multiple enters multi mode", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - var nic2 = { - id: makeInteger(100, 200), - link_id: makeInteger(0, 100) - }; - var key1 = $scope.getUniqueKey(nic1); - var key2 = $scope.getUniqueKey(nic2); - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - expect($scope.selectedInterfaces).toEqual([key1, key2]); - expect($scope.selectedMode).toBe("multi"); - }); - }); - - describe("isInterfaceSelected", function() { - - it("returns true when selected", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - var key = $scope.getUniqueKey(nic); - $scope.selectedInterfaces = [key]; - expect($scope.isInterfaceSelected(nic)).toBe(true); - }); - - it("returns false when not selected", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - $scope.selectedInterfaces = []; - expect($scope.isInterfaceSelected(nic)).toBe(false); - }); - }); - - describe("cannotEditInterface", function() { - - it("returns true when only one selected", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - var key = $scope.getUniqueKey(nic); - $scope.selectedInterfaces = [key]; - expect($scope.cannotEditInterface(nic)).toBe(false); - }); - - it("returns false when multiple selected", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - var nic2 = { - id: makeInteger(100, 200), - link_id: makeInteger(0, 100) - }; - var key1 = $scope.getUniqueKey(nic1); - var key2 = $scope.getUniqueKey(nic2); - $scope.selectedInterfaces = [key1, key2]; - expect($scope.cannotEditInterface(nic1)).toBe(false); - }); - - it("returns false when not selected", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - $scope.selectedInterfaces = []; - expect($scope.cannotEditInterface(nic)).toBe(false); - }); - }); - - describe("isShowingDeleteConfirm", function() { - - it("returns true in delete mode", function() { - makeController(); - $scope.selectedMode = "delete"; - expect($scope.isShowingDeleteConfirm()).toBe(true); - }); - - it("returns false not in delete mode", function() { - makeController(); - $scope.selectedMode = "single"; - expect($scope.isShowingDeleteConfirm()).toBe(false); - }); - }); - - describe("isShowingAdd", function() { - - it("returns true in add mode", function() { - makeController(); - $scope.selectedMode = "add"; - expect($scope.isShowingAdd()).toBe(true); - }); - - it("returns false not in add mode", function() { - makeController(); - $scope.selectedMode = "delete"; - expect($scope.isShowingAdd()).toBe(false); - }); - }); - - describe("canAddAliasOrVLAN", function() { - - it("returns false if isController", function() { - makeController(); - $parentScope.isController = true; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); - spyOn($scope, "canAddAlias").and.returnValue(true); - spyOn($scope, "canAddVLAN").and.returnValue(true); - expect($scope.canAddAliasOrVLAN({})).toBe(false); - }); - - it("returns false if no node editing", function() { - makeController(); - $parentScope.isController = false; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(true); - spyOn($scope, "canAddAlias").and.returnValue(true); - spyOn($scope, "canAddVLAN").and.returnValue(true); - expect($scope.canAddAliasOrVLAN({})).toBe(false); - }); - - it("returns true if can edit alias", function() { - makeController(); - $parentScope.isController = false; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); - spyOn($scope, "canAddAlias").and.returnValue(true); - spyOn($scope, "canAddVLAN").and.returnValue(false); - expect($scope.canAddAliasOrVLAN({})).toBe(true); - }); - - it("returns true if can edit VLAN", function() { - makeController(); - $parentScope.isController = false; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); - spyOn($scope, "canAddAlias").and.returnValue(false); - spyOn($scope, "canAddVLAN").and.returnValue(true); - expect($scope.canAddAliasOrVLAN({})).toBe(true); - }); - }); - - describe("canAddAlias", function() { - - it("returns false if nic undefined", function() { - makeController(); - expect($scope.canAddAlias()).toBe(false); - }); - - it("returns false if nic type is alias", function() { - makeController(); - var nic = { - type: "alias" - }; - expect($scope.canAddAlias(nic)).toBe(false); - }); - - it("returns false if nic has no links", function() { - makeController(); - var nic = { - type: "physical", - links: [] - }; - expect($scope.canAddAlias(nic)).toBe(false); - }); - - it("returns false if nic has link_up", function() { - makeController(); - var nic = { - type: "physical", - links: [{ - mode: "link_up" - }] - }; - expect($scope.canAddAlias(nic)).toBe(false); - }); - - it("returns true if nic has dhcp", function() { - makeController(); - var nic = { - type: "physical", - links: [{ - mode: "dhcp" - }] - }; - expect($scope.canAddAlias(nic)).toBe(true); - }); - - it("returns true if nic has static", function() { - makeController(); - var nic = { - type: "physical", - links: [{ - mode: "static" - }] - }; - expect($scope.canAddAlias(nic)).toBe(true); - }); - - it("returns true if nic has auto", function() { - makeController(); - var nic = { - type: "physical", - links: [{ - mode: "auto" - }] - }; - expect($scope.canAddAlias(nic)).toBe(true); - }); - }); - - describe("canAddVLAN", function() { - - it("returns false if nic undefined", function() { - makeController(); - expect($scope.canAddVLAN()).toBe(false); - }); - - it("returns false if nic type is alias", function() { - makeController(); - var nic = { - type: "alias" - }; - expect($scope.canAddVLAN(nic)).toBe(false); - }); - - it("returns false if nic type is vlan", function() { - makeController(); - var nic = { - type: "vlan" - }; - expect($scope.canAddVLAN(nic)).toBe(false); - }); - - it("returns false if no unused vlans", function() { - makeController(); - var fabric = { - id: 0 - }; - var vlans = [ - { - id: 0, - fabric: 0 - }, - { - id: 1, - fabric: 0 - }, - { - id: 2, - fabric: 0 - } - ]; - var originalInterfaces = [ - { - id: 0, - type: "physical", - parents: [], - children: [1, 2, 3], - vlan_id: 0 - }, - { - id: 1, - type: "vlan", - parents: [0], - children: [], - vlan_id: 0 - }, - { - id: 2, - type: "vlan", - parents: [0], - children: [], - vlan_id: 1 - }, - { - id: 3, - type: "vlan", - parents: [0], - children: [], - vlan_id: 2 - } - ]; - var nic = { - id: 0, - type: "physical", - fabric: fabric - }; - $scope.originalInterfaces = originalInterfaces; - $scope.vlans = vlans; - expect($scope.canAddVLAN(nic)).toBe(false); - }); - - it("returns true if unused vlans", function() { - makeController(); - var fabric = { - id: 0 - }; - var vlans = [ - { - id: 0, - fabric: 0 - }, - { - id: 1, - fabric: 0 - }, - { - id: 2, - fabric: 0 - } - ]; - var originalInterfaces = [ - { - id: 0, - type: "physical", - parents: [], - children: [1, 2, 3], - vlan_id: 0 - }, - { - id: 1, - type: "vlan", - parents: [0], - children: [], - vlan_id: 0 - }, - { - id: 2, - type: "vlan", - parents: [0], - children: [], - vlan_id: 1 - } - ]; - var nic = { - id: 0, - type: "physical", - fabric: fabric - }; - $scope.originalInterfaces = originalInterfaces; - $scope.vlans = vlans; - expect($scope.canAddVLAN(nic)).toBe(true); - }); - }); - - describe("canAddAnotherVLAN", function() { - - it("returns false if canAddVLAN returns false", function() { - makeController(); - spyOn($scope, "canAddVLAN").and.returnValue(false); - expect($scope.canAddAnotherVLAN()).toBe(false); - }); - - it("returns false if only 1 unused vlans", function() { - makeController(); - var fabric = { - id: 0 - }; - var vlans = [ - { - id: 0, - fabric: 0 - }, - { - id: 1, - fabric: 0 - }, - { - id: 2, - fabric: 0 - } - ]; - var originalInterfaces = [ - { - id: 0, - type: "physical", - parents: [], - children: [1, 2, 3], - vlan_id: 0 - }, - { - id: 1, - type: "vlan", - parents: [0], - children: [], - vlan_id: 0 - }, - { - id: 2, - type: "vlan", - parents: [0], - children: [], - vlan_id: 1 - } - ]; - var nic = { - id: 0, - type: "physical", - fabric: fabric - }; - $scope.originalInterfaces = originalInterfaces; - $scope.vlans = vlans; - expect($scope.canAddAnotherVLAN(nic)).toBe(false); - }); - - it("returns true if more than 1 unused vlans", function() { - makeController(); - var fabric = { - id: 0 - }; - var vlans = [ - { - id: 0, - fabric: 0 - }, - { - id: 1, - fabric: 0 - }, - { - id: 2, - fabric: 0 - } - ]; - var originalInterfaces = [ - { - id: 0, - type: "physical", - parents: [], - children: [1, 2, 3], - vlan_id: 0 - }, - { - id: 1, - type: "vlan", - parents: [0], - children: [], - vlan_id: 0 - } - ]; - var nic = { - id: 0, - type: "physical", - fabric: fabric - }; - $scope.originalInterfaces = originalInterfaces; - $scope.vlans = vlans; - expect($scope.canAddAnotherVLAN(nic)).toBe(true); - }); - }); - - describe("getRemoveTypeText", function() { - - it("returns interface for physical interface", function() { - makeController(); - var nic = { - type: "physical" - }; - expect($scope.getRemoveTypeText(nic)).toBe("interface"); - }); - - it("returns VLAN for VLAN interface", function() { - makeController(); - var nic = { - type: "vlan" - }; - expect($scope.getRemoveTypeText(nic)).toBe("VLAN"); - }); - - it("returns type for other types", function() { - makeController(); - var type = makeName("type"); - var nic = { - type: type - }; - expect($scope.getRemoveTypeText(nic)).toBe(type); - }); - }); - - describe("canBeRemoved", function() { - - it("false if isController", function() { - makeController(); - $parentScope.isController = true; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); - expect($scope.canBeRemoved()).toBe(false); - }); - - it("false if no node editing", function() { - makeController(); - $parentScope.isController = false; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(true); - expect($scope.canBeRemoved()).toBe(false); - }); - - it("true if node can be edited", function() { - makeController(); - $parentScope.isController = false; - spyOn($scope, "isAllNetworkingDisabled").and.returnValue(false); - expect($scope.canBeRemoved()).toBe(true); - }); - }); - - describe("remove", function() { - - it("sets selectedMode to delete", function() { - makeController(); - $scope.remove(); - expect($scope.selectedMode).toBe("delete"); - }); - }); - - describe("quickRemove", function() { - - it("selects interface and sets selectedMode to delete", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - $scope.quickRemove(nic); - expect($scope.isInterfaceSelected(nic)).toBe(true); - expect($scope.selectedMode).toBe("delete"); - }); - }); - - describe("cancel", function() { - - it("clears newInterface and sets selectedMode to single", function() { - makeController(); - var newInterface = {}; - $scope.newInterface = newInterface; - $scope.selectedMode = "delete"; - $scope.cancel(); - expect($scope.newInterface).not.toBe(newInterface); - expect($scope.selectedMode).toBe("single"); - }); - - it("clears newInterface and create resets to none", function() { - makeController(); - var newInterface = {}; - $scope.newInterface = newInterface; - $scope.selectedMode = "create-physical"; - $scope.cancel(); - expect($scope.newInterface).not.toBe(newInterface); - expect($scope.selectedMode).toBeNull(); - }); - - it("clears newBondInterface and sets selectedMode to multi", - function() { - makeController(); - var newBondInterface = {}; - $scope.newBondInterface = newBondInterface; - $scope.selectedMode = "create-bond"; - $scope.cancel(); - expect($scope.newBondInterface).not.toBe(newBondInterface); - expect($scope.selectedMode).toBe("multi"); - }); - }); - - describe("confirmRemove", function() { - - it("sets selectedMode to none", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - type: "physical", - link_id: makeInteger(0, 100) - }; - $scope.toggleInterfaceSelect(nic); - $scope.selectedMode = "delete"; - - spyOn(MachinesManager, "deleteInterface"); - $scope.confirmRemove(nic); - - expect($scope.selectedMode).toBeNull(); - expect($scope.selectedInterfaces).toEqual([]); - }); - - it("calls MachinesManager.deleteInterface", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - type: "physical", - link_id: makeInteger(0, 100) - }; - $scope.toggleInterfaceSelect(nic); - $scope.selectedMode = "delete"; - - spyOn(MachinesManager, "deleteInterface"); - $scope.confirmRemove(nic); - - expect(MachinesManager.deleteInterface).toHaveBeenCalledWith( - node, nic.id); - }); - - it("calls MachinesManager.unlinkSubnet", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - type: "alias", - link_id: makeInteger(0, 100) - }; - $scope.toggleInterfaceSelect(nic); - $scope.selectedMode = "delete"; - - spyOn(MachinesManager, "unlinkSubnet"); - $scope.confirmRemove(nic); - - expect(MachinesManager.unlinkSubnet).toHaveBeenCalledWith( - node, nic.id, nic.link_id); - }); - - it("removes nic from interfaces", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - type: "alias", - link_id: makeInteger(0, 100) - }; - $scope.interfaces = [nic]; - $scope.toggleInterfaceSelect(nic); - $scope.selectedMode = "delete"; - - spyOn(MachinesManager, "unlinkSubnet"); - $scope.confirmRemove(nic); - - expect($scope.interfaces).toEqual([]); - }); - }); - - describe("add", function() { - - it("sets up newInterface for alias", function() { - makeController(); - var vlan = {id:0}; - var subnet = {id:0, vlan:0}; - $scope.subnets = [subnet]; - var nic = { - id: makeInteger(0, 100), - type: "physical", - link_id: makeInteger(0, 100), - vlan: vlan - }; - - $scope.add('alias', nic); - expect($scope.newInterface).toEqual({ - type: "alias", - vlan: vlan, - subnet: subnet, - mode: "auto", - parent: nic, - tags: [] - }); - expect($scope.newInterface.vlan).toBe(vlan); - expect($scope.newInterface.subnet).toBe(subnet); - expect($scope.newInterface.parent).toBe(nic); - expect($scope.selectedMode).toBe("add"); - }); - - it("sets up newInterface for vlan", function() { - makeController(); - var fabric = { - id: 0 - }; - var vlans = [ - { - id: 0, - fabric: 0 - }, - { - id: 1, - fabric: 0 - }, - { - id: 2, - fabric: 0 - } - ]; - var originalInterfaces = [ - { - id: 0, - type: "physical", - parents: [], - children: [1], - vlan_id: 0 - }, - { - id: 1, - type: "vlan", - parents: [0], - children: [], - vlan_id: 0 - } - ]; - var nic = { - id: 0, - type: "physical", - link_id: -1, - fabric: fabric, - vlan: vlans[0] - }; - $scope.originalInterfaces = originalInterfaces; - $scope.vlans = vlans; - $scope.newInterface = { - vlan: vlans[1] - }; - - $scope.add('vlan', nic); - expect($scope.newInterface).toEqual({ - type: "vlan", - vlan: vlans[2], - subnet: null, - mode: "link_up", - parent: nic, - tags: [] - }); - expect($scope.newInterface.vlan).toBe(vlans[2]); - expect($scope.newInterface.parent).toBe(nic); - expect($scope.selectedMode).toBe("add"); - }); - }); - - describe("quickAdd", function() { - - it("selects nic and calls add with alias", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - - $scope.selectedInterfaces = [{}, {}, {}]; - spyOn($scope, "canAddAlias").and.returnValue(true); - spyOn($scope, "add"); - - $scope.quickAdd(nic); - expect($scope.selectedInterfaces).toEqual( - [$scope.getUniqueKey(nic)]); - expect($scope.add).toHaveBeenCalledWith('alias', nic); - }); - - it("selects nic and calls add with vlan", function() { - makeController(); - var nic = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100) - }; - - $scope.selectedInterfaces = [{}, {}, {}]; - spyOn($scope, "canAddAlias").and.returnValue(false); - spyOn($scope, "add"); - - $scope.quickAdd(nic); - expect($scope.selectedInterfaces).toEqual( - [$scope.getUniqueKey(nic)]); - expect($scope.add).toHaveBeenCalledWith('vlan', nic); - }); - }); - - describe("getAddName", function() { - - it("returns alias name based on links length", function() { - makeController(); - var name = makeName("eth"); - var parent = { - id: makeInteger(0, 100), - name: name, - link_id: makeInteger(0, 100), - links: [{}, {}, {}] - }; - $scope.newInterface = { - type: "alias", - parent: parent - }; - - expect($scope.getAddName()).toBe(name + ":3"); - }); - - it("returns VLAN name based on VLAN vid", function() { - makeController(); - var name = makeName("eth"); - var vid = makeInteger(0, 100); - var parent = { - id: makeInteger(0, 100), - name: name, - link_id: makeInteger(0, 100) - }; - $scope.newInterface = { - type: "vlan", - parent: parent, - vlan: { - vid: vid - } - }; - - expect($scope.getAddName()).toBe(name + "." + vid); - }); - }); - - describe("addTypeChanged", function() { - - it("reset properties based on the new type alias", function() { - makeController(); - var vlan = {id:0}; - var subnet = {id:0, vlan:0}; - $scope.subnets = [subnet]; - var parent = { - id: makeInteger(0, 100), - name: name, - link_id: makeInteger(0, 100), - vlan: vlan - }; - $scope.newInterface = { - type: "alias", - parent: parent - }; - $scope.addTypeChanged(); - - expect($scope.newInterface.vlan).toBe(vlan); - expect($scope.newInterface.subnet).toBe(subnet); - expect($scope.newInterface.mode).toBe("auto"); - }); - - it("reset properties based on the new type VLAN", function() { - makeController(); - var fabric = { - id: 0 - }; - var vlans = [ - { - id: 0, - fabric: 0 - }, - { - id: 1, - fabric: 0 - }, - { - id: 2, - fabric: 0 - } - ]; - var originalInterfaces = [ - { - id: 0, - type: "physical", - parents: [], - children: [1], - vlan_id: 0 - }, - { - id: 1, - type: "vlan", - parents: [0], - children: [], - vlan_id: 0 - } - ]; - var parent = { - id: 0, - type: "physical", - link_id: -1, - fabric: fabric, - vlan: vlans[0] - }; - - $scope.originalInterfaces = originalInterfaces; - $scope.vlans = vlans; - $scope.newInterface = { - type: "vlan", - parent: parent - }; - $scope.addTypeChanged(); - - expect($scope.newInterface.vlan).toBe(vlans[1]); - expect($scope.newInterface.subnet).toBeNull(); - expect($scope.newInterface.mode).toBe("link_up"); - }); - }); - - describe("vlanChanged", function() { - - it("clears subnets on newInterface", function() { - makeController(); - $scope.newInterface = { - subnet: {} - }; - $scope.vlanChanged($scope.newInterface); - expect($scope.newInterface.subnet).toBeNull(); - }); - }); - - describe("vlanChangedForm", function() { - - it("clears subnets on newInterface", function() { - makeController(); - $scope.newInterface = { - getValue: function(name) { return this["_" + name];}, - updateValue: function(name, val) { this["_" + name] = val; }, - _subnet: {} - }; - $scope.vlanChangedForm('vlan', {}, $scope.newInterface); - expect($scope.newInterface._subnet).toBeNull(); - }); - }); - - describe("subnetChanged", function() { - - it("sets mode to link_up if no subnet", function() { - makeController(); - $scope.newInterface = { - mode: "auto" - }; - $scope.subnetChanged($scope.newInterface); - expect($scope.newInterface.mode).toBe("link_up"); - }); - - it("leaves mode to alone when subnet", function() { - makeController(); - $scope.newInterface = { - mode: "auto", - subnet: {} - }; - $scope.subnetChanged($scope.newInterface); - expect($scope.newInterface.mode).toBe("auto"); - }); - }); - - describe("addInterface", function() { - - it("calls saveInterfaceLink with correct params", function() { - makeController(); - var parent = { - id: makeInteger(0, 100) - }; - var subnet = {}; - $scope.newInterface = { - type: "alias", - mode: "auto", - subnet: subnet, - parent: parent - }; - $scope.selectedInterfaces = [{}]; - $scope.selectedMode = "add"; - spyOn($scope, "saveInterfaceLink"); - $scope.addInterface(); - expect($scope.saveInterfaceLink).toHaveBeenCalledWith({ - id: parent.id, - mode: "auto", - subnet: subnet, - ip_address: undefined - }); - expect($scope.selectedMode).toBeNull(); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.newInterface).toEqual({}); - }); - - it("calls createVLANInterface with correct params", function() { - makeController(); - var parent = { - id: makeInteger(0, 100) - }; - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - $scope.newInterface = { - type: "vlan", - mode: "auto", - parent: parent, - tags: [], - vlan: vlan, - subnet: subnet, - ip_address: undefined - }; - $scope.selectedInterfaces = [{}]; - $scope.selectedMode = "add"; - spyOn(MachinesManager, "createVLANInterface").and.returnValue( - $q.defer().promise); - - $scope.addInterface(); - expect(MachinesManager.createVLANInterface).toHaveBeenCalledWith( - node, { - parent: parent.id, - tags: [], - vlan: vlan.id, - mode: "auto", - subnet: subnet.id, - ip_address: undefined - }); - expect($scope.selectedMode).toBeNull(); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.newInterface).toEqual({}); - }); - - it("calls add again with type", function() { - makeController(); - var parent = { - id: makeInteger(0, 100) - }; - $scope.newInterface = { - type: "alias", - mode: "auto", - subnet: {}, - parent: parent - }; - var selection = [{}]; - $scope.selectedInterfaces = selection; - $scope.selectedMode = "add"; - spyOn($scope, "saveInterfaceLink"); - spyOn($scope, "add"); - $scope.addInterface("alias"); - - expect($scope.add).toHaveBeenCalledWith("alias", parent); - expect($scope.selectedMode).toBe("add"); - expect($scope.selectedInterfaces).toBe(selection); - }); - }); - - describe("isDisabled", function() { - - it("returns false when in none, single, or multi mode", function() { - makeController(); - $scope.canEdit = function() { return true; }; - // Node needs to be Ready or Broken for the mode to be considered. - $scope.node = {status: "Ready"}; - $scope.selectedMode = null; - expect($scope.isDisabled()).toBe(false); - $scope.selectedMode = "single"; - expect($scope.isDisabled()).toBe(false); - $scope.selectedMode = "multi"; - expect($scope.isDisabled()).toBe(false); - }); - - it("returns true when in delete, add, or create modes", function() { - makeController(); - $scope.canEdit = function() { return true; }; - // Node needs to be Ready or Broken for the mode to be considered. - $scope.node = {status: "Ready"}; - $scope.selectedMode = "create-bond"; - expect($scope.isDisabled()).toBe(true); - $scope.selectedMode = "add"; - expect($scope.isDisabled()).toBe(true); - $scope.selectedMode = "delete"; - expect($scope.isDisabled()).toBe(true); - }); - - it("returns true when the node state is not 'Ready' or 'Broken'", - function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node = {status: "Ready"}; - expect($scope.isDisabled()).toBe(false); - $scope.node = {status: "Broken"}; - expect($scope.isDisabled()).toBe(false); - ["New", - "Commissioning", - "Failed commissioning", - "Missing", - "Reserved", - "Allocated", - "Deploying", - "Deployed", - "Retired", - "Failed deployment", - "Releasing", - "Releasing failed", - "Disk erasing", - "Failed disk erasing"].forEach(function (s) { - $scope.node = {state: s}; - expect($scope.isDisabled()).toBe(true); - }); - }); - - it("returns true if the user is not a superuser", function() { - makeController(); - $scope.canEdit = function() { return false; }; - $scope.node = {status: "Ready"}; - expect($scope.isDisabled()).toBe(true); - $scope.node = {status: "Broken"}; - expect($scope.isDisabled()).toBe(true); - }); - }); - - describe("isLimitedEditingAllowed", function() { - - it("returns false when not superuser", function() { - makeController(); - $scope.canEdit = function() { return false; }; - expect($scope.isLimitedEditingAllowed()).toBe(false); - }); - - it("returns false when isController", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $parentScope.isController = true; - expect($scope.isLimitedEditingAllowed()).toBe(false); - }); - - it("returns true when deployed and not vlan", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $parentScope.isController = false; - $scope.node = { - status: "Deployed" - }; - var nic = { - type: "physical" - }; - expect($scope.isLimitedEditingAllowed(nic)).toBe(true); - }); - }); - - describe("isAllNetworkingDisabled", function() { - - it("returns true if the user is not a superuser " + - "and the node is not a device", - function() { - makeController(); - $parentScope.isDevice = false; - $scope.canEdit = function() { return false; }; - expect($scope.isAllNetworkingDisabled()).toBe(true); - }); - - it("returns false if the user is not a superuser " + - "and the node is not a device", - function() { - makeController(); - $parentScope.isDevice = true; - $scope.canEdit = function() { return false; }; - expect($scope.isAllNetworkingDisabled()).toBe(false); - }); - - it("returns false when a non-controller node state " + - "is 'New', 'Ready', 'Allocated' or 'Broken' and we are a superuser", - function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node = {status: "New"}; - expect($scope.isAllNetworkingDisabled()).toBe(false); - $scope.node = {status: "Ready"}; - expect($scope.isAllNetworkingDisabled()).toBe(false); - $scope.node = {status: "Allocated"}; - expect($scope.isAllNetworkingDisabled()).toBe(false); - $scope.node = {status: "Broken"}; - expect($scope.isAllNetworkingDisabled()).toBe(false); - ["Commissioning", - "Failed commissioning", - "Missing", - "Reserved", - "Allocated", - "Deploying", - "Deployed", - "Retired", - "Failed deployment", - "Releasing", - "Releasing failed", - "Disk erasing", - "Failed disk erasing"].forEach(function (s) { - $scope.node = {state: s}; - expect($scope.isAllNetworkingDisabled()).toBe(true); - }); - }); - - it("returns false for controllers, in any state, even if superuser", - function() { - makeController(); - $parentScope.isController = true; - $scope.canEdit = function() { return true; }; - ["Ready", - "Broken", - "New", - "Commissioning", - "Failed commissioning", - "Missing", - "Reserved", - "Allocated", - "Deploying", - "Deployed", - "Retired", - "Failed deployment", - "Releasing", - "Releasing failed", - "Disk erasing", - "Failed disk erasing"].forEach(function (s) { - $scope.node = {state: s}; - expect($scope.isAllNetworkingDisabled()).toBe(false); - }); - }); - }); - - describe("canCreateBond", function() { - - it("returns false if not in multi mode", function() { - makeController(); - var modes = [null, "add", "delete", "single", "delete"]; - angular.forEach(modes, function(mode) { - $scope.selectedMode = mode; - expect($scope.canCreateBond()).toBe(false); - }); - }); - - it("returns false if selected interface is bond", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "bond" - }; - var nic2 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "bond" - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - expect($scope.canCreateBond()).toBe(false); - }); - - it("returns false if selected interface is alias", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "alias" - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "alias" - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - expect($scope.canCreateBond()).toBe(false); - }); - - it("returns false if not same selected vlan", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: {} - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: {} - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - expect($scope.canCreateBond()).toBe(false); - }); - - it("returns true if same selected vlan", function() { - makeController(); - var vlan = {}; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - expect($scope.canCreateBond()).toBe(true); - }); - }); - - describe("isShowingCreateBond", function() { - - it("returns true in create-bond mode", function() { - makeController(); - $scope.selectedMode = "create-bond"; - expect($scope.isShowingCreateBond()).toBe(true); - }); - - it("returns false in multi mode", function() { - makeController(); - $scope.selectedMode = "multi"; - expect($scope.isShowingCreateBond()).toBe(false); - }); - }); - - describe("showCreateBond", function() { - - it("sets mode to create-bond", function() { - makeController(); - $scope.selectedMode = "multi"; - spyOn($scope, "canCreateBond").and.returnValue(true); - $scope.showCreateBond(); - expect($scope.selectedMode).toBe("create-bond"); - }); - - it("creates the newBondInterface", function() { - makeController(); - var vlan = {}; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - $scope.showCreateBond(); - expect($scope.newBondInterface).toEqual({ - name: "bond0", - parents: [nic1, nic2], - primary: nic1, - tags: [], - mac_address: "", - bond_mode: "active-backup", - fabric: '', - vlan: {}, - subnet: '', - lacpRate: "fast", - xmitHashPolicy: "layer2", - bond_updelay: 0, - bond_downdelay: 0, - bond_miimon: 100 - }); - }); - }); - - describe("toggleInterfaces", function() { - - it("sets isShowingInterfaces to false if showing", function() { - makeController(); - $scope.isShowingInterfaces = true; - $scope.toggleInterfaces(); - expect($scope.isShowingInterfaces).toBe(false); - }); - - it("sets isShowingInterfaces to true if not showing", function() { - makeController(); - $scope.isShowingInterfaces = false; - $scope.toggleInterfaces(); - expect($scope.isShowingInterfaces).toBe(true); - }); - }); - - describe("isCorrectInterfaceType", function() { - var bondInterface = { - id: 35, - type: 'physical' - }; - - var parents = [ - { - id: 34, - type: 'physical' - } - ]; - - it("returns true if not parent & type is same as parent", function() { - makeController(); - expect( - $scope.isCorrectInterfaceType(bondInterface, parents) - ).toBe(true); - }); - - it("returns false if parent", function() { - makeController(); - - bondInterface.id = 34; - - expect( - $scope.isCorrectInterfaceType(bondInterface, parents) - ).toBe(false); - }); - - it("returns false is type is not same as parent", function() { - makeController(); - - bondInterface.id = 33; - bondInterface.type = 'vlan'; - - expect( - $scope.isCorrectInterfaceType(bondInterface, parents) - ).toBe(false); - }); - }); - - describe("hasBootInterface", function() { - - it("returns false if bond has no members with is_boot", function() { - makeController(); - $scope.newBondInterface = { - parents: [ - { - is_boot: false - }, - { - is_boot: false - } - ] - }; - expect( - $scope.hasBootInterface($scope.newBondInterface)).toBe(false); - }); - - it("returns true if bond has member with is_boot", function() { - makeController(); - $scope.newBondInterface = { - parents: [ - { - is_boot: false - }, - { - is_boot: true - } - ] - }; - expect( - $scope.hasBootInterface($scope.newBondInterface)).toBe(true); - }); - }); - - describe("getInterfacePlaceholderMACAddress", function() { - - it("returns empty string if primary not set", function() { - makeController(); - expect($scope.getInterfacePlaceholderMACAddress({})).toBe(""); - }); - - it("returns the MAC address of the primary interface", function() { - makeController(); - var mac_address = makeName("mac"); - $scope.newBondInterface.primary = { - mac_address: mac_address - }; - expect( - $scope.getInterfacePlaceholderMACAddress( - $scope.newBondInterface)).toBe(mac_address); - }); - }); - - describe("isMACAddressInvalid", function() { - - it("returns false when the mac_address blank and not invalidEmpty", - function() { - makeController(); - expect($scope.isMACAddressInvalid("")).toBe(false); - }); - - it("returns truw when the mac_address is blank and invalidEmpty", - function() { - makeController(); - expect($scope.isMACAddressInvalid("", true)).toBe(true); - }); - - it("returns false if valid mac_address", function() { - makeController(); - expect($scope.isMACAddressInvalid("00:11:22:33:44:55")).toBe(false); - }); - - it("returns true if invalid mac_address", function() { - makeController(); - expect($scope.isMACAddressInvalid("00:11:22:33:44")).toBe(true); - }); - }); - - describe("showLACPRate", function() { - - it("returns true if in 802.3ad mode", function() { - makeController(); - $scope.newBondInterface.bond_mode = "802.3ad"; - expect($scope.showLACPRate()).toBe(true); - }); - - it("returns false if not in 802.3ad mode", function() { - makeController(); - $scope.newBondInterface.bond_mode = makeName("otherMode"); - expect($scope.showLACPRate()).toBe(false); - }); - }); - - describe("modeAndPolicyCompliant", function() { - - it("returns true if policy is layer3+4", function() { - makeController(); - $scope.newBondInterface.bond_mode = "802.3ad"; - $scope.newBondInterface.xmitHashPolicy = "layer3+4"; - expect($scope.showLACPRate()).toBe(true); - }); - - it("returns true if policy is encap3+4", function() { - makeController(); - $scope.newBondInterface.bond_mode = "802.3ad"; - $scope.newBondInterface.xmitHashPolicy = "encap3+4"; - expect($scope.showLACPRate()).toBe(true); - }); - - it("returns false if 802.3ad compliant", function() { - makeController(); - $scope.editInterface = { - id: 0, - link_id: -1 - }; - $scope.newBondInterface.bond_mode = "802.3ad"; - $scope.newBondInterface.xmitHashPolicy = "layer2+3"; - expect($scope.showLACPRate()).toBe(false); - }); - }); - - describe("showXMITHashPolicy", function() { - - it("returns true if in balance-xor mode", function() { - makeController(); - $scope.newBondInterface.bond_mode = "balance-xor"; - expect($scope.showXMITHashPolicy()).toBe(true); - }); - - it("returns true if in 802.3ad mode", function() { - makeController(); - $scope.newBondInterface.bond_mode = "802.3ad"; - expect($scope.showXMITHashPolicy()).toBe(true); - }); - - it("returns true if in balance-tlb mode", function() { - makeController(); - $scope.newBondInterface.bond_mode = "balance-tlb"; - expect($scope.showXMITHashPolicy()).toBe(true); - }); - - it("returns false if not in other modes", function() { - makeController(); - $scope.newBondInterface.bond_mode = makeName("otherMode"); - expect($scope.showXMITHashPolicy()).toBe(false); - }); - }); - - describe("cannotAddBond", function() { - - it("returns true when isInterfaceNameInvalid is true", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(true); - expect($scope.cannotAddBond()).toBe(true); - }); - - it("returns true when isMACAddressInvalid is true", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); - spyOn($scope, "isMACAddressInvalid").and.returnValue(true); - expect($scope.cannotAddBond()).toBe(true); - }); - - it("returns false when both are false", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); - spyOn($scope, "isMACAddressInvalid").and.returnValue(false); - expect($scope.cannotAddBond()).toBe(false); - }); - }); - - describe("addBond", function() { - - it("does nothing if cannotAddBond returns true", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - $scope.showCreateBond(); - - spyOn(MachinesManager, "createBondInterface").and.returnValue( - $q.defer().promise); - spyOn($scope, "cannotAddBond").and.returnValue(true); - $scope.newBondInterface.name = "bond0"; - $scope.newBondInterface.mac_address = "00:11:22:33:44:55"; - - $scope.addBond(); - expect(MachinesManager.createBondInterface).not.toHaveBeenCalled(); - }); - - it("calls createBondInterface and removes selection", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - $scope.showCreateBond(); - - spyOn(MachinesManager, "createBondInterface").and.returnValue( - $q.defer().promise); - spyOn($scope, "cannotAddBond").and.returnValue(false); - $scope.newBondInterface.name = "bond0"; - $scope.newBondInterface.mac_address = "00:11:22:33:44:55"; - $scope.newBondInterface.vlan = vlan; - $scope.newBondInterface.subnet = subnet; - $scope.newBondInterface.mode = "static"; - $scope.newBondInterface.ip_address = "192.168.1.100"; - $scope.addBond(); - - expect(MachinesManager.createBondInterface).toHaveBeenCalledWith( - node, { - name: "bond0", - mac_address: "00:11:22:33:44:55", - tags: [], - parents: [nic1.id, nic2.id], - bond_mode: "active-backup", - bond_lacp_rate: "fast", - bond_xmit_hash_policy: "layer2", - vlan: vlan.id, - subnet: subnet.id, - mode: "static", - ip_address: "192.168.1.100", - bond_miimon: 100, - bond_updelay: 0, - bond_downdelay: 0 - }); - expect($scope.newBondInterface).toEqual({}); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.selectedMode).toBeNull(); - }); - - it("calls createBondInterface even when disconnected", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: null - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: null - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - $scope.showCreateBond(); - - spyOn(MachinesManager, "createBondInterface").and.returnValue( - $q.defer().promise); - spyOn($scope, "cannotAddBond").and.returnValue(false); - $scope.newBondInterface.name = "bond0"; - $scope.newBondInterface.mac_address = "00:11:22:33:44:55"; - $scope.addBond(); - - expect(MachinesManager.createBondInterface).toHaveBeenCalledWith( - node, { - name: "bond0", - mac_address: "00:11:22:33:44:55", - tags: [], - parents: [nic1.id, nic2.id], - bond_mode: "active-backup", - bond_lacp_rate: "fast", - bond_xmit_hash_policy: "layer2", - vlan: undefined, - subnet: null, - mode: undefined, - ip_address: undefined, - bond_miimon: 100, - bond_updelay: 0, - bond_downdelay: 0 - }); - expect($scope.newBondInterface).toEqual({}); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.selectedMode).toBeNull(); - }); - }); - - describe("canCreateBridge", function() { - - it("returns false if not in single mode", function() { - makeController(); - var modes = [null, "add", "delete", "multi", "delete"]; - angular.forEach(modes, function(mode) { - $scope.selectedMode = mode; - expect($scope.canCreateBridge()).toBe(false); - }); - }); - - it("returns false if selected interface is bridge", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "bridge" - }; - $scope.interfaces = [nic1]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.toggleInterfaceSelect(nic1); - expect($scope.canCreateBridge()).toBe(false); - }); - - it("returns false if selected interface is alias", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "alias" - }; - $scope.interfaces = [nic1]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.toggleInterfaceSelect(nic1); - expect($scope.canCreateBridge()).toBe(false); - }); - - it("returns false if muliple selected", function() { - makeController(); - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: {} - }; - var nic2 = { - id: makeInteger(101, 200), - link_id: makeInteger(0, 100), - type: "physical", - vlan: {} - }; - $scope.interfaces = [nic1, nic2]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.interfaceLinksMap[nic2.id] = {}; - $scope.interfaceLinksMap[nic2.id][nic2.link_id] = nic2; - $scope.toggleInterfaceSelect(nic1); - $scope.toggleInterfaceSelect(nic2); - expect($scope.canCreateBridge()).toBe(false); - }); - - it("returns true if selected", function() { - makeController(); - var vlan = {}; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.toggleInterfaceSelect(nic1); - expect($scope.canCreateBridge()).toBe(true); - }); - }); - - describe("isShowingCreateBridge", function() { - - it("returns true in create-bridge mode", function() { - makeController(); - $scope.selectedMode = "create-bridge"; - expect($scope.isShowingCreateBridge()).toBe(true); - }); - - it("returns false in single mode", function() { - makeController(); - $scope.selectedMode = "single"; - expect($scope.isShowingCreateBridge()).toBe(false); - }); - }); - - describe("isShowingEdit", function() { - - it("returns true in edit mode", function() { - makeController(); - $scope.selectedMode = "edit"; - expect($scope.isShowingEdit()).toBe(true); - }); - - it("returns false in single mode", function() { - makeController(); - $scope.selectedMode = "single"; - expect($scope.isShowingEdit()).toBe(false); - }); - }); - - describe("showCreateBridge", function() { - - it("sets mode to create-bridge", function() { - makeController(); - $scope.selectedMode = "single"; - spyOn($scope, "canCreateBridge").and.returnValue(true); - $scope.showCreateBridge(); - expect($scope.selectedMode).toBe("create-bridge"); - }); - - it("creates the newBridgeInterface", function() { - makeController(); - var vlan = {}; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.toggleInterfaceSelect(nic1); - $scope.showCreateBridge(); - expect($scope.newBridgeInterface).toEqual({ - name: "br0", - parents: [nic1], - primary: nic1, - tags: [], - mac_address: "", - vlan: {}, - fabric: '', - bridge_stp: false, - bridge_fd: 15 - }); - }); - }); - - describe("cannotAddBridge", function() { - - it("returns true when isInterfaceNameInvalid is true", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(true); - expect($scope.cannotAddBridge()).toBe(true); - }); - - it("returns true when isMACAddressInvalid is true", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); - spyOn($scope, "isMACAddressInvalid").and.returnValue(true); - expect($scope.cannotAddBridge()).toBe(true); - }); - - it("returns false when both are false", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); - spyOn($scope, "isMACAddressInvalid").and.returnValue(false); - expect($scope.cannotAddBridge()).toBe(false); - }); - }); - - describe("addBridge", function() { - - it("does nothing if cannotAddBridge returns true", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan - }; - $scope.interfaces = [nic1]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.toggleInterfaceSelect(nic1); - $scope.showCreateBridge(); - - spyOn(MachinesManager, "createBridgeInterface").and.returnValue( - $q.defer().promise); - spyOn($scope, "cannotAddBridge").and.returnValue(true); - $scope.newBridgeInterface.name = "br0"; - $scope.newBridgeInterface.mac_address = "00:11:22:33:44:55"; - - $scope.addBridge(); - expect( - MachinesManager.createBridgeInterface).not.toHaveBeenCalled(); - }); - - it("calls createBridgeInterface and removes selection", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - var fabric = { - id: makeInteger(0, 100) - }; - var nic1 = { - id: makeInteger(0, 100), - link_id: makeInteger(0, 100), - type: "physical", - vlan: vlan, - fabric: fabric - }; - - $scope.interfaces = [nic1]; - $scope.interfaceLinksMap = {}; - $scope.interfaceLinksMap[nic1.id] = {}; - $scope.interfaceLinksMap[nic1.id][nic1.link_id] = nic1; - $scope.toggleInterfaceSelect(nic1); - $scope.showCreateBridge(); - - spyOn(MachinesManager, "createBridgeInterface").and.returnValue( - $q.defer().promise); - spyOn($scope, "cannotAddBridge").and.returnValue(false); - $scope.newBridgeInterface.name = "br0"; - $scope.newBridgeInterface.mac_address = "00:11:22:33:44:55"; - $scope.newBridgeInterface.vlan = vlan; - $scope.newBridgeInterface.subnet = subnet; - $scope.newBridgeInterface.mode = "static"; - $scope.newBridgeInterface.ip_address = "192.168.1.100"; - $scope.addBridge(); - - expect(MachinesManager.createBridgeInterface).toHaveBeenCalledWith( - node, { - name: "br0", - mac_address: "00:11:22:33:44:55", - tags: [], - parents: [nic1.id], - bridge_stp: false, - bridge_fd: 15, - vlan: vlan.id, - subnet: subnet.id, - mode: "static", - ip_address: "192.168.1.100" - }); - expect($scope.interfaces).toEqual([]); - expect($scope.newBridgeInterface).toEqual({}); - expect($scope.selectedInterfaces).toEqual([]); - expect($scope.selectedMode).toBeNull(); - }); - }); - - describe("isShowingCreatePhysical", function() { - - it("returns true in create-physical mode", function() { - makeController(); - $scope.selectedMode = "create-physical"; - expect($scope.isShowingCreatePhysical()).toBe(true); - }); - - it("returns false in single mode", function() { - makeController(); - $scope.selectedMode = "single"; - expect($scope.isShowingCreatePhysical()).toBe(false); - }); - }); - - describe("showCreatePhysical", function() { - - it("sets mode to create-physical", function() { - makeController(); - var vlan = { id: 0, fabric: 0 }; - var fabric = { - id: 0, name: makeName("fabric"), - default_vlan_id: 0, vlan_ids: [0] - }; - VLANsManager._items = [vlan]; - $scope.fabrics = [fabric]; - $scope.selectedMode = null; - $scope.showCreatePhysical(); - expect($scope.selectedMode).toBe("create-physical"); - }); - - it("creates the newInterface", function() { - makeController(); - var vlan = { id: 0, fabric: 0 }; - var fabric = { - id: 0, name: makeName("fabric"), - default_vlan_id: 0, vlan_ids: [0] - }; - VLANsManager._items = [vlan]; - $scope.fabrics = [fabric]; - $scope.selectedMode = null; - $scope.showCreatePhysical(); - expect($scope.newInterface).toEqual({ - name: "eth0", - mac_address: "", - macError: false, - tags: [], - errorMsg: null, - fabric: fabric, - vlan: vlan, - subnet: null, - mode: "link_up" - }); - }); - }); - - describe("fabricChanged", function() { - - it("sets newInterface.vlan with new fabric", function() { - makeController(); - var vlan = { id: 0, fabric: 0 }; - var fabric = { - id: 0, name: makeName("fabric"), - default_vlan_id: 0, vlan_ids: [0] - }; - VLANsManager._items = [vlan]; - $scope.newInterface.fabric = fabric; - $scope.newInterface.subnet = {}; - $scope.newInterface.mode = "auto"; - $scope.fabricChanged($scope.newInterface); - expect($scope.newInterface.vlan).toBe(vlan); - expect($scope.newInterface.subnet).toBeNull(); - expect($scope.newInterface.mode).toBe("link_up"); - }); - }); - - describe("fabricChangedForm", function() { - - it("sets newInterface.vlan with new fabric", function() { - makeController(); - var vlan = { id: 0, fabric: 0 }; - var fabric = { - id: 0, name: makeName("fabric"), - default_vlan_id: 0, vlan_ids: [0] - }; - VLANsManager._items = [vlan]; - $scope.newInterface._fabric = fabric; - $scope.newInterface._subnet = {}; - $scope.newInterface._mode = "auto"; - $scope.newInterface.getValue = function(name) { - return this["_" + name];}; - $scope.newInterface.updateValue = function(name, val) { - this["_" + name] = val; }; - $scope.fabricChangedForm('fabric', fabric, $scope.newInterface); - expect($scope.newInterface._vlan).toBe(vlan); - expect($scope.newInterface._subnet).toBeNull(); - expect($scope.newInterface._mode).toBe("link_up"); - }); - }); - - describe("subnetChanged", function() { - - it("sets mode to link_up when no subnet", function() { - makeController(); - $scope.newInterface.subnet = null; - $scope.newInterface.mode = "auto"; - $scope.subnetChanged($scope.newInterface); - expect($scope.newInterface.mode).toBe("link_up"); - }); - - it("leaves mode to original when subnet", function() { - makeController(); - $scope.newInterface.subnet = {}; - $scope.newInterface.mode = "auto"; - $scope.subnetChanged($scope.newInterface); - expect($scope.newInterface.mode).toBe("auto"); - }); - }); - - describe("subnetChangedForm", function() { - - it("sets mode to link_up when no subnet", function() { - makeController(); - $scope.newInterface = { - getValue: function(name) { return this["_" + name];}, - updateValue: function(name, val) { this["_" + name] = val; }, - _subnet: null, - _mode: "auto" - }; - $scope.subnetChangedForm("subnet", null, $scope.newInterface); - expect($scope.newInterface._mode).toBe("link_up"); - }); - - it("leaves mode to original when subnet", function() { - makeController(); - $scope.newInterface = { - getValue: function(name) { return this["_" + name];}, - updateValue: function(name, val) { this["_" + name] = val; }, - _subnet: {}, - _mode: "auto" - }; - $scope.subnetChangedForm("subnet", {}, $scope.newInterface); - expect($scope.newInterface._mode).toBe("auto"); - }); - }); - - describe("cannotAddPhysicalInterface", function() { - - it("returns true when isInterfaceNameInvalid is true", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(true); - expect($scope.cannotAddPhysicalInterface()).toBe(true); - }); - - it("returns true when isMACAddressInvalid is true", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); - spyOn($scope, "isMACAddressInvalid").and.returnValue(true); - expect($scope.cannotAddPhysicalInterface()).toBe(true); - }); - - it("returns false when both are false", function() { - makeController(); - spyOn($scope, "isInterfaceNameInvalid").and.returnValue(false); - spyOn($scope, "isMACAddressInvalid").and.returnValue(false); - expect($scope.cannotAddPhysicalInterface()).toBe(false); - }); - }); - - describe("addPhysicalInterface", function() { - - it("does nothing if cannotAddInterface returns true", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - $scope.newInterface = { - name: "eth0", - mac_address: "00:11:22:33:44:55", - tags: [], - vlan: vlan, - subnet: subnet, - mode: "auto" - }; - - spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( - $q.defer().promise); - spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(true); - $scope.addPhysicalInterface(); - - expect( - MachinesManager.createPhysicalInterface).not.toHaveBeenCalled(); - }); - - it("calls createPhysicalInterface and removes selection", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - $scope.newInterface = { - name: "eth0", - mac_address: "00:11:22:33:44:55", - tags: [], - vlan: vlan, - subnet: subnet, - mode: "auto" - }; - $scope.selectedMode = "create-physical"; - - var defer = $q.defer(); - spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( - defer.promise); - spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(false); - $scope.addPhysicalInterface(); - defer.resolve(); - $scope.$digest(); - - expect( - MachinesManager.createPhysicalInterface).toHaveBeenCalledWith( - node, { - name: "eth0", - mac_address: "00:11:22:33:44:55", - tags: [], - vlan: vlan.id, - subnet: subnet.id, - mode: "auto", - ip_address: undefined - }); - expect($scope.newInterface).toEqual({}); - expect($scope.selectedMode).toBeNull(); - }); - - it("clears error on call", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - $scope.newInterface = { - name: "eth0", - mac_address: "00:11:22:33:44:55", - tags: [], - vlan: vlan, - subnet: subnet, - mode: "auto", - macError: true, - errorMsg: "error" - }; - - var defer = $q.defer(); - spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( - defer.promise); - spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(false); - $scope.addPhysicalInterface(); - - expect($scope.newInterface.macError).toBe(false); - expect($scope.newInterface.errorMsg).toBeNull(); - }); - - it("handles mac_address error", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var subnet = { - id: makeInteger(0, 100) - }; - $scope.newInterface = { - name: "eth0", - mac_address: "00:11:22:33:44:55", - tags: [], - vlan: vlan, - subnet: subnet, - mode: "auto" - }; - - var defer = $q.defer(); - spyOn(MachinesManager, "createPhysicalInterface").and.returnValue( - defer.promise); - spyOn($scope, "cannotAddPhysicalInterface").and.returnValue(false); - $scope.addPhysicalInterface(); - - var error = { - "mac_address": ["MACAddress is already in use"] - }; - defer.reject(angular.toJson(error)); - $scope.$digest(); - - expect($scope.newInterface.macError).toBe(true); - expect($scope.newInterface.errorMsg).toBe( - "MACAddress is already in use"); - }); - }); - - describe("isBond", function() { - it("returns true if has type of bond", function() { - makeController(); - var item = { type: "bond" }; - expect($scope.isBond(item)).toBe(true); - }); - - it("returns false if doesn't have type of bond", function() { - makeController(); - var item = {type: "physical" }; - expect($scope.isBond(item)).toBe(false); - }); - }); - - describe("showEditButton", function() { - it("returns true if all conditions are met", function() { - makeController(); - - var editInterface = { - id: "foo", - type: "bond", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - }, - members: [] - }; - - var interfaces = [ - { - id: "foo", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - }, - { - id: "bar", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - }, - { - id: "baz", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - } - ]; - - expect($scope.showEditButton(editInterface, interfaces)).toBe(true); - }); - - it("returns false if editInterface is not bond", function() { - makeController(); - var editInterface = { id: "foo", type: "physical", fabric: { - name: "fabric-1" - }}; - var interfaces = [ - { id: "foo", fabric: { name: "fabric-1" }}, - { id: "bar", fabric: { name: "fabric-1" }}, - { id: "baz", fabric: { name: "fabric-1" }} - ]; - expect($scope.showEditButton(editInterface, interfaces)) - .toBe(false); - }); - - it("returns false if no interfaces", function() { - makeController(); - var editInterface = { - id: "foo", - type: "bond", - fabric: { - name: "fabric-1" - }, - members: [] - }; - var interfaces = []; - expect($scope.showEditButton(editInterface, interfaces)) - .toBe(false); - }); - }); - - describe("showCreateEditButton", function() { - it("returns true if interfaces exist", function() { - makeController(); - $scope.selectedInterfaces = []; - $scope.newBondInterface = { - id: "bar", - fabric: { - name: "fabric-1" - }, - vlan: { - id: 2 - } - }; - $scope.interfaces = [{ id: "foo", fabric: { name: "fabric-1" }, - vlan: { id: 2}}]; - expect($scope.showCreateEditButton()).toBe(true); - }); - - it("returns false if no interfaces", function() { - makeController(); - $scope.selectedInterfaces = []; - $scope.newBondInterface = {id: "bar", fabric: { - name: "fabric-1" - }}; - $scope.interfaces = []; - expect($scope.showCreateEditButton()).toBe(false); - }); - }); - - describe("toggleEditInterfaceSelect", function() { - it("adds item to selectedInterfaces", function() { - makeController(); - var nic = { - id: 1, - link_id: -1 - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [] - }; - $scope.selectedInterfaces = []; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.selectedInterfaces).toEqual(["1/-1"]); - }); - - it("adds item to interfaces", function() { - makeController(); - var nic = { - id: 1, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" } - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [], - primary: nic - }; - $scope.selectedInterfaces = ["1/-1"]; - $scope.interfaces = []; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.interfaces).toEqual([nic]); - }); - - it("adds item to members", function() { - makeController(); - var nic = { - id: 1, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" } - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [] - }; - $scope.selectedInterfaces = []; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.editInterface.members).toEqual([nic]); - }); - - it("adds item to parents", function() { - makeController(); - var nic = { - id: 1, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" } - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [] - }; - $scope.selectedInterfaces = []; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.editInterface.parents).toEqual([nic.id]); - }); - - it("removes item from selectedInterfaces", function() { - makeController(); - var nic = { - id: 1, - link_id: -1 - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [], - primary: nic - }; - $scope.selectedInterfaces = ["1/-1"]; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.selectedInterfaces).toEqual([]); - }); - - it("removes item from interfaces", function() { - makeController(); - var nic = { - id: 1, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" } - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [] - }; - $scope.selectedInterfaces = []; - $scope.interfaces = [nic]; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.interfaces).toEqual([]); - }); - - it("removes item from members", function() { - makeController(); - var nic = { - id: 1, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" } - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [nic], - parents: [], - primary: nic - }; - $scope.selectedInterfaces = ["1/-1"]; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.editInterface.members).toEqual([]); - }); - - it("removes item from parents", function() { - makeController(); - var nic = { - id: 1, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" } - }; - $scope.editInterface = { - id: 2, - link_id: -1, - vlan: { id: 2 }, - fabric: { name: "fabric-2" }, - members: [], - parents: [nic.id], - primary: nic - }; - $scope.selectedInterfaces = ["1/-1"]; - $scope.toggleEditInterfaceSelect(nic); - expect($scope.editInterface.parents).toEqual([]); - }); + it("returns false if no interfaces", function() { + makeController(); + $scope.selectedInterfaces = []; + $scope.newBondInterface = { + id: "bar", + fabric: { + name: "fabric-1" + } + }; + $scope.interfaces = []; + expect($scope.showCreateEditButton()).toBe(false); + }); + }); + + describe("toggleEditInterfaceSelect", function() { + it("adds item to selectedInterfaces", function() { + makeController(); + var nic = { + id: 1, + link_id: -1 + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [] + }; + $scope.selectedInterfaces = []; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.selectedInterfaces).toEqual(["1/-1"]); + }); + + it("adds item to interfaces", function() { + makeController(); + var nic = { + id: 1, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" } + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [], + primary: nic + }; + $scope.selectedInterfaces = ["1/-1"]; + $scope.interfaces = []; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.interfaces).toEqual([nic]); + }); + + it("adds item to members", function() { + makeController(); + var nic = { + id: 1, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" } + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [] + }; + $scope.selectedInterfaces = []; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.editInterface.members).toEqual([nic]); + }); + + it("adds item to parents", function() { + makeController(); + var nic = { + id: 1, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" } + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [] + }; + $scope.selectedInterfaces = []; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.editInterface.parents).toEqual([nic.id]); + }); + + it("removes item from selectedInterfaces", function() { + makeController(); + var nic = { + id: 1, + link_id: -1 + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [], + primary: nic + }; + $scope.selectedInterfaces = ["1/-1"]; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.selectedInterfaces).toEqual([]); + }); + + it("removes item from interfaces", function() { + makeController(); + var nic = { + id: 1, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" } + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [] + }; + $scope.selectedInterfaces = []; + $scope.interfaces = [nic]; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.interfaces).toEqual([]); + }); + + it("removes item from members", function() { + makeController(); + var nic = { + id: 1, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" } + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [nic], + parents: [], + primary: nic + }; + $scope.selectedInterfaces = ["1/-1"]; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.editInterface.members).toEqual([]); + }); + + it("removes item from parents", function() { + makeController(); + var nic = { + id: 1, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" } + }; + $scope.editInterface = { + id: 2, + link_id: -1, + vlan: { id: 2 }, + fabric: { name: "fabric-2" }, + members: [], + parents: [nic.id], + primary: nic + }; + $scope.selectedInterfaces = ["1/-1"]; + $scope.toggleEditInterfaceSelect(nic); + expect($scope.editInterface.parents).toEqual([]); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage_filesystems.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage_filesystems.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage_filesystems.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage_filesystems.js 2019-06-01 02:18:13.000000000 +0000 @@ -1,120 +1,120 @@ -describe('NodeAddSpecialFilesystemController', function() { - // Load the MAAS module. - beforeEach(module('MAAS')); - - // Grab the needed angular pieces. - var $controller, $rootScope, $parentScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get('$controller'); - $rootScope = $injector.get('$rootScope'); - $parentScope = $rootScope.$new(); - $scope = $parentScope.$new(); - })); - - // Load the required dependencies for the - // NodeAddSpecialFilesystemController. - var MachinesManager; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get('MachinesManager'); - })); - - // Create the node and functions that will be called on the parent. - var node; - beforeEach(function() { - node = { - system_id: 0, - }; - $parentScope.node = node; - $parentScope.controllerLoaded = jasmine.createSpy('controllerLoaded'); - }); - - // Makes the NodeAddSpecialFilesystemController - function makeController() { - // Create the controller. - var controller = $controller('NodeAddSpecialFilesystemController', { - $scope: $scope, - MachinesManager: MachinesManager - }); - return controller; - } - - it('sets initial values', function() { - makeController(); - - expect($scope.specialFilesystemTypes).toEqual(['tmpfs', 'ramfs']); - expect($scope.description).toBeNull(); - expect($scope.filesystem.fstype).toBeNull(); - expect($scope.filesystem.mountPoint).toEqual(''); - expect($scope.filesystem.mountOptions).toEqual(''); - expect($scope.newFilesystem).toEqual({system_id: 0}) - }); - - it('correctly validates mountpoints', function() { - makeController(); - var specialFilesystem = $scope.filesystem; - - specialFilesystem.mountPoint = 'foo'; - expect(specialFilesystem.isValid()).toBe(false); - - specialFilesystem.mountPoint = '/'; - expect(specialFilesystem.isValid()).toBe(true); - - specialFilesystem.mountPoint = '/foo'; - expect(specialFilesystem.isValid()).toBe(true); - }); - - it('describes a filesystem only if fstype is set', function() { - makeController(); - var specialFilesystem = $scope.filesystem; - - specialFilesystem.fstype = null; - - expect(specialFilesystem.describe()).not.toBeDefined(); - }); - - it('describes a filesystem if mountPoint is set', function() { - makeController(); - var specialFilesystem = $scope.filesystem; +describe("NodeAddSpecialFilesystemController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); + + // Grab the needed angular pieces. + var $controller, $rootScope, $parentScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $parentScope = $rootScope.$new(); + $scope = $parentScope.$new(); + })); + + // Load the required dependencies for the + // NodeAddSpecialFilesystemController. + var MachinesManager; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + })); + + // Create the node and functions that will be called on the parent. + var node; + beforeEach(function() { + node = { + system_id: 0 + }; + $parentScope.node = node; + $parentScope.controllerLoaded = jasmine.createSpy("controllerLoaded"); + }); + + // Makes the NodeAddSpecialFilesystemController + function makeController() { + // Create the controller. + var controller = $controller("NodeAddSpecialFilesystemController", { + $scope: $scope, + MachinesManager: MachinesManager + }); + return controller; + } + + it("sets initial values", function() { + makeController(); + + expect($scope.specialFilesystemTypes).toEqual(["tmpfs", "ramfs"]); + expect($scope.description).toBeNull(); + expect($scope.filesystem.fstype).toBeNull(); + expect($scope.filesystem.mountPoint).toEqual(""); + expect($scope.filesystem.mountOptions).toEqual(""); + expect($scope.newFilesystem).toEqual({ system_id: 0 }); + }); + + it("correctly validates mountpoints", function() { + makeController(); + var specialFilesystem = $scope.filesystem; + + specialFilesystem.mountPoint = "foo"; + expect(specialFilesystem.isValid()).toBe(false); + + specialFilesystem.mountPoint = "/"; + expect(specialFilesystem.isValid()).toBe(true); + + specialFilesystem.mountPoint = "/foo"; + expect(specialFilesystem.isValid()).toBe(true); + }); + + it("describes a filesystem only if fstype is set", function() { + makeController(); + var specialFilesystem = $scope.filesystem; + + specialFilesystem.fstype = null; + + expect(specialFilesystem.describe()).not.toBeDefined(); + }); + + it("describes a filesystem if mountPoint is set", function() { + makeController(); + var specialFilesystem = $scope.filesystem; + + specialFilesystem.fstype = "tmpfs"; + specialFilesystem.mountPoint = "/"; + + expect(specialFilesystem.describe()).toEqual("tmpfs at /"); + }); + + it("describes a percentage size if mountOptions is set", function() { + makeController(); + var specialFilesystem = $scope.filesystem; + + specialFilesystem.fstype = "tmpfs"; + specialFilesystem.mountPoint = "/"; + specialFilesystem.mountOptions = "size=20%"; + + expect(specialFilesystem.describe()).toEqual( + "tmpfs at / limited to 20% of memory" + ); + }); + + it("describes a size in bytes if mountOptions is set", function() { + makeController(); + var specialFilesystem = $scope.filesystem; + + specialFilesystem.fstype = "tmpfs"; + specialFilesystem.mountPoint = "/"; + specialFilesystem.mountOptions = "size=5000"; + + expect(specialFilesystem.describe()).toEqual( + "tmpfs at / limited to 5000 bytes" + ); + }); + + it("updates the description when the form is updated", function() { + makeController(); + $scope.newFilesystem.$maasForm = { getValue: function() {} }; + spyOn($scope.newFilesystem.$maasForm, "getValue").and.returnValue("foo"); - specialFilesystem.fstype = 'tmpfs'; - specialFilesystem.mountPoint = '/'; - - expect(specialFilesystem.describe()).toEqual('tmpfs at /'); - }); - - it('describes a percentage size if mountOptions is set', function() { - makeController(); - var specialFilesystem = $scope.filesystem; - - specialFilesystem.fstype = 'tmpfs'; - specialFilesystem.mountPoint = '/'; - specialFilesystem.mountOptions = 'size=20%'; - - expect(specialFilesystem.describe()).toEqual( - 'tmpfs at / limited to 20% of memory'); - }); - - it('describes a size in bytes if mountOptions is set', function() { - makeController(); - var specialFilesystem = $scope.filesystem; - - specialFilesystem.fstype = 'tmpfs'; - specialFilesystem.mountPoint = '/'; - specialFilesystem.mountOptions = 'size=5000'; - - expect(specialFilesystem.describe()).toEqual( - 'tmpfs at / limited to 5000 bytes'); - }); - - it('updates the description when the form is updated', function() { - makeController(); - $scope.newFilesystem.$maasForm = {getValue: function() {}}; - spyOn($scope.newFilesystem.$maasForm, 'getValue'). - and.returnValue('foo'); - - $scope.$digest(); - - expect($scope.description).toEqual('foo'); - }); + $scope.$digest(); + expect($scope.description).toEqual("foo"); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_details_storage.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,4890 +4,5553 @@ * Unit tests for NodeStorageController. */ -describe("removeAvailableByNew", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Load the removeAvailableByNew. - var removeAvailableByNew; - beforeEach(inject(function($filter) { - removeAvailableByNew = $filter("removeAvailableByNew"); - })); +import { makeInteger, makeName } from "testing/utils"; - it("returns disks if undefined availableNew", function() { - var i, disk, disks = []; - for(i = 0; i < 3; i++) { - disk = { - id: i - }; - disks.push(disk); - } - expect(removeAvailableByNew(disks)).toBe(disks); - }); - - it("returns disks if undefined device(s) in availableNew", function() { - var i, disk, disks = []; - for(i = 0; i < 3; i++) { - disk = { - id: i - }; - disks.push(disk); - } - var availableNew = {}; - expect(removeAvailableByNew(disks, availableNew)).toBe(disks); - }); - - it("removes availableNew.device from disks", function() { - var i, disk, disks = []; - for(i = 0; i < 3; i++) { - disk = { - id: i - }; - disks.push(disk); - } +describe("removeAvailableByNew", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - var availableNew = { - device: disks[0] - }; - var expectedDisks = angular.copy(disks); - expectedDisks.splice(0, 1); + // Load the removeAvailableByNew. + var removeAvailableByNew; + beforeEach(inject(function($filter) { + removeAvailableByNew = $filter("removeAvailableByNew"); + })); + + it("returns disks if undefined availableNew", function() { + var i, + disk, + disks = []; + for (i = 0; i < 3; i++) { + disk = { + id: i + }; + disks.push(disk); + } + expect(removeAvailableByNew(disks)).toBe(disks); + }); - expect(removeAvailableByNew(disks, availableNew)).toEqual( - expectedDisks); - }); + it("returns disks if undefined device(s) in availableNew", function() { + var i, + disk, + disks = []; + for (i = 0; i < 3; i++) { + disk = { + id: i + }; + disks.push(disk); + } + var availableNew = {}; + expect(removeAvailableByNew(disks, availableNew)).toBe(disks); + }); + + it("removes availableNew.device from disks", function() { + var i, + disk, + disks = []; + for (i = 0; i < 3; i++) { + disk = { + id: i + }; + disks.push(disk); + } - it("removes availableNew.devices from disks", function() { - var i, disk, disks = []; - for(i = 0; i < 6; i++) { - disk = { - id: i - }; - disks.push(disk); - } + var availableNew = { + device: disks[0] + }; + var expectedDisks = angular.copy(disks); + expectedDisks.splice(0, 1); + + expect(removeAvailableByNew(disks, availableNew)).toEqual(expectedDisks); + }); + + it("removes availableNew.devices from disks", function() { + var i, + disk, + disks = []; + for (i = 0; i < 6; i++) { + disk = { + id: i + }; + disks.push(disk); + } - var availableNew = { - devices: [disks[0], disks[1]] - }; - var expectedDisks = angular.copy(disks); - expectedDisks.splice(0, 2); + var availableNew = { + devices: [disks[0], disks[1]] + }; + var expectedDisks = angular.copy(disks); + expectedDisks.splice(0, 2); - expect(removeAvailableByNew(disks, availableNew)).toEqual( - expectedDisks); - }); + expect(removeAvailableByNew(disks, availableNew)).toEqual(expectedDisks); + }); }); describe("NodeStorageController", function() { - // Load the MAAS module. - beforeEach(module("MAAS")); + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Grab the needed angular pieces. - var $controller, $rootScope, $parentScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $parentScope = $rootScope.$new(); - $scope = $parentScope.$new(); - $q = $injector.get("$q"); - })); + // Grab the needed angular pieces. + var $controller, $rootScope, $parentScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $parentScope = $rootScope.$new(); + $scope = $parentScope.$new(); + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the NodeStorageController. + var MachinesManager; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + })); + + // Create the node and functions that will be called on the parent. + var node, updateNodeSpy, canEditSpy; + beforeEach(function() { + node = { + system_id: makeName("system_id"), + architecture: "amd64/generic", + disks: [] + }; + updateNodeSpy = jasmine.createSpy("updateNode"); + canEditSpy = jasmine.createSpy("canEdit"); + $parentScope.node = node; + $parentScope.updateNode = updateNodeSpy; + $parentScope.canEdit = canEditSpy; + $parentScope.controllerLoaded = jasmine.createSpy("controllerLoaded"); + }); + + // Makes the NodeStorageController + function makeController() { + // Create the controller. + var controller = $controller("NodeStorageController", { + $scope: $scope, + MachinesManager: MachinesManager + }); + return controller; + } + + // Return a known set of disks for testing the loading of disks + // into the controller. + function makeDisks() { + return [ + { + // Blank disk + id: 0, + is_boot: true, + name: makeName("name"), + model: makeName("model"), + serial: makeName("serial"), + tags: [], + type: makeName("type"), + size: Math.pow(1024, 4), + size_human: "1024 GB", + available_size: Math.pow(1024, 4), + available_size_human: "1024 GB", + used_size: 0, + used_size_human: "0.0 Bytes", + partition_table_type: makeName("partition_table_type"), + used_for: "Unused", + filesystem: null, + partitions: null, + test_status: 0, + firmware_version: makeName("firmware_version") + }, + { + // Disk with filesystem, no mount point + id: 1, + is_boot: false, + name: makeName("name"), + model: makeName("model"), + serial: makeName("serial"), + tags: [], + type: makeName("type"), + size: Math.pow(1024, 4), + size_human: "1024 GB", + available_size: 0, + available_size_human: "0 GB", + used_size: Math.pow(1024, 4), + used_size_human: "1024 GB", + partition_table_type: makeName("partition_table_type"), + used_for: "Unmounted ext4 formatted filesystem.", + filesystem: { + id: 0, + is_format_fstype: true, + fstype: "ext4", + mount_point: null, + mount_options: null + }, + partitions: null, + test_status: 1, + firmware_version: makeName("firmware_version") + }, + { + // Disk with mounted filesystem + id: 2, + is_boot: false, + name: makeName("name"), + model: makeName("model"), + serial: makeName("serial"), + tags: [], + type: makeName("type"), + size: Math.pow(1024, 4), + size_human: "1024 GB", + available_size: 0, + available_size_human: "0 GB", + used_size: Math.pow(1024, 4), + used_size_human: "1024 GB", + partition_table_type: makeName("partition_table_type"), + used_for: "ext4 formatted filesystem mounted at /.", + filesystem: { + id: 1, + is_format_fstype: true, + fstype: "ext4", + mount_point: "/", + mount_options: makeName("options") + }, + partitions: null, + test_status: 2, + firmware_version: makeName("firmware_version") + }, + { + // Partitioned disk, one partition free one used + id: 3, + is_boot: false, + name: makeName("name"), + model: makeName("model"), + serial: makeName("serial"), + tags: [], + type: makeName("type"), + size: Math.pow(1024, 4), + size_human: "1024 GB", + available_size: 0, + available_size_human: "0 GB", + used_size: Math.pow(1024, 4), + used_size_human: "1024 GB", + partition_table_type: "GPT", + filesystem: null, + partitions: [ + { + id: 0, + name: makeName("partition_name"), + size_human: "512 GB", + type: "partition", + filesystem: null, + used_for: "Unused" + }, + { + id: 1, + name: makeName("partition_name"), + size_human: "512 GB", + type: "partition", + filesystem: { + id: 2, + is_format_fstype: true, + fstype: "ext4", + mount_point: "/mnt", + mount_options: makeName("options") + }, + used_for: "ext4 formatted filesystem mounted at /mnt." + } + ], + test_status: 3, + firmware_version: makeName("firmware_version") + }, + { + // Disk that is a cache set. + id: 4, + is_boot: false, + name: "cache0", + model: "", + serial: "", + tags: [], + type: "cache-set", + size: Math.pow(1024, 4), + size_human: "1024 GB", + available_size: 0, + available_size_human: "0 GB", + used_size: Math.pow(1024, 4), + used_size_human: "1024 GB", + partition_table_type: null, + used_for: "", + filesystem: null, + partitions: null, + test_status: 4, + firmware_version: makeName("firmware_version") + } + ]; + } + + it("sets initial values", function() { + makeController(); + expect($scope.tableInfo.column).toBe("name"); + expect($scope.has_disks).toBe(false); + expect($scope.filesystems).toEqual([]); + expect($scope.filesystemsMap).toEqual({}); + expect($scope.filesystemMode).toBeNull(); + expect($scope.filesystemAllSelected).toBe(false); + expect($scope.available).toEqual([]); + expect($scope.availableMap).toEqual({}); + expect($scope.availableMode).toBeNull(); + expect($scope.availableAllSelected).toBe(false); + expect($scope.cachesets).toEqual([]); + expect($scope.cachesetsMap).toEqual({}); + expect($scope.cachesetsMode).toBeNull(); + expect($scope.cachesetsAllSelected).toBe(false); + expect($scope.used).toEqual([]); + }); + + it("starts watching disks once nodeLoaded called", function() { + makeController(); + + spyOn($scope, "$watch"); + $scope.nodeLoaded(); + + var watches = []; + var i, + calls = $scope.$watch.calls.allArgs(); + for (i = 0; i < calls.length; i++) { + watches.push(calls[i][0]); + } - // Load the required dependencies for the NodeStorageController. - var MachinesManager; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get("MachinesManager"); - })); + expect(watches).toEqual(["node.disks"]); + }); - // Create the node and functions that will be called on the parent. - var node, updateNodeSpy, canEditSpy; - beforeEach(function() { - node = { - system_id: makeName("system_id"), - architecture: "amd64/generic", - disks: [] - }; - updateNodeSpy = jasmine.createSpy("updateNode"); - canEditSpy = jasmine.createSpy("canEdit"); - $parentScope.node = node; - $parentScope.updateNode = updateNodeSpy; - $parentScope.canEdit = canEditSpy; - $parentScope.controllerLoaded = jasmine.createSpy("controllerLoaded"); - }); + it("disks updated once nodeLoaded called", function() { + var disks = makeDisks(); + node.disks = disks; + + var filesystems = [ + { + type: "filesystem", + name: disks[2].name, + size_human: disks[2].size_human, + fstype: disks[2].filesystem.fstype, + mount_point: disks[2].filesystem.mount_point, + mount_options: disks[2].filesystem.mount_options, + block_id: disks[2].id, + partition_id: null, + filesystem_id: disks[2].filesystem.id, + original_type: disks[2].type, + original: disks[2], + $selected: false + }, + { + type: "filesystem", + name: disks[3].partitions[1].name, + size_human: disks[3].partitions[1].size_human, + fstype: disks[3].partitions[1].filesystem.fstype, + mount_point: disks[3].partitions[1].filesystem.mount_point, + mount_options: disks[3].partitions[1].filesystem.mount_options, + block_id: disks[3].id, + partition_id: disks[3].partitions[1].id, + filesystem_id: disks[3].partitions[1].filesystem.id, + original_type: "partition", + original: disks[3].partitions[1], + $selected: false + } + ]; + var cachesets = [ + { + type: "cache-set", + name: disks[4].name, + size_human: disks[4].size_human, + cache_set_id: disks[4].id, + used_by: disks[4].used_for, + $selected: false + } + ]; + var available = [ + { + name: disks[0].name, + is_boot: disks[0].is_boot, + size_human: disks[0].size_human, + size: disks[0].size, + available_size_human: disks[0].available_size_human, + used_size_human: disks[0].used_size_human, + type: disks[0].type, + model: disks[0].model, + serial: disks[0].serial, + tags: disks[0].tags, + fstype: null, + mount_point: null, + mount_options: null, + block_id: 0, + partition_id: null, + has_partitions: false, + original: disks[0], + test_status: disks[0].test_status, + firmware_version: disks[0].firmware_version, + $selected: false, + $options: {} + }, + { + name: disks[1].name, + is_boot: disks[1].is_boot, + size_human: disks[1].size_human, + size: disks[1].size, + available_size_human: disks[1].available_size_human, + used_size_human: disks[1].used_size_human, + type: disks[1].type, + model: disks[1].model, + serial: disks[1].serial, + tags: disks[1].tags, + fstype: "ext4", + mount_point: null, + mount_options: null, + block_id: 1, + partition_id: null, + has_partitions: false, + original: disks[1], + test_status: disks[1].test_status, + firmware_version: disks[1].firmware_version, + $selected: false, + $options: {} + }, + { + name: disks[3].partitions[0].name, + is_boot: false, + size_human: disks[3].partitions[0].size_human, + size: disks[3].partitions[0].size, + available_size_human: disks[3].partitions[0].available_size_human, + used_size_human: disks[3].partitions[0].used_size_human, + type: disks[3].partitions[0].type, + model: "", + serial: "", + tags: [], + fstype: null, + mount_point: null, + mount_options: null, + block_id: 3, + partition_id: 0, + has_partitions: false, + original: disks[3].partitions[0], + $selected: false, + $options: {} + } + ]; + var used = [ + { + name: disks[2].name, + is_boot: disks[2].is_boot, + type: disks[2].type, + model: disks[2].model, + serial: disks[2].serial, + size_human: disks[2].size_human, + tags: disks[2].tags, + used_for: disks[2].used_for, + has_partitions: false, + test_status: disks[2].test_status, + firmware_version: disks[2].firmware_version + }, + { + name: disks[3].name, + is_boot: disks[3].is_boot, + type: disks[3].type, + model: disks[3].model, + serial: disks[3].serial, + size_human: disks[3].size_human, + tags: disks[3].tags, + used_for: disks[3].used_for, + has_partitions: true, + test_status: disks[3].test_status, + firmware_version: disks[3].firmware_version + }, + { + name: disks[3].partitions[1].name, + is_boot: false, + type: "partition", + model: "", + serial: "", + size_human: disks[3].partitions[1].size_human, + tags: [], + used_for: disks[3].partitions[1].used_for + } + ]; + makeController(); + $scope.nodeLoaded(); + $rootScope.$digest(); + expect($scope.has_disks).toEqual(true); + expect($scope.filesystems).toEqual(filesystems); + expect($scope.cachesets).toEqual(cachesets); + expect($scope.available).toEqual(available); + expect($scope.used).toEqual(used); + }); + + it("disks $selected and $options not lost on update", function() { + makeController(); + var disks = makeDisks(); + node.disks = disks; + + // Load the filesystems, cachesets, available, and used once. + $scope.nodeLoaded(); + $rootScope.$digest(); + + // Set all filesystems, cachesets, and available to selected. + angular.forEach($scope.filesystems, function(filesystem) { + filesystem.$selected = true; + }); + angular.forEach($scope.cachesets, function(cacheset) { + cacheset.$selected = true; + }); + angular.forEach($scope.available, function(disk) { + disk.$selected = true; + }); + + // Get all the options for available. + var options = []; + angular.forEach($scope.available, function(disk) { + options.push(disk.$options); + }); + + // Force the disks to change so the filesystems, cachesets, available, + // and used are reloaded. + var firstFilesystem = $scope.filesystems[0]; + node.disks = angular.copy(node.disks); + $rootScope.$digest(); + expect($scope.filesystems[0]).not.toBe(firstFilesystem); + expect($scope.filesystems[0]).toEqual(firstFilesystem); + + // All filesystems, cachesets and available should be selected. + angular.forEach($scope.filesystems, function(filesystem) { + expect(filesystem.$selected).toBe(true); + }); + angular.forEach($scope.cachesets, function(cacheset) { + expect(cacheset.$selected).toBe(true); + }); + angular.forEach($scope.available, function(disk) { + expect(disk.$selected).toBe(true); + }); + + // All available should have the same options. + angular.forEach($scope.available, function(disk, idx) { + expect(disk.$options).toBe(options[idx]); + }); + }); + + it("availableNew.device object is updated", function() { + makeController(); + var disks = makeDisks(); + node.disks = disks; + + // Load the filesystems, cachesets, available, and used once. + $scope.nodeLoaded(); + $rootScope.$digest(); + + // Set availableNew.device to a disk from available. + var disk = $scope.available[0]; + $scope.availableNew.device = disk; + + // Force the update. The device should be the same value but + // a new object. + node.disks = angular.copy(node.disks); + $rootScope.$digest(); + expect($scope.availableNew.device).toEqual(disk); + expect($scope.availableNew.device).not.toBe(disk); + }); + + it("availableNew.devices array is updated", function() { + makeController(); + var disks = makeDisks(); + node.disks = disks; + + // Load the filesystems, cachesets, available, and used once. + $scope.nodeLoaded(); + $rootScope.$digest(); + + // Set availableNew.device to a disk from available. + var disk0 = $scope.available[0]; + var disk1 = $scope.available[1]; + $scope.availableNew.devices = [disk0, disk1]; + + // Force the update. The devices should be the same values but + // a new objects. + node.disks = angular.copy(node.disks); + $rootScope.$digest(); + expect($scope.availableNew.devices[0]).toEqual(disk0); + expect($scope.availableNew.devices[0]).not.toBe(disk0); + expect($scope.availableNew.devices[1]).toEqual(disk1); + expect($scope.availableNew.devices[1]).not.toBe(disk1); + }); + + describe("isBootDiskDisabled", function() { + it("returns true when not editable", function() { + makeController(); + $scope.canEdit = function() { + return false; + }; + $scope.node.status = "Ready"; + var disk = { type: "physical" }; + + expect($scope.isBootDiskDisabled(disk, "available")).toBe(true); + }); + + it("returns true when not node not ready", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node.status = "Deploying"; + var disk = { type: "physical" }; + + expect($scope.isBootDiskDisabled(disk, "available")).toBe(true); + }); + + it("returns true if not physical", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node.status = "Ready"; + var disk = { type: "virtual" }; + + expect($scope.isBootDiskDisabled(disk, "available")).toBe(true); + }); + + it("returns false if in available", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node.status = "Ready"; + var disk = { type: "physical" }; + + expect($scope.isBootDiskDisabled(disk, "available")).toBe(false); + }); + + it("returns true when used and no partitions", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node.status = "Ready"; + var disk = { type: "physical", has_partitions: false }; + + expect($scope.isBootDiskDisabled(disk, "used")).toBe(true); + }); + + it("returns false when ready, used and partitions", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node.status = "Ready"; + var disk = { type: "physical", has_partitions: true }; + + expect($scope.isBootDiskDisabled(disk, "used")).toBe(false); + }); + + it("returns false when allocated, used and partitions", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + $scope.node.status = "Allocated"; + var disk = { type: "physical", has_partitions: true }; + + expect($scope.isBootDiskDisabled(disk, "used")).toBe(false); + }); + }); + + describe("setAsBootDisk", function() { + it("does nothing if already boot disk", function() { + makeController(); + var disk = { is_boot: true }; + spyOn(MachinesManager, "setBootDisk"); + spyOn($scope, "isBootDiskDisabled").and.returnValue(false); + + $scope.setAsBootDisk(disk); + + expect(MachinesManager.setBootDisk).not.toHaveBeenCalled(); + }); + + it("does nothing if set boot disk disabled", function() { + makeController(); + var disk = { is_boot: false }; + spyOn(MachinesManager, "setBootDisk"); + spyOn($scope, "isBootDiskDisabled").and.returnValue(true); + + $scope.setAsBootDisk(disk); + + expect(MachinesManager.setBootDisk).not.toHaveBeenCalled(); + }); + + it("calls MachinesManager.setBootDisk", function() { + makeController(); + var disk = { block_id: makeInteger(0, 100), is_boot: false }; + spyOn(MachinesManager, "setBootDisk"); + spyOn($scope, "isBootDiskDisabled").and.returnValue(false); + + $scope.setAsBootDisk(disk); + + expect(MachinesManager.setBootDisk).toHaveBeenCalledWith( + node, + disk.block_id + ); + }); + }); + + describe("getSelectedFilesystems", function() { + it("returns selected filesystems", function() { + makeController(); + var filesystems = [ + { $selected: true }, + { $selected: true }, + { $selected: false }, + { $selected: false } + ]; + $scope.filesystems = filesystems; + expect($scope.getSelectedFilesystems()).toEqual([ + filesystems[0], + filesystems[1] + ]); + }); + }); + + describe("updateFilesystemSelection", function() { + it("sets filesystemMode to NONE when none selected", function() { + makeController(); + spyOn($scope, "getSelectedFilesystems").and.returnValue([]); + $scope.filesystemMode = "other"; + + $scope.updateFilesystemSelection(); + + expect($scope.filesystemMode).toBeNull(); + }); + + it("doesn't sets filesystemMode to SINGLE when not force", function() { + makeController(); + spyOn($scope, "getSelectedFilesystems").and.returnValue([{}]); + $scope.filesystemMode = "other"; + + $scope.updateFilesystemSelection(); + + expect($scope.filesystemMode).toBe("other"); + }); + + it("sets filesystemMode to SINGLE when force", function() { + makeController(); + spyOn($scope, "getSelectedFilesystems").and.returnValue([{}]); + $scope.filesystemMode = "other"; + + $scope.updateFilesystemSelection(true); + + expect($scope.filesystemMode).toBe("single"); + }); + + it("doesn't sets filesystemMode to MUTLI when not force", function() { + makeController(); + spyOn($scope, "getSelectedFilesystems").and.returnValue([{}, {}]); + $scope.filesystemMode = "other"; + + $scope.updateFilesystemSelection(); + + expect($scope.filesystemMode).toBe("other"); + }); + + it("sets filesystemMode to MULTI when force", function() { + makeController(); + spyOn($scope, "getSelectedFilesystems").and.returnValue([{}, {}]); + $scope.filesystemMode = "other"; + + $scope.updateFilesystemSelection(true); + + expect($scope.filesystemMode).toBe("multi"); + }); + + it("sets filesystemAllSelected to false when none selected", function() { + makeController(); + spyOn($scope, "getSelectedFilesystems").and.returnValue([]); + $scope.filesystemAllSelected = true; + + $scope.updateFilesystemSelection(); + + expect($scope.filesystemAllSelected).toBe(false); + }); + + it("sets filesystemAllSelected to false when not all selected", function() { + makeController(); + $scope.filesystems = [{}, {}]; + spyOn($scope, "getSelectedFilesystems").and.returnValue([{}]); + $scope.filesystemAllSelected = true; + + $scope.updateFilesystemSelection(); + + expect($scope.filesystemAllSelected).toBe(false); + }); + + it("sets filesystemAllSelected to true when all selected", function() { + makeController(); + $scope.filesystems = [{}, {}]; + spyOn($scope, "getSelectedFilesystems").and.returnValue([{}, {}]); + $scope.filesystemAllSelected = false; + + $scope.updateFilesystemSelection(); + + expect($scope.filesystemAllSelected).toBe(true); + }); + }); + + describe("toggleFilesystemSelect", function() { + it("inverts $selected", function() { + makeController(); + var filesystem = { $selected: true }; + spyOn($scope, "updateFilesystemSelection"); + + $scope.toggleFilesystemSelect(filesystem); + + expect(filesystem.$selected).toBe(false); + $scope.toggleFilesystemSelect(filesystem); + expect(filesystem.$selected).toBe(true); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(true); + }); + }); + + describe("toggleFilesystemAllSelect", function() { + it("sets all to true if not all selected", function() { + makeController(); + var filesystems = [{ $selected: true }, { $selected: false }]; + $scope.filesystems = filesystems; + $scope.filesystemAllSelected = false; + spyOn($scope, "updateFilesystemSelection"); + + $scope.toggleFilesystemAllSelect(); + + expect(filesystems[0].$selected).toBe(true); + expect(filesystems[1].$selected).toBe(true); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(true); + }); + + it("sets all to false if all selected", function() { + makeController(); + var filesystems = [{ $selected: true }, { $selected: true }]; + $scope.filesystems = filesystems; + $scope.filesystemAllSelected = true; + spyOn($scope, "updateFilesystemSelection"); + + $scope.toggleFilesystemAllSelect(); + + expect(filesystems[0].$selected).toBe(false); + expect(filesystems[1].$selected).toBe(false); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(true); + }); + }); + + describe("isFilesystemsDisabled", function() { + it("returns false for NONE", function() { + makeController(); + $scope.filesystemMode = null; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.isFilesystemsDisabled()).toBe(false); + }); + + it("returns false for SINGLE", function() { + makeController(); + $scope.filesystemMode = "single"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.isFilesystemsDisabled()).toBe(false); + }); + + it("returns false for MULTI", function() { + makeController(); + $scope.filesystemMode = "multi"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.isFilesystemsDisabled()).toBe(false); + }); + + it("returns true for UNMOUNT", function() { + makeController(); + $scope.filesystemMode = "unmount"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.isFilesystemsDisabled()).toBe(true); + }); + + it("returns true when isAllStorageDisabled", function() { + makeController(); + $scope.filesystemMode = "multi"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(true); + + expect($scope.isFilesystemsDisabled()).toBe(true); + }); + }); + + describe("filesystemCancel", function() { + it("calls updateFilesystemSelection with force true", function() { + makeController(); + var filesystems = [{ $selected: true }, { $selected: false }]; + $scope.filesystems = filesystems; + spyOn($scope, "updateFilesystemSelection"); + + $scope.filesystemCancel(); + + expect(filesystems[0].$selected).toBe(false); + expect(filesystems[1].$selected).toBe(false); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(true); + }); + }); + + describe("filesystemUnmount", function() { + it("sets filesystemMode to UNMOUNT", function() { + makeController(); + $scope.filesystemMode = "other"; + + $scope.filesystemUnmount(); + + expect($scope.filesystemMode).toBe("unmount"); + }); + }); + + describe("quickFilesystemUnmount", function() { + it("selects filesystem and calls filesystemUnmount", function() { + makeController(); + var filesystems = [{ $selected: true }, { $selected: false }]; + $scope.filesystems = filesystems; + spyOn($scope, "updateFilesystemSelection"); + spyOn($scope, "filesystemUnmount"); + + $scope.quickFilesystemUnmount(filesystems[1]); + + expect(filesystems[0].$selected).toBe(false); + expect(filesystems[1].$selected).toBe(true); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(true); + expect($scope.filesystemUnmount).toHaveBeenCalled(); + }); + }); + + describe("filesystemConfirmUnmount", function() { + it("calls MachinesManager.updateFilesystem", function() { + makeController(); + var filesystem = { + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100), + fstype: makeName("fs") + }; + $scope.filesystems = [filesystem]; + spyOn(MachinesManager, "updateFilesystem"); + spyOn($scope, "updateFilesystemSelection"); + + $scope.filesystemConfirmUnmount(filesystem); + + expect(MachinesManager.updateFilesystem).toHaveBeenCalledWith( + node, + filesystem.block_id, + filesystem.partition_id, + filesystem.fstype, + null, + null + ); + }); + + it("removes filesystem from filesystems", function() { + makeController(); + var filesystem = { + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100), + fstype: makeName("fs") + }; + $scope.filesystems = [filesystem]; + spyOn(MachinesManager, "updateFilesystem"); + spyOn($scope, "updateFilesystemSelection"); + + $scope.filesystemConfirmUnmount(filesystem); + + expect($scope.filesystems).toEqual([]); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(); + }); + }); + + describe("filesystemDelete", function() { + it("sets filesystemMode to DELETE", function() { + makeController(); + $scope.filesystemMode = "other"; + + $scope.filesystemDelete(); + + expect($scope.filesystemMode).toBe("delete"); + }); + }); + + describe("quickFilesystemDelete", function() { + it("selects filesystem and calls filesystemDelete", function() { + makeController(); + var filesystems = [{ $selected: true }, { $selected: false }]; + $scope.filesystems = filesystems; + spyOn($scope, "updateFilesystemSelection"); + spyOn($scope, "filesystemDelete"); + + $scope.quickFilesystemDelete(filesystems[1]); + + expect(filesystems[0].$selected).toBe(false); + expect(filesystems[1].$selected).toBe(true); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(true); + expect($scope.filesystemDelete).toHaveBeenCalled(); + }); + }); + + describe("filesystemConfirmDelete", function() { + it("calls MachinesManager.deletePartition for partition", function() { + makeController(); + var filesystem = { + original_type: "partition", + original: { + id: makeInteger(0, 100) + } + }; + $scope.filesystems = [filesystem]; + spyOn(MachinesManager, "deletePartition"); + spyOn($scope, "updateFilesystemSelection"); - // Makes the NodeStorageController - function makeController() { - // Create the controller. - var controller = $controller("NodeStorageController", { - $scope: $scope, - MachinesManager: MachinesManager - }); - return controller; - } + $scope.filesystemConfirmDelete(filesystem); + expect(MachinesManager.deletePartition).toHaveBeenCalledWith( + node, + filesystem.original.id + ); + expect($scope.filesystems).toEqual([]); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(); + }); - // Return a known set of disks for testing the loading of disks - // into the controller. - function makeDisks() { - return [ - { - // Blank disk - id: 0, - is_boot: true, - name: makeName("name"), - model: makeName("model"), - serial: makeName("serial"), - tags: [], - type: makeName("type"), - size: Math.pow(1024, 4), - size_human: "1024 GB", - available_size: Math.pow(1024, 4), - available_size_human: "1024 GB", - used_size: 0, - used_size_human: "0.0 Bytes", - partition_table_type: makeName("partition_table_type"), - used_for: "Unused", - filesystem: null, - partitions: null, - test_status: 0, - firmware_version: makeName("firmware_version") - }, - { - // Disk with filesystem, no mount point - id: 1, - is_boot: false, - name: makeName("name"), - model: makeName("model"), - serial: makeName("serial"), - tags: [], - type: makeName("type"), - size: Math.pow(1024, 4), - size_human: "1024 GB", - available_size: 0, - available_size_human: "0 GB", - used_size: Math.pow(1024, 4), - used_size_human: "1024 GB", - partition_table_type: makeName("partition_table_type"), - used_for: "Unmounted ext4 formatted filesystem.", - filesystem: { - id: 0, - is_format_fstype: true, - fstype: "ext4", - mount_point: null, - mount_options: null - }, - partitions: null, - test_status: 1, - firmware_version: makeName("firmware_version") - }, - { - // Disk with mounted filesystem - id: 2, - is_boot: false, - name: makeName("name"), - model: makeName("model"), - serial: makeName("serial"), - tags: [], - type: makeName("type"), - size: Math.pow(1024, 4), - size_human: "1024 GB", - available_size: 0, - available_size_human: "0 GB", - used_size: Math.pow(1024, 4), - used_size_human: "1024 GB", - partition_table_type: makeName("partition_table_type"), - used_for: "ext4 formatted filesystem mounted at /.", - filesystem: { - id: 1, - is_format_fstype: true, - fstype: "ext4", - mount_point: "/", - mount_options: makeName("options") - }, - partitions: null, - test_status: 2, - firmware_version: makeName("firmware_version") - }, - { - // Partitioned disk, one partition free one used - id: 3, - is_boot: false, - name: makeName("name"), - model: makeName("model"), - serial: makeName("serial"), - tags: [], - type: makeName("type"), - size: Math.pow(1024, 4), - size_human: "1024 GB", - available_size: 0, - available_size_human: "0 GB", - used_size: Math.pow(1024, 4), - used_size_human: "1024 GB", - partition_table_type: "GPT", - filesystem: null, - partitions: [ - { - id: 0, - name: makeName("partition_name"), - size_human: "512 GB", - type: "partition", - filesystem: null, - used_for: "Unused" - }, - { - id: 1, - name: makeName("partition_name"), - size_human: "512 GB", - type: "partition", - filesystem: { - id: 2, - is_format_fstype: true, - fstype: "ext4", - mount_point: "/mnt", - mount_options: makeName("options") - }, - used_for: "ext4 formatted filesystem mounted at /mnt." - } - ], - test_status: 3, - firmware_version: makeName("firmware_version") - }, - { - // Disk that is a cache set. - id: 4, - is_boot: false, - name: "cache0", - model: "", - serial: "", - tags: [], - type: "cache-set", - size: Math.pow(1024, 4), - size_human: "1024 GB", - available_size: 0, - available_size_human: "0 GB", - used_size: Math.pow(1024, 4), - used_size_human: "1024 GB", - partition_table_type: null, - used_for: "", - filesystem: null, - partitions: null, - test_status: 4, - firmware_version: makeName("firmware_version") - } - ]; - } + it("calls MachinesManager.deleteFilesystem for disk", function() { + makeController(); + var filesystem = { + original_type: "physical", + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100), + filesystem_id: makeInteger(0, 100) + }; + $scope.filesystems = [filesystem]; + spyOn(MachinesManager, "deleteFilesystem"); + spyOn($scope, "updateFilesystemSelection"); - it("sets initial values", function() { - makeController(); - expect($scope.tableInfo.column).toBe('name'); - expect($scope.has_disks).toBe(false); - expect($scope.filesystems).toEqual([]); - expect($scope.filesystemsMap).toEqual({}); - expect($scope.filesystemMode).toBeNull(); - expect($scope.filesystemAllSelected).toBe(false); - expect($scope.available).toEqual([]); - expect($scope.availableMap).toEqual({}); - expect($scope.availableMode).toBeNull(); - expect($scope.availableAllSelected).toBe(false); - expect($scope.cachesets).toEqual([]); - expect($scope.cachesetsMap).toEqual({}); - expect($scope.cachesetsMode).toBeNull(); - expect($scope.cachesetsAllSelected).toBe(false); - expect($scope.used).toEqual([]); + $scope.filesystemConfirmDelete(filesystem); + expect(MachinesManager.deleteFilesystem).toHaveBeenCalledWith( + node, + filesystem.block_id, + filesystem.partition_id, + filesystem.filesystem_id + ); + expect($scope.filesystems).toEqual([]); + expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(); }); + }); - it("starts watching disks once nodeLoaded called", function() { - makeController(); + describe("hasUnmountedFilesystem", function() { + it("returns false if no fstype", function() { + makeController(); + var disk = { + fstype: null + }; - spyOn($scope, "$watch"); - $scope.nodeLoaded(); + expect($scope.hasUnmountedFilesystem(disk)).toBe(false); + }); - var watches = []; - var i, calls = $scope.$watch.calls.allArgs(); - for(i = 0; i < calls.length; i++) { - watches.push(calls[i][0]); - } + it("returns false if empty fstype", function() { + makeController(); + var disk = { + fstype: "" + }; - expect(watches).toEqual(["node.disks"]); + expect($scope.hasUnmountedFilesystem(disk)).toBe(false); }); - it("disks updated once nodeLoaded called", function() { - var disks = makeDisks(); - node.disks = disks; + it("returns true if no mount_point", function() { + makeController(); + var disk = { + fstype: "ext4", + mount_point: null + }; - var filesystems = [ - { - type: "filesystem", - name: disks[2].name, - size_human: disks[2].size_human, - fstype: disks[2].filesystem.fstype, - mount_point: disks[2].filesystem.mount_point, - mount_options: disks[2].filesystem.mount_options, - block_id: disks[2].id, - partition_id: null, - filesystem_id: disks[2].filesystem.id, - original_type: disks[2].type, - original: disks[2], - $selected: false - }, - { - type: "filesystem", - name: disks[3].partitions[1].name, - size_human: disks[3].partitions[1].size_human, - fstype: disks[3].partitions[1].filesystem.fstype, - mount_point: disks[3].partitions[1].filesystem.mount_point, - mount_options: disks[3].partitions[1].filesystem.mount_options, - block_id: disks[3].id, - partition_id: disks[3].partitions[1].id, - filesystem_id: disks[3].partitions[1].filesystem.id, - original_type: "partition", - original: disks[3].partitions[1], - $selected: false - } - ]; - var cachesets = [ - { - type: "cache-set", - name: disks[4].name, - size_human: disks[4].size_human, - cache_set_id: disks[4].id, - used_by: disks[4].used_for, - $selected: false - } - ]; - var available = [ - { - name: disks[0].name, - is_boot: disks[0].is_boot, - size_human: disks[0].size_human, - available_size_human: disks[0].available_size_human, - used_size_human: disks[0].used_size_human, - type: disks[0].type, - model: disks[0].model, - serial: disks[0].serial, - tags: disks[0].tags, - fstype: null, - mount_point: null, - mount_options: null, - block_id: 0, - partition_id: null, - has_partitions: false, - original: disks[0], - test_status: disks[0].test_status, - firmware_version: disks[0].firmware_version, - $selected: false, - $options: {} - }, - { - name: disks[1].name, - is_boot: disks[1].is_boot, - size_human: disks[1].size_human, - available_size_human: disks[1].available_size_human, - used_size_human: disks[1].used_size_human, - type: disks[1].type, - model: disks[1].model, - serial: disks[1].serial, - tags: disks[1].tags, - fstype: "ext4", - mount_point: null, - mount_options: null, - block_id: 1, - partition_id: null, - has_partitions: false, - original: disks[1], - test_status: disks[1].test_status, - firmware_version: disks[1].firmware_version, - $selected: false, - $options: {} - }, - { - name: disks[3].partitions[0].name, - is_boot: false, - size_human: disks[3].partitions[0].size_human, - available_size_human: ( - disks[3].partitions[0].available_size_human), - used_size_human: disks[3].partitions[0].used_size_human, - type: disks[3].partitions[0].type, - model: "", - serial: "", - tags: [], - fstype: null, - mount_point: null, - mount_options: null, - block_id: 3, - partition_id: 0, - has_partitions: false, - original: disks[3].partitions[0], - $selected: false, - $options: {} - } - ]; - var used = [ - { - name: disks[2].name, - is_boot: disks[2].is_boot, - type: disks[2].type, - model: disks[2].model, - serial: disks[2].serial, - tags: disks[2].tags, - used_for: disks[2].used_for, - has_partitions: false, - test_status: disks[2].test_status, - firmware_version: disks[2].firmware_version - }, - { - name: disks[3].name, - is_boot: disks[3].is_boot, - type: disks[3].type, - model: disks[3].model, - serial: disks[3].serial, - tags: disks[3].tags, - used_for: disks[3].used_for, - has_partitions: true, - test_status: disks[3].test_status, - firmware_version: disks[3].firmware_version - }, - { - name: disks[3].partitions[1].name, - is_boot: false, - type: "partition", - model: "", - serial: "", - tags: [], - used_for: disks[3].partitions[1].used_for - } - ]; - makeController(); - $scope.nodeLoaded(); - $rootScope.$digest(); - expect($scope.has_disks).toEqual(true); - expect($scope.filesystems).toEqual(filesystems); - expect($scope.cachesets).toEqual(cachesets); - expect($scope.available).toEqual(available); - expect($scope.used).toEqual(used); + expect($scope.hasUnmountedFilesystem(disk)).toBe(true); }); - it("disks $selected and $options not lost on update", function() { - makeController(); - var disks = makeDisks(); - node.disks = disks; + it("returns true if empty mount_point", function() { + makeController(); + var disk = { + fstype: "ext4", + mount_point: "" + }; - // Load the filesystems, cachesets, available, and used once. - $scope.nodeLoaded(); - $rootScope.$digest(); - - // Set all filesystems, cachesets, and available to selected. - angular.forEach($scope.filesystems, function(filesystem) { - filesystem.$selected = true; - }); - angular.forEach($scope.cachesets, function(cacheset) { - cacheset.$selected = true; - }); - angular.forEach($scope.available, function(disk) { - disk.$selected = true; - }); + expect($scope.hasUnmountedFilesystem(disk)).toBe(true); + }); - // Get all the options for available. - var options = []; - angular.forEach($scope.available, function(disk) { - options.push(disk.$options); - }); + it("returns false if has mount_point", function() { + makeController(); + var disk = { + fstype: "ext4", + mount_point: "/" + }; - // Force the disks to change so the filesystems, cachesets, available, - // and used are reloaded. - var firstFilesystem = $scope.filesystems[0]; - node.disks = angular.copy(node.disks); - $rootScope.$digest(); - expect($scope.filesystems[0]).not.toBe(firstFilesystem); - expect($scope.filesystems[0]).toEqual(firstFilesystem); - - // All filesystems, cachesets and available should be selected. - angular.forEach($scope.filesystems, function(filesystem) { - expect(filesystem.$selected).toBe(true); - }); - angular.forEach($scope.cachesets, function(cacheset) { - expect(cacheset.$selected).toBe(true); - }); - angular.forEach($scope.available, function(disk) { - expect(disk.$selected).toBe(true); - }); + expect($scope.hasUnmountedFilesystem(disk)).toBe(false); + }); + }); - // All available should have the same options. - angular.forEach($scope.available, function(disk, idx) { - expect(disk.$options).toBe(options[idx]); - }); + describe("showFreeSpace", function() { + it("returns true if volume group", function() { + makeController(); + var disk = { + type: "lvm-vg" + }; + + expect($scope.showFreeSpace(disk)).toBe(true); }); - it("availableNew.device object is updated", function() { - makeController(); - var disks = makeDisks(); - node.disks = disks; + it("returns true if physical with partitions", function() { + makeController(); + var disk = { + type: "physical", + has_partitions: true + }; - // Load the filesystems, cachesets, available, and used once. - $scope.nodeLoaded(); - $rootScope.$digest(); - - // Set availableNew.device to a disk from available. - var disk = $scope.available[0]; - $scope.availableNew.device = disk; - - // Force the update. The device should be the same value but - // a new object. - node.disks = angular.copy(node.disks); - $rootScope.$digest(); - expect($scope.availableNew.device).toEqual(disk); - expect($scope.availableNew.device).not.toBe(disk); + expect($scope.showFreeSpace(disk)).toBe(true); }); - it("availableNew.devices array is updated", function() { - makeController(); - var disks = makeDisks(); - node.disks = disks; + it("returns false if physical without partitions", function() { + makeController(); + var disk = { + type: "physical", + has_partitions: false + }; + + expect($scope.showFreeSpace(disk)).toBe(false); + }); - // Load the filesystems, cachesets, available, and used once. - $scope.nodeLoaded(); - $rootScope.$digest(); - - // Set availableNew.device to a disk from available. - var disk0 = $scope.available[0]; - var disk1 = $scope.available[1]; - $scope.availableNew.devices = [disk0, disk1]; - - // Force the update. The devices should be the same values but - // a new objects. - node.disks = angular.copy(node.disks); - $rootScope.$digest(); - expect($scope.availableNew.devices[0]).toEqual(disk0); - expect($scope.availableNew.devices[0]).not.toBe(disk0); - expect($scope.availableNew.devices[1]).toEqual(disk1); - expect($scope.availableNew.devices[1]).not.toBe(disk1); - }); - - describe("isBootDiskDisabled", function() { - - it("returns true when not editable", function() { - makeController(); - $scope.canEdit = function() { return false; }; - $scope.node.status = "Ready"; - var disk = { type: "physical" }; + it("returns true if virtual with partitions", function() { + makeController(); + var disk = { + type: "virtual", + has_partitions: true + }; - expect($scope.isBootDiskDisabled(disk, "available")).toBe(true); - }); + expect($scope.showFreeSpace(disk)).toBe(true); + }); - it("returns true when not node not ready", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node.status = "Deploying"; - var disk = { type: "physical" }; + it("returns false if virtual without partitions", function() { + makeController(); + var disk = { + type: "virtual", + has_partitions: false + }; - expect($scope.isBootDiskDisabled(disk, "available")).toBe(true); - }); + expect($scope.showFreeSpace(disk)).toBe(false); + }); - it("returns true if not physical", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node.status = "Ready"; - var disk = { type: "virtual" }; + it("returns false otherwise", function() { + makeController(); + var disk = { + type: "other" + }; - expect($scope.isBootDiskDisabled(disk, "available")).toBe(true); - }); + expect($scope.showFreeSpace(disk)).toBe(false); + }); + }); - it("returns false if in available", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node.status = "Ready"; - var disk = { type: "physical" }; + describe("getDeviceType", function() { + it("returns logical volume", function() { + makeController(); + var disk = { + type: "virtual", + parent_type: "lvm-vg" + }; - expect($scope.isBootDiskDisabled(disk, "available")).toBe(false); - }); + expect($scope.getDeviceType(disk)).toBe("Logical volume"); + }); - it("returns true when used and no partitions", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node.status = "Ready"; - var disk = { type: "physical", has_partitions: false }; + it("returns raid", function() { + makeController(); + var disk = { + type: "virtual", + parent_type: "raid-5" + }; - expect($scope.isBootDiskDisabled(disk, "used")).toBe(true); - }); + expect($scope.getDeviceType(disk)).toBe("RAID 5"); + }); - it("returns false when ready, used and partitions", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node.status = "Ready"; - var disk = { type: "physical", has_partitions: true }; + it("returns parent_type", function() { + makeController(); + var disk = { + type: "virtual", + parent_type: "other" + }; - expect($scope.isBootDiskDisabled(disk, "used")).toBe(false); - }); + expect($scope.getDeviceType(disk)).toBe("Other"); + }); - it("returns false when allocated, used and partitions", function() { - makeController(); - $scope.canEdit = function() { return true; }; - $scope.node.status = "Allocated"; - var disk = { type: "physical", has_partitions: true }; + it("returns volume group", function() { + makeController(); + var disk = { + type: "lvm-vg" + }; - expect($scope.isBootDiskDisabled(disk, "used")).toBe(false); - }); + expect($scope.getDeviceType(disk)).toBe("Volume group"); }); - describe("setAsBootDisk", function() { + it("returns type", function() { + makeController(); + var disk = { + type: "physical" + }; - it("does nothing if already boot disk", function() { - makeController(); - var disk = { is_boot: true }; - spyOn(MachinesManager, "setBootDisk"); - spyOn($scope, "isBootDiskDisabled").and.returnValue(false); + expect($scope.getDeviceType(disk)).toBe("Physical"); + }); + }); - $scope.setAsBootDisk(disk); + describe("getDeviceTypeLower", function() { + it("returns logical volume", function() { + makeController(); + var disk = { + type: "virtual", + parent_type: "lvm-vg" + }; - expect(MachinesManager.setBootDisk).not.toHaveBeenCalled(); - }); + expect($scope.getDeviceTypeLower(disk)).toBe("logical volume"); + }); - it("does nothing if set boot disk disabled", function() { - makeController(); - var disk = { is_boot: false }; - spyOn(MachinesManager, "setBootDisk"); - spyOn($scope, "isBootDiskDisabled").and.returnValue(true); + it("returns raid", function() { + makeController(); + var disk = { + type: "virtual", + parent_type: "raid-5" + }; - $scope.setAsBootDisk(disk); + expect($scope.getDeviceTypeLower(disk)).toBe("raid 5"); + }); - expect(MachinesManager.setBootDisk).not.toHaveBeenCalled(); - }); + it("returns parent_type", function() { + makeController(); + var disk = { + type: "virtual", + parent_type: "other" + }; - it("calls MachinesManager.setBootDisk", function() { - makeController(); - var disk = { block_id: makeInteger(0, 100), is_boot: false }; - spyOn(MachinesManager, "setBootDisk"); - spyOn($scope, "isBootDiskDisabled").and.returnValue(false); + expect($scope.getDeviceTypeLower(disk)).toBe("other"); + }); - $scope.setAsBootDisk(disk); + it("returns volume group", function() { + makeController(); + var disk = { + type: "lvm-vg" + }; - expect(MachinesManager.setBootDisk).toHaveBeenCalledWith( - node, disk.block_id); - }); + expect($scope.getDeviceTypeLower(disk)).toBe("volume group"); }); - describe("getSelectedFilesystems", function() { + it("returns type", function() { + makeController(); + var disk = { + type: "physical" + }; - it("returns selected filesystems", function() { - makeController(); - var filesystems = [ - { $selected: true }, - { $selected: true }, - { $selected: false }, - { $selected: false } - ]; - $scope.filesystems = filesystems; - expect($scope.getSelectedFilesystems()).toEqual( - [filesystems[0], filesystems[1]]); - }); + expect($scope.getDeviceTypeLower(disk)).toBe("physical"); }); + }); - describe("updateFilesystemSelection", function() { + describe("getSelectedAvailable", function() { + it("returns selected available", function() { + makeController(); + var available = [ + { $selected: true }, + { $selected: true }, + { $selected: false }, + { $selected: false } + ]; + $scope.available = available; + expect($scope.getSelectedAvailable()).toEqual([ + available[0], + available[1] + ]); + }); + }); - it("sets filesystemMode to NONE when none selected", function() { - makeController(); - spyOn($scope, "getSelectedFilesystems").and.returnValue([]); - $scope.filesystemMode = "other"; + describe("updateAvailableSelection", function() { + it("sets availableMode to NONE when none selected", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([]); + $scope.availableMode = "other"; - $scope.updateFilesystemSelection(); + $scope.updateAvailableSelection(); - expect($scope.filesystemMode).toBeNull(); - }); + expect($scope.availableMode).toBeNull(); + }); - it("doesn't sets filesystemMode to SINGLE when not force", function() { - makeController(); - spyOn($scope, "getSelectedFilesystems").and.returnValue([{}]); - $scope.filesystemMode = "other"; + it("doesn't sets availableMode to SINGLE when not force", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + $scope.availableMode = "other"; - $scope.updateFilesystemSelection(); + $scope.updateAvailableSelection(); - expect($scope.filesystemMode).toBe("other"); - }); + expect($scope.availableMode).toBe("other"); + }); - it("sets filesystemMode to SINGLE when force", function() { - makeController(); - spyOn($scope, "getSelectedFilesystems").and.returnValue([{}]); - $scope.filesystemMode = "other"; + it("sets availableMode to SINGLE when force", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + $scope.availableMode = "other"; - $scope.updateFilesystemSelection(true); + $scope.updateAvailableSelection(true); - expect($scope.filesystemMode).toBe("single"); - }); + expect($scope.availableMode).toBe("single"); + }); - it("doesn't sets filesystemMode to MUTLI when not force", function() { - makeController(); - spyOn($scope, "getSelectedFilesystems").and.returnValue([{}, {}]); - $scope.filesystemMode = "other"; + it("doesn't sets availableMode to MUTLI when not force", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); + $scope.availableMode = "other"; - $scope.updateFilesystemSelection(); + $scope.updateAvailableSelection(); - expect($scope.filesystemMode).toBe("other"); - }); + expect($scope.availableMode).toBe("other"); + }); - it("sets filesystemMode to MULTI when force", function() { - makeController(); - spyOn($scope, "getSelectedFilesystems").and.returnValue([{}, {}]); - $scope.filesystemMode = "other"; + it("sets availableMode to MULTI when force", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); + $scope.availableMode = "other"; - $scope.updateFilesystemSelection(true); + $scope.updateAvailableSelection(true); - expect($scope.filesystemMode).toBe("multi"); - }); + expect($scope.availableMode).toBe("multi"); + }); - it("sets filesystemAllSelected to false when none selected", - function() { - makeController(); - spyOn($scope, "getSelectedFilesystems").and.returnValue([]); - $scope.filesystemAllSelected = true; + it("sets availableAllSelected to false when none selected", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([]); + $scope.availableAllSelected = true; - $scope.updateFilesystemSelection(); + $scope.updateAvailableSelection(); - expect($scope.filesystemAllSelected).toBe(false); - }); + expect($scope.availableAllSelected).toBe(false); + }); - it("sets filesystemAllSelected to false when not all selected", - function() { - makeController(); - $scope.filesystems = [{}, {}]; - spyOn($scope, "getSelectedFilesystems").and.returnValue([{}]); - $scope.filesystemAllSelected = true; + it("sets availableAllSelected to false when not all selected", function() { + makeController(); + $scope.available = [{}, {}]; + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + $scope.availableAllSelected = true; - $scope.updateFilesystemSelection(); + $scope.updateAvailableSelection(); - expect($scope.filesystemAllSelected).toBe(false); - }); + expect($scope.availableAllSelected).toBe(false); + }); - it("sets filesystemAllSelected to true when all selected", - function() { - makeController(); - $scope.filesystems = [{}, {}]; - spyOn($scope, "getSelectedFilesystems").and.returnValue( - [{}, {}]); - $scope.filesystemAllSelected = false; + it("sets availableAllSelected to true when all selected", function() { + makeController(); + $scope.available = [{}, {}]; + spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); + $scope.availableAllSelected = false; - $scope.updateFilesystemSelection(); + $scope.updateAvailableSelection(); - expect($scope.filesystemAllSelected).toBe(true); - }); + expect($scope.availableAllSelected).toBe(true); }); + }); - describe("toggleFilesystemSelect", function() { - - it("inverts $selected", function() { - makeController(); - var filesystem = { $selected: true }; - spyOn($scope, "updateFilesystemSelection"); + describe("toggleAvailableSelect", function() { + it("inverts $selected", function() { + makeController(); + var disk = { $selected: true }; + spyOn($scope, "updateAvailableSelection"); - $scope.toggleFilesystemSelect(filesystem); + $scope.toggleAvailableSelect(disk); - expect(filesystem.$selected).toBe(false); - $scope.toggleFilesystemSelect(filesystem); - expect(filesystem.$selected).toBe(true); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith( - true); - }); + expect(disk.$selected).toBe(false); + $scope.toggleAvailableSelect(disk); + expect(disk.$selected).toBe(true); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); }); + }); - describe("toggleFilesystemAllSelect", function() { + describe("toggleAvailableAllSelect", function() { + it("sets all to true if not all selected", function() { + makeController(); + var available = [{ $selected: true }, { $selected: false }]; + $scope.available = available; + $scope.availableAllSelected = false; + spyOn($scope, "updateAvailableSelection"); - it("sets all to true if not all selected", function() { - makeController(); - var filesystems = [{ $selected: true }, { $selected: false }]; - $scope.filesystems = filesystems; - $scope.filesystemAllSelected = false; - spyOn($scope, "updateFilesystemSelection"); - - $scope.toggleFilesystemAllSelect(); - - expect(filesystems[0].$selected).toBe(true); - expect(filesystems[1].$selected).toBe(true); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith( - true); - }); + $scope.toggleAvailableAllSelect(); - it("sets all to false if all selected", function() { - makeController(); - var filesystems = [{ $selected: true }, { $selected: true }]; - $scope.filesystems = filesystems; - $scope.filesystemAllSelected = true; - spyOn($scope, "updateFilesystemSelection"); - - $scope.toggleFilesystemAllSelect(); - - expect(filesystems[0].$selected).toBe(false); - expect(filesystems[1].$selected).toBe(false); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith( - true); - }); + expect(available[0].$selected).toBe(true); + expect(available[1].$selected).toBe(true); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); }); - describe("isFilesystemsDisabled", function() { + it("sets all to false if all selected", function() { + makeController(); + var available = [{ $selected: true }, { $selected: true }]; + $scope.available = available; + $scope.availableAllSelected = true; + spyOn($scope, "updateAvailableSelection"); - it("returns false for NONE", function() { - makeController(); - $scope.filesystemMode = null; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.toggleAvailableAllSelect(); - expect($scope.isFilesystemsDisabled()).toBe(false); - }); + expect(available[0].$selected).toBe(false); + expect(available[1].$selected).toBe(false); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); + }); - it("returns false for SINGLE", function() { - makeController(); - $scope.filesystemMode = "single"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + describe("isAvailableDisabled", function() { + it("returns false for NONE", function() { + makeController(); + $scope.availableMode = null; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isFilesystemsDisabled()).toBe(false); - }); + expect($scope.isAvailableDisabled()).toBe(false); + }); - it("returns false for MULTI", function() { - makeController(); - $scope.filesystemMode = "multi"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + it("returns false for SINGLE", function() { + makeController(); + $scope.availableMode = "single"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isFilesystemsDisabled()).toBe(false); - }); + expect($scope.isAvailableDisabled()).toBe(false); + }); - it("returns true for UNMOUNT", function() { - makeController(); - $scope.filesystemMode = "unmount"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + it("returns false for MULTI", function() { + makeController(); + $scope.availableMode = "multi"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isFilesystemsDisabled()).toBe(true); - }); + expect($scope.isAvailableDisabled()).toBe(false); + }); - it("returns true when isAllStorageDisabled", function() { - makeController(); - $scope.filesystemMode = "multi"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(true); + it("returns true for UNMOUNT", function() { + makeController(); + $scope.availableMode = "unmount"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isFilesystemsDisabled()).toBe(true); - }); + expect($scope.isAvailableDisabled()).toBe(true); }); + }); - describe("filesystemCancel", function() { + describe("canFormatAndMount", function() { + it("returns false if lvm-vg", function() { + makeController(); + var disk = { type: "lvm-vg" }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + expect($scope.canFormatAndMount(disk)).toBe(false); + }); - it("calls updateFilesystemSelection with force true", function() { - makeController(); - var filesystems = [{ $selected: true }, { $selected: false }]; - $scope.filesystems = filesystems; - spyOn($scope, "updateFilesystemSelection"); - - $scope.filesystemCancel(); - - expect(filesystems[0].$selected).toBe(false); - expect(filesystems[1].$selected).toBe(false); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith( - true); - }); + it("returns false if has_partitions", function() { + makeController(); + var disk = { type: "physical", has_partitions: true }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + expect($scope.canFormatAndMount(disk)).toBe(false); }); - describe("filesystemUnmount", function() { + it("returns false if physical and is boot disk", function() { + makeController(); + var disk = { + type: "physical", + has_partitions: false, + original: { + is_boot: true + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + expect($scope.canFormatAndMount(disk)).toBe(false); + }); - it("sets filesystemMode to UNMOUNT", function() { - makeController(); - $scope.filesystemMode = "other"; + it("returns true otherwise", function() { + makeController(); + var disk = { + type: "physical", + has_partitions: false, + original: { + is_boot: false + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + expect($scope.canFormatAndMount(disk)).toBe(true); + }); + }); + + describe("getPartitionButtonText", function() { + it("returns Add Partition if already has partitions", function() { + makeController(); + expect( + $scope.getPartitionButtonText({ + has_partitions: true + }) + ).toBe("Add partition"); + }); + + it("returns Partition if no partitions", function() { + makeController(); + expect( + $scope.getPartitionButtonText({ + has_partitions: false + }) + ).toBe("Partition"); + }); + }); + + describe("canAddPartition", function() { + it("returns false if partition", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect( + $scope.canAddPartition({ + type: "partition" + }) + ).toBe(false); + }); + + it("returns false if lvm-vg", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect( + $scope.canAddPartition({ + type: "lvm-vg" + }) + ).toBe(false); + }); + + it("returns false if logical volume", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect( + $scope.canAddPartition({ + type: "virtual", + parent_type: "lvm-vg" + }) + ).toBe(false); + }); + + it("returns false if bcache", function() { + makeController(); + $scope.canEdit = function() { + return true; + }; + expect( + $scope.canAddPartition({ + type: "virtual", + parent_type: "bcache" + }) + ).toBe(false); + }); + + it("returns false if formatted", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect( + $scope.canAddPartition({ + type: "physical", + fstype: "ext4" + }) + ).toBe(false); + }); + + it( + "returns false if available_size is less than partition size " + + "and partition table extra space", + function() { + makeController(); + var disk = { + type: "physical", + fstype: "", + original: { + partition_table_type: null, + available_size: 2.5 * 1024 * 1024, + block_size: 1024 + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canAddPartition(disk)).toBe(false); + } + ); + + it(`returns false if available_size is + less than partition size`, function() { + makeController(); + var disk = { + type: "physical", + fstype: "", + original: { + partition_table_type: "mbr", + available_size: 1024 * 1024, + block_size: 1024 + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canAddPartition(disk)).toBe(false); + }); - $scope.filesystemUnmount(); + it( + "returns false if available_size is less than partition size " + + "when node is ppc64el architecture", + function() { + makeController(); + var disk = { + type: "physical", + fstype: "", + original: { + partition_table_type: null, + available_size: 2.5 * 1024 * 1024 + 8 * 1024 * 1024, + block_size: 1024 + } + }; + node.architecture = "ppc64el/generic"; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canAddPartition(disk)).toBe(false); + } + ); + + it("returns false if not super user", function() { + makeController(); + var disk = { + type: "physical", + fstype: "", + original: { + partition_table_type: null, + available_size: 10 * 1024 * 1024, + block_size: 1024 + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return false; + }; + expect($scope.canAddPartition(disk)).toBe(false); + }); + + it("returns false if isAllStorageDisabled", function() { + makeController(); + var disk = { + type: "physical", + fstype: "", + original: { + partition_table_type: null, + available_size: 10 * 1024 * 1024, + block_size: 1024 + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; + expect($scope.canAddPartition(disk)).toBe(false); + }); + + it("returns true otherwise", function() { + makeController(); + var disk = { + type: "physical", + fstype: "", + original: { + partition_table_type: null, + available_size: 10 * 1024 * 1024, + block_size: 1024 + } + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canAddPartition(disk)).toBe(true); + }); + }); + + describe("isNameInvalid", function() { + it("returns false if name is blank", function() { + makeController(); + var disk = { + name: "" + }; + + expect($scope.isNameInvalid(disk)).toBe(false); + }); + + it("returns true if name is already used by another disk", function() { + makeController(); + var otherId = makeInteger(0, 100); + var id = makeInteger(100, 200); + var name = makeName("name"); + var otherDisk = { + id: otherId, + type: "physical", + name: name + }; + var thisDisk = { + id: id, + type: "physical", + name: name + }; + + $scope.node.disks = [otherDisk, thisDisk]; + var disk = { + name: name, + block_id: id + }; + + expect($scope.isNameInvalid(disk)).toBe(true); + }); + + it("returns false if name is the same as self", function() { + makeController(); + var id = makeInteger(100, 200); + var name = makeName("name"); + var thisDisk = { + id: id, + type: "physical", + name: name + }; + + $scope.node.disks = [thisDisk]; + var disk = { + name: name, + type: "physical", + block_id: id + }; + + expect($scope.isNameInvalid(disk)).toBe(false); + }); + }); + + describe("nameHasChanged", function() { + it("logical volume resets name to include parents name", function() { + makeController(); + var disk = { + name: "", + type: "virtual", + parent_type: "lvm-vg", + original: { + name: "vg0-lvname" + } + }; - expect($scope.filesystemMode).toBe("unmount"); - }); + $scope.nameHasChanged(disk); + expect(disk.name).toBe("vg0-"); }); + }); - describe("quickFilesystemUnmount", function() { + describe("availableCancel", function() { + it("calls updateAvailableSelection with force true", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); - it("selects filesystem and calls filesystemUnmount", function() { - makeController(); - var filesystems = [{ $selected: true }, { $selected: false }]; - $scope.filesystems = filesystems; - spyOn($scope, "updateFilesystemSelection"); - spyOn($scope, "filesystemUnmount"); - - $scope.quickFilesystemUnmount(filesystems[1]); - - expect(filesystems[0].$selected).toBe(false); - expect(filesystems[1].$selected).toBe(true); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith( - true); - expect($scope.filesystemUnmount).toHaveBeenCalled(); - }); + $scope.availableCancel(available[0].$selected); + + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); }); + }); - describe("filesystemConfirmUnmount", function() { + describe("usesMountPoint", function() { + it("returns false if filesystem is undefined", function() { + makeController(); - it("calls MachinesManager.updateFilesystem", function() { - makeController(); - var filesystem = { - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100), - fstype: makeName("fs") - }; - $scope.filesystems = [filesystem]; - spyOn(MachinesManager, "updateFilesystem"); - spyOn($scope, "updateFilesystemSelection"); - - $scope.filesystemConfirmUnmount(filesystem); - - expect(MachinesManager.updateFilesystem).toHaveBeenCalledWith( - node, filesystem.block_id, filesystem.partition_id, - filesystem.fstype, null, null); - }); + expect($scope.usesMountPoint(undefined)).toBe(false); + }); - it("removes filesystem from filesystems", function() { - makeController(); - var filesystem = { - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100), - fstype: makeName("fs") - }; - $scope.filesystems = [filesystem]; - spyOn(MachinesManager, "updateFilesystem"); - spyOn($scope, "updateFilesystemSelection"); + it("returns false if filesystem is null", function() { + makeController(); - $scope.filesystemConfirmUnmount(filesystem); + expect($scope.usesMountPoint(null)).toBe(false); + }); - expect($scope.filesystems).toEqual([]); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(); - }); + it("returns false if filesystem is not a string", function() { + makeController(); + + expect($scope.usesMountPoint(1234)).toBe(false); }); - describe("filesystemDelete", function() { + it("returns false if filesystem is 'swap'", function() { + makeController(); - it("sets filesystemMode to DELETE", function() { - makeController(); - $scope.filesystemMode = "other"; + expect($scope.usesMountPoint("swap")).toBe(false); + }); - $scope.filesystemDelete(); + it("returns true if filesystem is not 'swap'", function() { + makeController(); - expect($scope.filesystemMode).toBe("delete"); - }); + expect($scope.usesMountPoint("any-string")).toBe(true); }); + }); - describe("quickFilesystemDelete", function() { + describe("isMountPointInvalid", function() { + it("returns false if mount_point is undefined", function() { + makeController(); - it("selects filesystem and calls filesystemDelete", function() { - makeController(); - var filesystems = [{ $selected: true }, { $selected: false }]; - $scope.filesystems = filesystems; - spyOn($scope, "updateFilesystemSelection"); - spyOn($scope, "filesystemDelete"); - - $scope.quickFilesystemDelete(filesystems[1]); - - expect(filesystems[0].$selected).toBe(false); - expect(filesystems[1].$selected).toBe(true); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith( - true); - expect($scope.filesystemDelete).toHaveBeenCalled(); - }); + expect($scope.isMountPointInvalid()).toBe(false); }); - describe("filesystemConfirmDelete", function() { + it("returns false if mount_point is empty", function() { + makeController(); - it("calls MachinesManager.deletePartition for partition", function() { - makeController(); - var filesystem = { - original_type: "partition", - original: { - id: makeInteger(0, 100) - } - }; - $scope.filesystems = [filesystem]; - spyOn(MachinesManager, "deletePartition"); - spyOn($scope, "updateFilesystemSelection"); - - $scope.filesystemConfirmDelete(filesystem); - expect(MachinesManager.deletePartition).toHaveBeenCalledWith( - node, filesystem.original.id); - expect($scope.filesystems).toEqual([]); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(); - }); + expect($scope.isMountPointInvalid("")).toBe(false); + }); - it("calls MachinesManager.deleteFilesystem for disk", function() { - makeController(); - var filesystem = { - original_type: "physical", - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100), - filesystem_id: makeInteger(0, 100) - }; - $scope.filesystems = [filesystem]; - spyOn(MachinesManager, "deleteFilesystem"); - spyOn($scope, "updateFilesystemSelection"); - - $scope.filesystemConfirmDelete(filesystem); - expect(MachinesManager.deleteFilesystem).toHaveBeenCalledWith( - node, filesystem.block_id, filesystem.partition_id, - filesystem.filesystem_id); - expect($scope.filesystems).toEqual([]); - expect($scope.updateFilesystemSelection).toHaveBeenCalledWith(); - }); + it("returns false if mount_point is 'none'", function() { + makeController(); + + expect($scope.isMountPointInvalid("none")).toBe(false); }); - describe("hasUnmountedFilesystem", function() { + it("returns true if mount_point doesn't start with '/'", function() { + makeController(); - it("returns false if no fstype", function() { - makeController(); - var disk = { - fstype: null - }; + expect($scope.isMountPointInvalid("a")).toBe(true); + }); - expect($scope.hasUnmountedFilesystem(disk)).toBe(false); - }); + it("returns false if mount_point start with '/'", function() { + makeController(); - it("returns false if empty fstype", function() { - makeController(); - var disk = { - fstype: "" - }; + expect($scope.isMountPointInvalid("/")).toBe(false); + }); + }); - expect($scope.hasUnmountedFilesystem(disk)).toBe(false); - }); + describe("canDelete", function() { + it("returns true if volume group not used", function() { + makeController(); + var disk = { + type: "lvm-vg", + fstype: null, + has_partitions: false, + original: { + used_size: 0 + } + }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(true); + }); + + it("returns false if not super user", function() { + makeController(); + var disk = { + type: "lvm-vg", + fstype: null, + has_partitions: false, + original: { + used_size: 0 + } + }; + $scope.canEdit = function() { + return false; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(false); + }); + + it("returns false if isAllStorageDisabled", function() { + makeController(); + var disk = { + type: "lvm-vg", + fstype: null, + has_partitions: false, + original: { + used_size: 0 + } + }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(true); + + expect($scope.canDelete(disk)).toBe(false); + }); + + it("returns false if volume group used", function() { + makeController(); + var disk = { + type: "lvm-vg", + fstype: null, + has_partitions: false, + original: { + used_size: makeInteger(100, 10000) + } + }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(false); + }); + + it("returns true if fstype is null", function() { + makeController(); + var disk = { fstype: null, has_partitions: false }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(true); + }); + + it("returns true if fstype is empty", function() { + makeController(); + var disk = { fstype: "", has_partitions: false }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(true); + }); + + it("returns true if fstype is not empty", function() { + makeController(); + var disk = { fstype: "ext4" }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(true); + }); + + it("returns false if has_partitions is true", function() { + makeController(); + var disk = { fstype: "", has_partitions: true }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.canDelete(disk)).toBe(false); + }); + }); + + describe("canDeleteFilesystem", function() { + it("returns true if special", function() { + makeController(); + var filesystem = { + original_type: "special" + }; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canDeleteFilesystem(filesystem)).toBe(true); + }); + + it("returns canEdit otherwise", function() { + makeController(); + var filesystem = { + original_type: "other" + }; + $scope.canEdit = function() { + return false; + }; + + expect($scope.canDeleteFilesystem(filesystem)).toBe(false); + }); + }); + + describe("availableDelete", function() { + it("sets availableMode to DELETE", function() { + makeController(); + $scope.availableMode = "other"; + + $scope.availableDelete(); + + expect($scope.availableMode).toBe("delete"); + }); + }); + + describe("availableQuickDelete", function() { + it("selects disks and deselects others", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + $scope.available = available; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availableDelete"); + + $scope.availableQuickDelete(available[0]); + + expect(available[0].$selected).toBe(true); + expect(available[1].$selected).toBe(false); + }); + + it("calls updateAvailableSelection with force true", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availableDelete"); + + $scope.availableQuickDelete(available[0]); + + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); + + it("calls availableDelete", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availableDelete"); + + $scope.availableQuickDelete(available[0]); + + expect($scope.availableDelete).toHaveBeenCalledWith(); + }); + }); + + describe("getRemoveTypeText", function() { + it("returns 'physical disk' for physical on filesystem", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "filesystem", + original: { + type: "physical" + } + }) + ).toBe("physical disk"); + }); + + it("returns 'physical disk' for physical", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "physical" + }) + ).toBe("physical disk"); + }); + + it("returns 'partition' for partition", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "partition" + }) + ).toBe("partition"); + }); + + it("returns 'volume group' for lvm-vg", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "lvm-vg" + }) + ).toBe("volume group"); + }); + + it("returns 'logical volume' for virtual on lvm-vg", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "virtual", + parent_type: "lvm-vg" + }) + ).toBe("logical volume"); + }); + + it("returns 'RAID %d' for virtual on raid", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "virtual", + parent_type: "raid-1" + }) + ).toBe("RAID 1 disk"); + }); + + it("returns parent_type + 'disk' for other virtual", function() { + makeController(); + expect( + $scope.getRemoveTypeText({ + type: "virtual", + parent_type: "raid0" + }) + ).toBe("raid0 disk"); + }); + }); + + describe("availableConfirmDelete", function() { + it("calls MachinesManager.deleteVolumeGroup for lvm-vg", function() { + makeController(); + var disk = { + type: "lvm-vg", + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100) + }; + $scope.available = [disk]; + spyOn(MachinesManager, "deleteVolumeGroup"); + spyOn($scope, "updateAvailableSelection"); + + $scope.availableConfirmDelete(disk); + expect(MachinesManager.deleteVolumeGroup).toHaveBeenCalledWith( + node, + disk.block_id + ); + expect($scope.available).toEqual([]); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); + + it("calls MachinesManager.deletePartition for partition", function() { + makeController(); + var disk = { + type: "partition", + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100) + }; + $scope.available = [disk]; + spyOn(MachinesManager, "deletePartition"); + spyOn($scope, "updateAvailableSelection"); + + $scope.availableConfirmDelete(disk); + expect(MachinesManager.deletePartition).toHaveBeenCalledWith( + node, + disk.partition_id + ); + expect($scope.available).toEqual([]); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); + + it("calls MachinesManager.deleteDisk for disk", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100) + }; + $scope.available = [disk]; + spyOn(MachinesManager, "deleteDisk"); + spyOn($scope, "updateAvailableSelection"); + + $scope.availableConfirmDelete(disk); + expect(MachinesManager.deleteDisk).toHaveBeenCalledWith( + node, + disk.block_id + ); + expect($scope.available).toEqual([]); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); + }); + + describe("availablePartition", function() { + it("sets availableMode to 'partition'", function() { + makeController(); + var disk = { + available_size_human: "10 GB" + }; + $scope.availableMode = "other"; + $scope.availablePartition(disk); + expect($scope.availableMode).toBe("partition"); + }); + + it("sets $options to values from available_size_human", function() { + makeController(); + var disk = { + available_size_human: "10 GB" + }; + $scope.availablePartition(disk); + expect(disk.$options).toEqual({ + size: "10", + sizeUnits: "GB", + fstype: null, + mountPoint: "", + mountOptions: "" + }); + }); + }); - it("returns true if no mount_point", function() { - makeController(); - var disk = { - fstype: "ext4", - mount_point: null - }; + describe("availableQuickPartition", function() { + it("selects disks and deselects others", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + $scope.available = available; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availablePartition"); - expect($scope.hasUnmountedFilesystem(disk)).toBe(true); - }); + $scope.availableQuickPartition(available[0]); - it("returns true if empty mount_point", function() { - makeController(); - var disk = { - fstype: "ext4", - mount_point: "" - }; + expect(available[0].$selected).toBe(true); + expect(available[1].$selected).toBe(false); + }); - expect($scope.hasUnmountedFilesystem(disk)).toBe(true); - }); + it("calls updateAvailableSelection with force true", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availablePartition"); - it("returns false if has mount_point", function() { - makeController(); - var disk = { - fstype: "ext4", - mount_point: "/" - }; + $scope.availableQuickPartition(available[0]); - expect($scope.hasUnmountedFilesystem(disk)).toBe(false); - }); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); }); - describe("showFreeSpace", function() { + it("calls availablePartition", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availablePartition"); - it("returns true if volume group", function() { - makeController(); - var disk = { - type: "lvm-vg" - }; + $scope.availableQuickPartition(available[0]); - expect($scope.showFreeSpace(disk)).toBe(true); - }); + expect($scope.availablePartition).toHaveBeenCalledWith(available[0]); + }); + }); - it("returns true if physical with partitions", function() { - makeController(); - var disk = { - type: "physical", - has_partitions: true - }; + describe("getAddPartitionName", function() { + it("returns disk.name with -part#", function() { + makeController(); + var name = makeName("sda"); + var disk = { + name: name, + original: { + partition_table_type: "gpt", + partitions: [{}, {}] + } + }; - expect($scope.showFreeSpace(disk)).toBe(true); - }); + expect($scope.getAddPartitionName(disk)).toBe(name + "-part3"); + }); - it("returns false if physical without partitions", function() { - makeController(); - var disk = { - type: "physical", - has_partitions: false - }; + it("returns disk.name with -part2 for ppc64el", function() { + node.architecture = "ppc64el/generic"; + makeController(); + var name = makeName("sda"); + var disk = { + name: name, + original: { + is_boot: true, + partition_table_type: "gpt" + } + }; - expect($scope.showFreeSpace(disk)).toBe(false); - }); + expect($scope.getAddPartitionName(disk)).toBe(name + "-part2"); + }); - it("returns true if virtual with partitions", function() { - makeController(); - var disk = { - type: "virtual", - has_partitions: true - }; + it("returns disk.name with -part4 for ppc64el", function() { + node.architecture = "ppc64el/generic"; + makeController(); + var name = makeName("sda"); + var disk = { + name: name, + original: { + is_boot: true, + partition_table_type: "gpt", + partitions: [{}, {}] + } + }; - expect($scope.showFreeSpace(disk)).toBe(true); - }); + expect($scope.getAddPartitionName(disk)).toBe(name + "-part4"); + }); - it("returns false if virtual without partitions", function() { - makeController(); - var disk = { - type: "virtual", - has_partitions: false - }; + it("returns disk.name with -part3 for MBR", function() { + makeController(); + var name = makeName("sda"); + var disk = { + name: name, + original: { + partition_table_type: "mbr", + partitions: [{}, {}] + } + }; - expect($scope.showFreeSpace(disk)).toBe(false); - }); + expect($scope.getAddPartitionName(disk)).toBe(name + "-part3"); + }); - it("returns false otherwise", function() { - makeController(); - var disk = { - type: "other" - }; + it("returns disk.name with -part5 for MBR", function() { + makeController(); + var name = makeName("sda"); + var disk = { + name: name, + original: { + partition_table_type: "mbr", + partitions: [{}, {}, {}] + } + }; - expect($scope.showFreeSpace(disk)).toBe(false); - }); + expect($scope.getAddPartitionName(disk)).toBe(name + "-part5"); }); + }); - describe("getDeviceType", function() { + describe("isAddPartitionSizeInvalid", function() { + it("returns true if blank", function() { + makeController(); + var size = ""; + var disk = { + $options: { + sizeUnits: "GB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); + }); - it("returns logical volume", function() { - makeController(); - var disk = { - type: "virtual", - parent_type: "lvm-vg" - }; + it("returns true if not numbers", function() { + makeController(); + var size = makeName("invalid"); + var disk = { + $options: { + sizeUnits: "GB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); + }); - expect($scope.getDeviceType(disk)).toBe("Logical volume"); - }); + it("returns true if smaller than MIN_PARTITION_SIZE", function() { + makeController(); + var size = "1"; + var disk = { + $options: { + sizeUnits: "MB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); + }); + + it(`returns true if larger than available_size + more than tolerance`, function() { + makeController(); + var size = "4"; + var disk = { + original: { + available_size: 2 * 1000 * 1000 * 1000 + }, + $options: { + size: "4", + sizeUnits: "GB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); + }); + + it("returns false if larger than available_size in tolerance", function() { + makeController(); + var size = "2.62"; + var disk = { + original: { + available_size: 2.6 * 1000 * 1000 * 1000 + }, + $options: { + sizeUnits: "GB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + expect($scope.isAddPartitionSizeInvalid(disk)).toBe(false); + }); + + it("returns false if less than available_size", function() { + makeController(); + var size = "1.6"; + var disk = { + original: { + available_size: 2.6 * 1000 * 1000 * 1000 + }, + $options: { + sizeUnits: "GB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + expect($scope.isAddPartitionSizeInvalid(disk)).toBe(false); + }); + }); + + describe("availableConfirmPartition", function() { + it("does nothing if invalid", function() { + makeController(); + var size = ""; + var disk = { + $options: { + sizeUnits: "GB" + } + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.returnValue(size); + $scope.$digest(); + + spyOn(MachinesManager, "createPartition"); + + $scope.availableConfirmPartition(disk); + + expect(MachinesManager.createPartition).not.toHaveBeenCalled(); + }); + + it("calls createPartition with bytes", function() { + makeController(); + var disk = { + block_id: makeInteger(0, 100), + original: { + partition_table_type: "mbr", + available_size: 4 * 1000 * 1000 * 1000, + available_size_human: "4.0 GB", + block_size: 512 + }, + $options: { + sizeUnits: "GB" + } + }; + var params = { + size: "2", + mount_point: makeName("/path"), + mount_options: makeName("options") + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.callFake(function( + param + ) { + return params[param]; + }); + $scope.$digest(); - it("returns raid", function() { - makeController(); - var disk = { - type: "virtual", - parent_type: "raid-5" - }; + $scope.availableConfirmPartition(disk); - expect($scope.getDeviceType(disk)).toBe("RAID 5"); - }); + expect($scope.newPartition.system_id).toEqual(node.system_id); + expect($scope.newPartition.block_id).toEqual(disk.block_id); + expect($scope.newPartition.partition_size).toEqual( + 2 * 1000 * 1000 * 1000 + ); + }); - it("returns parent_type", function() { - makeController(); - var disk = { - type: "virtual", - parent_type: "other" - }; + it( + "calls createPartition with fstype, " + "mountPoint, and mountOptions", + function() { + makeController(); + var disk = { + block_id: makeInteger(0, 100), + original: { + partition_table_type: "mbr", + available_size: 4 * 1000 * 1000 * 1000, + available_size_human: "4.0 GB", + block_size: 512 + }, + $options: { + sizeUnits: "GB", + fstype: "ext4" + } + }; + var params = { + size: "2", + mount_point: makeName("/path"), + mount_options: makeName("options") + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.callFake(function( + param + ) { + return params[param]; + }); + $scope.$digest(); + + $scope.availableConfirmPartition(disk); + + expect($scope.newPartition.params).toEqual({ + fstype: "ext4", + mount_point: params["mount_point"], + mount_options: params["mount_options"] + }); + } + ); + + it("calls createPartition with available_size bytes", function() { + makeController(); + var available_size = 2.6 * 1000 * 1000 * 1000; + var disk = { + block_id: makeInteger(0, 100), + original: { + partition_table_type: "mbr", + available_size: available_size, + available_size_human: "2.6 GB", + block_size: 512 + }, + $options: { + sizeUnits: "GB" + } + }; + var params = { + size: "2.62", + mount_point: makeName("/path"), + mount_options: makeName("options") + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.callFake(function( + param + ) { + return params[param]; + }); + $scope.$digest(); - expect($scope.getDeviceType(disk)).toBe("Other"); - }); + $scope.availableConfirmPartition(disk); - it("returns volume group", function() { - makeController(); - var disk = { - type: "lvm-vg" - }; + // Align to 4MiB. + var align_size = 4 * 1024 * 1024; + var expected = align_size * Math.floor(available_size / align_size); - expect($scope.getDeviceType(disk)).toBe("Volume group"); - }); + expect($scope.newPartition.partition_size).toEqual(expected); + }); - it("returns type", function() { - makeController(); - var disk = { - type: "physical" - }; + // regression test for https://bugs.launchpad.net/maas/+bug/1509535 + it( + "calls createPartition with available_size bytes" + + " even when human size gets rounded down", + function() { + makeController(); + var available_size = 2.035 * 1000 * 1000 * 1000; + var disk = { + block_id: makeInteger(0, 100), + original: { + partition_table_type: "mbr", + available_size: available_size, + available_size_human: "2.0 GB", + block_size: 512 + }, + $options: { + sizeUnits: "GB" + } + }; + var params = { + size: "2.0", + mount_point: makeName("/path"), + mount_options: makeName("options") + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.callFake(function( + param + ) { + return params[param]; + }); + $scope.$digest(); + + $scope.availableConfirmPartition(disk); + + // Align to 4MiB. + var align_size = 4 * 1024 * 1024; + var expected = align_size * Math.floor(available_size / align_size); + + expect($scope.newPartition.partition_size).toEqual(expected); + } + ); + + it(`calls createPartition with bytes + minus partition table extra`, function() { + makeController(); + var available_size = 2.6 * 1000 * 1000 * 1000; + var disk = { + block_id: makeInteger(0, 100), + original: { + partition_table_type: "", + available_size: available_size, + available_size_human: "2.6 GB", + block_size: 512 + }, + $options: { + sizeUnits: "GB" + } + }; + var params = { + size: "2.62", + mount_point: makeName("/path"), + mount_options: makeName("options") + }; + $scope.newPartition.$maasForm = { getValue: function() {} }; + spyOn($scope.newPartition.$maasForm, "getValue").and.callFake(function( + param + ) { + return params[param]; + }); + $scope.$digest(); - expect($scope.getDeviceType(disk)).toBe("Physical"); - }); - }); + $scope.availableConfirmPartition(disk); - describe("getSelectedAvailable", function() { + // Remove partition extra space and align to 4MiB. + var align_size = 4 * 1024 * 1024; + var expected = + align_size * + Math.floor((available_size - 5 * 1024 * 1024) / align_size); - it("returns selected available", function() { - makeController(); - var available = [ - { $selected: true }, - { $selected: true }, - { $selected: false }, - { $selected: false } - ]; - $scope.available = available; - expect($scope.getSelectedAvailable()).toEqual( - [available[0], available[1]]); - }); + expect($scope.newPartition.partition_size).toEqual(expected); }); + }); - describe("updateAvailableSelection", function() { + describe("getSelectedCacheSets", function() { + it("returns selected cachesets", function() { + makeController(); + var cachesets = [ + { $selected: true }, + { $selected: true }, + { $selected: false }, + { $selected: false } + ]; + $scope.cachesets = cachesets; + expect($scope.getSelectedCacheSets()).toEqual([ + cachesets[0], + cachesets[1] + ]); + }); + }); - it("sets availableMode to NONE when none selected", function() { - makeController(); - spyOn($scope, "getSelectedAvailable").and.returnValue([]); - $scope.availableMode = "other"; + describe("updateCacheSetsSelection", function() { + it("sets cachesetsMode to NONE when none selected", function() { + makeController(); + spyOn($scope, "getSelectedCacheSets").and.returnValue([]); + $scope.cachesetsMode = "other"; - $scope.updateAvailableSelection(); + $scope.updateCacheSetsSelection(); - expect($scope.availableMode).toBeNull(); - }); + expect($scope.cachesetsMode).toBeNull(); + }); - it("doesn't sets availableMode to SINGLE when not force", function() { - makeController(); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - $scope.availableMode = "other"; + it("doesn't sets cachesetsMode to SINGLE when not force", function() { + makeController(); + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); + $scope.cachesetsMode = "other"; - $scope.updateAvailableSelection(); + $scope.updateCacheSetsSelection(); - expect($scope.availableMode).toBe("other"); - }); + expect($scope.cachesetsMode).toBe("other"); + }); - it("sets availableMode to SINGLE when force", function() { - makeController(); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - $scope.availableMode = "other"; + it("sets cachesetsMode to SINGLE when force", function() { + makeController(); + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); + $scope.cachesetsMode = "other"; - $scope.updateAvailableSelection(true); + $scope.updateCacheSetsSelection(true); - expect($scope.availableMode).toBe("single"); - }); + expect($scope.cachesetsMode).toBe("single"); + }); - it("doesn't sets availableMode to MUTLI when not force", function() { - makeController(); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); - $scope.availableMode = "other"; + it("doesn't sets cachesetsMode to MUTLI when not force", function() { + makeController(); + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); + $scope.cachesetsMode = "other"; - $scope.updateAvailableSelection(); + $scope.updateCacheSetsSelection(); - expect($scope.availableMode).toBe("other"); - }); + expect($scope.cachesetsMode).toBe("other"); + }); - it("sets availableMode to MULTI when force", function() { - makeController(); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); - $scope.availableMode = "other"; + it("sets cachesetsMode to MULTI when force", function() { + makeController(); + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); + $scope.cachesetsMode = "other"; - $scope.updateAvailableSelection(true); + $scope.updateCacheSetsSelection(true); - expect($scope.availableMode).toBe("multi"); - }); + expect($scope.cachesetsMode).toBe("multi"); + }); - it("sets availableAllSelected to false when none selected", - function() { - makeController(); - spyOn($scope, "getSelectedAvailable").and.returnValue([]); - $scope.availableAllSelected = true; + it("sets cachesetsAllSelected to false when none selected", function() { + makeController(); + spyOn($scope, "getSelectedCacheSets").and.returnValue([]); + $scope.cachesetsAllSelected = true; - $scope.updateAvailableSelection(); + $scope.updateCacheSetsSelection(); - expect($scope.availableAllSelected).toBe(false); - }); + expect($scope.cachesetsAllSelected).toBe(false); + }); - it("sets availableAllSelected to false when not all selected", - function() { - makeController(); - $scope.available = [{}, {}]; - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - $scope.availableAllSelected = true; + it("sets cachesetsAllSelected to false when not all selected", function() { + makeController(); + $scope.cachesets = [{}, {}]; + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); + $scope.cachesetsAllSelected = true; - $scope.updateAvailableSelection(); + $scope.updateCacheSetsSelection(); - expect($scope.availableAllSelected).toBe(false); - }); + expect($scope.cachesetsAllSelected).toBe(false); + }); - it("sets availableAllSelected to true when all selected", - function() { - makeController(); - $scope.available = [{}, {}]; - spyOn($scope, "getSelectedAvailable").and.returnValue( - [{}, {}]); - $scope.availableAllSelected = false; + it("sets cachesetsAllSelected to true when all selected", function() { + makeController(); + $scope.cachesets = [{}, {}]; + spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); + $scope.cachesetsAllSelected = false; - $scope.updateAvailableSelection(); + $scope.updateCacheSetsSelection(); - expect($scope.availableAllSelected).toBe(true); - }); + expect($scope.cachesetsAllSelected).toBe(true); }); + }); - describe("toggleAvailableSelect", function() { - - it("inverts $selected", function() { - makeController(); - var disk = { $selected: true }; - spyOn($scope, "updateAvailableSelection"); + describe("toggleCacheSetSelect", function() { + it("inverts $selected", function() { + makeController(); + var cacheset = { $selected: true }; + spyOn($scope, "updateCacheSetsSelection"); - $scope.toggleAvailableSelect(disk); + $scope.toggleCacheSetSelect(cacheset); - expect(disk.$selected).toBe(false); - $scope.toggleAvailableSelect(disk); - expect(disk.$selected).toBe(true); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); + expect(cacheset.$selected).toBe(false); + $scope.toggleCacheSetSelect(cacheset); + expect(cacheset.$selected).toBe(true); + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(true); }); + }); - describe("toggleAvailableAllSelect", function() { + describe("toggleCacheSetAllSelect", function() { + it("sets all to true if not all selected", function() { + makeController(); + var cachesets = [{ $selected: true }, { $selected: false }]; + $scope.cachesets = cachesets; + $scope.cachesetsAllSelected = false; + spyOn($scope, "updateCacheSetsSelection"); - it("sets all to true if not all selected", function() { - makeController(); - var available = [{ $selected: true }, { $selected: false }]; - $scope.available = available; - $scope.availableAllSelected = false; - spyOn($scope, "updateAvailableSelection"); - - $scope.toggleAvailableAllSelect(); - - expect(available[0].$selected).toBe(true); - expect(available[1].$selected).toBe(true); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); + $scope.toggleCacheSetAllSelect(); - it("sets all to false if all selected", function() { - makeController(); - var available = [{ $selected: true }, { $selected: true }]; - $scope.available = available; - $scope.availableAllSelected = true; - spyOn($scope, "updateAvailableSelection"); - - $scope.toggleAvailableAllSelect(); - - expect(available[0].$selected).toBe(false); - expect(available[1].$selected).toBe(false); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); + expect(cachesets[0].$selected).toBe(true); + expect(cachesets[1].$selected).toBe(true); + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(true); }); - describe("isAvailableDisabled", function() { + it("sets all to false if all selected", function() { + makeController(); + var cachesets = [{ $selected: true }, { $selected: true }]; + $scope.cachesets = cachesets; + $scope.cachesetsAllSelected = true; + spyOn($scope, "updateCacheSetsSelection"); - it("returns false for NONE", function() { - makeController(); - $scope.availableMode = null; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.toggleCacheSetAllSelect(); - expect($scope.isAvailableDisabled()).toBe(false); - }); + expect(cachesets[0].$selected).toBe(false); + expect(cachesets[1].$selected).toBe(false); + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(true); + }); + }); - it("returns false for SINGLE", function() { - makeController(); - $scope.availableMode = "single"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + describe("isCacheSetsDisabled", function() { + it("returns false for NONE", function() { + makeController(); + $scope.cachesetsMode = null; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isAvailableDisabled()).toBe(false); - }); + expect($scope.isCacheSetsDisabled()).toBe(false); + }); - it("returns false for MULTI", function() { - makeController(); - $scope.availableMode = "multi"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + it("returns false for SINGLE", function() { + makeController(); + $scope.cachesetsMode = "single"; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isAvailableDisabled()).toBe(false); - }); + expect($scope.isCacheSetsDisabled()).toBe(false); + }); - it("returns true for UNMOUNT", function() { - makeController(); - $scope.availableMode = "unmount"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + it("returns false for MULTI", function() { + makeController(); + $scope.cachesetsMode = "multi"; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.isAvailableDisabled()).toBe(true); - }); + expect($scope.isCacheSetsDisabled()).toBe(false); }); - describe("canFormatAndMount", function() { + it("returns true for when not super user", function() { + makeController(); + $scope.cachesetsMode = "delete"; + $scope.canEdit = function() { + return false; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - it("returns false if lvm-vg", function() { - makeController(); - var disk = { type: "lvm-vg" }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.canFormatAndMount(disk)).toBe(false); - }); + expect($scope.isCacheSetsDisabled()).toBe(true); + }); - it("returns false if has_partitions", function() { - makeController(); - var disk = { type: "physical", has_partitions: true }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.canFormatAndMount(disk)).toBe(false); - }); + it("returns true for when isAllStorageDisabled", function() { + makeController(); + $scope.cachesetsMode = "delete"; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - it("returns false if physical and is boot disk", function() { - makeController(); - var disk = { - type: "physical", - has_partitions: false, - original: { - is_boot: true - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.canFormatAndMount(disk)).toBe(false); - }); + expect($scope.isCacheSetsDisabled()).toBe(true); + }); - it("returns true otherwise", function() { - makeController(); - var disk = { - type: "physical", - has_partitions: false, - original: { - is_boot: false - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.canFormatAndMount(disk)).toBe(true); - }); + it("returns true for DELETE", function() { + makeController(); + $scope.cachesetsMode = "delete"; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + + expect($scope.isCacheSetsDisabled()).toBe(true); }); + }); - describe("getPartitionButtonText", function() { + describe("cacheSetCancel", function() { + it("calls updateCacheSetsSelection with force true", function() { + makeController(); + spyOn($scope, "updateCacheSetsSelection"); - it("returns Add Partition if already has partitions", function() { - makeController(); - expect($scope.getPartitionButtonText({ - has_partitions: true - })).toBe("Add partition"); - }); + $scope.cacheSetCancel(); - it("returns Partition if no partitions", function() { - makeController(); - expect($scope.getPartitionButtonText({ - has_partitions: false - })).toBe("Partition"); - }); + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(true); }); + }); - describe("canAddPartition", function() { + describe("canDeleteCacheSet", function() { + it("returns true when not being used", function() { + makeController(); + var cacheset = { used_by: "" }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - it("returns false if partition", function() { - makeController(); - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition({ - type: "partition" - })).toBe(false); - }); + expect($scope.canDeleteCacheSet(cacheset)).toBe(true); + }); - it("returns false if lvm-vg", function() { - makeController(); - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition({ - type: "lvm-vg" - })).toBe(false); - }); + it("returns false when being used", function() { + makeController(); + var cacheset = { used_by: "bcache0" }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - it("returns false if logical volume", function() { - makeController(); - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition({ - type: "virtual", - parent_type: "lvm-vg" - })).toBe(false); - }); + expect($scope.canDeleteCacheSet(cacheset)).toBe(false); + }); - it("returns false if bcache", function() { - makeController(); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition({ - type: "virtual", - parent_type: "bcache" - })).toBe(false); - }); + it("returns false when not super user", function() { + makeController(); + var cacheset = { used_by: "" }; + $scope.canEdit = function() { + return false; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - it("returns false if formatted", function() { - makeController(); - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition({ - type: "physical", - fstype: "ext4" - })).toBe(false); - }); - - it("returns false if available_size is less than partition size " + - "and partition table extra space", function() { - makeController(); - var disk = { - type: "physical", - fstype: "", - original: { - partition_table_type: null, - available_size: 2.5 * 1024 * 1024, - block_size: 1024 - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition(disk)).toBe(false); - }); - - it("returns false if available_size is less than partition size ", - function() { - makeController(); - var disk = { - type: "physical", - fstype: "", - original: { - partition_table_type: "mbr", - available_size: 1024 * 1024, - block_size: 1024 - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition(disk)).toBe(false); - }); - - it("returns false if available_size is less than partition size " + - "when node is ppc64el architecture", - function() { - makeController(); - var disk = { - type: "physical", - fstype: "", - original: { - partition_table_type: null, - available_size: (2.5 * 1024 * 1024) + (8 * 1024 * 1024), - block_size: 1024 - } - }; - node.architecture = "ppc64el/generic"; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition(disk)).toBe(false); - }); - - it("returns false if not super user", function() { - makeController(); - var disk = { - type: "physical", - fstype: "", - original: { - partition_table_type: null, - available_size: 10 * 1024 * 1024, - block_size: 1024 - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return false; }; - expect($scope.canAddPartition(disk)).toBe(false); - }); - - it("returns false if isAllStorageDisabled", function() { - makeController(); - var disk = { - type: "physical", - fstype: "", - original: { - partition_table_type: null, - available_size: 10 * 1024 * 1024, - block_size: 1024 - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition(disk)).toBe(false); - }); - - it("returns true otherwise", function() { - makeController(); - var disk = { - type: "physical", - fstype: "", - original: { - partition_table_type: null, - available_size: 10 * 1024 * 1024, - block_size: 1024 - } - }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canAddPartition(disk)).toBe(true); - }); - }); - - describe("isNameInvalid", function() { - - it("returns false if name is blank", function() { - makeController(); - var disk = { - name: "" - }; - - expect($scope.isNameInvalid(disk)).toBe(false); - }); - - it("returns true if name is already used by another disk", function() { - makeController(); - var otherId = makeInteger(0, 100); - var id = makeInteger(100, 200); - var name = makeName("name"); - var otherDisk = { - id: otherId, - type: "physical", - name: name - }; - var thisDisk = { - id: id, - type: "physical", - name: name - }; - - $scope.node.disks = [otherDisk, thisDisk]; - var disk = { - name: name, - block_id: id - }; - - expect($scope.isNameInvalid(disk)).toBe(true); - }); - - it("returns false if name is the same as self", function() { - makeController(); - var id = makeInteger(100, 200); - var name = makeName("name"); - var thisDisk = { - id: id, - type: "physical", - name: name - }; - - $scope.node.disks = [thisDisk]; - var disk = { - name: name, - type: "physical", - block_id: id - }; - - expect($scope.isNameInvalid(disk)).toBe(false); - }); - }); - - describe("nameHasChanged", function() { - - it("logical volume resets name to include parents name", function() { - makeController(); - var disk = { - name: "", - type: "virtual", - parent_type: "lvm-vg", - original: { - name: "vg0-lvname" - } - }; - - $scope.nameHasChanged(disk); - expect(disk.name).toBe("vg0-"); - }); - }); - - describe("availableCancel", function() { - - it("calls updateAvailableSelection with force true", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - - $scope.availableCancel(available[0].$selected); - - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - }); - - describe("usesMountPoint", function() { - - it("returns false if filesystem is undefined", function() { - makeController(); - - expect($scope.usesMountPoint(undefined)).toBe(false); - }); - - it("returns false if filesystem is null", function() { - makeController(); - - expect($scope.usesMountPoint(null)).toBe(false); - }); - - it("returns false if filesystem is not a string", function() { - makeController(); - - expect($scope.usesMountPoint(1234)).toBe(false); - }); - - it("returns false if filesystem is 'swap'", function() { - makeController(); - - expect($scope.usesMountPoint("swap")).toBe(false); - }); - - it("returns true if filesystem is not 'swap'", function() { - makeController(); - - expect($scope.usesMountPoint("any-string")).toBe(true); - }); - - }); - - describe("isMountPointInvalid", function() { - - it("returns false if mount_point is undefined", function() { - makeController(); - - expect($scope.isMountPointInvalid()).toBe(false); - }); - - it("returns false if mount_point is empty", function() { - makeController(); - - expect($scope.isMountPointInvalid("")).toBe(false); - }); - - it("returns false if mount_point is 'none'", function() { - makeController(); - - expect($scope.isMountPointInvalid("none")).toBe(false); - }); - - it("returns true if mount_point doesn't start with '/'", function() { - makeController(); - - expect($scope.isMountPointInvalid("a")).toBe(true); - }); - - it("returns false if mount_point start with '/'", function() { - makeController(); - - expect($scope.isMountPointInvalid("/")).toBe(false); - }); - }); - - describe("canDelete", function() { - - it("returns true if volume group not used", function() { - makeController(); - var disk = { - type: "lvm-vg", - fstype: null, - has_partitions: false, - original: { - used_size: 0 - } - }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(true); - }); - - it("returns false if not super user", function() { - makeController(); - var disk = { - type: "lvm-vg", - fstype: null, - has_partitions: false, - original: { - used_size: 0 - } - }; - $scope.canEdit = function() { return false; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(false); - }); - - it("returns false if isAllStorageDisabled", function() { - makeController(); - var disk = { - type: "lvm-vg", - fstype: null, - has_partitions: false, - original: { - used_size: 0 - } - }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - - expect($scope.canDelete(disk)).toBe(false); - }); - - it("returns false if volume group used", function() { - makeController(); - var disk = { - type: "lvm-vg", - fstype: null, - has_partitions: false, - original: { - used_size: makeInteger(100, 10000) - } - }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(false); - }); - - it("returns true if fstype is null", function() { - makeController(); - var disk = { fstype: null, has_partitions: false }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(true); - }); - - it("returns true if fstype is empty", function() { - makeController(); - var disk = { fstype: "", has_partitions: false }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(true); - }); - - it("returns true if fstype is not empty", function() { - makeController(); - var disk = { fstype: "ext4" }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(true); - }); - - it("returns false if has_partitions is true", function() { - makeController(); - var disk = { fstype: "", has_partitions: true }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDelete(disk)).toBe(false); - }); - }); - - describe("availableDelete", function() { - - it("sets availableMode to DELETE", function() { - makeController(); - $scope.availableMode = "other"; - - $scope.availableDelete(); - - expect($scope.availableMode).toBe("delete"); - }); - }); - - describe("availableQuickDelete", function() { - - it("selects disks and deselects others", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - $scope.available = available; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availableDelete"); - - $scope.availableQuickDelete(available[0]); - - expect(available[0].$selected).toBe(true); - expect(available[1].$selected).toBe(false); - }); - - it("calls updateAvailableSelection with force true", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availableDelete"); - - $scope.availableQuickDelete(available[0]); - - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - - it("calls availableDelete", - function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availableDelete"); - - $scope.availableQuickDelete(available[0]); - - expect($scope.availableDelete).toHaveBeenCalledWith(); - }); - }); - - describe("getRemoveTypeText", function() { - - it("returns 'physical disk' for physical on filesystem", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "filesystem", - original: { - type: "physical" - } - })).toBe("physical disk"); - }); - - it("returns 'physical disk' for physical", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "physical" - })).toBe("physical disk"); - }); - - it("returns 'partition' for partition", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "partition" - })).toBe("partition"); - }); - - it("returns 'volume group' for lvm-vg", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "lvm-vg" - })).toBe("volume group"); - }); - - it("returns 'logical volume' for virtual on lvm-vg", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "virtual", - parent_type: "lvm-vg" - })).toBe("logical volume"); - }); - - it("returns 'RAID %d' for virtual on raid", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "virtual", - parent_type: "raid-1" - })).toBe("RAID 1 disk"); - }); - - it("returns parent_type + 'disk' for other virtual", function() { - makeController(); - expect($scope.getRemoveTypeText({ - type: "virtual", - parent_type: "raid0" - })).toBe("raid0 disk"); - }); - }); - - describe("availableConfirmDelete", function() { - - it("calls MachinesManager.deleteVolumeGroup for lvm-vg", function() { - makeController(); - var disk = { - type: "lvm-vg", - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100) - }; - $scope.available = [disk]; - spyOn(MachinesManager, "deleteVolumeGroup"); - spyOn($scope, "updateAvailableSelection"); - - $scope.availableConfirmDelete(disk); - expect(MachinesManager.deleteVolumeGroup).toHaveBeenCalledWith( - node, disk.block_id); - expect($scope.available).toEqual([]); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - - it("calls MachinesManager.deletePartition for partition", function() { - makeController(); - var disk = { - type: "partition", - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100) - }; - $scope.available = [disk]; - spyOn(MachinesManager, "deletePartition"); - spyOn($scope, "updateAvailableSelection"); - - $scope.availableConfirmDelete(disk); - expect(MachinesManager.deletePartition).toHaveBeenCalledWith( - node, disk.partition_id); - expect($scope.available).toEqual([]); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - - it("calls MachinesManager.deleteDisk for disk", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100) - }; - $scope.available = [disk]; - spyOn(MachinesManager, "deleteDisk"); - spyOn($scope, "updateAvailableSelection"); - - $scope.availableConfirmDelete(disk); - expect(MachinesManager.deleteDisk).toHaveBeenCalledWith( - node, disk.block_id); - expect($scope.available).toEqual([]); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - }); - - describe("availablePartition", function() { - - it("sets availableMode to 'partition'", function() { - makeController(); - var disk = { - available_size_human: "10 GB" - }; - $scope.availableMode = "other"; - $scope.availablePartition(disk); - expect($scope.availableMode).toBe("partition"); - }); - - it("sets $options to values from available_size_human", function() { - makeController(); - var disk = { - available_size_human: "10 GB" - }; - $scope.availablePartition(disk); - expect(disk.$options).toEqual({ - size: "10", - sizeUnits: "GB", - fstype: null, - mountPoint: "", - mountOptions: "" - }); - }); - }); - - describe("availableQuickPartition", function() { - - it("selects disks and deselects others", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - $scope.available = available; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availablePartition"); - - $scope.availableQuickPartition(available[0]); - - expect(available[0].$selected).toBe(true); - expect(available[1].$selected).toBe(false); - }); - - it("calls updateAvailableSelection with force true", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availablePartition"); - - $scope.availableQuickPartition(available[0]); - - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - - it("calls availablePartition", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availablePartition"); - - $scope.availableQuickPartition(available[0]); - - expect($scope.availablePartition).toHaveBeenCalledWith( - available[0]); - }); - }); - - describe("getAddPartitionName", function() { - - it("returns disk.name with -part#", function() { - makeController(); - var name = makeName("sda"); - var disk = { - name: name, - original: { - partition_table_type: "gpt", - partitions: [{}, {}] - } - }; - - expect($scope.getAddPartitionName(disk)).toBe(name + "-part3"); - }); - - it("returns disk.name with -part2 for ppc64el", function() { - node.architecture = "ppc64el/generic"; - makeController(); - var name = makeName("sda"); - var disk = { - name: name, - original: { - is_boot: true, - partition_table_type: "gpt" - } - }; - - expect($scope.getAddPartitionName(disk)).toBe(name + "-part2"); - }); - - it("returns disk.name with -part4 for ppc64el", function() { - node.architecture = "ppc64el/generic"; - makeController(); - var name = makeName("sda"); - var disk = { - name: name, - original: { - is_boot: true, - partition_table_type: "gpt", - partitions: [{}, {}] - } - }; - - expect($scope.getAddPartitionName(disk)).toBe(name + "-part4"); - }); - - it("returns disk.name with -part3 for MBR", function() { - makeController(); - var name = makeName("sda"); - var disk = { - name: name, - original: { - partition_table_type: "mbr", - partitions: [{}, {}] - } - }; - - expect($scope.getAddPartitionName(disk)).toBe(name + "-part3"); - }); - - it("returns disk.name with -part5 for MBR", function() { - makeController(); - var name = makeName("sda"); - var disk = { - name: name, - original: { - partition_table_type: "mbr", - partitions: [{}, {}, {}] - } - }; - - expect($scope.getAddPartitionName(disk)).toBe(name + "-part5"); - }); - }); - - describe("isAddPartitionSizeInvalid", function() { - - it("returns true if blank", function() { - makeController(); - var size = ""; - var disk = { - $options: { - sizeUnits: "GB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); - }); - - it("returns true if not numbers", function() { - makeController(); - var size = makeName("invalid"); - var disk = { - $options: { - sizeUnits: "GB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); - }); - - it("returns true if smaller than MIN_PARTITION_SIZE", function() { - makeController(); - var size = "1"; - var disk = { - $options: { - sizeUnits: "MB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); - }); - - it("returns true if larger than available_size more than tolerance", - function() { - makeController(); - var size = "4"; - var disk = { - original: { - available_size: 2 * 1000 * 1000 * 1000 - }, - $options: { - size: "4", - sizeUnits: "GB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - expect($scope.isAddPartitionSizeInvalid(disk)).toBe(true); - }); - - it("returns false if larger than available_size in tolerance", - function() { - makeController(); - var size = "2.62"; - var disk = { - original: { - available_size: 2.6 * 1000 * 1000 * 1000 - }, - $options: { - sizeUnits: "GB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - expect($scope.isAddPartitionSizeInvalid(disk)).toBe(false); - }); - - it("returns false if less than available_size", - function() { - makeController(); - var size = "1.6" - var disk = { - original: { - available_size: 2.6 * 1000 * 1000 * 1000 - }, - $options: { - sizeUnits: "GB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - expect($scope.isAddPartitionSizeInvalid(disk)).toBe(false); - }); - }); - - describe("availableConfirmPartition", function() { - - it("does nothing if invalid", function() { - makeController(); - var size = ""; - var disk = { - $options: { - sizeUnits: "GB" - } - }; - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue'). - and.returnValue(size); - $scope.$digest(); - - spyOn(MachinesManager, "createPartition"); - - $scope.availableConfirmPartition(disk); - - expect(MachinesManager.createPartition).not.toHaveBeenCalled(); - }); - - it("calls createPartition with bytes", function() { - makeController(); - var disk = { - block_id: makeInteger(0, 100), - original: { - partition_table_type: "mbr", - available_size: 4 * 1000 * 1000 * 1000, - available_size_human: "4.0 GB", - block_size: 512 - }, - $options: { - sizeUnits: "GB" - } - }; - var params = { - "size": "2", - "mount_point": makeName("/path"), - "mount_options": makeName("options") - } - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue') - .and.callFake(function(param) { - return params[param]; - }); - $scope.$digest(); - - $scope.availableConfirmPartition(disk); - - expect($scope.newPartition.system_id).toEqual(node.system_id) - expect($scope.newPartition.block_id).toEqual(disk.block_id) - expect($scope.newPartition.partition_size) - .toEqual(2 * 1000 * 1000 * 1000) - }); - - it("calls createPartition with fstype, " + - "mountPoint, and mountOptions", function() { - makeController(); - var disk = { - block_id: makeInteger(0, 100), - original: { - partition_table_type: "mbr", - available_size: 4 * 1000 * 1000 * 1000, - available_size_human: "4.0 GB", - block_size: 512 - }, - $options: { - sizeUnits: "GB", - fstype: "ext4", - } - }; - var params = { - "size": "2", - "mount_point": makeName("/path"), - "mount_options": makeName("options") - } - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue') - .and.callFake(function(param) { - return params[param]; - }); - $scope.$digest(); - - $scope.availableConfirmPartition(disk); - - expect($scope.newPartition.params).toEqual({ - fstype: "ext4", - mount_point: params['mount_point'], - mount_options: params['mount_options'] - }); - }); - - it("calls createPartition with available_size bytes", function() { - makeController(); - var available_size = 2.6 * 1000 * 1000 * 1000; - var disk = { - block_id: makeInteger(0, 100), - original: { - partition_table_type: "mbr", - available_size: available_size, - available_size_human: "2.6 GB", - block_size: 512 - }, - $options: { - sizeUnits: "GB" - } - }; - var params = { - "size": "2.62", - "mount_point": makeName("/path"), - "mount_options": makeName("options") - } - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue') - .and.callFake(function(param) { - return params[param]; - }); - $scope.$digest(); - - $scope.availableConfirmPartition(disk); - - // Align to 4MiB. - var align_size = (4 * 1024 * 1024); - var expected = align_size * - Math.floor(available_size / align_size); - - expect($scope.newPartition.partition_size).toEqual(expected); - }); - - // regression test for https://bugs.launchpad.net/maas/+bug/1509535 - it("calls createPartition with available_size bytes" + - " even when human size gets rounded down", function() { - - makeController(); - var available_size = 2.035 * 1000 * 1000 * 1000; - var disk = { - block_id: makeInteger(0, 100), - original: { - partition_table_type: "mbr", - available_size: available_size, - available_size_human: "2.0 GB", - block_size: 512 - }, - $options: { - sizeUnits: "GB" - } - }; - var params = { - "size": "2.0", - "mount_point": makeName("/path"), - "mount_options": makeName("options") - } - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue') - .and.callFake(function(param) { - return params[param]; - }); - $scope.$digest(); - - $scope.availableConfirmPartition(disk); - - // Align to 4MiB. - var align_size = (4 * 1024 * 1024); - var expected = align_size * - Math.floor(available_size / align_size); - - expect($scope.newPartition.partition_size).toEqual(expected); - }); - - it("calls createPartition with bytes minus partition table extra", - function() { - makeController(); - var available_size = 2.6 * 1000 * 1000 * 1000; - var disk = { - block_id: makeInteger(0, 100), - original: { - partition_table_type: "", - available_size: available_size, - available_size_human: "2.6 GB", - block_size: 512 - }, - $options: { - sizeUnits: "GB" - } - }; - var params = { - "size": "2.62", - "mount_point": makeName("/path"), - "mount_options": makeName("options") - } - $scope.newPartition.$maasForm = {getValue: function() {}}; - spyOn($scope.newPartition.$maasForm, 'getValue') - .and.callFake(function(param) { - return params[param]; - }); - $scope.$digest(); - - $scope.availableConfirmPartition(disk); - - // Remove partition extra space and align to 4MiB. - var align_size = (4 * 1024 * 1024); - var expected = align_size * Math.floor( - (available_size - (5 * 1024 * 1024)) / - align_size); - - expect($scope.newPartition.partition_size).toEqual(expected); - }); - }); - - describe("getSelectedCacheSets", function() { - - it("returns selected cachesets", function() { - makeController(); - var cachesets = [ - { $selected: true }, - { $selected: true }, - { $selected: false }, - { $selected: false } - ]; - $scope.cachesets = cachesets; - expect($scope.getSelectedCacheSets()).toEqual( - [cachesets[0], cachesets[1]]); - }); - }); - - describe("updateCacheSetsSelection", function() { - - it("sets cachesetsMode to NONE when none selected", function() { - makeController(); - spyOn($scope, "getSelectedCacheSets").and.returnValue([]); - $scope.cachesetsMode = "other"; - - $scope.updateCacheSetsSelection(); - - expect($scope.cachesetsMode).toBeNull(); - }); - - it("doesn't sets cachesetsMode to SINGLE when not force", function() { - makeController(); - spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); - $scope.cachesetsMode = "other"; - - $scope.updateCacheSetsSelection(); - - expect($scope.cachesetsMode).toBe("other"); - }); - - it("sets cachesetsMode to SINGLE when force", function() { - makeController(); - spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); - $scope.cachesetsMode = "other"; - - $scope.updateCacheSetsSelection(true); - - expect($scope.cachesetsMode).toBe("single"); - }); - - it("doesn't sets cachesetsMode to MUTLI when not force", function() { - makeController(); - spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); - $scope.cachesetsMode = "other"; - - $scope.updateCacheSetsSelection(); - - expect($scope.cachesetsMode).toBe("other"); - }); - - it("sets cachesetsMode to MULTI when force", function() { - makeController(); - spyOn($scope, "getSelectedCacheSets").and.returnValue([{}, {}]); - $scope.cachesetsMode = "other"; - - $scope.updateCacheSetsSelection(true); - - expect($scope.cachesetsMode).toBe("multi"); - }); - - it("sets cachesetsAllSelected to false when none selected", - function() { - makeController(); - spyOn($scope, "getSelectedCacheSets").and.returnValue([]); - $scope.cachesetsAllSelected = true; - - $scope.updateCacheSetsSelection(); - - expect($scope.cachesetsAllSelected).toBe(false); - }); - - it("sets cachesetsAllSelected to false when not all selected", - function() { - makeController(); - $scope.cachesets = [{}, {}]; - spyOn($scope, "getSelectedCacheSets").and.returnValue([{}]); - $scope.cachesetsAllSelected = true; - - $scope.updateCacheSetsSelection(); - - expect($scope.cachesetsAllSelected).toBe(false); - }); - - it("sets cachesetsAllSelected to true when all selected", - function() { - makeController(); - $scope.cachesets = [{}, {}]; - spyOn($scope, "getSelectedCacheSets").and.returnValue( - [{}, {}]); - $scope.cachesetsAllSelected = false; - - $scope.updateCacheSetsSelection(); - - expect($scope.cachesetsAllSelected).toBe(true); - }); - }); - - describe("toggleCacheSetSelect", function() { - - it("inverts $selected", function() { - makeController(); - var cacheset = { $selected: true }; - spyOn($scope, "updateCacheSetsSelection"); - - $scope.toggleCacheSetSelect(cacheset); - - expect(cacheset.$selected).toBe(false); - $scope.toggleCacheSetSelect(cacheset); - expect(cacheset.$selected).toBe(true); - expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( - true); - }); - }); - - describe("toggleCacheSetAllSelect", function() { - - it("sets all to true if not all selected", function() { - makeController(); - var cachesets = [{ $selected: true }, { $selected: false }]; - $scope.cachesets = cachesets; - $scope.cachesetsAllSelected = false; - spyOn($scope, "updateCacheSetsSelection"); - - $scope.toggleCacheSetAllSelect(); - - expect(cachesets[0].$selected).toBe(true); - expect(cachesets[1].$selected).toBe(true); - expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( - true); - }); - - it("sets all to false if all selected", function() { - makeController(); - var cachesets = [{ $selected: true }, { $selected: true }]; - $scope.cachesets = cachesets; - $scope.cachesetsAllSelected = true; - spyOn($scope, "updateCacheSetsSelection"); - - $scope.toggleCacheSetAllSelect(); - - expect(cachesets[0].$selected).toBe(false); - expect(cachesets[1].$selected).toBe(false); - expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( - true); - }); - }); - - describe("isCacheSetsDisabled", function() { - - it("returns false for NONE", function() { - makeController(); - $scope.cachesetsMode = null; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.isCacheSetsDisabled()).toBe(false); - }); - - it("returns false for SINGLE", function() { - makeController(); - $scope.cachesetsMode = "single"; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.isCacheSetsDisabled()).toBe(false); - }); - - it("returns false for MULTI", function() { - makeController(); - $scope.cachesetsMode = "multi"; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.isCacheSetsDisabled()).toBe(false); - }); - - it("returns true for when not super user", function() { - makeController(); - $scope.cachesetsMode = "delete"; - $scope.canEdit = function() { return false; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.isCacheSetsDisabled()).toBe(true); - }); - - it("returns true for when isAllStorageDisabled", function() { - makeController(); - $scope.cachesetsMode = "delete"; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - - expect($scope.isCacheSetsDisabled()).toBe(true); - }); - - it("returns true for DELETE", function() { - makeController(); - $scope.cachesetsMode = "delete"; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.isCacheSetsDisabled()).toBe(true); - }); - }); - - describe("cacheSetCancel", function() { - - it("calls updateCacheSetsSelection with force true", function() { - makeController(); - spyOn($scope, "updateCacheSetsSelection"); - - $scope.cacheSetCancel(); - - expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( - true); - }); - }); - - describe("canDeleteCacheSet", function() { - - it("returns true when not being used", function() { - makeController(); - var cacheset = { used_by: "" }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDeleteCacheSet(cacheset)).toBe(true); - }); - - it("returns false when being used", function() { - makeController(); - var cacheset = { used_by: "bcache0" }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDeleteCacheSet(cacheset)).toBe(false); - }); - - it("returns false when not super user", function() { - makeController(); - var cacheset = { used_by: "" }; - $scope.canEdit = function() { return false; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - - expect($scope.canDeleteCacheSet(cacheset)).toBe(false); - }); - - it("returns false when isAllStorageDisabled", function() { - makeController(); - var cacheset = { used_by: "" }; - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - - expect($scope.canDeleteCacheSet(cacheset)).toBe(false); - }); - }); - - describe("cacheSetDelete", function() { - - it("sets cachesetsMode to DELETE", function() { - makeController(); - $scope.cachesetsMode = "other"; - - $scope.cacheSetDelete(); - - expect($scope.cachesetsMode).toBe("delete"); - }); - }); - - describe("quickCacheSetDelete", function() { - - it("selects cacheset and calls cacheSetDelete", function() { - makeController(); - var cachesets = [{ $selected: true }, { $selected: false }]; - $scope.cachesets = cachesets; - spyOn($scope, "updateCacheSetsSelection"); - spyOn($scope, "cacheSetDelete"); - - $scope.quickCacheSetDelete(cachesets[1]); - - expect(cachesets[0].$selected).toBe(false); - expect(cachesets[1].$selected).toBe(true); - expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith( - true); - expect($scope.cacheSetDelete).toHaveBeenCalled(); - }); - }); - - describe("cacheSetConfirmDelete", function() { - - it("calls MachinesManager.deleteCacheSet and removes from list", - function() { - makeController(); - var cacheset = { - cache_set_id: makeInteger(0, 100) - }; - $scope.cachesets = [cacheset]; - spyOn(MachinesManager, "deleteCacheSet"); - spyOn($scope, "updateCacheSetsSelection"); - - $scope.cacheSetConfirmDelete(cacheset); - - expect(MachinesManager.deleteCacheSet).toHaveBeenCalledWith( - node, cacheset.cache_set_id); - expect($scope.cachesets).toEqual([]); - expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(); - }); - }); - - describe("canCreateCacheSet", function() { - - it("returns false if isAvailableDisabled returns true", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(true); - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateCacheSet()).toBe(false); - }); - - it("returns false if two selected", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ { $selected: true }, { $selected: true }]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateCacheSet()).toBe(false); - }); - - it("returns false if selected has fstype", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: "ext4", - $selected: true - } - ]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateCacheSet()).toBe(false); - }); - - it("returns false if selected is volume group", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - type: "lvm-vg", - fstype: null, - $selected: true - } - ]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateCacheSet()).toBe(false); - }); - - it("returns false if not super user", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: null, - $selected: true - } - ]; - $scope.canEdit = function() { return false; }; - - expect($scope.canCreateCacheSet()).toBe(false); - }); - - it("returns true if selected has no fstype", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: null, - $selected: true - } - ]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateCacheSet()).toBe(true); - }); - }); - - describe("createCacheSet", function() { - - it("does nothing if canCreateCacheSet returns false", function() { - makeController(); - var disk = { - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100), - $selected: true - }; - $scope.available = [disk]; - spyOn($scope, "canCreateCacheSet").and.returnValue(false); - spyOn(MachinesManager, "createCacheSet"); - - $scope.createCacheSet(); - expect(MachinesManager.createCacheSet).not.toHaveBeenCalled(); - }); - - it("calls MachinesManager.createCacheSet and removes from available", - function() { - makeController(); - var disk = { - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100), - $selected: true - }; - $scope.available = [disk]; - spyOn($scope, "canCreateCacheSet").and.returnValue(true); - spyOn(MachinesManager, "createCacheSet"); - - $scope.createCacheSet(); - expect(MachinesManager.createCacheSet).toHaveBeenCalledWith( - node, disk.block_id, disk.partition_id); - expect($scope.available).toEqual([]); - }); - }); - - describe("getCannotCreateBcacheMsg", function() { - - it("returns msg if no cachesets", - function() { - makeController(); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = []; - expect($scope.getCannotCreateBcacheMsg()).toBe( - "Create at least one cache set to create bcache"); - }); - - it("returns msg if two selected", function() { - makeController(); - $scope.cachesets = [{}]; - $scope.available = [ { $selected: true }, { $selected: true }]; - expect($scope.getCannotCreateBcacheMsg()).toBe( - "Select only one available device to create bcache"); - }); - - it("returns msg if selected has fstype", function() { - makeController(); - $scope.available = [ - { - fstype: "ext4", - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - - expect($scope.getCannotCreateBcacheMsg()).toBe( - "Device is formatted; unformat the device to create bcache"); - }); - - it("returns msg if selected is volume group", function() { - makeController(); - $scope.available = [ - { - type: "lvm-vg", - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - expect($scope.getCannotCreateBcacheMsg()).toBe( - "Cannot use a logical volume as a backing device for bcache."); - }); - - it("returns msg if selected has partitions", function() { - makeController(); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: true - } - ]; - $scope.cachesets = [{}]; - expect($scope.getCannotCreateBcacheMsg()).toBe( - "Device has already been partitioned; create a " + - "new partition to use as the bcache backing " + - "device"); - }); - - it("returns null if selected is valid", - function() { - makeController(); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - expect($scope.getCannotCreateBcacheMsg()).toBeNull(); - }); - }); - - describe("canEdit", function() { - - it("returns false when $parent.canEdit is false", function() { - makeController(); - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - $scope.$parent.canEdit = function() { return false; }; - - expect($scope.canEdit()).toBe(false); - }); - - it("returns false when isAllStorageDisabled is false", function() { - makeController(); - spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - $scope.$parent.canEdit = function() { return true; }; - - expect($scope.canEdit()).toBe(false); - }); - }); - - describe("availableEdit", function() { - - it("calls availableEdit for volumn group", function() { - makeController(); - var disk = { - type: "lvm-vg" - }; - $scope.availableEdit(disk); - expect(disk.$options).toEqual({ - editingTags: false, - editingFilesystem: false - }); - }); - - it("calls availableEdit for partition", function() { - makeController(); - var disk = { - type: "partition" - }; - $scope.availableEdit(disk); - expect(disk.$options).toEqual({ - editingTags: false, - editingFilesystem: true, - fstype: disk.fstype - }); - }); - - it("calls availableEdit for disk with partitions", function() { - makeController(); - var disk = { - type: "physical", - has_partitions: true - }; - $scope.availableEdit(disk); - expect(disk.$options).toEqual({ - editingFilesystem: false, - editingTags: true, - tags: undefined, - fstype: undefined - }); - }); - - it("calls availableEdit for disk and is boot disk", function() { - makeController(); - var disk = { - type: "physical", - has_partitions: false, - original: { - is_boot: true - } - }; - $scope.availableEdit(disk); - expect(disk.$options).toEqual({ - editingFilesystem: false, - editingTags: true, - tags: undefined, - fstype: undefined - }); - }); - }); - - describe("availableQuickEdit", function() { - - it("selects disks and deselects others", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - $scope.available = available; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availableEdit"); - - $scope.availableQuickEdit(available[0]); - - expect(available[0].$selected).toBe(true); - expect(available[1].$selected).toBe(false); - }); - - it("calls updateAvailableSelection with force true", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availableEdit"); - - $scope.availableQuickEdit(available[0]); - - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - - it("calls availableEdit", function() { - makeController(); - var available = [{ $selected: false }, { $selected: true }]; - spyOn($scope, "updateAvailableSelection"); - spyOn($scope, "availableEdit"); - - $scope.availableQuickEdit(available[0]); - - expect($scope.availableEdit).toHaveBeenCalledWith( - available[0]); - }); - }); - - describe("availableConfirmEdit", function() { - - it("does nothing if invalid", function() { - makeController(); - var disk = { - $options: { - mountPoint: "!#$%" - } - }; - spyOn(MachinesManager, "updateDisk"); - - $scope.availableConfirmEdit(disk); - expect(MachinesManager.updateDisk).not.toHaveBeenCalled(); - }); - - it("resets name to original if empty", function() { - makeController(); - var name = makeName("name"); - var disk = { - name: "", - $options: { - mountPoint: "" - }, - original: { - name: name - } - }; - spyOn(MachinesManager, "updateDisk"); - - $scope.availableConfirmEdit(disk); - expect(disk.name).toBe(name); - expect(MachinesManager.updateDisk).toHaveBeenCalled(); - }); - - it("calls updateDisk with new name for logical volume", function() { - makeController(); - var name = "vg0-lvnew"; - var disk = { - name: name, - type: "virtual", - parent_type: "lvm-vg", - block_id: makeInteger(0, 100), - partition_id: makeInteger(0, 100), - $options: { - fstype: "", - mountPoint: "", - mountOptions: "" - }, - original: { - name: "vg0-lvold" - } - }; - spyOn(MachinesManager, "updateDisk"); - - $scope.availableConfirmEdit(disk); - expect(disk.name).toBe(name); - expect(MachinesManager.updateDisk).toHaveBeenCalled(); - }); - - it("calls updateFilesystem for partition", function() { - makeController(); - var name = makeName("name"); - var disk = { - name: "", - type: "partition", - $options: { - mountPoint: "" - }, - original: { - name: name - } - }; - spyOn(MachinesManager, "updateFilesystem"); - - $scope.availableConfirmEdit(disk); - expect(disk.name).toBe(name); - expect(MachinesManager.updateFilesystem).toHaveBeenCalled(); - }); - - }); - - describe("canCreateBcache", function() { - - it("returns false when isAvailableDisabled is true", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(true); - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns false if two selected", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ { $selected: true }, { $selected: true }]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns false if selected has fstype", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: "ext4", - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns false if selected is volume group", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - type: "lvm-vg", - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns false if selected has partitions", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: true - } - ]; - $scope.cachesets = [{}]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns false if selected has no fstype but not cachesets ", - function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = []; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns false if not super user ", - function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - $scope.canEdit = function() { return false; }; - - expect($scope.canCreateBcache()).toBe(false); - }); - - it("returns true if selected has no fstype but has cachesets ", - function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - $scope.available = [ - { - fstype: null, - $selected: true, - has_partitions: false - } - ]; - $scope.cachesets = [{}]; - $scope.canEdit = function() { return true; }; - - expect($scope.canCreateBcache()).toBe(true); - }); - }); - - describe("createBcache", function() { - - it("does nothing if canCreateBcache returns false", function() { - makeController(); - $scope.availableMode = "other"; - spyOn($scope, "canCreateBcache").and.returnValue(false); - - $scope.createBcache(); - expect($scope.availableMode).toBe("other"); - }); - - it("sets availableMode and availableNew", function() { - makeController(); - $scope.availableMode = "other"; - spyOn($scope, "canCreateBcache").and.returnValue(true); - - // Add bcache name to create a name after that index. - var otherBcache = { - name: "bcache4" - }; - node.disks = [otherBcache]; - - // Will be set as the device. - var disk = { - $selected: true - }; - $scope.available = [disk]; - - // Will be set as the cacheset. - var cacheset = {}; - $scope.cachesets = [cacheset]; - - $scope.createBcache(); - expect($scope.availableMode).toBe("bcache"); - expect($scope.availableNew).toEqual({ - name: "bcache5", - device: disk, - cacheset: cacheset, - cacheMode: "writeback", - fstype: null, - mountPoint: "", - mountOptions: "", - tags: [] - }); - expect($scope.availableNew.device).toBe(disk); - expect($scope.availableNew.cacheset).toBe(cacheset); - }); - }); - - describe("fstypeChanged", function() { - - it("leaves mountPoint when fstype is not null", function() { - makeController(); - var mountPoint = makeName("srv"); - var mountOptions = makeName("options"); - var options = { - fstype: "ext4", - mountPoint: mountPoint, - mountOptions: mountOptions - }; - - $scope.fstypeChanged(options); - expect(options.mountPoint).toBe(mountPoint); - expect(options.mountOptions).toBe(mountOptions); - }); - - it("clears mountPoint when fstype null", function() { - makeController(); - var options = { - fstype: null, - mountPoint: makeName("srv"), - mountOptions: makeName("options") - }; - - $scope.fstypeChanged(options); - expect(options.mountPoint).toBe(""); - expect(options.mountOptions).toBe(""); - }); - - it("sets mountPoint to 'none' for a partition that " + - "cannot be mounted at a directory", function() { - makeController(); - var mountPoint = makeName("srv"); - var mountOptions = makeName("options"); - var options = { - fstype: "swap", - mountPoint: mountPoint, - mountOptions: mountOptions - }; - - $scope.fstypeChanged(options); - expect(options.mountPoint).toBe("none"); - // Mount options are unchanged. - expect(options.mountOptions).toBe(mountOptions); - }); - - it("clears mountPoint from 'none' for a partition that " + - "can be mounted at a directory", function() { - makeController(); - var mountOptions = makeName("options"); - var options = { - fstype: "ext4", - mountPoint: "none", - mountOptions: mountOptions - }; - - $scope.fstypeChanged(options); - expect(options.mountPoint).toBe(""); - // Mount options are unchanged. - expect(options.mountOptions).toBe(mountOptions); - }); - - }); - - describe("isNewDiskNameInvalid", function() { - - it("returns true if blank name", function() { - makeController(); - $scope.node.disks = []; - $scope.availableNew.name = ""; - - expect($scope.isNewDiskNameInvalid()).toBe(true); - }); - - it("returns true if name used by disk", function() { - makeController(); - var name = makeName("disk"); - $scope.node.disks = [{ - name: name - }]; - $scope.availableNew.name = name; - - expect($scope.isNewDiskNameInvalid()).toBe(true); - }); - - it("returns true if name used by partition", function() { - makeController(); - var name = makeName("disk"); - $scope.node.disks = [{ - name: makeName("other"), - partitions: [ - { - name: name - } - ] - }]; - $scope.availableNew.name = name; - - expect($scope.isNewDiskNameInvalid()).toBe(true); - }); - - it("returns false if the name is not already used", function() { - makeController(); - var name = makeName("disk"); - $scope.node.disks = [{ - name: makeName("other"), - partitions: [ - { - name: makeName("part") - } - ] - }]; - $scope.availableNew.name = name; - - expect($scope.isNewDiskNameInvalid()).toBe(false); - }); - }); - - describe("createBcacheCanSave", function() { - - it("returns false if isNewDiskNameInvalid returns true", function() { - makeController(); - $scope.availableNew.mountPoint = "/"; - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); - - expect($scope.createBcacheCanSave()).toBe(false); - }); - - it("returns false if isMountPointInvalid returns true", function() { - makeController(); - $scope.availableNew.mountPoint = "not/absolute"; - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); - - expect($scope.createBcacheCanSave()).toBe(false); - }); - - it("returns true if both return false", function() { - makeController(); - $scope.availableNew.mountPoint = "/"; - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); - - expect($scope.createBcacheCanSave()).toBe(true); - }); - }); - - describe("availableConfirmCreateBcache", function() { - - it("does nothing if createBcacheCanSave returns false", function() { - makeController(); - spyOn($scope, "createBcacheCanSave").and.returnValue(false); - var availableNew = { - name: makeName("bcache"), - cacheset: { - cache_set_id: makeInteger(0, 100) - }, - cacheMode: "writearound", - device: { - type: "partition", - partition_id: makeInteger(0, 100) - }, - fstype: null, - mountPoint: "", - mountOptions: "" - }; - $scope.availableNew = availableNew; - spyOn(MachinesManager, "createBcache"); - - $scope.availableConfirmCreateBcache(); - expect(MachinesManager.createBcache).not.toHaveBeenCalled(); - }); - - it("calls MachinesManager.createBcache for partition", function() { - makeController(); - spyOn($scope, "createBcacheCanSave").and.returnValue(true); - var device = { - type: "partition", - partition_id: makeInteger(0, 100), - $selected: true - }; - var availableNew = { - name: makeName("bcache"), - cacheset: { - cache_set_id: makeInteger(0, 100) - }, - cacheMode: "writearound", - device: device, - fstype: "ext4", - mountPoint: makeName("/path"), - mountOptions: makeName("options") - }; - $scope.available = [device]; - $scope.availableNew = availableNew; - spyOn(MachinesManager, "createBcache"); - spyOn($scope, "updateAvailableSelection"); - - $scope.availableConfirmCreateBcache(); - expect(MachinesManager.createBcache).toHaveBeenCalledWith( - node, { - name: availableNew.name, - cache_set: availableNew.cacheset.cache_set_id, - cache_mode: "writearound", - partition_id: device.partition_id, - fstype: "ext4", - mount_point: availableNew.mountPoint, - mount_options: availableNew.mountOptions - }); - expect($scope.available).toEqual([]); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - - it("calls MachinesManager.createBcache for block device", function() { - makeController(); - spyOn($scope, "createBcacheCanSave").and.returnValue(true); - var device = { - type: "physical", - block_id: makeInteger(0, 100), - $selected: true - }; - var availableNew = { - name: makeName("bcache"), - cacheset: { - cache_set_id: makeInteger(0, 100) - }, - cacheMode: "writearound", - device: device, - fstype: null, - mountPoint: "/", - mountOptions: makeName("options") - }; - $scope.available = [device]; - $scope.availableNew = availableNew; - spyOn(MachinesManager, "createBcache"); - spyOn($scope, "updateAvailableSelection"); - - $scope.availableConfirmCreateBcache(); - expect(MachinesManager.createBcache).toHaveBeenCalledWith( - node, { - name: availableNew.name, - cache_set: availableNew.cacheset.cache_set_id, - cache_mode: "writearound", - block_id: device.block_id - }); - expect($scope.available).toEqual([]); - expect($scope.updateAvailableSelection).toHaveBeenCalledWith( - true); - }); - }); - - describe("canCreateRAID", function() { - - it("returns false isAvailableDisabled returns true", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(true); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateRAID()).toBe(false); - }); - - it("returns false if less than 2 is selected", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateRAID()).toBe(false); - }); - - it("returns false if any selected has filesystem", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(true); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateRAID()).toBe(false); - }); - - it("returns false if any selected is volume group", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([ - { - type: "lvm-vg" - }, - { - type: "physical" - } - ]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateRAID()).toBe(false); - }); - - it("returns false if not super user", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); - $scope.canEdit = function() { return false; }; - expect($scope.canCreateRAID()).toBe(false); - }); - - it("returns true if more than 1 selected", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); - $scope.canEdit = function() { return true; }; - spyOn($scope, "isAllStorageDisabled").and.returnValue(false); - expect($scope.canCreateRAID()).toBe(true); - }); + expect($scope.canDeleteCacheSet(cacheset)).toBe(false); }); - describe("createRAID", function() { - - it("does nothing if canCreateRAID returns false", function() { - makeController(); - spyOn($scope, "canCreateRAID").and.returnValue(false); - $scope.availableMode = "other"; + it("returns false when isAllStorageDisabled", function() { + makeController(); + var cacheset = { used_by: "" }; + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(true); - $scope.createRAID(); - expect($scope.availableMode).toBe("other"); - }); - - it("sets up availableNew", function() { - makeController(); - spyOn($scope, "canCreateRAID").and.returnValue(true); - $scope.availableMode = "other"; - - // Add md name to create a name after that index. - var otherRAID = { - name: "md4" - }; - node.disks = [otherRAID]; - - // Will be set as the devices. - var disk0 = { - $selected: true - }; - var disk1 = { - $selected: true - }; - $scope.available = [disk0, disk1]; - - $scope.createRAID(); - expect($scope.availableMode).toBe("raid"); - expect($scope.availableNew.name).toBe("md5"); - expect($scope.availableNew.devices).toEqual([disk0, disk1]); - expect($scope.availableNew.mode.level).toEqual("raid-0"); - expect($scope.availableNew.spares).toEqual([]); - expect($scope.availableNew.fstype).toBeNull(); - expect($scope.availableNew.mountPoint).toEqual(""); - expect($scope.availableNew.mountOptions).toEqual(""); - }); + expect($scope.canDeleteCacheSet(cacheset)).toBe(false); }); + }); - describe("getAvailableRAIDModes", function() { - - it("returns empty list if availableNew null", function() { - makeController(); - $scope.availableNew = null; + describe("cacheSetDelete", function() { + it("sets cachesetsMode to DELETE", function() { + makeController(); + $scope.cachesetsMode = "other"; - expect($scope.getAvailableRAIDModes()).toEqual([]); - }); - - it("returns empty list if availableNew.devices not defined", - function() { - makeController(); - $scope.availableNew = {}; - - expect($scope.getAvailableRAIDModes()).toEqual([]); - }); - - it("returns raid 0 and 1 for 2 disks", function() { - makeController(); - $scope.availableNew.devices = [{}, {}]; - - var modes = $scope.getAvailableRAIDModes(); - expect(modes[0].level).toEqual("raid-0"); - expect(modes[1].level).toEqual("raid-1"); - expect(modes.length).toEqual(2); - }); - - it("returns raid 0,1,5,10 for 3 disks", function() { - makeController(); - $scope.availableNew.devices = [{}, {}, {}]; - - var modes = $scope.getAvailableRAIDModes(); - expect(modes[0].level).toEqual("raid-0"); - expect(modes[1].level).toEqual("raid-1"); - expect(modes[2].level).toEqual("raid-5"); - expect(modes[3].level).toEqual("raid-10"); - expect(modes.length).toEqual(4); - }); + $scope.cacheSetDelete(); - it("returns raid 0,1,5,6,10 for 4 disks", function() { - makeController(); - $scope.availableNew.devices = [{}, {}, {}, {}]; - - var modes = $scope.getAvailableRAIDModes(); - expect(modes[0].level).toEqual("raid-0"); - expect(modes[1].level).toEqual("raid-1"); - expect(modes[2].level).toEqual("raid-5"); - expect(modes[3].level).toEqual("raid-6"); - expect(modes[4].level).toEqual("raid-10"); - expect(modes.length).toEqual(5); - }); + expect($scope.cachesetsMode).toBe("delete"); }); + }); - describe("getTotalNumberOfAvailableSpares", function() { - - var modes = [ - { - level: "raid-0", - min_disks: 2, - allows_spares: false - }, - { - level: "raid-1", - min_disks: 2, - allows_spares: true - }, - { - level: "raid-5", - min_disks: 3, - allows_spares: true - }, - { - level: "raid-6", - min_disks: 4, - allows_spares: true - }, - { - level: "raid-10", - min_disks: 3, - allows_spares: true - } - ]; + describe("quickCacheSetDelete", function() { + it("selects cacheset and calls cacheSetDelete", function() { + makeController(); + var cachesets = [{ $selected: true }, { $selected: false }]; + $scope.cachesets = cachesets; + spyOn($scope, "updateCacheSetsSelection"); + spyOn($scope, "cacheSetDelete"); - angular.forEach(modes, function(mode) { + $scope.quickCacheSetDelete(cachesets[1]); - it("returns current result for " + mode.level, function() { - makeController(); - $scope.availableNew.mode = mode; - if(!mode.allows_spares) { - expect($scope.getTotalNumberOfAvailableSpares()).toBe(0); - } else { - var count = makeInteger(mode.min_disks, 100); - var i, devices = []; - for(i = 0; i < count; i++) { - devices.push({}); - } - $scope.availableNew.devices = devices; - expect( - $scope.getTotalNumberOfAvailableSpares(), - count - mode.min_disks); - } - }); - }); + expect(cachesets[0].$selected).toBe(false); + expect(cachesets[1].$selected).toBe(true); + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(true); + expect($scope.cacheSetDelete).toHaveBeenCalled(); }); + }); - describe("getNumberOfRemainingSpares", function() { + describe("cacheSetConfirmDelete", function() { + it(`calls MachinesManager.deleteCacheSet + and removes from list`, function() { + makeController(); + var cacheset = { + cache_set_id: makeInteger(0, 100) + }; + $scope.cachesets = [cacheset]; + spyOn(MachinesManager, "deleteCacheSet"); + spyOn($scope, "updateCacheSetsSelection"); - it("returns 0 when getTotalNumberOfAvailableSpares returns 0", - function() { - makeController(); - spyOn( - $scope, - "getTotalNumberOfAvailableSpares").and.returnValue(0); - - expect($scope.getNumberOfRemainingSpares()).toBe(0); - }); - - it("returns allowed minus the current number of spares", - function() { - makeController(); - var count = makeInteger(10, 100); - spyOn( - $scope, - "getTotalNumberOfAvailableSpares").and.returnValue(count); - var sparesCount = makeInteger(0, count); - var i, spares = []; - for(i = 0; i < sparesCount; i++) { - spares.push({}); - } - $scope.availableNew.spares = spares; - - expect($scope.getNumberOfRemainingSpares()).toBe( - count - sparesCount); - }); - }); - - describe("showSparesColumn", function() { - - it("returns true when getTotalNumberOfAvailableSpares greater than 0", - function() { - makeController(); - spyOn( - $scope, - "getTotalNumberOfAvailableSpares").and.returnValue(1); - - expect($scope.showSparesColumn()).toBe(true); - }); - - it("returns false when getTotalNumberOfAvailableSpares less than 1", - function() { - makeController(); - spyOn( - $scope, - "getTotalNumberOfAvailableSpares").and.returnValue(0); - - expect($scope.showSparesColumn()).toBe(false); - }); - }); - - describe("RAIDModeChanged", function() { - - it("clears availableNew.spares", function() { - makeController(); - $scope.availableNew.spares = [{}, {}]; + $scope.cacheSetConfirmDelete(cacheset); - $scope.RAIDModeChanged(); - expect($scope.availableNew.spares).toEqual([]); - }); + expect(MachinesManager.deleteCacheSet).toHaveBeenCalledWith( + node, + cacheset.cache_set_id + ); + expect($scope.cachesets).toEqual([]); + expect($scope.updateCacheSetsSelection).toHaveBeenCalledWith(); }); + }); - describe("isActiveRAIDMember", function() { - - it("returns true when disk key not in spares", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger() - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk]; - $scope.setAsActiveRAIDMember(disk); + describe("canCreateCacheSet", function() { + it("returns false if isAvailableDisabled returns true", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; - expect($scope.isActiveRAIDMember(disk)).toBe(true); - }); - - it("returns false when disk key in spares", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger() - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk]; - $scope.setAsSpareRAIDMember(disk); - - expect($scope.isActiveRAIDMember(disk)).toBe(false); - }); + expect($scope.canCreateCacheSet()).toBe(false); }); - describe("isSpareRAIDMember", function() { + it("returns false if two selected", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [{ $selected: true }, { $selected: true }]; + $scope.canEdit = function() { + return true; + }; - it("returns false when disk key not in spares", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger() - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk]; - $scope.setAsActiveRAIDMember(disk); - - expect($scope.isSpareRAIDMember(disk)).toBe(false); - }); - - it("returns true when disk key in spares", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger() - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk]; - $scope.setAsSpareRAIDMember(disk); - - expect($scope.isSpareRAIDMember(disk)).toBe(true); - }); + expect($scope.canCreateCacheSet()).toBe(false); }); - describe("setAsActiveRAIDMember", function() { - - it("sets the disk as an active RAID member", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger() - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk]; - - $scope.setAsSpareRAIDMember(disk); - expect($scope.isSpareRAIDMember(disk)).toBe(true); + it("returns false if selected has fstype", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: "ext4", + $selected: true + } + ]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateCacheSet()).toBe(false); + }); + + it("returns false if selected is volume group", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + type: "lvm-vg", + fstype: null, + $selected: true + } + ]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateCacheSet()).toBe(false); + }); - $scope.setAsActiveRAIDMember(disk); - expect($scope.isActiveRAIDMember(disk)).toBe(true); - }); + it("returns false if not super user", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: null, + $selected: true + } + ]; + $scope.canEdit = function() { + return false; + }; + + expect($scope.canCreateCacheSet()).toBe(false); }); - describe("setAsSpareRAIDMember", function() { + it("returns true if selected has no fstype", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: null, + $selected: true + } + ]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateCacheSet()).toBe(true); + }); + }); + + describe("createCacheSet", function() { + it("does nothing if canCreateCacheSet returns false", function() { + makeController(); + var disk = { + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100), + $selected: true + }; + $scope.available = [disk]; + spyOn($scope, "canCreateCacheSet").and.returnValue(false); + spyOn(MachinesManager, "createCacheSet"); + + $scope.createCacheSet(); + expect(MachinesManager.createCacheSet).not.toHaveBeenCalled(); + }); + + it(`calls MachinesManager.createCacheSet and + removes from available`, function() { + makeController(); + var disk = { + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100), + $selected: true + }; + $scope.available = [disk]; + spyOn($scope, "canCreateCacheSet").and.returnValue(true); + spyOn(MachinesManager, "createCacheSet"); + + $scope.createCacheSet(); + expect(MachinesManager.createCacheSet).toHaveBeenCalledWith( + node, + disk.block_id, + disk.partition_id + ); + expect($scope.available).toEqual([]); + }); + }); + + describe("getCannotCreateBcacheMsg", function() { + it("returns msg if no cachesets", function() { + makeController(); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = []; + expect($scope.getCannotCreateBcacheMsg()).toBe( + "Create at least one cache set to create bcache" + ); + }); + + it("returns msg if two selected", function() { + makeController(); + $scope.cachesets = [{}]; + $scope.available = [{ $selected: true }, { $selected: true }]; + expect($scope.getCannotCreateBcacheMsg()).toBe( + "Select only one available device to create bcache" + ); + }); + + it("returns msg if selected has fstype", function() { + makeController(); + $scope.available = [ + { + fstype: "ext4", + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; - it("sets the disk as a spare RAID member", function() { - makeController(); - var disk = { - type: "physical", - block_id: makeInteger() - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk]; + expect($scope.getCannotCreateBcacheMsg()).toBe( + "Device is formatted; unformat the device to create bcache" + ); + }); + + it("returns msg if selected is volume group", function() { + makeController(); + $scope.available = [ + { + type: "lvm-vg", + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; + expect($scope.getCannotCreateBcacheMsg()).toBe( + "Cannot use a logical volume as a backing device for bcache." + ); + }); - $scope.setAsActiveRAIDMember(disk); - expect($scope.isActiveRAIDMember(disk)).toBe(true); + it("returns msg if selected has partitions", function() { + makeController(); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: true + } + ]; + $scope.cachesets = [{}]; + expect($scope.getCannotCreateBcacheMsg()).toBe( + "Device has already been partitioned; create a " + + "new partition to use as the bcache backing " + + "device" + ); + }); - $scope.setAsSpareRAIDMember(disk); - expect($scope.isSpareRAIDMember(disk)).toBe(true); - }); + it("returns msg if selected is bcache", function() { + makeController(); + $scope.available = [ + { + $selected: true, + parent_type: "bcache" + } + ]; + $scope.cachesets = [{}]; + expect($scope.getCannotCreateBcacheMsg()).toBe( + "Device is already bcache" + ); }); - describe("getNewRAIDSize", function() { + it("returns null if selected is valid", function() { + makeController(); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; + expect($scope.getCannotCreateBcacheMsg()).toBeNull(); + }); + }); + + describe("canEdit", function() { + it("returns false when $parent.canEdit is false", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + $scope.$parent.canEdit = function() { + return false; + }; + + expect($scope.canEdit()).toBe(false); + }); + + it("returns false when isAllStorageDisabled is false", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(true); + $scope.$parent.canEdit = function() { + return true; + }; + + expect($scope.canEdit()).toBe(false); + }); + + it("returns true when partition", function() { + makeController(); + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + var disk = { + type: "partition" + }; + + expect($scope.canEdit(disk)).toBe(true); + }); + }); + + describe("availableEdit", function() { + it("calls availableEdit for volumn group", function() { + makeController(); + var disk = { + type: "lvm-vg" + }; + $scope.availableEdit(disk); + expect(disk.$options).toEqual({ + editingTags: false, + editingFilesystem: false + }); + }); - it("gets proper raid-0 size", function() { - makeController(); - var disk0 = { - original: { - available_size: 1000 * 1000 - } - }; - var disk1 = { - original: { - available_size: 1000 * 1000 - } - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk0, disk1]; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[0]; + it("calls availableEdit for partition", function() { + makeController(); + var disk = { + type: "partition" + }; + $scope.availableEdit(disk); + expect(disk.$options).toEqual({ + editingTags: false, + editingFilesystem: true, + fstype: disk.fstype + }); + }); - expect($scope.getNewRAIDSize()).toBe("2.0 MB"); - }); + it("calls availableEdit for disk with partitions", function() { + makeController(); + var disk = { + type: "physical", + has_partitions: true + }; + $scope.availableEdit(disk); + expect(disk.$options).toEqual({ + editingFilesystem: false, + editingTags: true, + tags: undefined, + fstype: undefined + }); + }); - it("gets proper raid-0 size using size", function() { - makeController(); - var disk0 = { - original: { - size: 1000 * 1000 - } - }; - var disk1 = { - original: { - size: 1000 * 1000 - } - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk0, disk1]; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[0]; + it("calls availableEdit for disk and is boot disk", function() { + makeController(); + var disk = { + type: "physical", + has_partitions: false, + original: { + is_boot: true + } + }; + $scope.availableEdit(disk); + expect(disk.$options).toEqual({ + editingFilesystem: false, + editingTags: true, + tags: undefined, + fstype: undefined + }); + }); + }); - expect($scope.getNewRAIDSize()).toBe("2.0 MB"); - }); + describe("availableQuickEdit", function() { + it("selects disks and deselects others", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + $scope.available = available; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availableEdit"); - it("gets proper raid-1 size", function() { - makeController(); - var disk0 = { - original: { - available_size: 1000 * 1000 - } - }; - var disk1 = { - original: { - available_size: 1000 * 1000 - } - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk0, disk1]; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[1]; + $scope.availableQuickEdit(available[0]); - expect($scope.getNewRAIDSize()).toBe("1.0 MB"); - }); + expect(available[0].$selected).toBe(true); + expect(available[1].$selected).toBe(false); + }); - it("gets proper raid-5 size", function() { - makeController(); - var disk0 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk1 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk2 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var spare0 = { - original: { - available_size: 1000 * 1000 - } - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk0, disk1, disk2, spare0]; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[2]; - $scope.setAsSpareRAIDMember(spare0); + it("calls updateAvailableSelection with force true", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availableEdit"); - // The 1MB spare causes us to only use 1MB of each active disk. - expect($scope.getNewRAIDSize()).toBe("2.0 MB"); - }); + $scope.availableQuickEdit(available[0]); - it("gets proper raid-6 size", function() { - makeController(); - var disk0 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk1 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk2 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk3 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var spare0 = { - original: { - available_size: 1000 * 1000 - } - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk0, disk1, disk2, disk3, spare0]; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[3]; - $scope.setAsSpareRAIDMember(spare0); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); - // The 1MB spare causes us to only use 1MB of each active disk. - expect($scope.getNewRAIDSize()).toBe("2.0 MB"); - }); + it("calls availableEdit", function() { + makeController(); + var available = [{ $selected: false }, { $selected: true }]; + spyOn($scope, "updateAvailableSelection"); + spyOn($scope, "availableEdit"); - it("gets proper raid-10 size", function() { - makeController(); - var disk0 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk1 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var disk2 = { - original: { - available_size: 2 * 1000 * 1000 - } - }; - var spare0 = { - original: { - available_size: 1000 * 1000 - } - }; - $scope.availableNew.spares = []; - $scope.availableNew.devices = [disk0, disk1, disk2, spare0]; - $scope.availableNew.mode = $scope.getAvailableRAIDModes()[4]; - $scope.setAsSpareRAIDMember(spare0); + $scope.availableQuickEdit(available[0]); - // The 1MB spare causes us to only use 1MB of each active disk. - expect($scope.getNewRAIDSize()).toBe("1.5 MB"); - }); + expect($scope.availableEdit).toHaveBeenCalledWith(available[0]); }); + }); - describe("createRAIDCanSave", function() { - - it("returns false if isNewDiskNameInvalid returns true", function() { - makeController(); - $scope.availableNew.mountPoint = "/"; - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); + describe("availableConfirmEdit", function() { + it("does nothing if invalid", function() { + makeController(); + var disk = { + $options: { + mountPoint: "!#$%" + } + }; + spyOn(MachinesManager, "updateDisk"); - expect($scope.createRAIDCanSave()).toBe(false); - }); + $scope.availableConfirmEdit(disk); + expect(MachinesManager.updateDisk).not.toHaveBeenCalled(); + }); - it("returns false if isMountPointInvalid returns true", function() { - makeController(); - $scope.availableNew.mountPoint = "not/absolute"; - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + it("resets name to original if empty", function() { + makeController(); + var name = makeName("name"); + var disk = { + name: "", + $options: { + mountPoint: "" + }, + original: { + name: name + } + }; + spyOn(MachinesManager, "updateDisk"); - expect($scope.createRAIDCanSave()).toBe(false); - }); + $scope.availableConfirmEdit(disk); + expect(disk.name).toBe(name); + expect(MachinesManager.updateDisk).toHaveBeenCalled(); + }); + + it("calls updateDisk with new name for logical volume", function() { + makeController(); + var name = "vg0-lvnew"; + var disk = { + name: name, + type: "virtual", + parent_type: "lvm-vg", + block_id: makeInteger(0, 100), + partition_id: makeInteger(0, 100), + $options: { + fstype: "", + mountPoint: "", + mountOptions: "" + }, + original: { + name: "vg0-lvold" + } + }; + spyOn(MachinesManager, "updateDisk"); - it("returns true if both return false", function() { - makeController(); - $scope.availableNew.mountPoint = "/"; - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + $scope.availableConfirmEdit(disk); + expect(disk.name).toBe(name); + expect(MachinesManager.updateDisk).toHaveBeenCalled(); + }); + + it("calls updateFilesystem for partition", function() { + makeController(); + var name = makeName("name"); + var disk = { + name: "", + type: "partition", + $options: { + mountPoint: "" + }, + original: { + name: name + } + }; + spyOn(MachinesManager, "updateFilesystem"); - expect($scope.createRAIDCanSave()).toBe(true); - }); + $scope.availableConfirmEdit(disk); + expect(disk.name).toBe(name); + expect(MachinesManager.updateFilesystem).toHaveBeenCalled(); + }); + }); + + describe("canCreateBcache", function() { + it("returns false when isAvailableDisabled is true", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it("returns false if two selected", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [{ $selected: true }, { $selected: true }]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it("returns false if selected has fstype", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: "ext4", + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it("returns false if selected is volume group", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + type: "lvm-vg", + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it("returns false if selected has partitions", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: true + } + ]; + $scope.cachesets = [{}]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it(`returns false if selected has no fstype + but not cachesets`, function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = []; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it("returns false if not super user ", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; + $scope.canEdit = function() { + return false; + }; + + expect($scope.canCreateBcache()).toBe(false); + }); + + it("returns false if selected is bcache", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + $selected: true, + parent_type: "bcache" + } + ]; + expect($scope.canCreateBcache()).toBe(false); }); - describe("availableConfirmCreateRAID", function() { + it("returns true if selected has no fstype but has cachesets ", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + $scope.available = [ + { + fstype: null, + $selected: true, + has_partitions: false + } + ]; + $scope.cachesets = [{}]; + $scope.canEdit = function() { + return true; + }; + + expect($scope.canCreateBcache()).toBe(true); + }); + }); + + describe("createBcache", function() { + it("does nothing if canCreateBcache returns false", function() { + makeController(); + $scope.availableMode = "other"; + spyOn($scope, "canCreateBcache").and.returnValue(false); + + $scope.createBcache(); + expect($scope.availableMode).toBe("other"); + }); + + it("sets availableMode and availableNew", function() { + makeController(); + $scope.availableMode = "other"; + spyOn($scope, "canCreateBcache").and.returnValue(true); + + // Add bcache name to create a name after that index. + var otherBcache = { + name: "bcache4" + }; + node.disks = [otherBcache]; + + // Will be set as the device. + var disk = { + $selected: true + }; + $scope.available = [disk]; + + // Will be set as the cacheset. + var cacheset = {}; + $scope.cachesets = [cacheset]; + + $scope.createBcache(); + expect($scope.availableMode).toBe("bcache"); + expect($scope.availableNew).toEqual({ + name: "bcache5", + device: disk, + cacheset: cacheset, + cacheMode: "writeback", + fstype: null, + mountPoint: "", + mountOptions: "", + tags: [] + }); + expect($scope.availableNew.device).toBe(disk); + expect($scope.availableNew.cacheset).toBe(cacheset); + }); + }); - it("does nothing if createRAIDCanSave returns false", function() { - makeController(); - spyOn($scope, "createRAIDCanSave").and.returnValue(false); - var partition0 = { - type: "partition", - block_id: makeInteger(0, 10), - partition_id: makeInteger(0, 10) - }; - var partition1 = { - type: "partition", - block_id: makeInteger(10, 20), - partition_id: makeInteger(10, 20) - }; - var disk0 = { - type: "physical", - block_id: makeInteger(0, 10) - }; - var disk1 = { - type: "physical", - block_id: makeInteger(10, 20) - }; - var availableNew = { - name: makeName("md"), - mode: { - level: "raid-1" - }, - devices: [partition0, partition1, disk0, disk1], - spares: [], - fstype: null, - mountPoint: "", - mountOptions: "" - }; - $scope.availableNew = availableNew; - $scope.setAsSpareRAIDMember(partition0); - $scope.setAsSpareRAIDMember(disk0); - spyOn(MachinesManager, "createRAID"); + describe("fstypeChanged", function() { + it("leaves mountPoint when fstype is not null", function() { + makeController(); + var mountPoint = makeName("srv"); + var mountOptions = makeName("options"); + var options = { + fstype: "ext4", + mountPoint: mountPoint, + mountOptions: mountOptions + }; + + $scope.fstypeChanged(options); + expect(options.mountPoint).toBe(mountPoint); + expect(options.mountOptions).toBe(mountOptions); + }); + + it("clears mountPoint when fstype null", function() { + makeController(); + var options = { + fstype: null, + mountPoint: makeName("srv"), + mountOptions: makeName("options") + }; + + $scope.fstypeChanged(options); + expect(options.mountPoint).toBe(""); + expect(options.mountOptions).toBe(""); + }); + + it( + "sets mountPoint to 'none' for a partition that " + + "cannot be mounted at a directory", + function() { + makeController(); + var mountPoint = makeName("srv"); + var mountOptions = makeName("options"); + var options = { + fstype: "swap", + mountPoint: mountPoint, + mountOptions: mountOptions + }; - $scope.availableConfirmCreateRAID(); - expect(MachinesManager.createRAID).not.toHaveBeenCalled(); - }); + $scope.fstypeChanged(options); + expect(options.mountPoint).toBe("none"); + // Mount options are unchanged. + expect(options.mountOptions).toBe(mountOptions); + } + ); + + it( + "clears mountPoint from 'none' for a partition that " + + "can be mounted at a directory", + function() { + makeController(); + var mountOptions = makeName("options"); + var options = { + fstype: "ext4", + mountPoint: "none", + mountOptions: mountOptions + }; - it("calls MachinesManager.createRAID", function() { - makeController(); - spyOn($scope, "createRAIDCanSave").and.returnValue(true); - var partition0 = { - type: "partition", - block_id: makeInteger(0, 10), - partition_id: makeInteger(0, 10) - }; - var partition1 = { - type: "partition", - block_id: makeInteger(10, 20), - partition_id: makeInteger(10, 20) - }; - var disk0 = { - type: "physical", - block_id: makeInteger(0, 10) - }; - var disk1 = { - type: "physical", - block_id: makeInteger(10, 20) - }; - var availableNew = { - name: makeName("md"), - mode: { - level: "raid-1" - }, - devices: [partition0, partition1, disk0, disk1], - spares: [], - fstype: null, - mountPoint: "", - mountOptions: "" - }; - $scope.availableNew = availableNew; - $scope.setAsSpareRAIDMember(partition0); - $scope.setAsSpareRAIDMember(disk0); - spyOn(MachinesManager, "createRAID"); - - $scope.availableConfirmCreateRAID(); - expect(MachinesManager.createRAID).toHaveBeenCalledWith( - node, { - name: availableNew.name, - level: "raid-1", - block_devices: [disk1.block_id], - partitions: [partition1.partition_id], - spare_devices: [disk0.block_id], - spare_partitions: [partition0.partition_id] - }); - }); + $scope.fstypeChanged(options); + expect(options.mountPoint).toBe(""); + // Mount options are unchanged. + expect(options.mountOptions).toBe(mountOptions); + } + ); + }); + + describe("isNewDiskNameInvalid", function() { + it("returns true if blank name", function() { + makeController(); + $scope.node.disks = []; + + expect($scope.isNewDiskNameInvalid("")).toBe(true); + }); + + it("returns true if name used by disk", function() { + makeController(); + var name = makeName("disk"); + $scope.node.disks = [ + { + name: name + } + ]; - it("calls MachinesManager.createRAID with filesystem", function() { - makeController(); - spyOn($scope, "createRAIDCanSave").and.returnValue(true); - var partition0 = { - type: "partition", - block_id: makeInteger(0, 10), - partition_id: makeInteger(0, 10) - }; - var partition1 = { - type: "partition", - block_id: makeInteger(10, 20), - partition_id: makeInteger(10, 20) - }; - var disk0 = { - type: "physical", - block_id: makeInteger(0, 10) - }; - var disk1 = { - type: "physical", - block_id: makeInteger(10, 20) - }; - var availableNew = { - name: makeName("md"), - mode: { - level: "raid-1" - }, - devices: [partition0, partition1, disk0, disk1], - spares: [], - fstype: "ext4", - mountPoint: makeName("/path"), - mountOptions: makeName("options") - }; - $scope.availableNew = availableNew; - $scope.setAsSpareRAIDMember(partition0); - $scope.setAsSpareRAIDMember(disk0); - spyOn(MachinesManager, "createRAID"); - - $scope.availableConfirmCreateRAID(); - expect(MachinesManager.createRAID).toHaveBeenCalledWith( - node, { - name: availableNew.name, - level: "raid-1", - block_devices: [disk1.block_id], - partitions: [partition1.partition_id], - spare_devices: [disk0.block_id], - spare_partitions: [partition0.partition_id], - fstype: "ext4", - mount_point: availableNew.mountPoint, - mount_options: availableNew.mountOptions - }); - }); + expect($scope.isNewDiskNameInvalid(name)).toBe(true); }); - describe("canCreateVolumeGroup", function() { - - it("returns false isAvailableDisabled returns true", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(true); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateVolumeGroup()).toBe(false); - }); - - it("returns false if any selected has filesystem", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(true); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateVolumeGroup()).toBe(false); - }); + it("returns true if name used by partition", function() { + makeController(); + var name = makeName("disk"); + $scope.node.disks = [ + { + name: makeName("other"), + partitions: [ + { + name: name + } + ] + } + ]; - it("returns false if any selected is volume group", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([ - { - type: "lvm-vg" - }, - { - type: "physical" - } - ]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateVolumeGroup()).toBe(false); - }); + expect($scope.isNewDiskNameInvalid(name)).toBe(true); + }); - it("returns false if not super user", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); - $scope.canEdit = function() { return false; }; - expect($scope.canCreateVolumeGroup()).toBe(false); - }); + it("returns false if the name is not already used", function() { + makeController(); + var name = makeName("disk"); + $scope.node.disks = [ + { + name: makeName("other"), + partitions: [ + { + name: makeName("part") + } + ] + } + ]; - it("returns true if aleast 1 selected", function() { - makeController(); - spyOn($scope, "isAvailableDisabled").and.returnValue(false); - spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); - spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); - $scope.canEdit = function() { return true; }; - expect($scope.canCreateVolumeGroup()).toBe(true); - }); + expect($scope.isNewDiskNameInvalid(name)).toBe(false); }); + }); - describe("createVolumeGroup", function() { + describe("createBcacheCanSave", function() { + it("returns false if isNewDiskNameInvalid returns true", function() { + makeController(); + $scope.availableNew.mountPoint = "/"; + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); + + expect($scope.createBcacheCanSave()).toBe(false); + }); + + it("returns false if isMountPointInvalid returns true", function() { + makeController(); + $scope.availableNew.mountPoint = "not/absolute"; + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + + expect($scope.createBcacheCanSave()).toBe(false); + }); + + it("returns true if both return false", function() { + makeController(); + $scope.availableNew.mountPoint = "/"; + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + + expect($scope.createBcacheCanSave()).toBe(true); + }); + }); + + describe("availableConfirmCreateBcache", function() { + it("does nothing if createBcacheCanSave returns false", function() { + makeController(); + spyOn($scope, "createBcacheCanSave").and.returnValue(false); + var availableNew = { + name: makeName("bcache"), + cacheset: { + cache_set_id: makeInteger(0, 100) + }, + cacheMode: "writearound", + device: { + type: "partition", + partition_id: makeInteger(0, 100) + }, + fstype: null, + mountPoint: "", + mountOptions: "" + }; + $scope.availableNew = availableNew; + spyOn(MachinesManager, "createBcache"); + + $scope.availableConfirmCreateBcache(); + expect(MachinesManager.createBcache).not.toHaveBeenCalled(); + }); + + it("calls MachinesManager.createBcache for partition", function() { + makeController(); + spyOn($scope, "createBcacheCanSave").and.returnValue(true); + var device = { + type: "partition", + partition_id: makeInteger(0, 100), + $selected: true + }; + var availableNew = { + name: makeName("bcache"), + cacheset: { + cache_set_id: makeInteger(0, 100) + }, + cacheMode: "writearound", + device: device, + fstype: "ext4", + mountPoint: makeName("/path"), + mountOptions: makeName("options") + }; + $scope.available = [device]; + $scope.availableNew = availableNew; + spyOn(MachinesManager, "createBcache"); + spyOn($scope, "updateAvailableSelection"); + + $scope.availableConfirmCreateBcache(); + expect(MachinesManager.createBcache).toHaveBeenCalledWith(node, { + name: availableNew.name, + cache_set: availableNew.cacheset.cache_set_id, + cache_mode: "writearound", + partition_id: device.partition_id, + fstype: "ext4", + mount_point: availableNew.mountPoint, + mount_options: availableNew.mountOptions + }); + expect($scope.available).toEqual([]); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); - it("does nothing if canCreateVolumeGroup returns false", function() { - makeController(); - spyOn($scope, "canCreateVolumeGroup").and.returnValue(false); - $scope.availableMode = "other"; + it("calls MachinesManager.createBcache for block device", function() { + makeController(); + spyOn($scope, "createBcacheCanSave").and.returnValue(true); + var device = { + type: "physical", + block_id: makeInteger(0, 100), + $selected: true + }; + var availableNew = { + name: makeName("bcache"), + cacheset: { + cache_set_id: makeInteger(0, 100) + }, + cacheMode: "writearound", + device: device, + fstype: null, + mountPoint: "/", + mountOptions: makeName("options") + }; + $scope.available = [device]; + $scope.availableNew = availableNew; + spyOn(MachinesManager, "createBcache"); + spyOn($scope, "updateAvailableSelection"); + + $scope.availableConfirmCreateBcache(); + expect(MachinesManager.createBcache).toHaveBeenCalledWith(node, { + name: availableNew.name, + cache_set: availableNew.cacheset.cache_set_id, + cache_mode: "writearound", + block_id: device.block_id + }); + expect($scope.available).toEqual([]); + expect($scope.updateAvailableSelection).toHaveBeenCalledWith(true); + }); + }); - $scope.createVolumeGroup(); - expect($scope.availableMode).toBe("other"); - }); + describe("canCreateRAID", function() { + it("returns false isAvailableDisabled returns true", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateRAID()).toBe(false); + }); + + it("returns false if less than 2 is selected", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateRAID()).toBe(false); + }); + + it("returns false if any selected has filesystem", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateRAID()).toBe(false); + }); + + it("returns false if any selected is volume group", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([ + { + type: "lvm-vg" + }, + { + type: "physical" + } + ]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateRAID()).toBe(false); + }); + + it("returns false if not super user", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); + $scope.canEdit = function() { + return false; + }; + expect($scope.canCreateRAID()).toBe(false); + }); + + it("returns true if more than 1 selected", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}, {}]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + spyOn($scope, "isAllStorageDisabled").and.returnValue(false); + expect($scope.canCreateRAID()).toBe(true); + }); + }); + + describe("createRAID", function() { + it("does nothing if canCreateRAID returns false", function() { + makeController(); + spyOn($scope, "canCreateRAID").and.returnValue(false); + $scope.availableMode = "other"; + + $scope.createRAID(); + expect($scope.availableMode).toBe("other"); + }); + + it("sets up availableNew", function() { + makeController(); + spyOn($scope, "canCreateRAID").and.returnValue(true); + $scope.availableMode = "other"; + + // Add md name to create a name after that index. + var otherRAID = { + name: "md4" + }; + node.disks = [otherRAID]; + + // Will be set as the devices. + var disk0 = { + $selected: true + }; + var disk1 = { + $selected: true + }; + $scope.available = [disk0, disk1]; + + $scope.createRAID(); + expect($scope.availableMode).toBe("raid"); + expect($scope.availableNew.name).toBe("md5"); + expect($scope.availableNew.devices).toEqual([disk0, disk1]); + expect($scope.availableNew.mode.level).toEqual("raid-0"); + expect($scope.availableNew.spares).toEqual([]); + expect($scope.availableNew.fstype).toBeNull(); + expect($scope.availableNew.mountPoint).toEqual(""); + expect($scope.availableNew.mountOptions).toEqual(""); + }); + }); + + describe("getAvailableRAIDModes", function() { + it("returns empty list if availableNew null", function() { + makeController(); + $scope.availableNew = null; + + expect($scope.getAvailableRAIDModes()).toEqual([]); + }); + + it("returns empty list if availableNew.devices not defined", function() { + makeController(); + $scope.availableNew = {}; + + expect($scope.getAvailableRAIDModes()).toEqual([]); + }); + + it("returns raid 0 and 1 for 2 disks", function() { + makeController(); + $scope.availableNew.devices = [{}, {}]; + + var modes = $scope.getAvailableRAIDModes(); + expect(modes[0].level).toEqual("raid-0"); + expect(modes[1].level).toEqual("raid-1"); + expect(modes.length).toEqual(2); + }); + + it("returns raid 0,1,5,10 for 3 disks", function() { + makeController(); + $scope.availableNew.devices = [{}, {}, {}]; + + var modes = $scope.getAvailableRAIDModes(); + expect(modes[0].level).toEqual("raid-0"); + expect(modes[1].level).toEqual("raid-1"); + expect(modes[2].level).toEqual("raid-5"); + expect(modes[3].level).toEqual("raid-10"); + expect(modes.length).toEqual(4); + }); + + it("returns raid 0,1,5,6,10 for 4 disks", function() { + makeController(); + $scope.availableNew.devices = [{}, {}, {}, {}]; + + var modes = $scope.getAvailableRAIDModes(); + expect(modes[0].level).toEqual("raid-0"); + expect(modes[1].level).toEqual("raid-1"); + expect(modes[2].level).toEqual("raid-5"); + expect(modes[3].level).toEqual("raid-6"); + expect(modes[4].level).toEqual("raid-10"); + expect(modes.length).toEqual(5); + }); + }); + + describe("getTotalNumberOfAvailableSpares", function() { + var modes = [ + { + level: "raid-0", + min_disks: 2, + allows_spares: false + }, + { + level: "raid-1", + min_disks: 2, + allows_spares: true + }, + { + level: "raid-5", + min_disks: 3, + allows_spares: true + }, + { + level: "raid-6", + min_disks: 4, + allows_spares: true + }, + { + level: "raid-10", + min_disks: 3, + allows_spares: true + } + ]; - it("sets up availableNew", function() { - makeController(); - spyOn($scope, "canCreateVolumeGroup").and.returnValue(true); - $scope.availableMode = "other"; - - // Add vg name to create a name after that index. - var otherVG = { - name: "vg4" - }; - node.disks = [otherVG]; - - // Will be set as the devices. - var disk0 = { - $selected: true - }; - var disk1 = { - $selected: true - }; - $scope.available = [disk0, disk1]; - - $scope.createVolumeGroup(); - expect($scope.availableMode).toBe("volume-group"); - expect($scope.availableNew.name).toBe("vg5"); - expect($scope.availableNew.devices).toEqual([disk0, disk1]); - }); + angular.forEach(modes, function(mode) { + it("returns current result for " + mode.level, function() { + makeController(); + $scope.availableNew.mode = mode; + if (!mode.allows_spares) { + expect($scope.getTotalNumberOfAvailableSpares()).toBe(0); + } else { + var count = makeInteger(mode.min_disks, 100); + var i, + devices = []; + for (i = 0; i < count; i++) { + devices.push({}); + } + $scope.availableNew.devices = devices; + expect( + $scope.getTotalNumberOfAvailableSpares(), + count - mode.min_disks + ); + } + }); }); + }); - describe("getNewVolumeGroupSize", function() { - - it("return the total of all devices", function() { - makeController(); - $scope.availableNew.devices = [ - { - original: { - available_size: 1000 * 1000 - } - }, - { - original: { - available_size: 1000 * 1000 - } - }, - { - original: { - available_size: 1000 * 1000 - } - } - ]; + describe("getNumberOfRemainingSpares", function() { + it("returns 0 when getTotalNumberOfAvailableSpares returns 0", function() { + makeController(); + spyOn($scope, "getTotalNumberOfAvailableSpares").and.returnValue(0); + + expect($scope.getNumberOfRemainingSpares()).toBe(0); + }); + + it("returns allowed minus the current number of spares", function() { + makeController(); + var count = makeInteger(10, 100); + spyOn($scope, "getTotalNumberOfAvailableSpares").and.returnValue(count); + var sparesCount = makeInteger(0, count); + var i, + spares = []; + for (i = 0; i < sparesCount; i++) { + spares.push({}); + } + $scope.availableNew.spares = spares; + + expect($scope.getNumberOfRemainingSpares()).toBe(count - sparesCount); + }); + }); + + describe("showSparesColumn", function() { + it(`returns true when getTotalNumberOfAvailableSpares + greater than 0`, function() { + makeController(); + spyOn($scope, "getTotalNumberOfAvailableSpares").and.returnValue(1); + + expect($scope.showSparesColumn()).toBe(true); + }); + + it(`returns false when getTotalNumberOfAvailableSpares + less than 1`, function() { + makeController(); + spyOn($scope, "getTotalNumberOfAvailableSpares").and.returnValue(0); + + expect($scope.showSparesColumn()).toBe(false); + }); + }); + + describe("RAIDModeChanged", function() { + it("clears availableNew.spares", function() { + makeController(); + $scope.availableNew.spares = [{}, {}]; + + $scope.RAIDModeChanged(); + expect($scope.availableNew.spares).toEqual([]); + }); + }); + + describe("isActiveRAIDMember", function() { + it("returns true when disk key not in spares", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger() + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk]; + $scope.setAsActiveRAIDMember(disk); + + expect($scope.isActiveRAIDMember(disk)).toBe(true); + }); + + it("returns false when disk key in spares", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger() + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk]; + $scope.setAsSpareRAIDMember(disk); + + expect($scope.isActiveRAIDMember(disk)).toBe(false); + }); + }); + + describe("isSpareRAIDMember", function() { + it("returns false when disk key not in spares", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger() + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk]; + $scope.setAsActiveRAIDMember(disk); + + expect($scope.isSpareRAIDMember(disk)).toBe(false); + }); + + it("returns true when disk key in spares", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger() + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk]; + $scope.setAsSpareRAIDMember(disk); + + expect($scope.isSpareRAIDMember(disk)).toBe(true); + }); + }); + + describe("setAsActiveRAIDMember", function() { + it("sets the disk as an active RAID member", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger() + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk]; + + $scope.setAsSpareRAIDMember(disk); + expect($scope.isSpareRAIDMember(disk)).toBe(true); + + $scope.setAsActiveRAIDMember(disk); + expect($scope.isActiveRAIDMember(disk)).toBe(true); + }); + }); + + describe("setAsSpareRAIDMember", function() { + it("sets the disk as a spare RAID member", function() { + makeController(); + var disk = { + type: "physical", + block_id: makeInteger() + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk]; + + $scope.setAsActiveRAIDMember(disk); + expect($scope.isActiveRAIDMember(disk)).toBe(true); + + $scope.setAsSpareRAIDMember(disk); + expect($scope.isSpareRAIDMember(disk)).toBe(true); + }); + }); + + describe("getNewRAIDSize", function() { + it("gets proper raid-0 size", function() { + makeController(); + var disk0 = { + original: { + available_size: 1000 * 1000 + } + }; + var disk1 = { + original: { + available_size: 1000 * 1000 + } + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk0, disk1]; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[0]; + + expect($scope.getNewRAIDSize()).toBe("2.0 MB"); + }); - expect($scope.getNewVolumeGroupSize()).toBe("3.0 MB"); - }); + it("gets proper raid-0 size using size", function() { + makeController(); + var disk0 = { + original: { + size: 1000 * 1000 + } + }; + var disk1 = { + original: { + size: 1000 * 1000 + } + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk0, disk1]; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[0]; + + expect($scope.getNewRAIDSize()).toBe("2.0 MB"); + }); - it("return the total of all devices using size", function() { - makeController(); - $scope.availableNew.devices = [ - { - original: { - size: 1000 * 1000 - } - }, - { - original: { - size: 1000 * 1000 - } - }, - { - original: { - size: 1000 * 1000 - } - } - ]; + it("gets proper raid-1 size", function() { + makeController(); + var disk0 = { + original: { + available_size: 1000 * 1000 + } + }; + var disk1 = { + original: { + available_size: 1000 * 1000 + } + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk0, disk1]; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[1]; + + expect($scope.getNewRAIDSize()).toBe("1.0 MB"); + }); - expect($scope.getNewVolumeGroupSize()).toBe("3.0 MB"); - }); + it("gets proper raid-5 size", function() { + makeController(); + var disk0 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk1 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk2 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var spare0 = { + original: { + available_size: 1000 * 1000 + } + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk0, disk1, disk2, spare0]; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[2]; + $scope.setAsSpareRAIDMember(spare0); + + // The 1MB spare causes us to only use 1MB of each active disk. + expect($scope.getNewRAIDSize()).toBe("2.0 MB"); }); - describe("createVolumeGroupCanSave", function() { + it("gets proper raid-6 size", function() { + makeController(); + var disk0 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk1 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk2 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk3 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var spare0 = { + original: { + available_size: 1000 * 1000 + } + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk0, disk1, disk2, disk3, spare0]; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[3]; + $scope.setAsSpareRAIDMember(spare0); + + // The 1MB spare causes us to only use 1MB of each active disk. + expect($scope.getNewRAIDSize()).toBe("2.0 MB"); + }); - it("return true if isNewDiskNameInvalid returns false", function() { - makeController(); - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + it("gets proper raid-10 size", function() { + makeController(); + var disk0 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk1 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var disk2 = { + original: { + available_size: 2 * 1000 * 1000 + } + }; + var spare0 = { + original: { + available_size: 1000 * 1000 + } + }; + $scope.availableNew.spares = []; + $scope.availableNew.devices = [disk0, disk1, disk2, spare0]; + $scope.availableNew.mode = $scope.getAvailableRAIDModes()[4]; + $scope.setAsSpareRAIDMember(spare0); + + // The 1MB spare causes us to only use 1MB of each active disk. + expect($scope.getNewRAIDSize()).toBe("1.5 MB"); + }); + }); + + describe("createRAIDCanSave", function() { + it("returns false if isNewDiskNameInvalid returns true", function() { + makeController(); + $scope.availableNew.mountPoint = "/"; + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); + + expect($scope.createRAIDCanSave()).toBe(false); + }); + + it("returns false if isMountPointInvalid returns true", function() { + makeController(); + $scope.availableNew.mountPoint = "not/absolute"; + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + + expect($scope.createRAIDCanSave()).toBe(false); + }); + + it("returns true if both return false", function() { + makeController(); + $scope.availableNew.mountPoint = "/"; + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + + expect($scope.createRAIDCanSave()).toBe(true); + }); + }); + + describe("availableConfirmCreateRAID", function() { + it("does nothing if createRAIDCanSave returns false", function() { + makeController(); + spyOn($scope, "createRAIDCanSave").and.returnValue(false); + var partition0 = { + type: "partition", + block_id: makeInteger(0, 10), + partition_id: makeInteger(0, 10) + }; + var partition1 = { + type: "partition", + block_id: makeInteger(10, 20), + partition_id: makeInteger(10, 20) + }; + var disk0 = { + type: "physical", + block_id: makeInteger(0, 10) + }; + var disk1 = { + type: "physical", + block_id: makeInteger(10, 20) + }; + var availableNew = { + name: makeName("md"), + mode: { + level: "raid-1" + }, + devices: [partition0, partition1, disk0, disk1], + spares: [], + fstype: null, + mountPoint: "", + mountOptions: "" + }; + $scope.availableNew = availableNew; + $scope.setAsSpareRAIDMember(partition0); + $scope.setAsSpareRAIDMember(disk0); + spyOn(MachinesManager, "createRAID"); + + $scope.availableConfirmCreateRAID(); + expect(MachinesManager.createRAID).not.toHaveBeenCalled(); + }); + + it("calls MachinesManager.createRAID", function() { + makeController(); + spyOn($scope, "createRAIDCanSave").and.returnValue(true); + var partition0 = { + type: "partition", + block_id: makeInteger(0, 10), + partition_id: makeInteger(0, 10) + }; + var partition1 = { + type: "partition", + block_id: makeInteger(10, 20), + partition_id: makeInteger(10, 20) + }; + var disk0 = { + type: "physical", + block_id: makeInteger(0, 10) + }; + var disk1 = { + type: "physical", + block_id: makeInteger(10, 20) + }; + var availableNew = { + name: makeName("md"), + mode: { + level: "raid-1" + }, + devices: [partition0, partition1, disk0, disk1], + spares: [], + fstype: null, + mountPoint: "", + mountOptions: "" + }; + $scope.availableNew = availableNew; + $scope.setAsSpareRAIDMember(partition0); + $scope.setAsSpareRAIDMember(disk0); + spyOn(MachinesManager, "createRAID"); + + $scope.availableConfirmCreateRAID(); + expect(MachinesManager.createRAID).toHaveBeenCalledWith(node, { + name: availableNew.name, + level: "raid-1", + block_devices: [disk1.block_id], + partitions: [partition1.partition_id], + spare_devices: [disk0.block_id], + spare_partitions: [partition0.partition_id] + }); + }); - expect($scope.createVolumeGroupCanSave()).toBe(true); - }); + it("calls MachinesManager.createRAID with filesystem", function() { + makeController(); + spyOn($scope, "createRAIDCanSave").and.returnValue(true); + var partition0 = { + type: "partition", + block_id: makeInteger(0, 10), + partition_id: makeInteger(0, 10) + }; + var partition1 = { + type: "partition", + block_id: makeInteger(10, 20), + partition_id: makeInteger(10, 20) + }; + var disk0 = { + type: "physical", + block_id: makeInteger(0, 10) + }; + var disk1 = { + type: "physical", + block_id: makeInteger(10, 20) + }; + var availableNew = { + name: makeName("md"), + mode: { + level: "raid-1" + }, + devices: [partition0, partition1, disk0, disk1], + spares: [], + fstype: "ext4", + mountPoint: makeName("/path"), + mountOptions: makeName("options") + }; + $scope.availableNew = availableNew; + $scope.setAsSpareRAIDMember(partition0); + $scope.setAsSpareRAIDMember(disk0); + spyOn(MachinesManager, "createRAID"); + + $scope.availableConfirmCreateRAID(); + expect(MachinesManager.createRAID).toHaveBeenCalledWith(node, { + name: availableNew.name, + level: "raid-1", + block_devices: [disk1.block_id], + partitions: [partition1.partition_id], + spare_devices: [disk0.block_id], + spare_partitions: [partition0.partition_id], + fstype: "ext4", + mount_point: availableNew.mountPoint, + mount_options: availableNew.mountOptions + }); + }); + }); - it("return false if isNewDiskNameInvalid returns true", function() { - makeController(); - spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); + describe("canCreateVolumeGroup", function() { + it("returns false isAvailableDisabled returns true", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateVolumeGroup()).toBe(false); + }); + + it("returns false if any selected has filesystem", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(true); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateVolumeGroup()).toBe(false); + }); + + it("returns false if any selected is volume group", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([ + { + type: "lvm-vg" + }, + { + type: "physical" + } + ]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateVolumeGroup()).toBe(false); + }); + + it("returns false if not super user", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); + $scope.canEdit = function() { + return false; + }; + expect($scope.canCreateVolumeGroup()).toBe(false); + }); + + it("returns true if aleast 1 selected", function() { + makeController(); + spyOn($scope, "isAvailableDisabled").and.returnValue(false); + spyOn($scope, "getSelectedAvailable").and.returnValue([{}]); + spyOn($scope, "hasUnmountedFilesystem").and.returnValue(false); + $scope.canEdit = function() { + return true; + }; + expect($scope.canCreateVolumeGroup()).toBe(true); + }); + }); + + describe("createVolumeGroup", function() { + it("does nothing if canCreateVolumeGroup returns false", function() { + makeController(); + spyOn($scope, "canCreateVolumeGroup").and.returnValue(false); + $scope.availableMode = "other"; + + $scope.createVolumeGroup(); + expect($scope.availableMode).toBe("other"); + }); + + it("sets up availableNew", function() { + makeController(); + spyOn($scope, "canCreateVolumeGroup").and.returnValue(true); + $scope.availableMode = "other"; + + // Add vg name to create a name after that index. + var otherVG = { + name: "vg4" + }; + node.disks = [otherVG]; + + // Will be set as the devices. + var disk0 = { + $selected: true + }; + var disk1 = { + $selected: true + }; + $scope.available = [disk0, disk1]; + + $scope.createVolumeGroup(); + expect($scope.availableMode).toBe("volume-group"); + expect($scope.availableNew.name).toBe("vg5"); + expect($scope.availableNew.devices).toEqual([disk0, disk1]); + }); + }); + + describe("getNewVolumeGroupSize", function() { + it("return the total of all devices", function() { + makeController(); + $scope.availableNew.devices = [ + { + original: { + available_size: 1000 * 1000 + } + }, + { + original: { + available_size: 1000 * 1000 + } + }, + { + original: { + available_size: 1000 * 1000 + } + } + ]; - expect($scope.createVolumeGroupCanSave()).toBe(false); - }); + expect($scope.getNewVolumeGroupSize()).toBe("3.0 MB"); }); - describe("availableConfirmCreateVolumeGroup", function() { + it("return the total of all devices using size", function() { + makeController(); + $scope.availableNew.devices = [ + { + original: { + size: 1000 * 1000 + } + }, + { + original: { + size: 1000 * 1000 + } + }, + { + original: { + size: 1000 * 1000 + } + } + ]; - it("does nothing if createVolumeGroupCanSave returns false", - function() { - makeController(); - spyOn($scope, "createVolumeGroupCanSave").and.returnValue( - false); - var partition0 = { - type: "partition", - block_id: makeInteger(0, 10), - partition_id: makeInteger(0, 10) - }; - var partition1 = { - type: "partition", - block_id: makeInteger(10, 20), - partition_id: makeInteger(10, 20) - }; - var disk0 = { - type: "physical", - block_id: makeInteger(0, 10) - }; - var disk1 = { - type: "physical", - block_id: makeInteger(10, 20) - }; - var availableNew = { - name: makeName("vg"), - devices: [partition0, partition1, disk0, disk1] - }; - $scope.availableNew = availableNew; - spyOn(MachinesManager, "createVolumeGroup"); - - $scope.availableConfirmCreateVolumeGroup(); - expect( - MachinesManager.createVolumeGroup).not.toHaveBeenCalled(); - }); - - it("calls MachinesManager.createVolumeGroup", function() { - makeController(); - spyOn($scope, "createVolumeGroupCanSave").and.returnValue(true); - var partition0 = { - type: "partition", - block_id: makeInteger(0, 10), - partition_id: makeInteger(0, 10) - }; - var partition1 = { - type: "partition", - block_id: makeInteger(10, 20), - partition_id: makeInteger(10, 20) - }; - var disk0 = { - type: "physical", - block_id: makeInteger(0, 10) - }; - var disk1 = { - type: "physical", - block_id: makeInteger(10, 20) - }; - var availableNew = { - name: makeName("vg"), - devices: [partition0, partition1, disk0, disk1] - }; - $scope.availableNew = availableNew; - spyOn(MachinesManager, "createVolumeGroup"); - - $scope.availableConfirmCreateVolumeGroup(); - expect(MachinesManager.createVolumeGroup).toHaveBeenCalledWith( - node, { - name: availableNew.name, - block_devices: [disk0.block_id, disk1.block_id], - partitions: [ - partition0.partition_id, partition1.partition_id] - }); - }); + expect($scope.getNewVolumeGroupSize()).toBe("3.0 MB"); }); + }); - describe("canAddLogicalVolume", function() { + describe("createVolumeGroupCanSave", function() { + it("return true if isNewDiskNameInvalid returns false", function() { + makeController(); + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(false); + + expect($scope.createVolumeGroupCanSave()).toBe(true); + }); + + it("return false if isNewDiskNameInvalid returns true", function() { + makeController(); + spyOn($scope, "isNewDiskNameInvalid").and.returnValue(true); + + expect($scope.createVolumeGroupCanSave()).toBe(false); + }); + }); + + describe("availableConfirmCreateVolumeGroup", function() { + it("does nothing if createVolumeGroupCanSave returns false", function() { + makeController(); + spyOn($scope, "createVolumeGroupCanSave").and.returnValue(false); + var partition0 = { + type: "partition", + block_id: makeInteger(0, 10), + partition_id: makeInteger(0, 10) + }; + var partition1 = { + type: "partition", + block_id: makeInteger(10, 20), + partition_id: makeInteger(10, 20) + }; + var disk0 = { + type: "physical", + block_id: makeInteger(0, 10) + }; + var disk1 = { + type: "physical", + block_id: makeInteger(10, 20) + }; + var availableNew = { + name: makeName("vg"), + devices: [partition0, partition1, disk0, disk1] + }; + $scope.availableNew = availableNew; + spyOn(MachinesManager, "createVolumeGroup"); + + $scope.availableConfirmCreateVolumeGroup(); + expect(MachinesManager.createVolumeGroup).not.toHaveBeenCalled(); + }); + + it("calls MachinesManager.createVolumeGroup", function() { + makeController(); + spyOn($scope, "createVolumeGroupCanSave").and.returnValue(true); + var partition0 = { + type: "partition", + block_id: makeInteger(0, 10), + partition_id: makeInteger(0, 10) + }; + var partition1 = { + type: "partition", + block_id: makeInteger(10, 20), + partition_id: makeInteger(10, 20) + }; + var disk0 = { + type: "physical", + block_id: makeInteger(0, 10) + }; + var disk1 = { + type: "physical", + block_id: makeInteger(10, 20) + }; + var availableNew = { + name: makeName("vg"), + devices: [partition0, partition1, disk0, disk1] + }; + $scope.availableNew = availableNew; + spyOn(MachinesManager, "createVolumeGroup"); + + $scope.availableConfirmCreateVolumeGroup(); + expect(MachinesManager.createVolumeGroup).toHaveBeenCalledWith(node, { + name: availableNew.name, + block_devices: [disk0.block_id, disk1.block_id], + partitions: [partition0.partition_id, partition1.partition_id] + }); + }); + }); - it("returns false if not volume group", function() { - makeController(); - expect($scope.canAddLogicalVolume({ - type: "physical" - })).toBe(false); - expect($scope.canAddLogicalVolume({ - type: "virtual" - })).toBe(false); - expect($scope.canAddLogicalVolume({ - type: "partition" - })).toBe(false); - }); + describe("canAddLogicalVolume", function() { + it("returns false if not volume group", function() { + makeController(); + expect( + $scope.canAddLogicalVolume({ + type: "physical" + }) + ).toBe(false); + expect( + $scope.canAddLogicalVolume({ + type: "virtual" + }) + ).toBe(false); + expect( + $scope.canAddLogicalVolume({ + type: "partition" + }) + ).toBe(false); + }); + + it("returns false if not enough space", function() { + makeController(); + expect( + $scope.canAddLogicalVolume({ + type: "lvm-vg", + original: { + available_size: 1.5 * 1024 * 1024 + } + }) + ).toBe(false); + }); + + it("returns true if enough space", function() { + makeController(); + expect( + $scope.canAddLogicalVolume({ + type: "lvm-vg", + original: { + available_size: 10 * 1024 * 1024 + } + }) + ).toBe(true); + }); + }); + + describe("availableLogicalVolume", function() { + it("sets availableMode to 'logical-volume'", function() { + makeController(); + var disk = { + type: "lvm-vg", + name: "vg0", + available_size_human: "10 GB", + fstype: null, + tags: [] + }; + $scope.availableMode = "other"; + $scope.availableLogicalVolume(disk); + expect($scope.availableMode).toBe("logical-volume"); + }); + + it("sets $options to correct values", function() { + makeController(); + var disk = { + type: "lvm-vg", + name: "vg0", + available_size_human: "10 GB", + fstype: null, + tags: [] + }; + $scope.availableLogicalVolume(disk); + expect(disk.$options).toEqual({ + name: "vg0-lv0", + size: "10", + sizeUnits: "GB", + fstype: null, + tags: [] + }); + }); + }); - it("returns false if not enough space", function() { - makeController(); - expect($scope.canAddLogicalVolume({ - type: "lvm-vg", - original: { - available_size: 1.5 * 1024 * 1024 - } - })).toBe(false); - }); + describe("isLogicalVolumeNameInvalid", function() { + it("returns true if doesn't start with volume group", function() { + makeController(); + var disk = { + type: "lvm-vg", + name: "vg0", + $options: { + name: "v" + } + }; - it("returns true if enough space", function() { - makeController(); - expect($scope.canAddLogicalVolume({ - type: "lvm-vg", - original: { - available_size: 10 * 1024 * 1024 - } - })).toBe(true); - }); + expect($scope.isLogicalVolumeNameInvalid(disk)).toBe(true); }); - describe("availableLogicalVolume", function() { - - it("sets availableMode to 'logical-volume'", function() { - makeController(); - var disk = { - type: "lvm-vg", - name: "vg0", - available_size_human: "10 GB", - fstype: null, - tags: [] - }; - $scope.availableMode = "other"; - $scope.availableLogicalVolume(disk); - expect($scope.availableMode).toBe("logical-volume"); - }); + it("returns true if equal to volume group", function() { + makeController(); + var disk = { + type: "lvm-vg", + name: "vg0", + $options: { + name: "vg0-" + } + }; - it("sets $options to correct values", function() { - makeController(); - var disk = { - type: "lvm-vg", - name: "vg0", - available_size_human: "10 GB", - fstype: null, - tags: [] - }; - $scope.availableLogicalVolume(disk); - expect(disk.$options).toEqual({ - name: "vg0-lv0", - size: "10", - sizeUnits: "GB", - fstype: null, - tags: [] - }); - }); + expect($scope.isLogicalVolumeNameInvalid(disk)).toBe(true); }); - describe("isLogicalVolumeNameInvalid", function() { + it("returns false has text after the volume group", function() { + makeController(); + var disk = { + type: "lvm-vg", + name: "vg0", + $options: { + name: "vg0-l" + } + }; - it("returns true if doesn't start with volume group", function() { - makeController(); - var disk = { - type: "lvm-vg", - name: "vg0", - $options: { - name: "v" - } - }; + expect($scope.isLogicalVolumeNameInvalid(disk)).toBe(false); + }); + }); - expect($scope.isLogicalVolumeNameInvalid(disk)).toBe(true); - }); + describe("newLogicalVolumeNameChanged", function() { + it("resets name to volume group name if not present", function() { + makeController(); + var disk = { + type: "lvm-vg", + name: "vg0", + $options: { + name: "v" + } + }; - it("returns true if equal to volume group", function() { - makeController(); - var disk = { - type: "lvm-vg", - name: "vg0", - $options: { - name: "vg0-" - } - }; + $scope.newLogicalVolumeNameChanged(disk); + expect(disk.$options.name).toBe("vg0-"); + }); + }); - expect($scope.isLogicalVolumeNameInvalid(disk)).toBe(true); - }); + describe("isAddLogicalVolumeSizeInvalid", function() { + it("returns value from isAddPartitionSizeInvalid", function() { + makeController(); + var sentinel = {}; + spyOn($scope, "isAddPartitionSizeInvalid").and.returnValue(sentinel); + + expect($scope.isAddLogicalVolumeSizeInvalid({})).toBe(sentinel); + }); + }); + + describe("availableConfirmLogicalVolume", function() { + it("does nothing if invalid", function() { + makeController(); + var disk = { + $options: { + size: "", + sizeUnits: "GB" + } + }; + spyOn(MachinesManager, "createLogicalVolume"); - it("returns false has text after the volume group", function() { - makeController(); - var disk = { - type: "lvm-vg", - name: "vg0", - $options: { - name: "vg0-l" - } - }; + $scope.availableConfirmLogicalVolume(disk); - expect($scope.isLogicalVolumeNameInvalid(disk)).toBe(false); - }); + expect(MachinesManager.createLogicalVolume).not.toHaveBeenCalled(); }); - describe("newLogicalVolumeNameChanged", function() { + it("calls createLogicalVolume with bytes", function() { + makeController(); + var disk = { + name: "vg0", + block_id: makeInteger(0, 100), + original: { + available_size: 4 * 1000 * 1000 * 1000, + available_size_human: "4.0 GB" + }, + $options: { + name: "vg0-lv0", + size: "2", + sizeUnits: "GB", + fstype: null, + mountPoint: "", + mountOptions: "" + } + }; + spyOn(MachinesManager, "createLogicalVolume"); - it("resets name to volume group name if not present", function() { - makeController(); - var disk = { - type: "lvm-vg", - name: "vg0", - $options: { - name: "v" - } - }; + $scope.availableConfirmLogicalVolume(disk); - $scope.newLogicalVolumeNameChanged(disk); - expect(disk.$options.name).toBe("vg0-"); - }); - }); + expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( + node, + disk.block_id, + "lv0", + 2 * 1000 * 1000 * 1000, + {} + ); + }); + + it( + "calls createLogicalVolume with fstype, " + + "mountPoint, and mountOptions", + function() { + makeController(); + var disk = { + name: "vg0", + block_id: makeInteger(0, 100), + original: { + available_size: 4 * 1000 * 1000 * 1000, + available_size_human: "4.0 GB" + }, + $options: { + name: "vg0-lv0", + size: "2", + sizeUnits: "GB", + fstype: "ext4", + mountPoint: makeName("/path"), + mountOptions: makeName("options") + } + }; + spyOn(MachinesManager, "createLogicalVolume"); - describe("isAddLogicalVolumeSizeInvalid", function() { + $scope.availableConfirmLogicalVolume(disk); - it("returns value from isAddPartitionSizeInvalid", function() { - makeController(); - var sentinel = {}; - spyOn($scope, "isAddPartitionSizeInvalid").and.returnValue( - sentinel); + expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( + node, + disk.block_id, + "lv0", + 2 * 1000 * 1000 * 1000, + { + fstype: "ext4", + mount_point: disk.$options.mountPoint, + mount_options: disk.$options.mountOptions + } + ); + } + ); + + it("calls createLogicalVolume with available_size bytes", function() { + makeController(); + var disk = { + name: "vg0", + block_id: makeInteger(0, 100), + original: { + available_size: 2.6 * 1000 * 1000 * 1000, + available_size_human: "2.6 GB" + }, + $options: { + name: "vg0-lv0", + size: "2.62", + sizeUnits: "GB", + fstype: null, + mountPoint: "", + mountOptions: "" + } + }; + spyOn(MachinesManager, "createLogicalVolume"); - expect($scope.isAddLogicalVolumeSizeInvalid({})).toBe(sentinel); - }); - }); + $scope.availableConfirmLogicalVolume(disk); - describe("availableConfirmLogicalVolume", function() { + expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( + node, + disk.block_id, + "lv0", + 2.6 * 1000 * 1000 * 1000, + {} + ); + }); + + // regression test for https://bugs.launchpad.net/maas/+bug/1509535 + it( + "calls createLogicalVolume with available_size bytes" + + " even when human size gets rounded down", + function() { + makeController(); + var disk = { + name: "vg0", + block_id: makeInteger(0, 100), + original: { + available_size: 2.035 * 1000 * 1000 * 1000, + available_size_human: "2.0 GB" + }, + $options: { + name: "vg0-lv0", + size: "2.0", + sizeUnits: "GB", + fstype: null, + mountPoint: "", + mountOptions: "" + } + }; + spyOn(MachinesManager, "createLogicalVolume"); - it("does nothing if invalid", function() { - makeController(); - var disk = { - $options: { - size: "", - sizeUnits: "GB" - } - }; - spyOn(MachinesManager, "createLogicalVolume"); + $scope.availableConfirmLogicalVolume(disk); - $scope.availableConfirmLogicalVolume(disk); + expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( + node, + disk.block_id, + "lv0", + 2.035 * 1000 * 1000 * 1000, + {} + ); + } + ); + }); - expect(MachinesManager.createLogicalVolume).not.toHaveBeenCalled(); - }); + describe("isAllStorageDisabled", function() { + var RegionConnection, UsersManager, webSocket; + beforeEach(inject(function($injector) { + UsersManager = $injector.get("UsersManager"); + RegionConnection = $injector.get("RegionConnection"); - it("calls createLogicalVolume with bytes", function() { - makeController(); - var disk = { - name: "vg0", - block_id: makeInteger(0, 100), - original: { - available_size: 4 * 1000 * 1000 * 1000, - available_size_human: "4.0 GB" - }, - $options: { - name: "vg0-lv0", - size: "2", - sizeUnits: "GB", - fstype: null, - mountPoint: "", - mountOptions: "" - } - }; - spyOn(MachinesManager, "createLogicalVolume"); + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); - $scope.availableConfirmLogicalVolume(disk); + it("false when status is Ready", function() { + makeController(); + $scope.node.status = "Ready"; + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: true + }); + expect($scope.isAllStorageDisabled()).toBe(false); + }); - expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( - node, disk.block_id, "lv0", 2 * 1000 * 1000 * 1000, {}); - }); + it("false when status is Allocated", function() { + makeController(); + $scope.node.status = "Allocated"; + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: true + }); + expect($scope.isAllStorageDisabled()).toBe(false); + }); - it("calls createLogicalVolume with fstype, " + - "mountPoint, and mountOptions", function() { - makeController(); - var disk = { - name: "vg0", - block_id: makeInteger(0, 100), - original: { - available_size: 4 * 1000 * 1000 * 1000, - available_size_human: "4.0 GB" - }, - $options: { - name: "vg0-lv0", - size: "2", - sizeUnits: "GB", - fstype: "ext4", - mountPoint: makeName("/path"), - mountOptions: makeName("options") - } - }; - spyOn(MachinesManager, "createLogicalVolume"); - - $scope.availableConfirmLogicalVolume(disk); - - expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( - node, disk.block_id, "lv0", 2 * 1000 * 1000 * 1000, { - fstype: "ext4", - mount_point: disk.$options.mountPoint, - mount_options: disk.$options.mountOptions - }); - }); + it("false when Allocated and owned", function() { + makeController(); + var user = makeName("user"); + $scope.node.status = "Allocated"; + $scope.node.owner = user; + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: false, + username: user + }); + expect($scope.isAllStorageDisabled()).toBe(false); + }); - it("calls createLogicalVolume with available_size bytes", function() { - makeController(); - var disk = { - name: "vg0", - block_id: makeInteger(0, 100), - original: { - available_size: 2.6 * 1000 * 1000 * 1000, - available_size_human: "2.6 GB" - }, - $options: { - name: "vg0-lv0", - size: "2.62", - sizeUnits: "GB", - fstype: null, - mountPoint: "", - mountOptions: "" - } - }; - spyOn(MachinesManager, "createLogicalVolume"); + it("true when not admin", function() { + makeController(); + $scope.node.status = "Allocated"; + $scope.node.owner = makeName("user"); + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: false, + username: makeName("user") + }); + expect($scope.isAllStorageDisabled()).toBe(true); + }); - $scope.availableConfirmLogicalVolume(disk); + it("true otherwise", function() { + makeController(); + $scope.node.status = makeName("status"); + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: true + }); + expect($scope.isAllStorageDisabled()).toBe(true); + }); + }); - expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( - node, disk.block_id, "lv0", 2.6 * 1000 * 1000 * 1000, {}); - }); + describe("hasStorageLayoutIssues", function() { + it("true when node.storage_layout_issues has issues", function() { + makeController(); + $scope.node.storage_layout_issues = [makeName("issue")]; + expect($scope.hasStorageLayoutIssues()).toBe(true); + }); + + it("false when node.storage_layout_issues has no issues", function() { + makeController(); + $scope.node.storage_layout_issues = []; + expect($scope.hasStorageLayoutIssues()).toBe(false); + }); + }); + + describe("openStorageLayoutConfirm", function() { + it("sets 'confirmStorageLayout' to true", function() { + makeController(); + $scope.confirmStorageLayout = false; + $scope.osFamilies = [ + { + id: "linux", + name: "Linux", + layouts: [ + { + id: "flat", + name: "Flat" + }, + { + id: "lvm", + name: "LVM" + }, + { + id: "bcache", + name: "bcache" + }, + { + id: "vmfs6", + name: "VMFS6 (VMware ESXI)" + }, + { + id: "blank", + name: "No storage (blank) layout" + } + ] + } + ]; + $scope.openStorageLayoutConfirm("flat"); + expect($scope.confirmStorageLayout).toBe(true); + }); - // regression test for https://bugs.launchpad.net/maas/+bug/1509535 - it("calls createLogicalVolume with available_size bytes" + - " even when human size gets rounded down", function() { - - makeController(); - var disk = { - name: "vg0", - block_id: makeInteger(0, 100), - original: { - available_size: 2.035 * 1000 * 1000 * 1000, - available_size_human: "2.0 GB" - }, - $options: { - name: "vg0-lv0", - size: "2.0", - sizeUnits: "GB", - fstype: null, - mountPoint: "", - mountOptions: "" - } - }; - spyOn(MachinesManager, "createLogicalVolume"); + it("sets 'newLayout' to layout argument", function() { + makeController(); + $scope.osFamilies = [ + { + id: "linux", + name: "Linux", + layouts: [ + { + id: "flat", + name: "Flat" + }, + { + id: "lvm", + name: "LVM" + }, + { + id: "bcache", + name: "bcache" + }, + { + id: "vmfs6", + name: "VMFS6 (VMware ESXI)" + }, + { + id: "blank", + name: "No storage (blank) layout" + } + ] + } + ]; + $scope.openStorageLayoutConfirm("flat"); + expect($scope.newLayout).toEqual($scope.osFamilies[0].layouts[0]); + }); + }); + + describe("closeStorageLayoutConfirm", function() { + it("sets 'confirmStorageLayout' to false", function() { + makeController(); + $scope.confirmStorageLayout = true; + $scope.closeStorageLayoutConfirm(); + expect($scope.confirmStorageLayout).toBe(false); + }); + }); + + describe("updateStorageLayout", function() { + it("calls 'applyStorageLayout'", function() { + makeController(); + spyOn(MachinesManager, "applyStorageLayout").and.callFake(function() { + var deferred = $q.defer(); + return deferred.promise; + }); + $scope.newLayout = { + id: "flat", + name: "Flat" + }; + $scope.updateStorageLayout($scope.newLayout); + expect(MachinesManager.applyStorageLayout).toHaveBeenCalled(); + }); - $scope.availableConfirmLogicalVolume(disk); + it("calls 'closeStorageLayoutConfirm'", function() { + makeController(); + spyOn(MachinesManager, "applyStorageLayout").and.callFake(function() { + var deferred = $q.defer(); + return deferred.promise; + }); + spyOn($scope, "closeStorageLayoutConfirm"); + $scope.updateStorageLayout({ + id: "flat", + name: "Flat" + }); + expect($scope.closeStorageLayoutConfirm).toHaveBeenCalled(); + }); + }); - expect(MachinesManager.createLogicalVolume).toHaveBeenCalledWith( - node, disk.block_id, "lv0", 2.035 * 1000 * 1000 * 1000, {}); - }); + describe("openNewDatastorePanel", function() { + it("sets 'createNewDatastore' to true", function() { + makeController(); + $scope.createNewDatastore = false; + $scope.available = [ + { + $selected: true, + id: 1 + } + ]; + $scope.openNewDatastorePanel(); + expect($scope.createNewDatastore).toBe(true); }); - describe("isAllStorageDisabled", function() { + it("sets newDatastore", function() { + makeController(); + $scope.available = [ + { + $selected: true, + id: 1, + mount_point: "dev/null", + size_human: "35 GB" + } + ]; + $scope.openNewDatastorePanel(); + expect($scope.datastores.new).toEqual({ + id: $scope.available[0].id, + name: "", + mountpoint: $scope.available[0].mount_point, + filesystem: "VMFS6", + size: $scope.available[0].size_human + }); + }); + }); - var RegionConnection, UserManager; - beforeEach(inject(function($injector) { - UsersManager = $injector.get("UsersManager"); - RegionConnection = $injector.get("RegionConnection"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - it("false when status is Ready", function() { - makeController(); - $scope.node.status = "Ready"; - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: true }); - expect($scope.isAllStorageDisabled()).toBe(false); - }); + describe("closeNewDatastorePanel", function() { + it("sets 'createNewDatastore' to false", function() { + makeController(); + $scope.createNewDatastore = true; + $scope.closeNewDatastorePanel(); + expect($scope.createNewDatastore).toBe(false); + }); + + it("sets 'newDatastore' to '{}'", function() { + makeController(); + $scope.datastores.new = { id: 1, name: "" }; + $scope.closeNewDatastorePanel(); + expect($scope.datastores.new).toEqual({}); + }); + }); + + describe("canPerformActionOnDatastoreSet", function() { + it("return false if not on vmsf6 storage layout", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = [1]; + $scope.storageLayout = { id: "flat" }; + expect($scope.canPerformActionOnDatastoreSet()).toBe(false); + }); + + it("return false if already editing datastores", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = true; + $scope.selectedAvailableDatastores = [1]; + $scope.storageLayout = { id: "vmfs6" }; + expect($scope.canPerformActionOnDatastoreSet()).toBe(false); + }); + + it("return false if no device is selected", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = []; + $scope.storageLayout = { id: "vmfs6" }; + expect($scope.canPerformActionOnDatastoreSet()).toBe(false); + }); + + it("return true when conditions are matched", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = [1]; + $scope.storageLayout = { id: "vmfs6" }; + expect($scope.canPerformActionOnDatastoreSet()).toBe(true); + }); + }); + + describe("canAddToDatastore", function() { + it("calls 'canPerformActionOnDatastoreSet", function() { + makeController(); + spyOn($scope, "canPerformActionOnDatastoreSet"); + $scope.canAddToDatastore(); + expect($scope.canPerformActionOnDatastoreSet).toHaveBeenCalled(); + }); + + it("return false if not on vmsf6 storage layout", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = [1]; + $scope.storageLayout = { id: "flat" }; + $scope.node.disks = []; + expect($scope.canAddToDatastore()).toBe(false); + }); + + it("return false if already editing datastores", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = true; + $scope.selectedAvailableDatastores = [1]; + $scope.storageLayout = { id: "vmfs6" }; + $scope.node.disks = []; + expect($scope.canAddToDatastore()).toBe(false); + }); + + it("return false if no device is selected", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = []; + $scope.storageLayout = { id: "vmfs6" }; + $scope.node.disks = []; + expect($scope.canAddToDatastore()).toBe(false); + }); + + it("return false when there are no datastores", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = []; + $scope.storageLayout = { id: "vmfs6" }; + $scope.node.disks = []; + expect($scope.canAddToDatastore()).toBe(false); + }); + + it("return true when conditions are matched", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.createNewDatastore = false; + $scope.selectedAvailableDatastores = [1]; + $scope.storageLayout = { id: "vmfs6" }; + $scope.node.disks = [{ parent_type: "vmfs6" }]; + expect($scope.canAddToDatastore()).toBe(true); + }); + }); + + describe("checkAddToDatastoreValid", function() { + it("selected disks are valid when that condition is true", function() { + makeController(); + var selected = { + has_partitions: false + }; + spyOn($scope, "getSelectedAvailable").and.returnValue([selected]); + expect($scope.addToDatastoreValid).toBe(false); + $scope.checkAddToDatastoreValid(); + expect($scope.addToDatastoreValid).toBe(true); + }); + + it("selected disks are not valid disk has a partition", function() { + makeController(); + var selected = { + has_partitions: true + }; + spyOn($scope, "getSelectedAvailable").and.returnValue([selected]); + expect($scope.addToDatastoreValid).toBe(false); + $scope.checkAddToDatastoreValid(); + expect($scope.addToDatastoreValid).toBe(false); + }); + + it("selected disks are not valid when no selected disks", function() { + makeController(); + spyOn($scope, "getSelectedAvailable").and.returnValue([]); + expect($scope.addToDatastoreValid).toBe(false); + $scope.checkAddToDatastoreValid(); + expect($scope.addToDatastoreValid).toBe(false); + }); + }); + + describe("openAddToExistingDatastorePanel", function() { + it("sets 'addToExistingDatastore' to true", function() { + makeController(); + $scope.addToExistingDatastore = false; + $scope.available = [ + { + $selected: true, + id: 1 + } + ]; + $scope.openAddToExistingDatastorePanel(); + expect($scope.addToExistingDatastore).toBe(true); + }); - it("false when status is Allocated", function() { - makeController(); - $scope.node.status = "Allocated"; - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: true }); - expect($scope.isAllStorageDisabled()).toBe(false); - }); + it("sets 'selectedAvailableDatastores' to selected", function() { + makeController(); + $scope.datastores.old = [ + { + $selected: true, + id: 1 + } + ]; + $scope.openAddToExistingDatastorePanel(); + expect($scope.selectedAvailableDatastores).toEqual($scope.available); + }); + + it("sets 'datastores.old' to first disk", function() { + makeController(); + $scope.openAddToExistingDatastorePanel(); + expect($scope.datastores.old).toBe($scope.node.disks[0]); + }); + }); + + describe("closeAddToExistingDatastorePanel", function() { + it("sets 'addToExistingDatastore' to false", function() { + makeController(); + $scope.addToExistingDatastore = true; + $scope.closeAddToExistingDatastorePanel(); + expect($scope.addToExistingDatastore).toBe(false); + }); + + it("sets, 'newDatasore' to '{}'", function() { + makeController(); + $scope.datastores.new = { id: 1, name: "" }; + $scope.closeAddToExistingDatastorePanel(); + expect($scope.datastores.new).toEqual({}); + }); + }); + + describe("createDatastore", function() { + it("sets 'createNewDatastore' to true", function() { + makeController(); + spyOn(MachinesManager, "createDatastore").and.callFake(function() { + var deferred = $q.defer(); + return deferred.promise; + }); + $scope.createNewDatastore = false; + $scope.createDatastore(); + expect($scope.createNewDatastore).toBe(true); + }); - it("false when Allocated and owned", function() { - makeController(); - var user = makeName("user"); - $scope.node.status = "Allocated"; - $scope.node.owner = user; - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: false, username: user }); - expect($scope.isAllStorageDisabled()).toBe(false); - }); + it("calls 'MachinesManager.createDatastore'", function() { + makeController(); + spyOn(MachinesManager, "createDatastore").and.callFake(function() { + var deferred = $q.defer(); + return deferred.promise; + }); + $scope.createDatastore(); + expect(MachinesManager.createDatastore).toHaveBeenCalled(); + }); + }); - it("true when not admin", function() { - makeController(); - $scope.node.status = "Allocated"; - $scope.node.owner = makeName("user"); - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: false, username: makeName("user") }); - expect($scope.isAllStorageDisabled()).toBe(true); - }); + describe("getRemoveDatastoreWarningText", function() { + it("returns correct string if more datastores exist", function() { + makeController(); + var disks = [ + { + parent_type: "vmfs6" + }, + { + parent_type: "vmfs6" + }, + {} + ]; + expect($scope.getRemoveDatastoreWarningText(disks)).toBe( + "Are you sure you want to remove this datastore?" + ); + }); + + it("returns correct string if last datastore", function() { + makeController(); + var disks = [ + { + parent_type: "vmfs6" + }, + {} + ]; + expect($scope.getRemoveDatastoreWarningText(disks)).toBe( + "Are you sure you want to remove this datastore? " + + "ESXi requires at least one VMFS datastore to deploy." + ); + }); + }); + + describe("getTotalDiskSize", function() { + it("returns total disk size", function() { + makeController(); + var disks = [ + { + size: 2000000 + }, + { + size: 1000000 + } + ]; + expect($scope.getTotalDiskSize(disks)).toBe(3000000); + }); + }); - it("true otherwise", function() { - makeController(); - $scope.node.status = makeName("status"); - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: true }); - expect($scope.isAllStorageDisabled()).toBe(true); - }); + describe("getFormattedTotalDiskSize", function() { + it("returns formatted string in MB", function() { + makeController(); + var disks = [ + { + size: 1000000 + }, + { + size: 2000000 + } + ]; + expect($scope.getFormattedTotalDiskSize(disks)).toBe("3 MB"); }); - describe("hasStorageLayoutIssues", function() { - it("true when node.storage_layout_issues has issues", function() { - makeController(); - $scope.node.storage_layout_issues = [makeName("issue")]; - expect($scope.hasStorageLayoutIssues()).toBe(true); - }); + it("returns formatted string in GB", function() { + makeController(); + var disks = [ + { + size: 1000000000 + }, + { + size: 2000000000 + } + ]; + expect($scope.getFormattedTotalDiskSize(disks)).toBe("3 GB"); + }); - it("false when node.storage_layout_issues has no issues", function() { - makeController(); - $scope.node.storage_layout_issues = []; - expect($scope.hasStorageLayoutIssues()).toBe(false); - }); + it("returns formtted string in TB", function() { + makeController(); + var disks = [ + { + size: 1000000000000 + }, + { + size: 2000000000000 + } + ]; + expect($scope.getFormattedTotalDiskSize(disks)).toBe("3 TB"); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_events.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_events.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_events.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_events.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,278 +4,280 @@ * Unit tests for NodeEventsController. */ -describe("NodeEventsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); +import { makeName } from "testing/utils"; - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the NodeEventsController and - // mock the websocket connection. - var MachinesManager, ControllersManager, EventsManagerFactory; - var ManagerHelperService, ErrorService, RegionConnection, webSocket; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get("MachinesManager"); - ControllersManager = $injector.get("ControllersManager"); - EventsManagerFactory = $injector.get("EventsManagerFactory"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - RegionConnection = $injector.get("RegionConnection"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Make a fake node. - var _id = 0; - function makeNode() { - var node = { - id: _id++, - system_id: makeName("system_id"), - fqdn: makeName("fqdn") - }; - MachinesManager._items.push(node); - ControllersManager._items.push(node); - return node; - } - - // Make a fake event. - function makeEvent() { - return { - type: { - description: makeName("type") - }, - description: makeName("description") - }; - } - - // Create the node that will be used and set the routeParams. - var node, $routeParams; - beforeEach(function() { - node = makeNode(); - $routeParams = { - system_id: node.system_id - }; - }); +describe("NodeEventsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Makes the NodeEventsController - function makeController(loadManagerDefer) { - var loadManager = spyOn(ManagerHelperService, "loadManager"); - if(angular.isObject(loadManagerDefer)) { - loadManager.and.returnValue(loadManagerDefer.promise); - } else { - loadManager.and.returnValue($q.defer().promise); - } - - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - return $controller("NodeEventsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - MachinesManager: MachinesManager, - ControllersManager: ControllersManager, - EventsManagerFactory: EventsManagerFactory, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); + // Grab the needed angular pieces. + var $controller, $location, $rootScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the NodeEventsController and + // mock the websocket connection. + var MachinesManager, ControllersManager, EventsManagerFactory; + var ManagerHelperService, ErrorService, RegionConnection, webSocket; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + ControllersManager = $injector.get("ControllersManager"); + EventsManagerFactory = $injector.get("EventsManagerFactory"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + RegionConnection = $injector.get("RegionConnection"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Make a fake node. + var _id = 0; + function makeNode() { + var node = { + id: _id++, + system_id: makeName("system_id"), + fqdn: makeName("fqdn") + }; + MachinesManager._items.push(node); + ControllersManager._items.push(node); + return node; + } + + // Make a fake event. + function makeEvent() { + return { + type: { + description: makeName("type") + }, + description: makeName("description") + }; + } + + // Create the node that will be used and set the routeParams. + var node, $routeParams; + beforeEach(function() { + node = makeNode(); + $routeParams = { + system_id: node.system_id + }; + }); + + // Makes the NodeEventsController + function makeController(loadManagerDefer) { + var loadManager = spyOn(ManagerHelperService, "loadManager"); + if (angular.isObject(loadManagerDefer)) { + loadManager.and.returnValue(loadManagerDefer.promise); + } else { + loadManager.and.returnValue($q.defer().promise); } - it("sets title to loading", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - }); - - it("sets the initial $scope values", function() { - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.events).toEqual([]); - expect($scope.eventsLoaded).toEqual(false); - expect($scope.days).toEqual(1); - expect($scope.nodesManager).toBe(MachinesManager); - expect($scope.type_name).toBe('machine'); - }); - - it("sets the initial $scope values when controller", function() { - $location.path('/controller'); - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.events).toEqual([]); - expect($scope.eventsLoaded).toEqual(false); - expect($scope.days).toEqual(1); - expect($scope.nodesManager).toBe(ControllersManager); - expect($scope.type_name).toBe('controller'); - }); - - it("calls loadManager with MachinesManager", function() { - makeController(); - expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( - $scope, MachinesManager); - }); - - it("doesnt call setActiveItem if node already loaded", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - spyOn(MachinesManager, "setActiveItem"); - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if node not loaded", function() { - var defer = $q.defer(); - makeController(defer); - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - - setActiveDefer.resolve(node); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - expect(MachinesManager.setActiveItem).toHaveBeenCalledWith( - node.system_id); - }); - - it("calls raiseError if setActiveItem is rejected", function() { - var defer = $q.defer(); - makeController(defer); - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - spyOn(ErrorService, "raiseError"); - - defer.resolve(); - $rootScope.$digest(); - - var error = makeName("error"); - setActiveDefer.reject(error); - $rootScope.$digest(); - - expect(ErrorService.raiseError).toHaveBeenCalledWith(error); - }); - - it("gets the events manager for the node", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - spyOn(EventsManagerFactory, "getManager").and.callThrough(); - - defer.resolve(); - $rootScope.$digest(); - expect(EventsManagerFactory.getManager).toHaveBeenCalledWith(node.id); - - var manager = EventsManagerFactory.getManager(node.id); - expect($scope.events).toBe(manager.getItems()); - }); - - it("calls loadItems on the events manager", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = EventsManagerFactory.getManager(node.id); - spyOn(manager, "loadItems").and.returnValue($q.defer().promise); - - defer.resolve(); - $rootScope.$digest(); - expect(manager.loadItems).toHaveBeenCalled(); - }); - - it("sets eventsLoaded once events manager loadItems resolves", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = EventsManagerFactory.getManager(node.id); - var loadDefer = $q.defer(); - spyOn(manager, "loadItems").and.returnValue(loadDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - loadDefer.resolve(); - $rootScope.$digest(); - expect($scope.eventsLoaded).toBe(true); - }); - - it("watches node.fqdn updates $rootScope.title", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - - defer.resolve(); - $rootScope.$digest(); - - node.fqdn = makeName("fqdn"); - $rootScope.$digest(); - expect($rootScope.title).toBe(node.fqdn + " - events"); - }); - - describe("getEventText", function() { - - it("returns just event type description without dash", function() { - makeController(); - var evt = makeEvent(); - delete evt.description; - expect($scope.getEventText(evt)).toBe(evt.type.description); - }); - - it("returns event type description with event description", - function() { - makeController(); - var evt = makeEvent(); - expect($scope.getEventText(evt)).toBe( - evt.type.description + " - " + evt.description); - }); - }); - - describe("loadMore", function() { - - it("adds 1 days to $scope.days", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - - defer.resolve(); - $rootScope.$digest(); - $scope.loadMore(); - - expect($scope.days).toBe(2); - }); - - it("calls loadMaximumDays with $scope.days", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = EventsManagerFactory.getManager(node.id); - spyOn(manager, "loadMaximumDays"); - - defer.resolve(); - $rootScope.$digest(); - $scope.loadMore(); + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + return $controller("NodeEventsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + MachinesManager: MachinesManager, + ControllersManager: ControllersManager, + EventsManagerFactory: EventsManagerFactory, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + } + + it("sets title to loading", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + }); + + it("sets the initial $scope values", function() { + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.events).toEqual([]); + expect($scope.eventsLoaded).toEqual(false); + expect($scope.days).toEqual(1); + expect($scope.nodesManager).toBe(MachinesManager); + expect($scope.type_name).toBe("machine"); + }); + + it("sets the initial $scope values when controller", function() { + $location.path("/controller"); + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.events).toEqual([]); + expect($scope.eventsLoaded).toEqual(false); + expect($scope.days).toEqual(1); + expect($scope.nodesManager).toBe(ControllersManager); + expect($scope.type_name).toBe("controller"); + }); + + it("calls loadManager with MachinesManager", function() { + makeController(); + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( + $scope, + MachinesManager + ); + }); + + it("doesnt call setActiveItem if node already loaded", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + spyOn(MachinesManager, "setActiveItem"); + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if node not loaded", function() { + var defer = $q.defer(); + makeController(defer); + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + + defer.resolve(); + $rootScope.$digest(); + + setActiveDefer.resolve(node); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + expect(MachinesManager.setActiveItem).toHaveBeenCalledWith(node.system_id); + }); + + it("calls raiseError if setActiveItem is rejected", function() { + var defer = $q.defer(); + makeController(defer); + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + spyOn(ErrorService, "raiseError"); + + defer.resolve(); + $rootScope.$digest(); + + var error = makeName("error"); + setActiveDefer.reject(error); + $rootScope.$digest(); + + expect(ErrorService.raiseError).toHaveBeenCalledWith(error); + }); + + it("gets the events manager for the node", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + spyOn(EventsManagerFactory, "getManager").and.callThrough(); + + defer.resolve(); + $rootScope.$digest(); + expect(EventsManagerFactory.getManager).toHaveBeenCalledWith(node.id); + + var manager = EventsManagerFactory.getManager(node.id); + expect($scope.events).toBe(manager.getItems()); + }); + + it("calls loadItems on the events manager", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = EventsManagerFactory.getManager(node.id); + spyOn(manager, "loadItems").and.returnValue($q.defer().promise); + + defer.resolve(); + $rootScope.$digest(); + expect(manager.loadItems).toHaveBeenCalled(); + }); + + it("sets eventsLoaded once events manager loadItems resolves", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = EventsManagerFactory.getManager(node.id); + var loadDefer = $q.defer(); + spyOn(manager, "loadItems").and.returnValue(loadDefer.promise); + + defer.resolve(); + $rootScope.$digest(); + loadDefer.resolve(); + $rootScope.$digest(); + expect($scope.eventsLoaded).toBe(true); + }); + + it("watches node.fqdn updates $rootScope.title", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + + defer.resolve(); + $rootScope.$digest(); + + node.fqdn = makeName("fqdn"); + $rootScope.$digest(); + expect($rootScope.title).toBe(node.fqdn + " - events"); + }); + + describe("getEventText", function() { + it("returns just event type description without dash", function() { + makeController(); + var evt = makeEvent(); + delete evt.description; + expect($scope.getEventText(evt)).toBe(evt.type.description); + }); + + it("returns event type description with event description", function() { + makeController(); + var evt = makeEvent(); + expect($scope.getEventText(evt)).toBe( + evt.type.description + " - " + evt.description + ); + }); + }); + + describe("loadMore", function() { + it("adds 1 days to $scope.days", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + + defer.resolve(); + $rootScope.$digest(); + $scope.loadMore(); + + expect($scope.days).toBe(2); + }); + + it("calls loadMaximumDays with $scope.days", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = EventsManagerFactory.getManager(node.id); + spyOn(manager, "loadMaximumDays"); + + defer.resolve(); + $rootScope.$digest(); + $scope.loadMore(); - expect(manager.loadMaximumDays).toHaveBeenCalledWith(2); - }); + expect(manager.loadMaximumDays).toHaveBeenCalledWith(2); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_result.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_result.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_result.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_result.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,272 +4,279 @@ * Unit tests for NodeResultController. */ -describe("NodeResultController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the NodeResultController and - // mock the websocket connection. - var MachinesManager, ControllersManager, RegionConnection; - var NodeResultsManagerFactory, ManagerHelperService; - var ErrorService, webSocket; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get("MachinesManager"); - ControllersManager = $injector.get("ControllersManager"); - RegionConnection = $injector.get("RegionConnection"); - NodeResultsManagerFactory = $injector.get("NodeResultsManagerFactory"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Make a fake result. - function makeResult() { - return { - id: makeInteger(0, 1000), - name: makeName("name") - }; - } +import { makeFakeResponse, makeInteger, makeName } from "testing/utils"; - // Make a fake node. - function makeNode() { - var node = { - system_id: makeName("system_id"), - fqdn: makeName("fqdn") - }; - MachinesManager._items.push(node); - ControllersManager._items.push(node); - return node; - } - - // Create the node that will be used and set the routeParams. - var node, $routeParams; - beforeEach(function() { - node = makeNode(); - script_result = makeResult(); - $routeParams = { - id: script_result.id, - system_id: node.system_id - }; - }); +describe("NodeResultController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Makes the NodeResultController - function makeController(loadManagerDefer) { - var loadManager = spyOn(ManagerHelperService, "loadManager"); - if(angular.isObject(loadManagerDefer)) { - loadManager.and.returnValue(loadManagerDefer.promise); - } else { - loadManager.and.returnValue($q.defer().promise); - } - - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - return $controller("NodeResultController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - MachinesManager: MachinesManager, - ControllersManager: ControllersManager, - NodeResultsManagerFactory: NodeResultsManagerFactory, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the NodeResultController and + // mock the websocket connection. + var MachinesManager, ControllersManager, RegionConnection; + var NodeResultsManagerFactory, ManagerHelperService; + var ErrorService, webSocket; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + ControllersManager = $injector.get("ControllersManager"); + RegionConnection = $injector.get("RegionConnection"); + NodeResultsManagerFactory = $injector.get("NodeResultsManagerFactory"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Make a fake result. + function makeResult() { + return { + id: makeInteger(0, 1000), + name: makeName("name") + }; + } + + // Make a fake node. + function makeNode() { + var node = { + system_id: makeName("system_id"), + fqdn: makeName("fqdn") + }; + MachinesManager._items.push(node); + ControllersManager._items.push(node); + return node; + } + + // Create the node that will be used and set the routeParams. + var node, $routeParams, script_result; + beforeEach(function() { + node = makeNode(); + script_result = makeResult(); + $routeParams = { + id: script_result.id, + system_id: node.system_id + }; + }); + + // Makes the NodeResultController + function makeController(loadManagerDefer) { + var loadManager = spyOn(ManagerHelperService, "loadManager"); + if (angular.isObject(loadManagerDefer)) { + loadManager.and.returnValue(loadManagerDefer.promise); + } else { + loadManager.and.returnValue($q.defer().promise); } - it("sets title to loading and page to nodes", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - }); - - it("sets the initial $scope values", function() { - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.resultLoaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.output).toBe('combined'); - expect($scope.result).toBeNull(); - expect($scope.nodesManager).toBe(MachinesManager); - expect($scope.type_name).toBe('machine'); - }); - - it("sets the initial $scope values when controller", function() { - $location.path('/controller'); - makeController(); - expect($scope.loaded).toBe(false); - expect($scope.resultLoaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.output).toBe('combined'); - expect($scope.result).toBeNull(); - expect($scope.nodesManager).toBe(ControllersManager); - expect($scope.type_name).toBe('controller'); - }); - - it("calls loadManager with MachinesManager", function() { - makeController(); - expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( - $scope, MachinesManager); - }); - - it("doesnt call setActiveItem if node already loaded", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - spyOn(MachinesManager, "setActiveItem"); - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if node not loaded", function() { - var defer = $q.defer(); - makeController(defer); - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - - setActiveDefer.resolve(node); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - expect(MachinesManager.setActiveItem).toHaveBeenCalledWith( - node.system_id); - }); - - it("loads result on load", function(done) { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var script_result = makeResult(); - webSocket.returnData.push(makeFakeResponse(script_result)); - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect($scope.loaded).toBe(true); - var expectFunc; - expectFunc = function() { - if($scope.resultLoaded) { - expect($scope.result.id).toBe(script_result.id); - done(); - }else{ - setTimeout(expectFunc); - } - }; + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + return $controller("NodeResultController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + MachinesManager: MachinesManager, + ControllersManager: ControllersManager, + NodeResultsManagerFactory: NodeResultsManagerFactory, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + } + + it("sets title to loading and page to nodes", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + }); + + it("sets the initial $scope values", function() { + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.resultLoaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.output).toBe("combined"); + expect($scope.result).toBeNull(); + expect($scope.nodesManager).toBe(MachinesManager); + expect($scope.type_name).toBe("machine"); + }); + + it("sets the initial $scope values when controller", function() { + $location.path("/controller"); + makeController(); + expect($scope.loaded).toBe(false); + expect($scope.resultLoaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.output).toBe("combined"); + expect($scope.result).toBeNull(); + expect($scope.nodesManager).toBe(ControllersManager); + expect($scope.type_name).toBe("controller"); + }); + + it("calls loadManager with MachinesManager", function() { + makeController(); + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( + $scope, + MachinesManager + ); + }); + + it("doesnt call setActiveItem if node already loaded", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + spyOn(MachinesManager, "setActiveItem"); + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if node not loaded", function() { + var defer = $q.defer(); + makeController(defer); + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + + defer.resolve(); + $rootScope.$digest(); + + setActiveDefer.resolve(node); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + expect(MachinesManager.setActiveItem).toHaveBeenCalledWith(node.system_id); + }); + + it("loads result on load", function(done) { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var script_result = makeResult(); + webSocket.returnData.push(makeFakeResponse(script_result)); + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect($scope.loaded).toBe(true); + var expectFunc; + expectFunc = function() { + if ($scope.resultLoaded) { + expect($scope.result.id).toBe(script_result.id); + done(); + } else { setTimeout(expectFunc); - }); - - it("calls raiseError if setActiveItem is rejected", function() { - var defer = $q.defer(); - makeController(defer); - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - spyOn(ErrorService, "raiseError"); - - defer.resolve(); - $rootScope.$digest(); - - var error = makeName("error"); - setActiveDefer.reject(error); - $rootScope.$digest(); - - expect(ErrorService.raiseError).toHaveBeenCalledWith(error); - }); - - it("watches node.fqdn updates $rootScope.title", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - $scope.result = script_result; - - defer.resolve(); - $rootScope.$digest(); - - node.fqdn = makeName("fqdn"); - $rootScope.$digest(); - expect($rootScope.title).toBe(node.fqdn + " - " + script_result.name); - }); - - describe("get_result_data", function() { + } + }; + setTimeout(expectFunc); + }); + + it("calls raiseError if setActiveItem is rejected", function() { + var defer = $q.defer(); + makeController(defer); + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + spyOn(ErrorService, "raiseError"); + + defer.resolve(); + $rootScope.$digest(); + + var error = makeName("error"); + setActiveDefer.reject(error); + $rootScope.$digest(); + + expect(ErrorService.raiseError).toHaveBeenCalledWith(error); + }); + + it("watches node.fqdn updates $rootScope.title", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + $scope.result = script_result; + + defer.resolve(); + $rootScope.$digest(); + + node.fqdn = makeName("fqdn"); + $rootScope.$digest(); + expect($rootScope.title).toBe(node.fqdn + " - " + script_result.name); + }); + + describe("get_result_data", function() { + it("sets initial variables", function() { + var defer = $q.defer(); + makeController(defer); + var output = makeName("output"); + MachinesManager._activeItem = node; + $scope.result = script_result; + + defer.resolve(); + $rootScope.$digest(); + $scope.get_result_data(output); + + expect($scope.output).toBe(output); + expect($scope.data).toBe("Loading..."); + }); + + it("returns result", function() { + var defer = $q.defer(); + makeController(); + var output = makeName("output"); + var data = makeName("data"); + $scope.node = node; + $scope.result = script_result; + var nodeResultsManager = NodeResultsManagerFactory.getManager( + $scope.node + ); + spyOn(nodeResultsManager, "get_result_data").and.returnValue( + defer.promise + ); + + $scope.get_result_data(output); + defer.resolve(data); + $rootScope.$digest(); + + expect($scope.output).toBe(output); + expect($scope.data).toBe(data); + }); + + it("returns empty file when empty", function() { + var defer = $q.defer(); + makeController(); + var output = makeName("output"); + $scope.node = node; + $scope.result = script_result; + var nodeResultsManager = NodeResultsManagerFactory.getManager( + $scope.node + ); + spyOn(nodeResultsManager, "get_result_data").and.returnValue( + defer.promise + ); + + $scope.get_result_data(output); + defer.resolve(""); + $rootScope.$digest(); - it("sets initial variables", function() { - var defer = $q.defer(); - makeController(defer); - var output = makeName("output"); - MachinesManager._activeItem = node; - $scope.result = script_result; - - defer.resolve(); - $rootScope.$digest(); - $scope.get_result_data(output); - - expect($scope.output).toBe(output); - expect($scope.data).toBe("Loading..."); - }); - - it("returns result", function() { - var defer = $q.defer(); - makeController(); - var output = makeName("output"); - var data = makeName("data"); - $scope.node = node; - $scope.result = script_result; - var nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - spyOn(nodeResultsManager, "get_result_data").and.returnValue( - defer.promise); - - $scope.get_result_data(output); - defer.resolve(data); - $rootScope.$digest(); - - expect($scope.output).toBe(output); - expect($scope.data).toBe(data); - }); - - it("returns empty file when empty", function() { - var defer = $q.defer(); - makeController(); - var output = makeName("output"); - $scope.node = node; - $scope.result = script_result; - var nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - spyOn(nodeResultsManager, "get_result_data").and.returnValue( - defer.promise); - - $scope.get_result_data(output); - defer.resolve(""); - $rootScope.$digest(); - - expect($scope.output).toBe(output); - expect($scope.data).toBe("Empty file."); - }); + expect($scope.output).toBe(output); + expect($scope.data).toBe("Empty file."); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_results.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_results.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_node_results.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_node_results.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,501 +4,666 @@ * Unit tests for NodeResultsController */ +import { + makeFakeResponse, + makeInteger, + makeName, + pickItem +} from "testing/utils"; + +// 2019-04-30 Caleb - Syntax error `import { ScriptStatus }from "../../enum"`; +// TODO - Fix es module imports in test files +const ScriptStatus = { + PENDING: 0, + RUNNING: 1, + PASSED: 2, + FAILED: 3, + TIMEDOUT: 4, + ABORTED: 5, + DEGRADED: 6, + INSTALLING: 7, + FAILED_INSTALLING: 8, + SKIPPED: 9 +}; + describe("NodeResultsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); + // Grab the needed angular pieces. + var $controller, $location, $rootScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $scope.section = { + area: pickItem(["testing", "commissioning", "summary"]) + }; + $q = $injector.get("$q"); + })); + + // Load the required dependencies for the NodeResultsController and + // mock the websocket connection. + var MachinesManager, ControllersManager, NodeResultsManagerFactory; + var ManagerHelperService, ErrorService, RegionConnection, webSocket; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + ControllersManager = $injector.get("ControllersManager"); + NodeResultsManagerFactory = $injector.get("NodeResultsManagerFactory"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + RegionConnection = $injector.get("RegionConnection"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Make a fake node. + function makeNode() { + var node = { + system_id: makeName("system_id"), + disks: [] + }; + MachinesManager._items.push(node); + ControllersManager._items.push(node); + return node; + } + + // Make a result. + function makeResult(type, status) { + if (type === null) { + type = makeInteger(0, 3); + } + if (status === null) { + status = makeInteger(0, 8); + } + var id = makeInteger(0, 1000); + var result = { + id: id, + name: makeName("name"), + type: type, + status: status, + history_list: [ + { + id: id, + status: status + } + ] + }; + var i; + for (i = 0; i < 3; i++) { + result.history_list.push({ + id: makeInteger(0, 1000), + status: makeInteger(0, 8) + }); + } + return result; + } - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $scope.section = { - area: pickItem(["testing", "commissioning", "summary"]) - }; - $q = $injector.get("$q"); - })); - - // Load the required dependencies for the NodeResultsController and - // mock the websocket connection. - var MachinesManager, ControllersManager, NodeResultsManagerFactory; - var ManagerHelperService, ErrorService, RegionConnection, webSocket; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get("MachinesManager"); - ControllersManager = $injector.get("ControllersManager"); - NodeResultsManagerFactory = $injector.get("NodeResultsManagerFactory"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - RegionConnection = $injector.get("RegionConnection"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Make a fake node. - function makeNode() { - var node = { - system_id: makeName("system_id"), - disks: [] - }; - MachinesManager._items.push(node); - ControllersManager._items.push(node); - return node; + // Makes the NodeResultsController + function makeController(loadManagerDefer) { + var loadManager = spyOn(ManagerHelperService, "loadManager"); + if (angular.isObject(loadManagerDefer)) { + loadManager.and.returnValue(loadManagerDefer.promise); + } else { + loadManager.and.returnValue($q.defer().promise); } + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + return $controller("NodeResultsController", { + $scope: $scope, + $routeParams: $routeParams, + MachinesManager: MachinesManager, + ControllersManager: ControllersManager, + NodeResultsManagerFactory: NodeResultsManagerFactory, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + } + + // Create the node that will be used and set the routeParams. + var node, $routeParams; + beforeEach(function() { + node = makeNode(); + $routeParams = { + system_id: node.system_id + }; + }); + + it("sets the initial $scope values", function() { + makeController(); + expect($scope.commissioning_results).toBeNull(); + expect($scope.testing_results).toBeNull(); + expect($scope.installation_results).toBeNull(); + expect($scope.results).toBeNull(); + expect($scope.logs.option).toBeNull(); + expect($scope.logs.availableOptions).toEqual([]); + expect($scope.logOutput).toEqual("Loading..."); + expect($scope.loaded).toBe(false); + expect($scope.resultsLoaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.nodesManager).toBe(MachinesManager); + }); + + it("sets the initial $scope values when controller", function() { + $location.path("/controller"); + makeController(); + expect($scope.commissioning_results).toBeNull(); + expect($scope.testing_results).toBeNull(); + expect($scope.installation_results).toBeNull(); + expect($scope.results).toBeNull(); + expect($scope.logs.option).toBeNull(); + expect($scope.logs.availableOptions).toEqual([]); + expect($scope.logOutput).toEqual("Loading..."); + expect($scope.loaded).toBe(false); + expect($scope.resultsLoaded).toBe(false); + expect($scope.node).toBeNull(); + expect($scope.nodesManager).toBe(ControllersManager); + }); + + it("calls loadManager with MachinesManager", function() { + makeController(); + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( + $scope, + MachinesManager + ); + }); + + it("doesnt call setActiveItem if node already loaded", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + spyOn(MachinesManager, "setActiveItem"); + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if node not loaded", function() { + var defer = $q.defer(); + makeController(defer); + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + + defer.resolve(); + $rootScope.$digest(); + + setActiveDefer.resolve(node); + $rootScope.$digest(); + + expect($scope.node).toBe(node); + expect(MachinesManager.setActiveItem).toHaveBeenCalledWith(node.system_id); + }); + + it("calls raiseError if setActiveItem is rejected", function() { + var defer = $q.defer(); + makeController(defer); + var setActiveDefer = $q.defer(); + spyOn(MachinesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + spyOn(ErrorService, "raiseError"); + + defer.resolve(); + $rootScope.$digest(); + + var error = makeName("error"); + setActiveDefer.reject(error); + $rootScope.$digest(); + + expect(ErrorService.raiseError).toHaveBeenCalledWith(error); + }); + + it("calls loadItems on the results manager", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = NodeResultsManagerFactory.getManager(node); + spyOn(manager, "loadItems").and.returnValue($q.defer().promise); + + defer.resolve(); + $rootScope.$digest(); + expect(manager.loadItems).toHaveBeenCalled(); + }); + + it("sets eventsLoaded once events manager loadItems resolves", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = NodeResultsManagerFactory.getManager(node); + var loadDefer = $q.defer(); + spyOn(manager, "loadItems").and.returnValue(loadDefer.promise); + + defer.resolve(); + $rootScope.$digest(); + loadDefer.resolve(); + $rootScope.$digest(); + expect($scope.resultsLoaded).toBe(true); + }); + + it("sets results once events manager loadItems resolves", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = NodeResultsManagerFactory.getManager(node); + var loadDefer = $q.defer(); + spyOn(manager, "loadItems").and.returnValue(loadDefer.promise); + + defer.resolve(); + $rootScope.$digest(); + loadDefer.resolve(); + $rootScope.$digest(); + expect($scope.resultsLoaded).toBe(true); + }); + + describe("updateLogs", function() { + it("only runs on logs page", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var manager = NodeResultsManagerFactory.getManager(node); + var loadDefer = $q.defer(); + + defer.resolve(); + $rootScope.$digest(); + loadDefer.resolve(); + $rootScope.$digest(); + expect($scope.logs.availableOptions).toEqual([]); + }); + + it("loads summary", function() { + var defer = $q.defer(); + makeController(defer); + $scope.section = { area: "logs" }; + MachinesManager._activeItem = node; + webSocket.returnData.push(makeFakeResponse([])); + var manager = NodeResultsManagerFactory.getManager(node); + + defer.resolve(); + $rootScope.$digest(); + var expectFunc; + expectFunc = function() { + if ($scope.resultsLoaded) { + expect($scope.logs.availableOptions).toEqual([ + { + title: "Machine output (YAML)", + id: "summary_yaml" + }, + { + title: "Machine output (XML)", + id: "summary_xml" + } + ]); + expect($scope.logs.option).toEqual({ + title: "Machine output (YAML)", + id: "summary_yaml" + }); + } else { + setTimeout(expectFunc); + } + }; + setTimeout(expectFunc); + }); + }); - // Make a result. - function makeResult(type, status) { - if(type === null) { - type = makeInteger(0, 3); + describe("updateLogOutput", function() { + it("sets to loading when no node", function() { + makeController(); + $scope.updateLogOutput(); + expect($scope.logOutput).toEqual("Loading..."); + }); + + it("sets summary xml", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var managerDefer = $q.defer(); + $scope.logs = { option: { id: "summary_xml" } }; + spyOn(MachinesManager, "getSummaryXML").and.returnValue( + managerDefer.promise + ); + + defer.resolve(); + $rootScope.$digest(); + managerDefer.resolve(); + $rootScope.$digest(); + + $scope.updateLogOutput(); + expect(MachinesManager.getSummaryXML).toHaveBeenCalledWith(node); + }); + + it("sets summary yaml", function() { + var defer = $q.defer(); + makeController(defer); + MachinesManager._activeItem = node; + var managerDefer = $q.defer(); + $scope.logs = { option: { id: "summary_yaml" } }; + spyOn(MachinesManager, "getSummaryYAML").and.returnValue( + managerDefer.promise + ); + + defer.resolve(); + $rootScope.$digest(); + managerDefer.resolve(); + $rootScope.$digest(); + + $scope.updateLogOutput(); + expect(MachinesManager.getSummaryYAML).toHaveBeenCalledWith(node); + }); + + it("sets system booting", function() { + makeController(); + var installation_result = makeResult(1, 0); + $scope.installation_results = [installation_result]; + $scope.node = node; + $scope.logs = { option: { id: installation_result.id } }; + + $scope.updateLogOutput(); + expect($scope.logOutput).toEqual("System is booting..."); + }); + + it("sets installation has begun", function() { + makeController(); + var installation_result = makeResult(1, 1); + $scope.installation_results = [installation_result]; + $scope.node = node; + $scope.logs = { option: { id: installation_result.id } }; + + $scope.updateLogOutput(); + expect($scope.logOutput).toEqual("Installation has begun!"); + }); + + it("sets installation output succeeded", function() { + var defer = $q.defer(); + makeController(defer); + var installation_result = makeResult(1, 2); + MachinesManager._activeItem = node; + var manager = NodeResultsManagerFactory.getManager(node); + var managerDefer = $q.defer(); + spyOn(manager, "get_result_data").and.returnValue(managerDefer.promise); + + defer.resolve(); + $rootScope.$digest(); + managerDefer.resolve(); + $rootScope.$digest(); + + $scope.installation_results = [installation_result]; + $scope.logs = { option: { id: installation_result.id } }; + $scope.updateLogOutput(); + expect(manager.get_result_data).toHaveBeenCalledWith( + installation_result.id, + "combined" + ); + }); + + it("sets installation output failed", function() { + var defer = $q.defer(); + makeController(defer); + var installation_result = makeResult(1, 3); + MachinesManager._activeItem = node; + var manager = NodeResultsManagerFactory.getManager(node); + var managerDefer = $q.defer(); + spyOn(manager, "get_result_data").and.returnValue(managerDefer.promise); + + defer.resolve(); + $rootScope.$digest(); + managerDefer.resolve(); + $rootScope.$digest(); + + $scope.installation_results = [installation_result]; + $scope.logs = { option: { id: installation_result.id } }; + $scope.updateLogOutput(); + expect(manager.get_result_data).toHaveBeenCalledWith( + installation_result.id, + "combined" + ); + }); + + it("sets timed out", function() { + makeController(); + var installation_result = makeResult(1, 4); + $scope.installation_results = [installation_result]; + $scope.node = node; + $scope.logs = { option: { id: installation_result.id } }; + + $scope.updateLogOutput(); + expect($scope.logOutput).toEqual("Installation failed after 40 minutes."); + }); + + it("sets installation aborted", function() { + makeController(); + var installation_result = makeResult(1, 5); + $scope.installation_results = [installation_result]; + $scope.node = node; + $scope.logs = { option: { id: installation_result.id } }; + + $scope.updateLogOutput(); + expect($scope.logOutput).toEqual("Installation was aborted."); + }); + + it("sets unknown status", function() { + makeController(); + var installation_result = makeResult(1, makeInteger(6, 100)); + $scope.installation_results = [installation_result]; + $scope.node = node; + $scope.logs = { option: { id: installation_result.id } }; + + $scope.updateLogOutput(); + expect($scope.logOutput).toEqual( + "BUG: Unknown log status " + installation_result.status + ); + }); + + it("sets install id to ScriptResult /tmp/install.log", function() { + var defer = $q.defer(); + var loadItems_defer = $q.defer(); + makeController(loadItems_defer); + $scope.section = { area: "logs" }; + MachinesManager._activeItem = node; + webSocket.returnData.push(makeFakeResponse([])); + var manager = NodeResultsManagerFactory.getManager(node); + spyOn(manager, "loadItems").and.returnValue(defer.promise); + manager.installation_results = []; + var i; + for (i = 0; i < 3; i++) { + manager.installation_results.push(makeResult()); + } + var installation_result = pickItem(manager.installation_results); + installation_result.name = "/tmp/install.log"; + defer.resolve(); + loadItems_defer.resolve(); + $rootScope.$digest(); + expect($scope.logs.availableOptions[0].id, installation_result.id); + }); + }); + + describe("loadHistory", function() { + it("loads results", function() { + var defer = $q.defer(); + makeController(); + var result = { + id: makeInteger(0, 100) + }; + var history_list = [ + { id: makeInteger(0, 100) }, + { id: makeInteger(0, 100) }, + { id: makeInteger(0, 100) } + ]; + $scope.node = node; + $scope.nodeResultsManager = NodeResultsManagerFactory.getManager( + $scope.node + ); + spyOn($scope.nodeResultsManager, "get_history").and.returnValue( + defer.promise + ); + + $scope.loadHistory(result); + defer.resolve(history_list); + $rootScope.$digest(); + + expect(result.history_list).toBe(history_list); + expect(result.loading_history).toBe(false); + expect(result.showing_history).toBe(true); + }); + + it("doesnt reload", function() { + makeController(); + var result = { + id: makeInteger(0, 100), + history_list: [{ id: makeInteger(0, 100) }] + }; + $scope.node = node; + $scope.nodeResultsManager = NodeResultsManagerFactory.getManager( + $scope.node + ); + spyOn($scope.nodeResultsManager, "get_history"); + + $scope.loadHistory(result); + + expect(result.showing_history).toBe(true); + expect($scope.nodeResultsManager.get_history).not.toHaveBeenCalled(); + }); + }); + + describe("hasSuppressedTests", () => { + it("returns whether there are suppressed tests in results", () => { + makeController(); + const suppressedResult = makeResult(1); + $scope.results = [ + { + hardware_type: 1, + results: { + subtype: [ + ...Array.from(Array(3)).map(() => makeResult(1)), + suppressedResult + ] + } + }, + { + hardware_type: 2, + results: { + subtype: Array.from(Array(3)).map(() => makeResult(2)) + } } - if(status === null) { - status = makeInteger(0, 8); + ]; + + expect($scope.hasSuppressedTests()).toEqual(false); + + suppressedResult.suppressed = true; + expect($scope.hasSuppressedTests()).toEqual(true); + }); + }); + + describe("isSuppressible", () => { + it(`returns true if a result's status is + FAILED, FAILED_INSTALLING or TIMEDOUT`, () => { + makeController(); + const results = [ + makeResult(0, ScriptStatus.FAILED), + makeResult(0, ScriptStatus.FAILED_INSTALLING), + makeResult(0, ScriptStatus.TIMEDOUT), + makeResult(0, ScriptStatus.PASSED) + ]; + + expect($scope.isSuppressible(results[0])).toBe(true); + expect($scope.isSuppressible(results[1])).toBe(true); + expect($scope.isSuppressible(results[2])).toBe(true); + expect($scope.isSuppressible(results[3])).toBe(false); + }); + }); + + describe("getSuppressedCount", () => { + it("returns number of suppressed tests in node test results", () => { + makeController(); + const results1 = Array.from(Array(5)).map((e, i) => { + const result = makeResult(1, ScriptStatus.FAILED); + if (i % 2 === 0) { + result.suppressed = true; } - var id = makeInteger(0, 1000); - var result = { - id: id, - name: makeName("name"), - type: type, - status: status, - history_list: [{ - id: id, - status: status - }] - }; - var i; - for(i = 0; i < 3; i++) { - result.history_list.push({ - id: makeInteger(0, 1000), - status: makeInteger(0, 8) - }); + return result; + }); + const results2 = Array.from(Array(5)).map((e, i) => { + const result = makeResult(2, ScriptStatus.FAILED); + if (i % 2 === 0) { + result.suppressed = true; } return result; - } + }); + $scope.results = [ + { + hardware_type: 1, + results: { + subtype: results1 + } + }, + { + hardware_type: 2, + results: { + subtype: results2 + } + } + ]; - // Makes the NodeResultsController - function makeController(loadManagerDefer) { - var loadManager = spyOn(ManagerHelperService, "loadManager"); - if(angular.isObject(loadManagerDefer)) { - loadManager.and.returnValue(loadManagerDefer.promise); - } else { - loadManager.and.returnValue($q.defer().promise); + expect($scope.getSuppressedCount()).toEqual(6); + }); + + it("returns 'All' if all suppressible tests are suppressed", () => { + makeController(); + const results = Array.from(Array(5)).map((e, i) => { + const result = makeResult(2, ScriptStatus.FAILED); + result.suppressed = true; + return result; + }); + $scope.results = [ + { + hardware_type: 1, + results: { + subtype: results + } } - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - return $controller("NodeResultsController", { - $scope: $scope, - $routeParams: $routeParams, - MachinesManager: MachinesManager, - ControllersManager: ControllersManager, - NodeResultsManagerFactory: NodeResultsManagerFactory, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); - } + ]; - // Create the node that will be used and set the routeParams. - var node, $routeParams; - beforeEach(function() { - node = makeNode(); - $routeParams = { - system_id: node.system_id - }; - }); - - it("sets the initial $scope values", function() { - makeController(); - expect($scope.commissioning_results).toBeNull(); - expect($scope.testing_results).toBeNull(); - expect($scope.installation_results).toBeNull(); - expect($scope.results).toBeNull(); - expect($scope.logs.option).toBeNull(); - expect($scope.logs.availableOptions).toEqual([]); - expect($scope.logOutput).toEqual("Loading..."); - expect($scope.loaded).toBe(false); - expect($scope.resultsLoaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.nodesManager).toBe(MachinesManager); - }); - - it("sets the initial $scope values when controller", function() { - $location.path('/controller'); - makeController(); - expect($scope.commissioning_results).toBeNull(); - expect($scope.testing_results).toBeNull(); - expect($scope.installation_results).toBeNull(); - expect($scope.results).toBeNull(); - expect($scope.logs.option).toBeNull(); - expect($scope.logs.availableOptions).toEqual([]); - expect($scope.logOutput).toEqual("Loading..."); - expect($scope.loaded).toBe(false); - expect($scope.resultsLoaded).toBe(false); - expect($scope.node).toBeNull(); - expect($scope.nodesManager).toBe(ControllersManager); - }); - - it("calls loadManager with MachinesManager", function() { - makeController(); - expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( - $scope, MachinesManager); - }); - - it("doesnt call setActiveItem if node already loaded", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - spyOn(MachinesManager, "setActiveItem"); - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect(MachinesManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if node not loaded", function() { - var defer = $q.defer(); - makeController(defer); - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - - setActiveDefer.resolve(node); - $rootScope.$digest(); - - expect($scope.node).toBe(node); - expect(MachinesManager.setActiveItem).toHaveBeenCalledWith( - node.system_id); - }); - - it("calls raiseError if setActiveItem is rejected", function() { - var defer = $q.defer(); - makeController(defer); - var setActiveDefer = $q.defer(); - spyOn(MachinesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - spyOn(ErrorService, "raiseError"); - - defer.resolve(); - $rootScope.$digest(); - - var error = makeName("error"); - setActiveDefer.reject(error); - $rootScope.$digest(); - - expect(ErrorService.raiseError).toHaveBeenCalledWith(error); - }); - - it("calls loadItems on the results manager", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = NodeResultsManagerFactory.getManager(node); - spyOn(manager, "loadItems").and.returnValue($q.defer().promise); - - defer.resolve(); - $rootScope.$digest(); - expect(manager.loadItems).toHaveBeenCalled(); - }); - - it("sets eventsLoaded once events manager loadItems resolves", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = NodeResultsManagerFactory.getManager(node); - var loadDefer = $q.defer(); - spyOn(manager, "loadItems").and.returnValue(loadDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - loadDefer.resolve(); - $rootScope.$digest(); - expect($scope.resultsLoaded).toBe(true); - }); - - it("sets results once events manager loadItems resolves", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = NodeResultsManagerFactory.getManager(node); - var loadDefer = $q.defer(); - spyOn(manager, "loadItems").and.returnValue(loadDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - loadDefer.resolve(); - $rootScope.$digest(); - expect($scope.resultsLoaded).toBe(true); - }); - - describe("updateLogs", function() { - it("only runs on logs page", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var manager = NodeResultsManagerFactory.getManager(node); - var loadDefer = $q.defer(); - - defer.resolve(); - $rootScope.$digest(); - loadDefer.resolve(); - $rootScope.$digest(); - expect($scope.logs.availableOptions).toEqual([]); - }); - - it("loads summary", function() { - var defer = $q.defer(); - makeController(defer); - $scope.section = {area: "logs"}; - MachinesManager._activeItem = node; - webSocket.returnData.push(makeFakeResponse([])); - var manager = NodeResultsManagerFactory.getManager(node); - - defer.resolve(); - $rootScope.$digest(); - var expectFunc; - expectFunc = function() { - if($scope.resultsLoaded) { - expect($scope.logs.availableOptions).toEqual([ - { - title: 'Machine output (YAML)', - id: 'summary_yaml' - }, - { - title: 'Machine output (XML)', - id: 'summary_xml' - } - ]); - expect($scope.logs.option).toEqual({ - title: 'Machine output (YAML)', - id: 'summary_yaml' - }); - }else{ - setTimeout(expectFunc); - } - }; - setTimeout(expectFunc); - }); - }); - - describe("updateLogOutput", function() { - it("sets to loading when no node", function() { - makeController(); - $scope.updateLogOutput(); - expect($scope.logOutput).toEqual("Loading..."); - }); - - it("sets summary xml", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var managerDefer = $q.defer(); - $scope.logs = {option: {id: "summary_xml"}}; - spyOn(MachinesManager, "getSummaryXML").and.returnValue( - managerDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - managerDefer.resolve(); - $rootScope.$digest(); - - $scope.updateLogOutput(); - expect(MachinesManager.getSummaryXML).toHaveBeenCalledWith(node); - }); - - it("sets summary yaml", function() { - var defer = $q.defer(); - makeController(defer); - MachinesManager._activeItem = node; - var managerDefer = $q.defer(); - $scope.logs = {option: {id: "summary_yaml"}}; - spyOn(MachinesManager, "getSummaryYAML").and.returnValue( - managerDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - managerDefer.resolve(); - $rootScope.$digest(); - - $scope.updateLogOutput(); - expect(MachinesManager.getSummaryYAML).toHaveBeenCalledWith(node); - }); - - it("sets system booting", function() { - makeController(); - var installation_result = makeResult(1, 0); - $scope.installation_results = [installation_result]; - $scope.node = node; - $scope.logs = {option: {id: installation_result.id}}; - - $scope.updateLogOutput(); - expect($scope.logOutput).toEqual("System is booting..."); - }); - - it("sets installation has begun", function() { - makeController(); - var installation_result = makeResult(1, 1); - $scope.installation_results = [installation_result]; - $scope.node = node; - $scope.logs = {option: {id: installation_result.id}}; - - $scope.updateLogOutput(); - expect($scope.logOutput).toEqual("Installation has begun!"); - }); - - it("sets installation output succeeded", function() { - var defer = $q.defer(); - makeController(defer); - var installation_result = makeResult(1, 2); - MachinesManager._activeItem = node; - var manager = NodeResultsManagerFactory.getManager(node); - var managerDefer = $q.defer(); - spyOn(manager, "get_result_data").and.returnValue( - managerDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - managerDefer.resolve(); - $rootScope.$digest(); - - $scope.installation_results = [installation_result]; - $scope.logs = {option: {id: installation_result.id}}; - $scope.updateLogOutput(); - expect(manager.get_result_data).toHaveBeenCalledWith( - installation_result.id, 'combined'); - }); - - it("sets installation output failed", function() { - var defer = $q.defer(); - makeController(defer); - var installation_result = makeResult(1, 3); - MachinesManager._activeItem = node; - var manager = NodeResultsManagerFactory.getManager(node); - var managerDefer = $q.defer(); - spyOn(manager, "get_result_data").and.returnValue( - managerDefer.promise); - - defer.resolve(); - $rootScope.$digest(); - managerDefer.resolve(); - $rootScope.$digest(); - - $scope.installation_results = [installation_result]; - $scope.logs = {option: {id: installation_result.id}}; - $scope.updateLogOutput(); - expect(manager.get_result_data).toHaveBeenCalledWith( - installation_result.id, 'combined'); - }); - - it("sets timed out", function() { - makeController(); - var installation_result = makeResult(1, 4); - $scope.installation_results = [installation_result]; - $scope.node = node; - $scope.logs = {option: {id: installation_result.id}}; - - $scope.updateLogOutput(); - expect($scope.logOutput).toEqual( - "Installation failed after 40 minutes."); - }); - - it("sets installation aborted", function() { - makeController(); - var installation_result = makeResult(1, 5); - $scope.installation_results = [installation_result]; - $scope.node = node; - $scope.logs = {option: {id: installation_result.id}}; - - $scope.updateLogOutput(); - expect($scope.logOutput).toEqual("Installation was aborted."); - }); - - it("sets unknown status", function() { - makeController(); - var installation_result = makeResult(1, makeInteger(6, 100)); - $scope.installation_results = [installation_result]; - $scope.node = node; - $scope.logs = {option: {id: installation_result.id}}; - - $scope.updateLogOutput(); - expect($scope.logOutput).toEqual( - "BUG: Unknown log status " + installation_result.status); - }); - - it("sets install id to ScriptResult /tmp/install.log", function() { - var defer = $q.defer(); - var loadItems_defer = $q.defer(); - makeController(loadItems_defer); - $scope.section = {area: "logs"}; - MachinesManager._activeItem = node; - webSocket.returnData.push(makeFakeResponse([])); - var manager = NodeResultsManagerFactory.getManager(node); - spyOn(manager, "loadItems").and.returnValue(defer.promise); - manager.installation_results = []; - var i; - for(i = 0; i < 3; i ++) { - manager.installation_results.push(makeResult()); - } - var installation_result = pickItem(manager.installation_results); - installation_result.name = '/tmp/install.log'; - defer.resolve(); - loadItems_defer.resolve(); - $rootScope.$digest(); - expect($scope.logs.availableOptions[0].id, installation_result.id); - }); - }); - - describe("loadHistory", function() { - it("loads results", function() { - var defer = $q.defer(); - makeController(); - var result = { - id: makeInteger(0, 100) - }; - var history_list = [ - {id: makeInteger(0, 100)}, - {id: makeInteger(0, 100)}, - {id: makeInteger(0, 100)} - ]; - $scope.node = node; - $scope.nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - spyOn($scope.nodeResultsManager, "get_history").and.returnValue( - defer.promise); - - $scope.loadHistory(result); - defer.resolve(history_list); - $rootScope.$digest(); - - expect(result.history_list).toBe(history_list); - expect(result.loading_history).toBe(false); - expect(result.showing_history).toBe(true); - }); - - it("doesnt reload", function() { - makeController(); - var result = { - id: makeInteger(0, 100), - history_list: [{id: makeInteger(0, 100)}] - }; - $scope.node = node; - $scope.nodeResultsManager = NodeResultsManagerFactory.getManager( - $scope.node); - spyOn($scope.nodeResultsManager, "get_history"); - - $scope.loadHistory(result); - - expect(result.showing_history).toBe(true); - expect( - $scope.nodeResultsManager.get_history).not.toHaveBeenCalled(); - }); + expect($scope.getSuppressedCount()).toEqual("All"); + }); + }); + + describe("toggleSuppressed", () => { + it("calls suppressTests manager method if test not suppressed", () => { + makeController(); + const result = makeResult(); + $scope.node = node; + spyOn($scope.nodesManager, "suppressTests"); + + $scope.toggleSuppressed(result); + + expect($scope.nodesManager.suppressTests).toHaveBeenCalledWith( + $scope.node, + [result] + ); + }); + + it("calls unsuppressTests manager method if test suppressed", () => { + makeController(); + const result = makeResult(); + result.suppressed = true; + $scope.node = node; + spyOn($scope.nodesManager, "unsuppressTests"); + + $scope.toggleSuppressed(result); + + expect($scope.nodesManager.unsuppressTests).toHaveBeenCalledWith( + $scope.node, + [result] + ); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_nodes_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,2166 +4,2149 @@ * Unit tests for NodesListController. */ +import { makeInteger, makeName } from "testing/utils"; + // Make a fake user. var userId = 0; function makeUser() { - return { - id: userId++, - username: makeName("username"), - first_name: makeName("first_name"), - last_name: makeName("last_name"), - email: makeName("email"), - is_superuser: false, - sshkeys_count: 0 - }; + return { + id: userId++, + username: makeName("username"), + first_name: makeName("first_name"), + last_name: makeName("last_name"), + email: makeName("email"), + is_superuser: false, + sshkeys_count: 0 + }; } -// Global MAAS_config; -MAAS_config = {}; - describe("NodesListController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q, $routeParams, $location; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load the required managers. - var MachinesManager, DevicesManager, ControllersManager, GeneralManager, - SwitchesManager, ZonesManager, UsersManager, ServicesManage, - ResourcePoolsManager, TagsManager; - var ManagerHelperService, SearchService; - var ScriptsManager, VLANsManager; - beforeEach(inject(function($injector) { - MachinesManager = $injector.get("MachinesManager"); - DevicesManager = $injector.get("DevicesManager"); - ControllersManager = $injector.get("ControllersManager"); - GeneralManager = $injector.get("GeneralManager"); - ZonesManager = $injector.get("ZonesManager"); - UsersManager = $injector.get("UsersManager"); - ServicesManager = $injector.get("ServicesManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - SearchService = $injector.get("SearchService"); - ScriptsManager = $injector.get("ScriptsManager"); - VLANsManager = $injector.get("VLANsManager"); - SwitchesManager = $injector.get("SwitchesManager"); - ResourcePoolsManager = $injector.get("ResourcePoolsManager"); - TagsManager = $injector.get("TagsManager"); - })); - - // Mock the websocket connection to the region - var RegionConnection, webSocket; - beforeEach(inject(function($injector) { - RegionConnection = $injector.get("RegionConnection"); - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Makes the NodesListController - function makeController(loadManagersDefer, defaultConnectDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - var defaultConnect = spyOn(RegionConnection, "defaultConnect"); - if(angular.isObject(defaultConnectDefer)) { - defaultConnect.and.returnValue(defaultConnectDefer.promise); - } else { - defaultConnect.and.returnValue($q.defer().promise); - } - - if($location.path() === '') { - $location.path("/machines"); - } - - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - // Create the controller. - var controller = $controller("NodesListController", { - $q: $q, - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - MachinesManager: MachinesManager, - DevicesManager: DevicesManager, - ControllersManager: ControllersManager, - GeneralManager: GeneralManager, - ZonesManager: ZonesManager, - UsersManager: UsersManager, - ServicesManager: ServicesManager, - ManagerHelperService: ManagerHelperService, - SearchService: SearchService, - ScriptsManager: ScriptsManager, - SwitchesManager: SwitchesManager - }); - - // Since the osSelection directive is not used in this test the - // osSelection item on the model needs to have $reset function added - // because it will be called throughout many of the tests. - $scope.tabs.machines.osSelection.$reset = jasmine.createSpy("$reset"); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q, $routeParams, $location; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load the required managers. + var MachinesManager, + DevicesManager, + ControllersManager, + GeneralManager, + SwitchesManager, + ZonesManager, + UsersManager, + ServicesManager, + ResourcePoolsManager, + TagsManager; + var ManagerHelperService, SearchService; + var ScriptsManager, VLANsManager; + beforeEach(inject(function($injector) { + MachinesManager = $injector.get("MachinesManager"); + DevicesManager = $injector.get("DevicesManager"); + ControllersManager = $injector.get("ControllersManager"); + GeneralManager = $injector.get("GeneralManager"); + ZonesManager = $injector.get("ZonesManager"); + UsersManager = $injector.get("UsersManager"); + ServicesManager = $injector.get("ServicesManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + SearchService = $injector.get("SearchService"); + ScriptsManager = $injector.get("ScriptsManager"); + VLANsManager = $injector.get("VLANsManager"); + SwitchesManager = $injector.get("SwitchesManager"); + ResourcePoolsManager = $injector.get("ResourcePoolsManager"); + TagsManager = $injector.get("TagsManager"); + })); + + // Mock the websocket connection to the region + var RegionConnection, webSocket; + beforeEach(inject(function($injector) { + RegionConnection = $injector.get("RegionConnection"); + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Makes the NodesListController + function makeController(loadManagersDefer, defaultConnectDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - // Makes a fake node/device. - function makeObject(tab) { - if (tab === 'machines') { - var node = { - system_id: makeName("system_id"), - $selected: false - }; - MachinesManager._items.push(node); - return node; - } - else if (tab === 'devices') { - var device = { - system_id: makeName("system_id"), - $selected: false - }; - DevicesManager._items.push(device); - return device; - } - else if (tab === 'controllers') { - var controller = { - system_id: makeName("system_id"), - $selected: false - }; - ControllersManager._items.push(controller); - return controller; - } - else if (tab === 'switches') { - var network_switch = { - system_id: makeName("system_id"), - $selected: false - }; - SwitchesManager._items.push(network_switch); - return network_switch; - } - return null; + var defaultConnect = spyOn(RegionConnection, "defaultConnect"); + if (angular.isObject(defaultConnectDefer)) { + defaultConnect.and.returnValue(defaultConnectDefer.promise); + } else { + defaultConnect.and.returnValue($q.defer().promise); } - describe("isSuperUser", function() { - it("returns true if the user is a superuser", function() { - makeController(); - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: true }); - expect($scope.isSuperUser()).toBe(true); - }); - - it("returns false if the user is not a superuser", function() { - makeController(); - spyOn(UsersManager, "getAuthUser").and.returnValue( - { is_superuser: false }); - expect($scope.isSuperUser()).toBe(false); - }); - }); - - describe("canAddMachine", function() { - it("calls hasGlobalPermission with machine_create", function() { - makeController(); - spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); - expect($scope.canAddMachine()).toBe(true); - expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( - 'machine_create'); - }); - }); - - describe("canCreateResourcePool", function() { - it("calls hasGlobalPermission with resource_pool_create", function() { - makeController(); - spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); - expect($scope.canCreateResourcePool()).toBe(true); - expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( - 'resource_pool_create'); - }); - }); - - describe("showResourcePoolActions", function() { - it("returns false if no permissions on any pool", function() { - makeController(); - $scope.pools = [ - { - permissions: [] - }, - {} - ]; - expect($scope.showResourcePoolActions()).toBe(false); - }); - - it("returns true if permissions on any pool", function() { - makeController(); - $scope.pools = [ - { - permissions: [] - }, - { - permissions: ['edit'] - }, - ]; - expect($scope.showResourcePoolActions()).toBe(true); - }); - }); - - describe("canEditResourcePool", function() { - it("returns false if not permissions on pool", function() { - makeController(); - var pool = {}; - expect($scope.canEditResourcePool(pool)).toBe(false); - }); - - it("returns false if no edit permission", function() { - makeController(); - var pool = { - permissions: ['delete'] - }; - expect($scope.canEditResourcePool(pool)).toBe(false); - }); + if ($location.path() === "") { + $location.path("/machines"); + } - it("returns true if edit permission", function() { - makeController(); - var pool = { - permissions: ['edit'] - }; - expect($scope.canEditResourcePool(pool)).toBe(true); - }); - }); + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + // Create the controller. + var controller = $controller("NodesListController", { + $q: $q, + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + MachinesManager: MachinesManager, + DevicesManager: DevicesManager, + ControllersManager: ControllersManager, + GeneralManager: GeneralManager, + ZonesManager: ZonesManager, + UsersManager: UsersManager, + ServicesManager: ServicesManager, + ManagerHelperService: ManagerHelperService, + SearchService: SearchService, + ScriptsManager: ScriptsManager, + SwitchesManager: SwitchesManager + }); + + // Since the osSelection directive is not used in this test the + // osSelection item on the model needs to have $reset function added + // because it will be called throughout many of the tests. + $scope.tabs.machines.osSelection.$reset = jasmine.createSpy("$reset"); + + return controller; + } + + // Makes a fake node/device. + function makeObject(tab) { + if (tab === "machines") { + var node = { + system_id: makeName("system_id"), + $selected: false + }; + MachinesManager._items.push(node); + return node; + } else if (tab === "devices") { + var device = { + system_id: makeName("system_id"), + $selected: false + }; + DevicesManager._items.push(device); + return device; + } else if (tab === "controllers") { + var controller = { + system_id: makeName("system_id"), + $selected: false + }; + ControllersManager._items.push(controller); + return controller; + } else if (tab === "switches") { + var network_switch = { + system_id: makeName("system_id"), + $selected: false + }; + SwitchesManager._items.push(network_switch); + return network_switch; + } + return null; + } - describe("canDeleteResourcePool", function() { - it("calls hasGlobalPermission with resource_pool_delete", function() { - makeController(); - spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); - expect($scope.canDeleteResourcePool()).toBe(true); - expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( - 'resource_pool_delete'); - }); + describe("isSuperUser", function() { + it("returns true if the user is a superuser", function() { + makeController(); + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: true + }); + expect($scope.isSuperUser()).toBe(true); + }); + + it("returns false if the user is not a superuser", function() { + makeController(); + spyOn(UsersManager, "getAuthUser").and.returnValue({ + is_superuser: false + }); + expect($scope.isSuperUser()).toBe(false); + }); + }); + + describe("canAddMachine", function() { + it("calls hasGlobalPermission with machine_create", function() { + makeController(); + spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); + expect($scope.canAddMachine()).toBe(true); + expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( + "machine_create" + ); + }); + }); + + describe("canCreateResourcePool", function() { + it("calls hasGlobalPermission with resource_pool_create", function() { + makeController(); + spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); + expect($scope.canCreateResourcePool()).toBe(true); + expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( + "resource_pool_create" + ); + }); + }); + + describe("showResourcePoolActions", function() { + it("returns false if no permissions on any pool", function() { + makeController(); + $scope.pools = [ + { + permissions: [] + }, + {} + ]; + expect($scope.showResourcePoolActions()).toBe(false); + }); + + it("returns true if permissions on any pool", function() { + makeController(); + $scope.pools = [ + { + permissions: [] + }, + { + permissions: ["edit"] + } + ]; + expect($scope.showResourcePoolActions()).toBe(true); }); + }); - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Machines"); - expect($rootScope.page).toBe("machines"); - }); + describe("canEditResourcePool", function() { + it("returns false if not permissions on pool", function() { + makeController(); + var pool = {}; + expect($scope.canEditResourcePool(pool)).toBe(false); + }); + + it("returns false if no edit permission", function() { + makeController(); + var pool = { + permissions: ["delete"] + }; + expect($scope.canEditResourcePool(pool)).toBe(false); + }); + + it("returns true if edit permission", function() { + makeController(); + var pool = { + permissions: ["edit"] + }; + expect($scope.canEditResourcePool(pool)).toBe(true); + }); + }); + + describe("canDeleteResourcePool", function() { + it("calls hasGlobalPermission with resource_pool_delete", function() { + makeController(); + spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); + expect($scope.canDeleteResourcePool()).toBe(true); + expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( + "resource_pool_delete" + ); + }); + }); + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Machines"); + expect($rootScope.page).toBe("machines"); + }); + + it("sets initial values on $scope", function() { + // tab-independent variables. + makeController(); + expect($scope.machines).toBe(MachinesManager.getItems()); + expect($scope.devices).toBe(DevicesManager.getItems()); + expect($scope.pools).toBe(ResourcePoolsManager.getItems()); + expect($scope.controllers).toBe(ControllersManager.getItems()); + expect($scope.osinfo).toBe(GeneralManager.getData("osinfo")); + expect($scope.addHardwareOption).toBeNull(); + expect($scope.addHardwareOptions).toEqual([ + { + name: "machine", + title: "Machine" + }, + { + name: "chassis", + title: "Chassis" + } + ]); + expect($scope.addHardwareScope).toBeNull(); + expect($scope.loading).toBe(true); + }); + + it(`saves current filters for nodes and + devices when scope destroyed`, function() { + makeController(); + var nodesFilters = {}; + var devicesFilters = {}; + var controllersFilters = {}; + var switchesFilters = {}; + $scope.tabs.machines.filters = nodesFilters; + $scope.tabs.devices.filters = devicesFilters; + $scope.tabs.controllers.filters = controllersFilters; + $scope.tabs.switches.filters = switchesFilters; + $scope.$destroy(); + expect(SearchService.retrieveFilters("machines")).toBe(nodesFilters); + expect(SearchService.retrieveFilters("devices")).toBe(devicesFilters); + expect(SearchService.retrieveFilters("controllers")).toBe( + controllersFilters + ); + expect(SearchService.retrieveFilters("switches")).toBe(switchesFilters); + }); + + angular.forEach(["machines", "devices", "controllers", "switches"], function( + node_type + ) { + it("calls loadManagers for " + node_type, function() { + $location.path("/" + node_type); + makeController(); + var page_managers = [$scope.tabs[node_type].manager]; + if ( + $scope.currentpage === "machines" || + $scope.currentpage === "controllers" + ) { + page_managers.push(ScriptsManager); + } + if ($scope.currentpage === "controllers") { + page_managers.push(VLANsManager); + } + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( + $scope, + page_managers.concat([ + GeneralManager, + ZonesManager, + UsersManager, + ResourcePoolsManager, + ServicesManager, + TagsManager + ]) + ); + }); + }); + + it("sets loading to false with loadManagers resolves", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.loading).toBe(false); + }); + + it("sets nodes search from SearchService", function() { + var query = makeName("query"); + SearchService.storeFilters( + "machines", + SearchService.getCurrentFilters(query) + ); + makeController(); + expect($scope.tabs.machines.search).toBe(query); + }); + + it("sets devices search from SearchService", function() { + var query = makeName("query"); + SearchService.storeFilters( + "devices", + SearchService.getCurrentFilters(query) + ); + makeController(); + expect($scope.tabs.devices.search).toBe(query); + }); + + it("sets controllers search from SearchService", function() { + var query = makeName("query"); + SearchService.storeFilters( + "controllers", + SearchService.getCurrentFilters(query) + ); + makeController(); + expect($scope.tabs.controllers.search).toBe(query); + }); + + it("sets switches search from SearchService", function() { + var query = makeName("query"); + SearchService.storeFilters( + "switches", + SearchService.getCurrentFilters(query) + ); + makeController(); + expect($scope.tabs.switches.search).toBe(query); + }); + + it("sets nodes search from $routeParams.query", function() { + var query = makeName("query"); + $routeParams.query = query; + makeController(); + expect($scope.tabs.machines.search).toBe(query); + }); + + it(`calls updateFilters for nodes if search + from $routeParams.query`, function() { + var query = makeName("query"); + $routeParams.query = query; + makeController(); + expect($scope.tabs.machines.filters._).toEqual([query]); + }); + + it("reloads osinfo on route update", function() { + makeController(); + spyOn(GeneralManager, "loadItems").and.returnValue($q.defer().promise); + $scope.$emit("$routeUpdate"); + expect(GeneralManager.loadItems).toHaveBeenCalledWith(["osinfo"]); + }); + + describe("toggleTab", function() { + it("sets $rootScope.title", function() { + makeController(); + $scope.toggleTab("devices"); + expect($rootScope.title).toBe($scope.tabs.devices.pagetitle); + $scope.toggleTab("machines"); + expect($rootScope.title).toBe($scope.tabs.machines.pagetitle); + $scope.toggleTab("switches"); + expect($rootScope.title).toBe($scope.tabs.switches.pagetitle); + }); + + it("sets currentpage and $rootScope.page", function() { + makeController(); + $scope.toggleTab("devices"); + expect($scope.currentpage).toBe("devices"); + expect($rootScope.page).toBe("devices"); + $scope.toggleTab("machines"); + expect($scope.currentpage).toBe("machines"); + expect($rootScope.page).toBe("machines"); + $scope.toggleTab("switches"); + expect($scope.currentpage).toBe("switches"); + expect($rootScope.page).toBe("switches"); + }); + }); + + angular.forEach(["machines", "devices", "controllers", "switches"], function( + tab + ) { + describe("tab(" + tab + ")", function() { + it("sets initial values on $scope", function() { + // Only controllers tab uses the registerUrl and + // registerSecret. Set the values before the controller is + // created. The create will pull the values into the scope. + var registerUrl, registerSecret; + if (tab === "controllers") { + registerUrl = makeName("url"); + registerSecret = makeName("secret"); + MAAS_config = { + register_url: registerUrl, + register_secret: registerSecret + }; + } - it("sets initial values on $scope", function() { - // tab-independent variables. makeController(); - expect($scope.machines).toBe(MachinesManager.getItems()); - expect($scope.devices).toBe(DevicesManager.getItems()); - expect($scope.pools).toBe(ResourcePoolsManager.getItems()); - expect($scope.controllers).toBe(ControllersManager.getItems()); - expect($scope.osinfo).toBe(GeneralManager.getData("osinfo")); - expect($scope.addHardwareOption).toBeNull(); - expect($scope.addHardwareOptions).toEqual([ - { - name: "machine", - title: "Machine" - }, - { - name: "chassis", - title: "Chassis" - } - ]); - expect($scope.addHardwareScope).toBeNull(); - expect($scope.loading).toBe(true); - }); + var tabScope = $scope.tabs[tab]; + expect(tabScope.previous_search).toBe(""); + expect(tabScope.search).toBe(""); + expect(tabScope.searchValid).toBe(true); + expect(tabScope.selectedItems).toBe( + tabScope.manager.getSelectedItems() + ); + expect(tabScope.metadata).toBe(tabScope.manager.getMetadata()); + expect(tabScope.filters).toEqual(SearchService.getEmptyFilter()); + expect(tabScope.actionOption).toBeNull(); + + // Only devices and controllers use the sorting and column + // as the nodes tab uses the maas-machines-table directive. + if (tab !== "machines" && tab !== "switches") { + expect(tabScope.filtered_items).toEqual([]); + expect(tabScope.predicate).toBe("fqdn"); + expect(tabScope.allViewableChecked).toBe(false); + expect(tabScope.column).toBe("fqdn"); + } - it("saves current filters for nodes and devices when scope destroyed", - function() { - makeController(); - var nodesFilters = {}; - var devicesFilters = {}; - var controllersFilters = {}; - var switchesFilters = {}; - $scope.tabs.machines.filters = nodesFilters; - $scope.tabs.devices.filters = devicesFilters; - $scope.tabs.controllers.filters = controllersFilters; - $scope.tabs.switches.filters = switchesFilters; - $scope.$destroy(); - expect(SearchService.retrieveFilters("machines")).toBe( - nodesFilters); - expect(SearchService.retrieveFilters("devices")).toBe( - devicesFilters); - expect(SearchService.retrieveFilters("controllers")).toBe( - controllersFilters); - expect(SearchService.retrieveFilters("switches")).toBe( - switchesFilters); - }); - - angular.forEach( - ["machines", "devices", "controllers", "switches"], - function(node_type) { - it("calls loadManagers for " + node_type, function() { - $location.path("/" + node_type); - makeController(); - var page_managers = [$scope.tabs[node_type].manager]; - if($scope.currentpage === "machines" || - $scope.currentpage === "controllers") { - page_managers.push(ScriptsManager); - } - if($scope.currentpage === "controllers") { - page_managers.push(VLANsManager); - } - expect( - ManagerHelperService.loadManagers - ).toHaveBeenCalledWith( - $scope, page_managers.concat([ - GeneralManager, ZonesManager, UsersManager, - ResourcePoolsManager, - ServicesManager, TagsManager])); - }); - }); + // The controllers page uses a function so it can handle + // different controller types + if (tab !== "controllers") { + expect(tabScope.takeActionOptions).toEqual([]); + } + expect(tabScope.actionErrorCount).toBe(0); + expect(tabScope.zoneSelection).toBeNull(); + expect(tabScope.poolSelection).toBeNull(); + + // Only the nodes tab uses the osSelection and + // commissionOptions fields. + if (tab === "machines" || tab === "switches") { + expect(tabScope.osSelection.osystem).toBeNull(); + expect(tabScope.osSelection.release).toBeNull(); + expect(tabScope.commissionOptions).toEqual({ + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }); + expect(tabScope.commissioningSelection).toEqual([]); + expect(tabScope.releaseOptions).toEqual({}); + } - it("sets loading to false with loadManagers resolves", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.loading).toBe(false); + // Only controllers tab uses the registerUrl and + // registerSecret. + if (tab === "controllers") { + expect(tabScope.registerUrl).toBe(registerUrl); + expect(tabScope.registerSecret).toBe(registerSecret); + } + }); }); + }); - it("sets nodes search from SearchService", - function() { - var query = makeName("query"); - SearchService.storeFilters( - "machines", SearchService.getCurrentFilters(query)); - makeController(); - expect($scope.tabs.machines.search).toBe(query); - }); - - it("sets devices search from SearchService", - function() { - var query = makeName("query"); - SearchService.storeFilters( - "devices", SearchService.getCurrentFilters(query)); - makeController(); - expect($scope.tabs.devices.search).toBe(query); - }); - - it("sets controllers search from SearchService", - function() { - var query = makeName("query"); - SearchService.storeFilters( - "controllers", SearchService.getCurrentFilters(query)); - makeController(); - expect($scope.tabs.controllers.search).toBe(query); - }); - - it("sets switches search from SearchService", - function() { - var query = makeName("query"); - SearchService.storeFilters( - "switches", SearchService.getCurrentFilters(query)); - makeController(); - expect($scope.tabs.switches.search).toBe(query); - }); - - it("sets nodes search from $routeParams.query", - function() { - var query = makeName("query"); - $routeParams.query = query; - makeController(); - expect($scope.tabs.machines.search).toBe(query); - }); - - it("calls updateFilters for nodes if search from $routeParams.query", - function() { - var query = makeName("query"); - $routeParams.query = query; - makeController(); - expect($scope.tabs.machines.filters._).toEqual([query]); - }); - - it("reloads osinfo on route update", function() { + angular.forEach(["machines", "devices", "controllers", "switches"], function( + tab + ) { + describe("tab(" + tab + ")", function() { + it(`resets search matches previous search + and empty filtered_items`, function() { makeController(); - spyOn(GeneralManager, "loadItems").and.returnValue( - $q.defer().promise); - $scope.$emit("$routeUpdate"); - expect(GeneralManager.loadItems).toHaveBeenCalledWith(["osinfo"]); - }); - - describe("toggleTab", function() { + var tabScope = $scope.tabs[tab]; + var search = makeName("search"); - it("sets $rootScope.title", function() { - makeController(); - $scope.toggleTab('devices'); - expect($rootScope.title).toBe($scope.tabs.devices.pagetitle); - $scope.toggleTab('machines'); - expect($rootScope.title).toBe($scope.tabs.machines.pagetitle); - $scope.toggleTab('switches'); - expect($rootScope.title).toBe($scope.tabs.switches.pagetitle); - }); + if (tab === "machines" || tab === "switches") { + // Nodes uses the maas-machines-table directive, so + // the interaction is a little different. + tabScope.search = "in:(Selected)"; + tabScope.previous_search = search; + $scope.onNodeListingChanged([makeObject(tab)], tab); + + // Empty the listing search should be reset. + tabScope.search = search; + $scope.onNodeListingChanged([], tab); + expect(tabScope.search).toBe(""); + } else { + // Add item to filtered_items. + tabScope.filtered_items.push(makeObject(tab)); + tabScope.search = "in:(Selected)"; + tabScope.previous_search = search; + $scope.$digest(); + + // Empty the filtered_items, which should clear the + // search. + tabScope.filtered_items.splice(0, 1); + tabScope.search = search; + $scope.$digest(); + expect(tabScope.search).toBe(""); + } + }); - it("sets currentpage and $rootScope.page", function() { - makeController(); - $scope.toggleTab('devices'); - expect($scope.currentpage).toBe('devices'); - expect($rootScope.page).toBe('devices'); - $scope.toggleTab('machines'); - expect($scope.currentpage).toBe('machines'); - expect($rootScope.page).toBe('machines'); - $scope.toggleTab('switches'); - expect($scope.currentpage).toBe('switches'); - expect($rootScope.page).toBe('switches'); - }); - }); - - angular.forEach(["machines", "devices", "controllers", "switches"], - function(tab) { - - describe("tab(" + tab + ")", function() { - - it("sets initial values on $scope", function() { - // Only controllers tab uses the registerUrl and - // registerSecret. Set the values before the controller is - // created. The create will pull the values into the scope. - var registerUrl, registerSecret; - if(tab === "controllers") { - registerUrl = makeName("url"); - registerSecret = makeName("secret"); - MAAS_config = { - register_url: registerUrl, - register_secret: registerSecret - }; - } - - makeController(); - var tabScope = $scope.tabs[tab]; - expect(tabScope.previous_search).toBe(""); - expect(tabScope.search).toBe(""); - expect(tabScope.searchValid).toBe(true); - expect(tabScope.selectedItems).toBe( - tabScope.manager.getSelectedItems()); - expect(tabScope.metadata).toBe(tabScope.manager.getMetadata()); - expect(tabScope.filters).toEqual( - SearchService.getEmptyFilter()); - expect(tabScope.actionOption).toBeNull(); - - // Only devices and controllers use the sorting and column - // as the nodes tab uses the maas-machines-table directive. - if(tab !== "machines" && tab !== "switches") { - expect(tabScope.filtered_items).toEqual([]); - expect(tabScope.predicate).toBe("fqdn"); - expect(tabScope.allViewableChecked).toBe(false); - expect(tabScope.column).toBe("fqdn"); - } - - // The controllers page uses a function so it can handle - // different controller types - if(tab !== "controllers") { - expect(tabScope.takeActionOptions).toEqual([]); - } - expect(tabScope.actionErrorCount).toBe(0); - expect(tabScope.zoneSelection).toBeNull(); - expect(tabScope.poolSelection).toBeNull(); - - // Only the nodes tab uses the osSelection and - // commissionOptions fields. - if(tab === "machines" || tab === "switches") { - expect(tabScope.osSelection.osystem).toBeNull(); - expect(tabScope.osSelection.release).toBeNull(); - expect(tabScope.commissionOptions).toEqual({ - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }); - expect(tabScope.commissioningSelection).toEqual([]); - expect(tabScope.releaseOptions).toEqual({}); - } - - // Only controllers tab uses the registerUrl and - // registerSecret. - if(tab === "controllers") { - expect(tabScope.registerUrl).toBe(registerUrl); - expect(tabScope.registerSecret).toBe(registerSecret); - } - }); - }); - }); - - angular.forEach(["machines", "devices", "controllers", "switches"], - function(tab) { - - describe("tab(" + tab + ")", function() { - - it("resets search matches previous search and empty filtered_items", - function() { - makeController(); - var tabScope = $scope.tabs[tab]; - var search = makeName("search"); - - if(tab === 'machines' || tab === 'switches') { - // Nodes uses the maas-machines-table directive, so - // the interaction is a little different. - tabScope.search = "in:(Selected)"; - tabScope.previous_search = search; - $scope.onNodeListingChanged([makeObject(tab)], tab); - - // Empty the listing search should be reset. - tabScope.search = search; - $scope.onNodeListingChanged([], tab); - expect(tabScope.search).toBe(""); - } else { - // Add item to filtered_items. - tabScope.filtered_items.push(makeObject(tab)); - tabScope.search = "in:(Selected)"; - tabScope.previous_search = search; - $scope.$digest(); - - // Empty the filtered_items, which should clear the - // search. - tabScope.filtered_items.splice(0, 1); - tabScope.search = search; - $scope.$digest(); - expect(tabScope.search).toBe(""); - } - }); - - it("doesnt reset search matches if not empty filtered_items", - function() { - makeController(); - var tabScope = $scope.tabs[tab]; - var search = makeName("search"); - var nodes = [makeObject(tab), makeObject(tab)]; - - if(tab === 'machines' || tab === 'switches') { - $scope.onNodeListingChanged(nodes, tab); - } else { - // Add item to filtered_items. - tabScope.filtered_items.push(nodes[0], nodes[1]); - } - tabScope.search = "in:(Selected)"; - tabScope.previous_search = search; - $scope.$digest(); - - // Remove one item from filtered_items, which should not - // clear the search. - if(tab === 'machines' || tab === 'switches') { - $scope.onNodeListingChanged([nodes[1]], tab); - } else { - tabScope.filtered_items.splice(0, 1); - } - tabScope.search = search; - $scope.$digest(); - expect(tabScope.search).toBe(search); - }); - - it("doesnt reset search when previous search doesnt match", - function() { - makeController(); - var tabScope = $scope.tabs[tab]; - var nodes = [makeObject(tab), makeObject(tab)]; - - if(tab === 'machines' || tab === 'switches') { - $scope.onNodeListingChanged(nodes, tab); - } else { - // Add item to filtered_items. - tabScope.filtered_items.push(nodes[0]); - } - - tabScope.search = "in:(Selected)"; - tabScope.previous_search = makeName("search"); - $scope.$digest(); - - // Empty the filtered_items, but change the search which - // should stop the search from being reset. - if(tab === 'machines' || tab === 'switches') { - $scope.onNodeListingChanged([nodes[1]], tab); - } else { - tabScope.filtered_items.splice(0, 1); - } - var search = makeName("search"); - tabScope.search = search; - $scope.$digest(); - expect(tabScope.search).toBe(search); - }); - }); - }); - - angular.forEach(["machines", "devices", "controllers", "switches"], - function(tab) { - - describe("tab(" + tab + ")", function() { - - describe("clearSearch", function() { - - it("sets search to empty string", function() { - makeController(); - $scope.tabs[tab].search = makeName("search"); - $scope.clearSearch(tab); - expect($scope.tabs[tab].search).toBe(""); - }); - - it("calls updateFilters", function() { - makeController(); - spyOn($scope, "updateFilters"); - $scope.clearSearch(tab); - expect($scope.updateFilters).toHaveBeenCalledWith(tab); - }); - }); - }); - }); - - angular.forEach(["machines", "switches"], function(tab) { - - describe("tab(" + tab + ")", function() { - - describe("toggleChecked", function() { - - var object, tabObj; - beforeEach(function() { - makeController(); - object = makeObject(tab); - tabObj = $scope.tabs[tab]; - }); - - it("resets search when in:selected and none selected", - function() { - tabObj.search = "in:(Selected)"; - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.search).toBe(""); - }); - - it("ignores search when not in:selected and none selected", - function() { - tabObj.search = "other"; - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.search).toBe("other"); - }); - - it("updates actionErrorCount", function() { - tabObj.manager.selectItem(object.system_id); - object.actions = []; - tabObj.actionOption = { - "name": "deploy" - }; - $scope.toggleChecked(object, tab); - expect(tabObj.actionErrorCount).toBe(1); - }); - - it("clears action option when none selected", function() { - object.actions = []; - tabObj.actionOption = {}; - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.actionOption).toBeNull(); - }); - }); - - describe("toggleCheckAll", function() { - - var object1, object2, tabObj; - beforeEach(function() { - makeController(); - object1 = makeObject(tab); - object2 = makeObject(tab); - tabObj = $scope.tabs[tab]; - }); - - it("resets search when in:selected and none selected", - function() { - tabObj.search = "in:(Selected)"; - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(tabObj.search).toBe(""); - }); - - it("ignores search when not in:selected and none selected", - function() { - tabObj.search = "other"; - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(tabObj.search).toBe("other"); - }); - - it("updates actionErrorCount", function() { - tabObj.manager.selectItem(object1.system_id); - tabObj.manager.selectItem(object2.system_id); - object1.actions = []; - object2.actions = []; - tabObj.actionOption = { - "name": "deploy" - }; - $scope.toggleCheckAll(tab); - expect(tabObj.actionErrorCount).toBe(2); - }); - - it("clears action option when none selected", function() { - $scope.actionOption = {}; - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(tabObj.actionOption).toBeNull(); - }); - }); - }); - }); - - angular.forEach(["devices", "controllers"], function(tab) { - - describe("tab(" + tab + ")", function() { - - describe("toggleChecked", function() { - - var object, tabObj; - beforeEach(function() { - makeController(); - object = makeObject(tab); - tabObj = $scope.tabs[tab]; - $scope.tabs.devices.filtered_items = $scope.devices; - $scope.tabs.controllers.filtered_items = $scope.controllers; - $scope.tabs.switches.filtered_items = $scope.switches; - }); - - it("selects object", function() { - $scope.toggleChecked(object, tab); - expect(object.$selected).toBe(true); - }); - - it("deselects object", function() { - tabObj.manager.selectItem(object.system_id); - $scope.toggleChecked(object, tab); - expect(object.$selected).toBe(false); - }); - - it("sets allViewableChecked to true when all objects selected", - function() { - $scope.toggleChecked(object, tab); - expect(tabObj.allViewableChecked).toBe(true); - }); - - it( - "sets allViewableChecked to false when not all objects " + - "selected", - function() { - var object2 = makeObject(tab); - $scope.toggleChecked(object, tab); - expect(tabObj.allViewableChecked).toBe(false); - }); - - it("sets allViewableChecked to false when selected and " + - "deselected", - function() { - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.allViewableChecked).toBe(false); - }); - - it("resets search when in:selected and none selected", - function() { - tabObj.search = "in:(Selected)"; - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.search).toBe(""); - }); - - it("ignores search when not in:selected and none selected", - function() { - tabObj.search = "other"; - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.search).toBe("other"); - }); - - it("updates actionErrorCount", function() { - object.actions = []; - tabObj.actionOption = { - "name": "deploy" - }; - $scope.toggleChecked(object, tab); - expect(tabObj.actionErrorCount).toBe(1); - }); - - it("clears action option when none selected", function() { - object.actions = []; - tabObj.actionOption = {}; - $scope.toggleChecked(object, tab); - $scope.toggleChecked(object, tab); - expect(tabObj.actionOption).toBeNull(); - }); - }); - - describe("toggleCheckAll", function() { - - var object1, object2, tabObj; - beforeEach(function() { - makeController(); - object1 = makeObject(tab); - object2 = makeObject(tab); - tabObj = $scope.tabs[tab]; - $scope.tabs.devices.filtered_items = $scope.devices; - $scope.tabs.controllers.filtered_items = $scope.controllers; - $scope.tabs.switches.filtered_items = $scope.switches; - }); - - it("selects all objects", function() { - $scope.toggleCheckAll(tab); - expect(object1.$selected).toBe(true); - expect(object2.$selected).toBe(true); - }); - - it("deselects all objects", function() { - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(object1.$selected).toBe(false); - expect(object2.$selected).toBe(false); - }); - - it("resets search when in:selected and none selected", - function() { - tabObj.search = "in:(Selected)"; - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(tabObj.search).toBe(""); - }); - - it("ignores search when not in:selected and none selected", - function() { - tabObj.search = "other"; - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(tabObj.search).toBe("other"); - }); - - it("updates actionErrorCount", function() { - object1.actions = []; - object2.actions = []; - tabObj.actionOption = { - "name": "deploy" - }; - $scope.toggleCheckAll(tab); - expect(tabObj.actionErrorCount).toBe(2); - }); - - it("clears action option when none selected", function() { - $scope.actionOption = {}; - $scope.toggleCheckAll(tab); - $scope.toggleCheckAll(tab); - expect(tabObj.actionOption).toBeNull(); - }); - }); - - describe("sortTable", function() { - - it("sets predicate", function() { - makeController(); - var predicate = makeName('predicate'); - $scope.sortTable(predicate, tab); - expect($scope.tabs[tab].predicate).toBe(predicate); - }); - - it("reverses reverse", function() { - makeController(); - $scope.tabs[tab].reverse = true; - $scope.sortTable(makeName('predicate'), tab); - expect($scope.tabs[tab].reverse).toBe(false); - }); - }); - - describe("selectColumnOrSort", function() { - - it("sets column if different", function() { - makeController(); - var column = makeName('column'); - $scope.selectColumnOrSort(column, tab); - expect($scope.tabs[tab].column).toBe(column); - }); - - it("calls sortTable if column already set", function() { - makeController(); - var column = makeName('column'); - $scope.tabs[tab].column = column; - spyOn($scope, "sortTable"); - $scope.selectColumnOrSort(column, tab); - expect($scope.sortTable).toHaveBeenCalledWith( - column, tab); - }); - }); - }); - }); - - angular.forEach(["machines", "devices", "controllers", "switches"], - function(tab) { - - describe("tab(" + tab + ")", function() { - - describe("showSelected", function() { - - it("sets search to in:selected", function() { - makeController(); - $scope.tabs[tab].selectedItems.push(makeObject(tab)); - $scope.tabs[tab].actionOption = {}; - $scope.showSelected(tab); - expect($scope.tabs[tab].search).toBe("in:(Selected)"); - }); - - it("updateFilters with the new search", function() { - makeController(); - $scope.tabs[tab].selectedItems.push(makeObject(tab)); - $scope.tabs[tab].actionOption = {}; - $scope.showSelected(tab); - expect($scope.tabs[tab].filters["in"]).toEqual( - ["Selected"]); - }); - }); - - describe("toggleFilter", function() { - - it("does nothing if actionOption", function() { - makeController(); - $scope.tabs[tab].actionOption = {}; - - var filters = { _: [], "in": ["Selected"] }; - $scope.tabs[tab].filters = filters; - $scope.toggleFilter("hostname", "test", tab); - expect($scope.tabs[tab].filters).toEqual(filters); - }); - - it("calls SearchService.toggleFilter", function() { - makeController(); - spyOn(SearchService, "toggleFilter").and.returnValue( - SearchService.getEmptyFilter()); - $scope.toggleFilter("hostname", "test", tab); - expect(SearchService.toggleFilter).toHaveBeenCalled(); - }); - - it("sets $scope.filters", function() { - makeController(); - var filters = { _: [], other: [] }; - spyOn(SearchService, "toggleFilter").and.returnValue( - filters); - $scope.toggleFilter("hostname", "test", tab); - expect($scope.tabs[tab].filters).toBe(filters); - }); - - it("calls SearchService.filtersToString", function() { - makeController(); - spyOn(SearchService, "filtersToString").and.returnValue( - ""); - $scope.toggleFilter("hostname", "test", tab); - expect(SearchService.filtersToString).toHaveBeenCalled(); - }); - - it("sets $scope.search", function() { - makeController(); - $scope.toggleFilter("hostname", "test", tab); - expect($scope.tabs[tab].search).toBe("hostname:(=test)"); - }); - }); - - describe("isFilterActive", function() { - - it("returns true when active", function() { - makeController(); - $scope.toggleFilter("hostname", "test", tab); - expect( - $scope.isFilterActive( - "hostname", "test", tab)).toBe(true); - }); - - it("returns false when inactive", function() { - makeController(); - $scope.toggleFilter("hostname", "test2", tab); - expect( - $scope.isFilterActive( - "hostname", "test", tab)).toBe(false); - }); - }); - - describe("updateFilters", function() { - - it("updates filters and sets searchValid to true", function() { - makeController(); - $scope.tabs[tab].search = "test hostname:name"; - $scope.updateFilters(tab); - expect($scope.tabs[tab].filters).toEqual({ - _: ["test"], - hostname: ["name"] - }); - expect($scope.tabs[tab].searchValid).toBe(true); - }); - - it("updates sets filters empty and sets searchValid to false", - function() { - makeController(); - $scope.tabs[tab].search = "test hostname:(name"; - $scope.updateFilters(tab); - expect( - $scope.tabs[tab].filters).toEqual( - SearchService.getEmptyFilter()); - expect($scope.tabs[tab].searchValid).toBe(false); - }); - }); - - describe("supportsAction", function() { - - it("returns true if actionOption is null", function() { - makeController(); - var object = makeObject(tab); - object.actions = ["start", "stop"]; - expect($scope.supportsAction(object, tab)).toBe(true); - }); - - it("returns true if actionOption in object.actions", - function() { - makeController(); - var object = makeObject(tab); - object.actions = ["start", "stop"]; - $scope.tabs.machines.actionOption = { name: "start" }; - expect($scope.supportsAction(object, tab)).toBe(true); - }); - - it("returns false if actionOption not in object.actions", - function() { - makeController(); - var object = makeObject(tab); - object.actions = ["start", "stop"]; - $scope.tabs[tab].actionOption = { name: "deploy" }; - expect($scope.supportsAction(object, tab)).toBe(false); - }); - }); - }); - }); - - angular.forEach(["machines", "devices", "controllers", "switches"], - function(tab) { - - describe("tab(" + tab + ")", function() { - - describe("actionOptionSelected", function() { - - it("sets actionErrorCount to zero", function() { - makeController(); - $scope.tabs[tab].actionErrorCount = 1; - $scope.actionOptionSelected(tab); - expect($scope.tabs[tab].actionErrorCount).toBe(0); - }); - - it("sets actionErrorCount to 1 when selected object doesn't " + - "support action", - function() { - makeController(); - var object = makeObject(tab); - object.actions = ['start', 'stop']; - $scope.tabs[tab].actionOption = { name: 'deploy' }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionOptionSelected(tab); - expect($scope.tabs[tab].actionErrorCount).toBe(1); - }); - - it("sets search to in:selected", function() { - makeController(); - $scope.actionOptionSelected(tab); - expect($scope.tabs[tab].search).toBe("in:(Selected)"); - }); - - it("sets previous_search to search value", function() { - makeController(); - var search = makeName("search"); - $scope.tabs[tab].search = search; - $scope.tabs[tab].actionErrorCount = 1; - $scope.actionOptionSelected(tab); - expect($scope.tabs[tab].previous_search).toBe(search); - }); - - it("calls hide on addHardwareScope", function() { - makeController(); - if (tab === 'machines') { - $scope.addHardwareScope = { - hide: jasmine.createSpy("hide") - }; - $scope.actionOptionSelected(tab); - expect( - $scope.addHardwareScope.hide).toHaveBeenCalled(); - } else if (tab === 'devices') { - $scope.addDeviceScope = { - hide: jasmine.createSpy("hide") - }; - $scope.actionOptionSelected(tab); - expect( - $scope.addDeviceScope.hide).toHaveBeenCalled(); - } - }); - - }); - - describe("isActionError", function() { - - it("returns true if actionErrorCount > 0", function() { - makeController(); - $scope.tabs[tab].actionErrorCount = 2; - expect($scope.isActionError(tab)).toBe(true); - }); - - it("returns false if actionErrorCount === 0", function() { - makeController(); - $scope.tabs[tab].actionErrorCount = 0; - expect($scope.isActionError(tab)).toBe(false); - }); - - it("returns true if deploy action missing osinfo", function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - $scope.osinfo = { - osystems: [] - }; - expect($scope.isActionError(tab)).toBe(true); - }); - - it("returns true if action missing ssh keys", - function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - $scope.osinfo = { - osystems: [makeName("os")] - }; - var firstUser = makeUser(); - UsersManager._authUser = firstUser; - firstUser.sshkeys_count = 0; - expect($scope.isActionError(tab)).toBe(true); - }); - - it("returns false if deploy action not missing osinfo or keys", - function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - $scope.osinfo = { - osystems: [makeName("os")] - }; - var firstUser = makeUser(); - firstUser.sshkeys_count = 1; - UsersManager._authUser = firstUser; - expect($scope.isActionError(tab)).toBe(false); - }); - }); - - describe("isSSHKeyError", function() { - - it("returns false if actionErrorCount > 0", function() { - makeController(); - $scope.tabs[tab].actionErrorCount = 2; - expect($scope.isSSHKeyError(tab)).toBe(false); - }); - - it("returns true if deploy action missing ssh keys", - function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - expect($scope.isSSHKeyError(tab)).toBe(true); - }); - - it("returns false if deploy action not missing ssh keys", - function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - var firstUser = makeUser(); - firstUser.sshkeys_count = 1; - UsersManager._authUser = firstUser; - expect($scope.isSSHKeyError(tab)).toBe(false); - }); - }); - - describe("isDeployError", function() { - - it("returns false if actionErrorCount > 0", function() { - makeController(); - $scope.tabs[tab].actionErrorCount = 2; - expect($scope.isDeployError(tab)).toBe(false); - }); - - it("returns true if deploy action missing osinfo", function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - $scope.osinfo = { - osystems: [] - }; - expect($scope.isDeployError(tab)).toBe(true); - }); - - it("returns false if deploy action not missing osinfo", - function() { - makeController(); - $scope.tabs[tab].actionOption = { - name: "deploy" - }; - $scope.tabs[tab].actionErrorCount = 0; - $scope.osinfo = { - osystems: [makeName("os")] - }; - expect($scope.isDeployError(tab)).toBe(false); - }); - }); - - describe("actionCancel", function() { - - it("clears search if in:selected", function() { - makeController(); - $scope.tabs[tab].search = "in:(Selected)"; - $scope.actionCancel(tab); - expect($scope.tabs[tab].search).toBe(""); - }); - - it("clears search if in:selected (device)", function() { - makeController(); - $scope.tabs.devices.search = "in:(Selected)"; - $scope.actionCancel('devices'); - expect($scope.tabs.devices.search).toBe(""); - }); - - it("clears search if in:selected (controller)", function() { - makeController(); - $scope.tabs.controllers.search = "in:(Selected)"; - $scope.actionCancel('controllers'); - expect($scope.tabs.controllers.search).toBe(""); - }); - - it("doesnt clear search if not in:Selected", function() { - makeController(); - $scope.tabs[tab].search = "other"; - $scope.actionCancel(tab); - expect($scope.tabs[tab].search).toBe("other"); - }); - - it("sets actionOption to null", function() { - makeController(); - $scope.tabs[tab].actionOption = {}; - $scope.actionCancel(tab); - expect($scope.tabs[tab].actionOption).toBeNull(); - }); - - it("supports pluralization of names based on tab", function() { - var singulars = { - 'machines': 'machine', - 'switches': 'switch', - 'devices': 'device', - 'controllers': 'controller', - }; - makeController(); - expect($scope.pluralize(tab)).toEqual(singulars[tab]); - $scope.tabs[tab].selectedItems.length = 2; - expect($scope.pluralize(tab)).toEqual(tab); - }); - - it("resets actionProgress", function() { - makeController(); - $scope.tabs[tab].actionProgress.total = makeInteger(0, 10); - $scope.tabs[tab].actionProgress.completed = - makeInteger(0, 10); - $scope.tabs[tab].actionProgress.errors[makeName("error")] = - [{}]; - $scope.tabs[tab].actionProgress.showing_confirmation = - true; - $scope.tabs[tab].actionProgress.confirmation_message = - makeName("message"); - $scope.tabs[tab].actionProgress.confirmation_details = - [makeName("detail"), makeName("detail")]; - $scope.tabs[tab].actionProgress.affected_nodes = - makeInteger(0, 10); - $scope.actionCancel(tab); - expect($scope.tabs[tab].actionProgress.total).toBe(0); - expect($scope.tabs[tab].actionProgress.completed).toBe(0); - expect($scope.tabs[tab].actionProgress.errors).toEqual({}); - expect($scope.tabs[ - tab].actionProgress.showing_confirmation).toBe(false); - expect($scope.tabs[ - tab].actionProgress.confirmation_message).toEqual(""); - expect($scope.tabs[ - tab].actionProgress.confirmation_details).toEqual([]); - expect($scope.tabs[ - tab].actionProgress.affected_nodes).toBe(0); - }); - }); - - describe("actionGo", function() { - - it("sets actionProgress.total to the number of selectedItems", - function() { - makeController(); - makeObject(tab); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [ - makeObject(tab), - makeObject(tab), - makeObject(tab) - ]; - $scope.actionGo(tab); - $scope.$digest(); - expect($scope.tabs[tab].actionProgress.total).toBe( - $scope.tabs[tab].selectedItems.length); - }); - - it("calls performAction for selected object", function() { - makeController(); - var object = makeObject(tab); - var spy = spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue($q.defer().promise); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionGo(tab); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "start", {}); - }); - - it("calls unselectItem after failed action", function() { - makeController(); - var object = makeObject(tab); - object.action_failed = false; - spyOn( - $scope, 'hasActionsFailed').and.returnValue(true); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - var spy = spyOn($scope.tabs[tab].manager, "unselectItem"); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect(spy).toHaveBeenCalled(); - }); - - it("keeps items selected after success", function() { - makeController(); - var object = makeObject(tab); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - var spy = spyOn($scope.tabs[tab].manager, "unselectItem"); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect($scope.tabs[tab].selectedItems).toEqual([object]); - }); - - it("increments actionProgress.completed after action complete", - function() { - makeController(); - var object = makeObject(tab); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(true); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect( - $scope.tabs[tab].actionProgress.completed).toBe(1); - }); - - it("set search to in:(Selected) search after complete", - function() { - makeController(); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var object = makeObject(tab); - $scope.tabs[tab].manager._items.push(object); - $scope.tabs[tab].manager._selectedItems.push(object); - $scope.tabs[tab].previous_search = makeName("search"); - $scope.tabs[tab].search = "in:(Selected)"; - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].filtered_items = [makeObject(tab)]; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect($scope.tabs[tab].search).toBe("in:(Selected)"); - }); - - it("clears action option when complete", function() { - makeController(); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var object = makeObject(tab); - $scope.tabs[tab].manager._items.push(object); - $scope.tabs[tab].manager._selectedItems.push(object); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect($scope.tabs[tab].actionOption).toBeNull(); - }); - - it("increments actionProgress.completed after action error", - function() { - makeController(); - var object = makeObject(tab); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionGo(tab); - defer.reject(makeName("error")); - $scope.$digest(); - expect( - $scope.tabs[tab].actionProgress.completed).toBe(1); - }); - - it("adds error to actionProgress.errors on action error", - function() { - makeController(); - var object = makeObject(tab); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - $scope.tabs[tab].actionOption = { name: "start" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.actionGo(tab); - var error = makeName("error"); - defer.reject(error); - $scope.$digest(); - var errorObjects = - $scope.tabs[tab].actionProgress.errors[error]; - expect(errorObjects[0].system_id).toBe( - object.system_id); - }); - }); - - describe("hasActionsInProgress", function() { - - it("returns false if actionProgress.total not > 0", function() { - makeController(); - $scope.tabs[tab].actionProgress.total = 0; - expect($scope.hasActionsInProgress(tab)).toBe(false); - }); - - it("returns true if actionProgress total != completed", - function() { - makeController(); - $scope.tabs[tab].actionProgress.total = 1; - $scope.tabs[tab].actionProgress.completed = 0; - expect($scope.hasActionsInProgress(tab)).toBe(true); - }); - - it("returns false if actionProgress total == completed", - function() { - makeController(); - $scope.tabs[tab].actionProgress.total = 1; - $scope.tabs[tab].actionProgress.completed = 1; - expect($scope.hasActionsInProgress(tab)).toBe(false); - }); - }); - - describe("hasActionsFailed", function() { - - it("returns false if no errors", function() { - makeController(); - $scope.tabs[tab].actionProgress.errors = {}; - expect($scope.hasActionsFailed(tab)).toBe(false); - }); - - it("returns true if errors", function() { - makeController(); - var error = makeName("error"); - var object = makeObject(tab); - var errors = $scope.tabs[tab].actionProgress.errors; - errors[error] = [object]; - expect($scope.hasActionsFailed(tab)).toBe(true); - }); - }); - - describe("actionSetZone", function () { - it("calls performAction with zone", - function() { - makeController(); - var spy = spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue( - $q.defer().promise); - var object = makeObject(tab); - $scope.tabs[tab].actionOption = { name: "set-zone" }; - $scope.tabs[tab].selectedItems = [object]; - $scope.tabs[tab].zoneSelection = { id: 1 }; - $scope.actionGo(tab); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "set-zone", { zone_id: 1 }); - }); - - it("clears action option when successfully complete", - function() { - makeController(); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var object = makeObject(tab); - $scope.tabs[tab].manager._items.push(object); - $scope.tabs[tab].manager._selectedItems.push(object); - $scope.tabs[tab].actionOption = { name: "set-zone" }; - $scope.tabs[tab].zoneSelection = { id: 1 }; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect($scope.tabs[tab].zoneSelection).toBeNull(); - }); - }); - - describe("actionSetPool", function () { - - it("calls performAction with pool", - function() { - makeController(); - var spy = spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue( - $q.defer().promise); - var object = makeObject(tab); - var tabScope = $scope.tabs[tab]; - tabScope.actionOption = { name: "set-pool" }; - tabScope.selectedItems = [object]; - tabScope.poolAction = 'select-pool'; - tabScope.poolSelection = { id: 1 }; - $scope.actionGo(tab); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "set-pool", { pool_id: 1 }); - }); - - it("calls performAction with new pool data", - function() { - makeController(); - var createDefer = $q.defer(); - var createSpy = spyOn( - ResourcePoolsManager, - "createItem").and.returnValue( - createDefer.promise); - var performSpy = spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue( - $q.defer().promise); - var object = makeObject(tab); - var newPoolData = { - name: 'my-pool', - description: 'desc', - }; - var tabScope = $scope.tabs[tab]; - tabScope.actionOption = { name: "set-pool" }; - tabScope.selectedItems = [object]; - tabScope.poolSelection = null; - tabScope.poolAction = 'create-pool'; - tabScope.newPool = newPoolData; - $scope.actionGo(tab); - createDefer.resolve({id: 84}); - $scope.$digest(); - expect(performSpy).toHaveBeenCalledWith( - object, "set-pool", { - pool_id:84 - }); - expect(createSpy).toHaveBeenCalledWith( - {name: newPoolData.name}); - }); - - it("clears action option when successfully complete", - function() { - makeController(); - var defer = $q.defer(); - spyOn( - $scope.tabs[tab].manager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var object = makeObject(tab); - $scope.tabs[tab].manager._items.push(object); - $scope.tabs[tab].manager._selectedItems.push(object); - $scope.tabs[tab].actionOption = { name: "set-pool" }; - $scope.tabs[tab].poolSelection = { id: 1 }; - $scope.actionGo(tab); - defer.resolve(); - $scope.$digest(); - expect($scope.tabs[tab].poolSelection).toBeNull(); - }); - }); - }); - }); - - describe("tab(nodes)", function() { - - describe("actionGo", function() { - - it("calls performAction with osystem and distro_series", - function() { - makeController(); - var object = makeObject("machines"); - var spy = spyOn( - $scope.tabs.machines.manager, - "performAction").and.returnValue( - $q.defer().promise); - $scope.tabs.machines.actionOption = { name: "deploy" }; - $scope.tabs.machines.selectedItems = [object]; - $scope.tabs.machines.osSelection.osystem = "ubuntu"; - $scope.tabs.machines.osSelection.release = "ubuntu/trusty"; - $scope.actionGo("machines"); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "deploy", { - osystem: "ubuntu", - distro_series: "trusty", - install_kvm: false - }); - }); - - it("calls performAction with tag", function() { - makeController(); - var object = makeObject("machines"); - var spy = spyOn($scope.tabs.machines.manager, "performAction") - .and - .returnValue($q.defer().promise); - - $scope.tabs.machines.actionOption = { name: "tag" }; - $scope.tabs.machines.selectedItems = [object]; - $scope.tags = [ - { text: 'foo' }, - { text: 'bar' }, - { text: 'baz' } - ]; - $scope.actionGo("machines"); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith(object, "tag", { - tags: ['foo', 'bar', 'baz'] - }); - }); - - it("calls performAction with install_kvm", - function() { - makeController(); - var object = makeObject("machines"); - var spy = spyOn( - $scope.tabs.machines.manager, - "performAction").and.returnValue( - $q.defer().promise); - $scope.tabs.machines.actionOption = {name: "deploy"}; - $scope.tabs.machines.selectedItems = [object]; - $scope.tabs.machines.osSelection.osystem = "debian"; - $scope.tabs.machines.osSelection.release = "etch"; - $scope.tabs.machines.deployOptions.installKVM = true; - $scope.actionGo("machines"); - $scope.$digest(); - // When deploying KVM, coerce the distro to ubuntu/bionic. - expect(spy).toHaveBeenCalledWith( - object, "deploy", { - osystem: "ubuntu", - distro_series: "bionic", - install_kvm: true - }); - }); - - it("clears selected os and release when successfully complete", - function() { - makeController(); - var defer = $q.defer(); - spyOn( - MachinesManager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var object = makeObject("machines"); - MachinesManager._items.push(object); - MachinesManager._selectedItems.push(object); - $scope.tabs.machines.actionOption = { name: "deploy" }; - $scope.tabs.machines.osSelection.osystem = "ubuntu"; - $scope.tabs.machines.osSelection.release = "ubuntu/trusty"; - $scope.actionGo("machines"); - defer.resolve(); - $scope.$digest(); - expect( - $scope.tabs.machines.osSelection.$reset - ).toHaveBeenCalled(); - }); - - it("calls performAction with commissionOptions", - function() { - makeController(); - var object = makeObject("machines"); - var spy = spyOn( - $scope.tabs.machines.manager, - "performAction").and.returnValue( - $q.defer().promise); - var commissioning_scripts_ids = [ - makeInteger(0, 100), makeInteger(0, 100)]; - var testing_scripts_ids = [ - makeInteger(0, 100), makeInteger(0, 100)]; - $scope.tabs.machines.actionOption = { name: "commission" }; - $scope.tabs.machines.selectedItems = [object]; - $scope.tabs.machines.commissionOptions.enableSSH = true; - $scope.tabs.machines.commissionOptions.skipBMCConfig = - false; - $scope.tabs.machines.commissionOptions.skipNetworking = - false; - $scope.tabs.machines.commissionOptions.skipStorage = false; - $scope.tabs.machines.commissionOptions.updateFirmware = - true; - $scope.tabs.machines.commissionOptions.configureHBA = true; - $scope.tabs.machines.commissioningSelection = []; - angular.forEach( - commissioning_scripts_ids, function(script_id) { - $scope.tabs.machines.commissioningSelection.push({ - id: script_id, - name: makeName("script_name") - }); - }); - $scope.tabs.machines.testSelection = []; - angular.forEach(testing_scripts_ids, function(script_id) { - $scope.tabs.machines.testSelection.push({ - id: script_id, - name: makeName("script_name") - }); - }); - $scope.actionGo("machines"); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "commission", { - enable_ssh: true, - skip_bmc_config: false, - skip_networking: false, - skip_storage: false, - commissioning_scripts: - commissioning_scripts_ids.concat([ - 'update_firmware', 'configure_hba']), - testing_scripts: testing_scripts_ids - }); - }); - - it("calls performAction with testOptions", - function() { - makeController(); - var object = makeObject("machines"); - var spy = spyOn( - $scope.tabs.machines.manager, - "performAction").and.returnValue( - $q.defer().promise); - var testing_script_ids = [ - makeInteger(0, 100), makeInteger(0, 100)]; - $scope.tabs.machines.actionOption = { name: "test" }; - $scope.tabs.machines.selectedItems = [object]; - $scope.tabs.machines.commissionOptions.enableSSH = true; - $scope.tabs.machines.testSelection = []; - angular.forEach(testing_script_ids, function(script_id) { - $scope.tabs.machines.testSelection.push({ - id: script_id, - name: makeName("script_name") - }); - }); - $scope.actionGo("machines"); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "test", { - enable_ssh: true, - testing_scripts: testing_script_ids - }); - }); - - it("sets showing_confirmation with testOptions", - function() { - makeController(); - var object = makeObject("machines"); - object.status_code = 6; - var spy = spyOn( - $scope.tabs.machines.manager, - "performAction").and.returnValue( - $q.defer().promise); - $scope.tabs.machines.actionOption = { name: "test" }; - $scope.tabs.machines.selectedItems = [object]; - $scope.actionGo("machines"); - expect($scope.tabs[ - "machines"].actionProgress.showing_confirmation).toBe( - true); - expect($scope.tabs[ - "machines"].actionProgress.confirmation_message - ).not.toBe(""); - expect($scope.tabs[ - "machines"].actionProgress.affected_nodes).toBe(1); - expect(spy).not.toHaveBeenCalled(); - }); - - it("calls performAction with releaseOptions", - function() { - makeController(); - var object = makeObject("machines"); - var spy = spyOn( - $scope.tabs.machines.manager, - "performAction").and.returnValue( - $q.defer().promise); - var secureErase = makeName("secureErase"); - var quickErase = makeName("quickErase"); - $scope.tabs.machines.actionOption = { name: "release" }; - $scope.tabs.machines.selectedItems = [object]; - $scope.tabs.machines.releaseOptions.erase = true; - $scope.tabs.machines.releaseOptions.secureErase = - secureErase; - $scope.tabs.machines.releaseOptions.quickErase = - quickErase; - $scope.actionGo("machines"); - $scope.$digest(); - expect(spy).toHaveBeenCalledWith( - object, "release", { - erase: true, - secure_erase: secureErase, - quick_erase: quickErase - }); - }); - - it("sets showing_confirmation with deleteOptions", - function() { - // Regression test for LP:1793478 - makeController(); - var object = makeObject("controllers"); - $scope.vlans = [{ - 'id': 0, - 'primary_rack': object.system_id, - 'name': 'Default VLAN' - }]; - var spy = spyOn( - $scope.tabs.controllers.manager, - "performAction").and.returnValue( - $q.defer().promise); - $scope.tabs.controllers.actionOption = { name: "delete" }; - $scope.tabs.controllers.selectedItems = [object]; - $scope.actionGo("controllers"); - expect($scope.tabs[ - "controllers"].actionProgress.showing_confirmation - ).toBe(true); - expect($scope.tabs[ - "controllers"].actionProgress.confirmation_message - ).not.toBe(""); - expect($scope.tabs[ - "controllers"].actionProgress.confirmation_details - ).not.toBe([]); - expect($scope.tabs[ - "controllers"].actionProgress.affected_nodes).toBe(1); - expect(spy).not.toHaveBeenCalled(); - }); - - it("clears commissionOptions when successfully complete", - function() { - makeController(); - var defer = $q.defer(); - spyOn( - MachinesManager, - "performAction").and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var object = makeObject("machines"); - MachinesManager._items.push(object); - MachinesManager._selectedItems.push(object); - $scope.tabs.machines.actionOption = { name: "commission" }; - $scope.tabs.machines.commissionOptions.enableSSH = true; - $scope.tabs.machines.commissionOptions.skipNetworking = true; - $scope.tabs.machines.commissionOptions.skipStorage = true; - $scope.tabs.machines.commissionOptions.updateFirmware = true; - $scope.tabs.machines.commissionOptions.configureHBA = true; - $scope.tabs.machines.commissioningSelection = [{ - id: makeInteger(0, 100), - name: makeName("script_name") - }]; - $scope.tabs.machines.testSelection = [{ - id: makeInteger(0, 100), - name: makeName("script_name") - }]; - - $scope.actionGo("machines"); - defer.resolve(); - $scope.$digest(); - expect($scope.tabs.machines.commissionOptions).toEqual({ - enableSSH: false, - skipBMCConfig: false, - skipNetworking: false, - skipStorage: false, - updateFirmware: false, - configureHBA: false - }); - expect($scope.tabs.machines.commissioningSelection).toEqual([]); - expect($scope.tabs.machines.testSelection).toEqual([]); - }); - }); - }); + it("doesnt reset search matches if not empty filtered_items", function() { + makeController(); + var tabScope = $scope.tabs[tab]; + var search = makeName("search"); + var nodes = [makeObject(tab), makeObject(tab)]; - describe('tab(pools)', function() { - it('sets the actionOption when addPool is called', function() { - makeController(); - var poolsTab = $scope.tabs.pools; - expect(poolsTab.actionOption).toBe(false); - poolsTab.addPool(); - expect(poolsTab.actionOption).toBe(true); - }); + if (tab === "machines" || tab === "switches") { + $scope.onNodeListingChanged(nodes, tab); + } else { + // Add item to filtered_items. + tabScope.filtered_items.push(nodes[0], nodes[1]); + } + tabScope.search = "in:(Selected)"; + tabScope.previous_search = search; + $scope.$digest(); + + // Remove one item from filtered_items, which should not + // clear the search. + if (tab === "machines" || tab === "switches") { + $scope.onNodeListingChanged([nodes[1]], tab); + } else { + tabScope.filtered_items.splice(0, 1); + } + tabScope.search = search; + $scope.$digest(); + expect(tabScope.search).toBe(search); + }); - it('resets actionOption and newPool when cancelAddPool is called', - function() { - makeController(); - var poolsTab = $scope.tabs.pools; - poolsTab.addPool(); - poolsTab.newPool = {'name': 'mypool'}, - poolsTab.cancelAddPool(); - expect(poolsTab.actionOption).toBe(false); - expect(poolsTab.newPool).toEqual({}); - }); - }); + it("doesnt reset search when previous search doesnt match", function() { + makeController(); + var tabScope = $scope.tabs[tab]; + var nodes = [makeObject(tab), makeObject(tab)]; - describe("addHardwareOptionChanged", function() { + if (tab === "machines" || tab === "switches") { + $scope.onNodeListingChanged(nodes, tab); + } else { + // Add item to filtered_items. + tabScope.filtered_items.push(nodes[0]); + } - it("calls show in addHardwareScope", function() { - makeController(); + tabScope.search = "in:(Selected)"; + tabScope.previous_search = makeName("search"); + $scope.$digest(); + + // Empty the filtered_items, but change the search which + // should stop the search from being reset. + if (tab === "machines" || tab === "switches") { + $scope.onNodeListingChanged([nodes[1]], tab); + } else { + tabScope.filtered_items.splice(0, 1); + } + var search = makeName("search"); + tabScope.search = search; + $scope.$digest(); + expect(tabScope.search).toBe(search); + }); + }); + }); + + angular.forEach(["machines", "devices", "controllers", "switches"], function( + tab + ) { + describe("tab(" + tab + ")", function() { + describe("clearSearch", function() { + it("sets search to empty string", function() { + makeController(); + $scope.tabs[tab].search = makeName("search"); + $scope.clearSearch(tab); + expect($scope.tabs[tab].search).toBe(""); + }); + + it("calls updateFilters", function() { + makeController(); + spyOn($scope, "updateFilters"); + $scope.clearSearch(tab); + expect($scope.updateFilters).toHaveBeenCalledWith(tab); + }); + }); + }); + }); + + angular.forEach(["machines", "switches"], function(tab) { + describe("tab(" + tab + ")", function() { + describe("toggleChecked", function() { + var object, tabObj; + beforeEach(function() { + makeController(); + object = makeObject(tab); + tabObj = $scope.tabs[tab]; + }); + + it("resets search when in:selected and none selected", function() { + tabObj.search = "in:(Selected)"; + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.search).toBe(""); + }); + + it("ignores search when not in:selected and none selected", function() { + tabObj.search = "other"; + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.search).toBe("other"); + }); + + it("updates actionErrorCount", function() { + tabObj.manager.selectItem(object.system_id); + object.actions = []; + tabObj.actionOption = { + name: "deploy" + }; + $scope.toggleChecked(object, tab); + expect(tabObj.actionErrorCount).toBe(1); + }); + + it("clears action option when none selected", function() { + object.actions = []; + tabObj.actionOption = {}; + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.actionOption).toBeNull(); + }); + }); + + describe("toggleCheckAll", function() { + var object1, object2, tabObj; + beforeEach(function() { + makeController(); + object1 = makeObject(tab); + object2 = makeObject(tab); + tabObj = $scope.tabs[tab]; + }); + + it("resets search when in:selected and none selected", function() { + tabObj.search = "in:(Selected)"; + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(tabObj.search).toBe(""); + }); + + it("ignores search when not in:selected and none selected", function() { + tabObj.search = "other"; + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(tabObj.search).toBe("other"); + }); + + it("updates actionErrorCount", function() { + tabObj.manager.selectItem(object1.system_id); + tabObj.manager.selectItem(object2.system_id); + object1.actions = []; + object2.actions = []; + tabObj.actionOption = { + name: "deploy" + }; + $scope.toggleCheckAll(tab); + expect(tabObj.actionErrorCount).toBe(2); + }); + + it("clears action option when none selected", function() { + $scope.actionOption = {}; + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(tabObj.actionOption).toBeNull(); + }); + }); + }); + }); + + angular.forEach(["devices", "controllers"], function(tab) { + describe("tab(" + tab + ")", function() { + describe("toggleChecked", function() { + var object, tabObj; + beforeEach(function() { + makeController(); + object = makeObject(tab); + tabObj = $scope.tabs[tab]; + $scope.tabs.devices.filtered_items = $scope.devices; + $scope.tabs.controllers.filtered_items = $scope.controllers; + $scope.tabs.switches.filtered_items = $scope.switches; + }); + + it("selects object", function() { + $scope.toggleChecked(object, tab); + expect(object.$selected).toBe(true); + }); + + it("deselects object", function() { + tabObj.manager.selectItem(object.system_id); + $scope.toggleChecked(object, tab); + expect(object.$selected).toBe(false); + }); + + it(`sets allViewableChecked to true when + all objects selected`, function() { + $scope.toggleChecked(object, tab); + expect(tabObj.allViewableChecked).toBe(true); + }); + + it(`sets allViewableChecked to false when + not all objects selected`, function() { + var object2 = makeObject(tab); + $scope.toggleChecked(object, tab); + expect(tabObj.allViewableChecked).toBe(false); + }); + + it(`sets allViewableChecked to false when + selected and deselected`, function() { + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.allViewableChecked).toBe(false); + }); + + it(`resets search when in:selected + and none selected`, function() { + tabObj.search = "in:(Selected)"; + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.search).toBe(""); + }); + + it(`ignores search when not in:selected + and none selected`, function() { + tabObj.search = "other"; + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.search).toBe("other"); + }); + + it("updates actionErrorCount", function() { + object.actions = []; + tabObj.actionOption = { + name: "deploy" + }; + $scope.toggleChecked(object, tab); + expect(tabObj.actionErrorCount).toBe(1); + }); + + it("clears action option when none selected", function() { + object.actions = []; + tabObj.actionOption = {}; + $scope.toggleChecked(object, tab); + $scope.toggleChecked(object, tab); + expect(tabObj.actionOption).toBeNull(); + }); + }); + + describe("toggleCheckAll", function() { + var object1, object2, tabObj; + beforeEach(function() { + makeController(); + object1 = makeObject(tab); + object2 = makeObject(tab); + tabObj = $scope.tabs[tab]; + $scope.tabs.devices.filtered_items = $scope.devices; + $scope.tabs.controllers.filtered_items = $scope.controllers; + $scope.tabs.switches.filtered_items = $scope.switches; + }); + + it("selects all objects", function() { + $scope.toggleCheckAll(tab); + expect(object1.$selected).toBe(true); + expect(object2.$selected).toBe(true); + }); + + it("deselects all objects", function() { + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(object1.$selected).toBe(false); + expect(object2.$selected).toBe(false); + }); + + it("resets search when in:selected and none selected", function() { + tabObj.search = "in:(Selected)"; + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(tabObj.search).toBe(""); + }); + + it("ignores search when not in:selected and none selected", function() { + tabObj.search = "other"; + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(tabObj.search).toBe("other"); + }); + + it("updates actionErrorCount", function() { + object1.actions = []; + object2.actions = []; + tabObj.actionOption = { + name: "deploy" + }; + $scope.toggleCheckAll(tab); + expect(tabObj.actionErrorCount).toBe(2); + }); + + it("clears action option when none selected", function() { + $scope.actionOption = {}; + $scope.toggleCheckAll(tab); + $scope.toggleCheckAll(tab); + expect(tabObj.actionOption).toBeNull(); + }); + }); + + describe("sortTable", function() { + it("sets predicate", function() { + makeController(); + var predicate = makeName("predicate"); + $scope.sortTable(predicate, tab); + expect($scope.tabs[tab].predicate).toBe(predicate); + }); + + it("reverses reverse", function() { + makeController(); + $scope.tabs[tab].reverse = true; + $scope.sortTable(makeName("predicate"), tab); + expect($scope.tabs[tab].reverse).toBe(false); + }); + }); + + describe("selectColumnOrSort", function() { + it("sets column if different", function() { + makeController(); + var column = makeName("column"); + $scope.selectColumnOrSort(column, tab); + expect($scope.tabs[tab].column).toBe(column); + }); + + it("calls sortTable if column already set", function() { + makeController(); + var column = makeName("column"); + $scope.tabs[tab].column = column; + spyOn($scope, "sortTable"); + $scope.selectColumnOrSort(column, tab); + expect($scope.sortTable).toHaveBeenCalledWith(column, tab); + }); + }); + }); + }); + + angular.forEach(["machines", "devices", "controllers", "switches"], function( + tab + ) { + describe("tab(" + tab + ")", function() { + describe("showSelected", function() { + it("sets search to in:selected", function() { + makeController(); + $scope.tabs[tab].selectedItems.push(makeObject(tab)); + $scope.tabs[tab].actionOption = {}; + $scope.showSelected(tab); + expect($scope.tabs[tab].search).toBe("in:(Selected)"); + }); + + it("updateFilters with the new search", function() { + makeController(); + $scope.tabs[tab].selectedItems.push(makeObject(tab)); + $scope.tabs[tab].actionOption = {}; + $scope.showSelected(tab); + expect($scope.tabs[tab].filters["in"]).toEqual(["Selected"]); + }); + }); + + describe("toggleFilter", function() { + it("does nothing if actionOption", function() { + makeController(); + $scope.tabs[tab].actionOption = {}; + + var filters = { _: [], in: ["Selected"] }; + $scope.tabs[tab].filters = filters; + $scope.toggleFilter("hostname", "test", tab); + expect($scope.tabs[tab].filters).toEqual(filters); + }); + + it("calls SearchService.toggleFilter", function() { + makeController(); + spyOn(SearchService, "toggleFilter").and.returnValue( + SearchService.getEmptyFilter() + ); + $scope.toggleFilter("hostname", "test", tab); + expect(SearchService.toggleFilter).toHaveBeenCalled(); + }); + + it("sets $scope.filters", function() { + makeController(); + var filters = { _: [], other: [] }; + spyOn(SearchService, "toggleFilter").and.returnValue(filters); + $scope.toggleFilter("hostname", "test", tab); + expect($scope.tabs[tab].filters).toBe(filters); + }); + + it("calls SearchService.filtersToString", function() { + makeController(); + spyOn(SearchService, "filtersToString").and.returnValue(""); + $scope.toggleFilter("hostname", "test", tab); + expect(SearchService.filtersToString).toHaveBeenCalled(); + }); + + it("sets $scope.search", function() { + makeController(); + $scope.toggleFilter("hostname", "test", tab); + expect($scope.tabs[tab].search).toBe("hostname:(=test)"); + }); + }); + + describe("isFilterActive", function() { + it("returns true when active", function() { + makeController(); + $scope.toggleFilter("hostname", "test", tab); + expect($scope.isFilterActive("hostname", "test", tab)).toBe(true); + }); + + it("returns false when inactive", function() { + makeController(); + $scope.toggleFilter("hostname", "test2", tab); + expect($scope.isFilterActive("hostname", "test", tab)).toBe(false); + }); + }); + + describe("updateFilters", function() { + it("updates filters and sets searchValid to true", function() { + makeController(); + $scope.tabs[tab].search = "test hostname:name"; + $scope.updateFilters(tab); + expect($scope.tabs[tab].filters).toEqual({ + _: ["test"], + hostname: ["name"] + }); + expect($scope.tabs[tab].searchValid).toBe(true); + }); + + it(`updates sets filters empty and + sets searchValid to false`, function() { + makeController(); + $scope.tabs[tab].search = "test hostname:(name"; + $scope.updateFilters(tab); + expect($scope.tabs[tab].filters).toEqual( + SearchService.getEmptyFilter() + ); + expect($scope.tabs[tab].searchValid).toBe(false); + }); + }); + + describe("supportsAction", function() { + it("returns true if actionOption is null", function() { + makeController(); + var object = makeObject(tab); + object.actions = ["start", "stop"]; + expect($scope.supportsAction(object, tab)).toBe(true); + }); + + it("returns true if actionOption in object.actions", function() { + makeController(); + var object = makeObject(tab); + object.actions = ["start", "stop"]; + $scope.tabs.machines.actionOption = { name: "start" }; + expect($scope.supportsAction(object, tab)).toBe(true); + }); + + it("returns false if actionOption not in object.actions", function() { + makeController(); + var object = makeObject(tab); + object.actions = ["start", "stop"]; + $scope.tabs[tab].actionOption = { name: "deploy" }; + expect($scope.supportsAction(object, tab)).toBe(false); + }); + }); + }); + }); + + angular.forEach(["machines", "devices", "controllers", "switches"], function( + tab + ) { + describe("tab(" + tab + ")", function() { + describe("actionOptionSelected", function() { + it("sets actionErrorCount to zero", function() { + makeController(); + $scope.tabs[tab].actionErrorCount = 1; + $scope.actionOptionSelected(tab); + expect($scope.tabs[tab].actionErrorCount).toBe(0); + }); + + it( + "sets actionErrorCount to 1 when selected object doesn't " + + "support action", + function() { + makeController(); + var object = makeObject(tab); + object.actions = ["start", "stop"]; + $scope.tabs[tab].actionOption = { name: "deploy" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionOptionSelected(tab); + expect($scope.tabs[tab].actionErrorCount).toBe(1); + } + ); + + it("sets search to in:selected", function() { + makeController(); + $scope.actionOptionSelected(tab); + expect($scope.tabs[tab].search).toBe("in:(Selected)"); + }); + + it("sets previous_search to search value", function() { + makeController(); + var search = makeName("search"); + $scope.tabs[tab].search = search; + $scope.tabs[tab].actionErrorCount = 1; + $scope.actionOptionSelected(tab); + expect($scope.tabs[tab].previous_search).toBe(search); + }); + + it("calls hide on addHardwareScope", function() { + makeController(); + if (tab === "machines") { $scope.addHardwareScope = { - show: jasmine.createSpy("show") + hide: jasmine.createSpy("hide") }; - $scope.addHardwareOption = { - name: "hardware" - }; - $scope.addHardwareOptionChanged(); - expect($scope.addHardwareScope.show).toHaveBeenCalledWith( - "hardware"); - }); - }); - - describe("addDevice", function() { - - it("calls show in addDeviceScope", function() { - makeController(); + $scope.actionOptionSelected(tab); + expect($scope.addHardwareScope.hide).toHaveBeenCalled(); + } else if (tab === "devices") { $scope.addDeviceScope = { - show: jasmine.createSpy("show") + hide: jasmine.createSpy("hide") }; - $scope.addDevice(); - expect($scope.addDeviceScope.show).toHaveBeenCalled(); - }); - }); - - describe("cancelAddDevice", function() { - - it("calls cancel in addDeviceScope", function() { - makeController(); - $scope.addDeviceScope = { - cancel: jasmine.createSpy("cancel") - }; - $scope.cancelAddDevice(); - expect($scope.addDeviceScope.cancel).toHaveBeenCalled(); - }); - }); - - describe("getDeviceIPAssignment", function() { - - it("returns 'External' for external assignment", function() { - makeController(); - expect($scope.getDeviceIPAssignment("external")).toBe( - "External"); - }); - - it("returns 'Dynamic' for dynamic assignment", function() { - makeController(); - expect($scope.getDeviceIPAssignment("dynamic")).toBe( - "Dynamic"); - }); - - it("returns 'Static' for static assignment", function() { - makeController(); - expect($scope.getDeviceIPAssignment("static")).toBe( - "Static"); - }); - }); - - describe("hasCustomCommissioningScripts", function() { - it("returns true with custom commissioning scripts", function() { - makeController(); - ScriptsManager._items.push({script_type: 0}); - expect($scope.hasCustomCommissioningScripts()).toBe(true); - }); - it("returns false without custom commissioning scripts", function() { - makeController(); - expect($scope.hasCustomCommissioningScripts()).toBe(false); - }); - }); - - describe("showswitches", function() { - it("is true if switches=on", function() { - $routeParams.switches = "on"; - makeController(); - expect($scope.showswitches).toBe(true); - }); - it("is false if switches=off", function() { - $routeParams.switches = "off"; - makeController(); - expect($scope.showswitches).toBe(false); - }); - it("is false if switches is not specified", function() { - makeController(); - expect($scope.showswitches).toBe(false); - }); - }); - - describe("resource pools listing", function() { - it("sets active target with initiatePoolAction", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 1, name: 'foo'}; - tab.initiatePoolAction(pool, 'action'); - expect(tab.activeTargetAction).toEqual('action'); - expect(tab.activeTarget).toEqual(pool); - }); - - it("unsets target with cancelPoolAction", function() { - makeController(); - var tab = $scope.tabs.pools; - tab.initiatePoolAction({id: 1, name: 'foo'}, 'action'); - tab.cancelPoolAction(); - expect(tab.activeTargetAction).toBe(null); - expect(tab.activeTarget).toBe(null); + $scope.actionOptionSelected(tab); + expect($scope.addDeviceScope.hide).toHaveBeenCalled(); + } + }); + }); + + describe("isActionError", function() { + it("returns true if actionErrorCount > 0", function() { + makeController(); + $scope.tabs[tab].actionErrorCount = 2; + expect($scope.isActionError(tab)).toBe(true); + }); + + it("returns false if actionErrorCount === 0", function() { + makeController(); + $scope.tabs[tab].actionErrorCount = 0; + expect($scope.isActionError(tab)).toBe(false); + }); + + it("returns true if deploy action missing osinfo", function() { + makeController(); + $scope.tabs[tab].actionOption = { + name: "deploy" + }; + $scope.tabs[tab].actionErrorCount = 0; + $scope.osinfo = { + osystems: [] + }; + expect($scope.isActionError(tab)).toBe(true); + }); + + it(`returns false if deploy action not missing + osinfo or keys`, function() { + makeController(); + $scope.tabs[tab].actionOption = { + name: "deploy" + }; + $scope.tabs[tab].actionErrorCount = 0; + $scope.osinfo = { + osystems: [makeName("os")] + }; + var firstUser = makeUser(); + firstUser.sshkeys_count = 1; + UsersManager._authUser = firstUser; + expect($scope.isActionError(tab)).toBe(false); + }); + }); + + describe("isSSHKeyWarning", function() { + it("returns false if actionErrorCount > 0", function() { + makeController(); + $scope.tabs[tab].actionErrorCount = 2; + expect($scope.isSSHKeyWarning(tab)).toBe(false); + }); + + it("returns true if deploy action missing ssh keys", function() { + makeController(); + $scope.tabs[tab].actionOption = { + name: "deploy" + }; + $scope.tabs[tab].actionErrorCount = 0; + expect($scope.isSSHKeyWarning(tab)).toBe(true); + }); + + it("returns false if deploy action not missing ssh keys", function() { + makeController(); + $scope.tabs[tab].actionOption = { + name: "deploy" + }; + $scope.tabs[tab].actionErrorCount = 0; + var firstUser = makeUser(); + firstUser.sshkeys_count = 1; + UsersManager._authUser = firstUser; + expect($scope.isSSHKeyWarning(tab)).toBe(false); + }); + }); + + describe("isDeployError", function() { + it("returns false if actionErrorCount > 0", function() { + makeController(); + $scope.tabs[tab].actionErrorCount = 2; + expect($scope.isDeployError(tab)).toBe(false); + }); + + it("returns true if deploy action missing osinfo", function() { + makeController(); + $scope.tabs[tab].actionOption = { + name: "deploy" + }; + $scope.tabs[tab].actionErrorCount = 0; + $scope.osinfo = { + osystems: [] + }; + expect($scope.isDeployError(tab)).toBe(true); + }); + + it("returns false if deploy action not missing osinfo", function() { + makeController(); + $scope.tabs[tab].actionOption = { + name: "deploy" + }; + $scope.tabs[tab].actionErrorCount = 0; + $scope.osinfo = { + osystems: [makeName("os")] + }; + expect($scope.isDeployError(tab)).toBe(false); + }); + }); + + describe("actionCancel", function() { + it("clears search if in:selected", function() { + makeController(); + $scope.tabs[tab].search = "in:(Selected)"; + $scope.actionCancel(tab); + expect($scope.tabs[tab].search).toBe(""); + }); + + it("clears search if in:selected (device)", function() { + makeController(); + $scope.tabs.devices.search = "in:(Selected)"; + $scope.actionCancel("devices"); + expect($scope.tabs.devices.search).toBe(""); + }); + + it("clears search if in:selected (controller)", function() { + makeController(); + $scope.tabs.controllers.search = "in:(Selected)"; + $scope.actionCancel("controllers"); + expect($scope.tabs.controllers.search).toBe(""); + }); + + it("doesnt clear search if not in:Selected", function() { + makeController(); + $scope.tabs[tab].search = "other"; + $scope.actionCancel(tab); + expect($scope.tabs[tab].search).toBe("other"); + }); + + it("sets actionOption to null", function() { + makeController(); + $scope.tabs[tab].actionOption = {}; + $scope.actionCancel(tab); + expect($scope.tabs[tab].actionOption).toBeNull(); + }); + + it("supports pluralization of names based on tab", function() { + var singulars = { + machines: "machine", + switches: "switch", + devices: "device", + controllers: "controller" + }; + makeController(); + expect($scope.pluralize(tab)).toEqual(singulars[tab]); + $scope.tabs[tab].selectedItems.length = 2; + expect($scope.pluralize(tab)).toEqual(tab); + }); + + it("resets actionProgress", function() { + makeController(); + $scope.tabs[tab].actionProgress.total = makeInteger(0, 10); + $scope.tabs[tab].actionProgress.completed = makeInteger(0, 10); + $scope.tabs[tab].actionProgress.errors[makeName("error")] = [{}]; + $scope.tabs[tab].actionProgress.showing_confirmation = true; + $scope.tabs[tab].actionProgress.confirmation_message = makeName( + "message" + ); + $scope.tabs[tab].actionProgress.confirmation_details = [ + makeName("detail"), + makeName("detail") + ]; + $scope.tabs[tab].actionProgress.affected_nodes = makeInteger(0, 10); + $scope.actionCancel(tab); + expect($scope.tabs[tab].actionProgress.total).toBe(0); + expect($scope.tabs[tab].actionProgress.completed).toBe(0); + expect($scope.tabs[tab].actionProgress.errors).toEqual({}); + expect($scope.tabs[tab].actionProgress.showing_confirmation).toBe( + false + ); + expect($scope.tabs[tab].actionProgress.confirmation_message).toEqual( + "" + ); + expect($scope.tabs[tab].actionProgress.confirmation_details).toEqual( + [] + ); + expect($scope.tabs[tab].actionProgress.affected_nodes).toBe(0); + }); + }); + + describe("actionGo", function() { + it(`sets actionProgress.total to the number + of selectedItems`, function() { + makeController(); + makeObject(tab); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [ + makeObject(tab), + makeObject(tab), + makeObject(tab) + ]; + $scope.actionGo(tab); + $scope.$digest(); + expect($scope.tabs[tab].actionProgress.total).toBe( + $scope.tabs[tab].selectedItems.length + ); + }); + + it("calls performAction for selected object", function() { + makeController(); + var object = makeObject(tab); + var spy = spyOn( + $scope.tabs[tab].manager, + "performAction" + ).and.returnValue($q.defer().promise); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionGo(tab); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "start", {}); + }); + + it("calls unselectItem after failed action", function() { + makeController(); + var object = makeObject(tab); + object.action_failed = false; + spyOn($scope, "hasActionsFailed").and.returnValue(true); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + var spy = spyOn($scope.tabs[tab].manager, "unselectItem"); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect(spy).toHaveBeenCalled(); + }); + + it("keeps items selected after success", function() { + makeController(); + var object = makeObject(tab); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + var spy = spyOn($scope.tabs[tab].manager, "unselectItem"); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs[tab].selectedItems).toEqual([object]); + }); + + it(`increments actionProgress.completed + after action complete`, function() { + makeController(); + var object = makeObject(tab); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + spyOn($scope, "hasActionsFailed").and.returnValue(true); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs[tab].actionProgress.completed).toBe(1); + }); + + it("set search to in:(Selected) search after complete", function() { + makeController(); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var object = makeObject(tab); + $scope.tabs[tab].manager._items.push(object); + $scope.tabs[tab].manager._selectedItems.push(object); + $scope.tabs[tab].previous_search = makeName("search"); + $scope.tabs[tab].search = "in:(Selected)"; + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].filtered_items = [makeObject(tab)]; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs[tab].search).toBe("in:(Selected)"); + }); + + it("clears action option when complete", function() { + makeController(); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var object = makeObject(tab); + $scope.tabs[tab].manager._items.push(object); + $scope.tabs[tab].manager._selectedItems.push(object); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs[tab].actionOption).toBeNull(); + }); + + it(`increments actionProgress.completed + after action error`, function() { + makeController(); + var object = makeObject(tab); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionGo(tab); + defer.reject(makeName("error")); + $scope.$digest(); + expect($scope.tabs[tab].actionProgress.completed).toBe(1); + }); + + it("adds error to actionProgress.errors on action error", function() { + makeController(); + var object = makeObject(tab); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + $scope.tabs[tab].actionOption = { name: "start" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.actionGo(tab); + var error = makeName("error"); + defer.reject(error); + $scope.$digest(); + var errorObjects = $scope.tabs[tab].actionProgress.errors[error]; + expect(errorObjects[0].system_id).toBe(object.system_id); + }); + }); + + describe("hasActionsInProgress", function() { + it("returns false if actionProgress.total not > 0", function() { + makeController(); + $scope.tabs[tab].actionProgress.total = 0; + expect($scope.hasActionsInProgress(tab)).toBe(false); + }); + + it("returns true if actionProgress total != completed", function() { + makeController(); + $scope.tabs[tab].actionProgress.total = 1; + $scope.tabs[tab].actionProgress.completed = 0; + expect($scope.hasActionsInProgress(tab)).toBe(true); + }); + + it("returns false if actionProgress total == completed", function() { + makeController(); + $scope.tabs[tab].actionProgress.total = 1; + $scope.tabs[tab].actionProgress.completed = 1; + expect($scope.hasActionsInProgress(tab)).toBe(false); + }); + }); + + describe("hasActionsFailed", function() { + it("returns false if no errors", function() { + makeController(); + $scope.tabs[tab].actionProgress.errors = {}; + expect($scope.hasActionsFailed(tab)).toBe(false); + }); + + it("returns true if errors", function() { + makeController(); + var error = makeName("error"); + var object = makeObject(tab); + var errors = $scope.tabs[tab].actionProgress.errors; + errors[error] = [object]; + expect($scope.hasActionsFailed(tab)).toBe(true); + }); + }); + + describe("actionSetZone", function() { + it("calls performAction with zone", function() { + makeController(); + var spy = spyOn( + $scope.tabs[tab].manager, + "performAction" + ).and.returnValue($q.defer().promise); + var object = makeObject(tab); + $scope.tabs[tab].actionOption = { name: "set-zone" }; + $scope.tabs[tab].selectedItems = [object]; + $scope.tabs[tab].zoneSelection = { id: 1 }; + $scope.actionGo(tab); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "set-zone", { zone_id: 1 }); + }); + + it("clears action option when successfully complete", function() { + makeController(); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var object = makeObject(tab); + $scope.tabs[tab].manager._items.push(object); + $scope.tabs[tab].manager._selectedItems.push(object); + $scope.tabs[tab].actionOption = { name: "set-zone" }; + $scope.tabs[tab].zoneSelection = { id: 1 }; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs[tab].zoneSelection).toBeNull(); + }); + }); + + describe("actionSetPool", function() { + it("calls performAction with pool", function() { + makeController(); + var spy = spyOn( + $scope.tabs[tab].manager, + "performAction" + ).and.returnValue($q.defer().promise); + var object = makeObject(tab); + var tabScope = $scope.tabs[tab]; + tabScope.actionOption = { name: "set-pool" }; + tabScope.selectedItems = [object]; + tabScope.poolAction = "select-pool"; + tabScope.poolSelection = { id: 1 }; + $scope.actionGo(tab); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "set-pool", { pool_id: 1 }); + }); + + it("calls performAction with new pool data", function() { + makeController(); + var createDefer = $q.defer(); + var createSpy = spyOn( + ResourcePoolsManager, + "createItem" + ).and.returnValue(createDefer.promise); + var performSpy = spyOn( + $scope.tabs[tab].manager, + "performAction" + ).and.returnValue($q.defer().promise); + var object = makeObject(tab); + var newPoolData = { + name: "my-pool", + description: "desc" + }; + var tabScope = $scope.tabs[tab]; + tabScope.actionOption = { name: "set-pool" }; + tabScope.selectedItems = [object]; + tabScope.poolSelection = null; + tabScope.poolAction = "create-pool"; + tabScope.newPool = newPoolData; + $scope.actionGo(tab); + createDefer.resolve({ id: 84 }); + $scope.$digest(); + expect(performSpy).toHaveBeenCalledWith(object, "set-pool", { + pool_id: 84 + }); + expect(createSpy).toHaveBeenCalledWith({ name: newPoolData.name }); + }); + + it("clears action option when successfully complete", function() { + makeController(); + var defer = $q.defer(); + spyOn($scope.tabs[tab].manager, "performAction").and.returnValue( + defer.promise + ); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var object = makeObject(tab); + $scope.tabs[tab].manager._items.push(object); + $scope.tabs[tab].manager._selectedItems.push(object); + $scope.tabs[tab].actionOption = { name: "set-pool" }; + $scope.tabs[tab].poolSelection = { id: 1 }; + $scope.actionGo(tab); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs[tab].poolSelection).toBeNull(); + }); + }); + }); + }); + + describe("tab(nodes)", function() { + describe("actionGo", function() { + it("calls performAction with osystem and distro_series", function() { + makeController(); + var object = makeObject("machines"); + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + $scope.tabs.machines.actionOption = { name: "deploy" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.tabs.machines.osSelection.osystem = "ubuntu"; + $scope.tabs.machines.osSelection.release = "ubuntu/trusty"; + $scope.actionGo("machines"); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "deploy", { + osystem: "ubuntu", + distro_series: "trusty", + install_kvm: false }); + }); - it("reports isPoolAction false when no action", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 1, name: 'foo'}; - expect(tab.isPoolAction(pool, 'action')).toBe(false); + it("calls performAction with tag", function() { + makeController(); + var object = makeObject("machines"); + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + + $scope.tabs.machines.actionOption = { name: "tag" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.tags = [{ text: "foo" }, { text: "bar" }, { text: "baz" }]; + $scope.actionGo("machines"); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "tag", { + tags: ["foo", "bar", "baz"] }); + }); - it("reports isPoolAction true when action on the pool", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 1, name: 'foo'}; - tab.initiatePoolAction(pool, 'action'); - expect(tab.isPoolAction(pool, 'action')).toBe(true); + it("calls performAction with install_kvm", function() { + makeController(); + var object = makeObject("machines"); + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + $scope.tabs.machines.actionOption = { name: "deploy" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.tabs.machines.osSelection.osystem = "debian"; + $scope.tabs.machines.osSelection.release = "etch"; + $scope.tabs.machines.deployOptions.installKVM = true; + $scope.actionGo("machines"); + $scope.$digest(); + // When deploying KVM, coerce the distro to ubuntu/bionic. + expect(spy).toHaveBeenCalledWith(object, "deploy", { + osystem: "ubuntu", + distro_series: "bionic", + install_kvm: true }); + }); - it("reports isPoolAction true with any action", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 1, name: 'foo'}; - tab.initiatePoolAction(pool, 'action'); - expect(tab.isPoolAction(pool)).toBe(true); - }); + it(`clears selected os and release when + successfully complete`, function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var object = makeObject("machines"); + MachinesManager._items.push(object); + MachinesManager._selectedItems.push(object); + $scope.tabs.machines.actionOption = { name: "deploy" }; + $scope.tabs.machines.osSelection.osystem = "ubuntu"; + $scope.tabs.machines.osSelection.release = "ubuntu/trusty"; + $scope.actionGo("machines"); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs.machines.osSelection.$reset).toHaveBeenCalled(); + }); - it("reports isPoolAction false when action on other pool", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool1 = {id: 1, name: 'foo'}; - var pool2 = {id: 2, name: 'bar'}; - tab.initiatePoolAction(pool1, 'action'); - expect(tab.isPoolAction(pool2, 'action')).toBe(false); + it("calls performAction with commissionOptions", function() { + makeController(); + var object = makeObject("machines"); + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + var commissioning_scripts_ids = [ + makeInteger(0, 100), + makeInteger(0, 100) + ]; + var testing_scripts_ids = [makeInteger(0, 100), makeInteger(0, 100)]; + $scope.tabs.machines.actionOption = { name: "commission" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.tabs.machines.commissionOptions.enableSSH = true; + $scope.tabs.machines.commissionOptions.skipBMCConfig = false; + $scope.tabs.machines.commissionOptions.skipNetworking = false; + $scope.tabs.machines.commissionOptions.skipStorage = false; + $scope.tabs.machines.commissionOptions.updateFirmware = true; + $scope.tabs.machines.commissionOptions.configureHBA = true; + $scope.tabs.machines.commissioningSelection = []; + angular.forEach(commissioning_scripts_ids, function(script_id) { + $scope.tabs.machines.commissioningSelection.push({ + id: script_id, + name: makeName("script_name") + }); + }); + $scope.tabs.machines.testSelection = []; + angular.forEach(testing_scripts_ids, function(script_id) { + $scope.tabs.machines.testSelection.push({ + id: script_id, + name: makeName("script_name") + }); + }); + $scope.actionGo("machines"); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "commission", { + enable_ssh: true, + skip_bmc_config: false, + skip_networking: false, + skip_storage: false, + commissioning_scripts: commissioning_scripts_ids.concat([ + "update_firmware", + "configure_hba" + ]), + testing_scripts: testing_scripts_ids }); + }); - it("reports isPoolAction false when different action", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 1, name: 'foo'}; - tab.initiatePoolAction(pool, 'action'); - expect(tab.isPoolAction(pool, 'other-action')).toBe(false); + it("calls performAction with testOptions", function() { + makeController(); + var object = makeObject("machines"); + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + var testing_script_ids = [makeInteger(0, 100), makeInteger(0, 100)]; + $scope.tabs.machines.actionOption = { name: "test" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.tabs.machines.commissionOptions.enableSSH = true; + $scope.tabs.machines.testSelection = []; + angular.forEach(testing_script_ids, function(script_id) { + $scope.tabs.machines.testSelection.push({ + id: script_id, + name: makeName("script_name") + }); + }); + $scope.actionGo("machines"); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "test", { + enable_ssh: true, + testing_scripts: testing_script_ids }); + }); - it("reports isDefaultPool false", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 1, name: 'foo'}; - expect(tab.isDefaultPool(pool)).toBe(false); - }); + it("sets showing_confirmation with testOptions", function() { + makeController(); + var object = makeObject("machines"); + object.status_code = 6; + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + $scope.tabs.machines.actionOption = { name: "test" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.actionGo("machines"); + expect( + $scope.tabs["machines"].actionProgress.showing_confirmation + ).toBe(true); + expect( + $scope.tabs["machines"].actionProgress.confirmation_message + ).not.toBe(""); + expect($scope.tabs["machines"].actionProgress.affected_nodes).toBe(1); + expect(spy).not.toHaveBeenCalled(); + }); - it("reports isDefaultPool true", function() { - makeController(); - var tab = $scope.tabs.pools; - var pool = {id: 0, name: 'foo'}; - expect(tab.isDefaultPool(pool)).toBe(true); + it("calls performAction with releaseOptions", function() { + makeController(); + var object = makeObject("machines"); + var spy = spyOn( + $scope.tabs.machines.manager, + "performAction" + ).and.returnValue($q.defer().promise); + var secureErase = makeName("secureErase"); + var quickErase = makeName("quickErase"); + $scope.tabs.machines.actionOption = { name: "release" }; + $scope.tabs.machines.selectedItems = [object]; + $scope.tabs.machines.releaseOptions.erase = true; + $scope.tabs.machines.releaseOptions.secureErase = secureErase; + $scope.tabs.machines.releaseOptions.quickErase = quickErase; + $scope.actionGo("machines"); + $scope.$digest(); + expect(spy).toHaveBeenCalledWith(object, "release", { + erase: true, + secure_erase: secureErase, + quick_erase: quickErase }); + }); - it("switches to the machine tab with pool filter", function() { - makeController(); - var machinesTab = $scope.tabs.machines; - var pool = {id: 10, name: 'foo'}; - $scope.tabs.pools.goToPoolMachines(pool); - expect(machinesTab.search).toEqual('pool:(=foo)'); - expect($location.path()).toEqual('/machines'); - }); - }); + it("sets showing_confirmation with deleteOptions", function() { + // Regression test for LP:1793478 + makeController(); + var object = makeObject("controllers"); + $scope.vlans = [ + { + id: 0, + primary_rack: object.system_id, + name: "Default VLAN" + } + ]; + var spy = spyOn( + $scope.tabs.controllers.manager, + "performAction" + ).and.returnValue($q.defer().promise); + $scope.tabs.controllers.actionOption = { name: "delete" }; + $scope.tabs.controllers.selectedItems = [object]; + $scope.actionGo("controllers"); + expect( + $scope.tabs["controllers"].actionProgress.showing_confirmation + ).toBe(true); + expect( + $scope.tabs["controllers"].actionProgress.confirmation_message + ).not.toBe(""); + expect( + $scope.tabs["controllers"].actionProgress.confirmation_details + ).not.toBe([]); + expect($scope.tabs["controllers"].actionProgress.affected_nodes).toBe( + 1 + ); + expect(spy).not.toHaveBeenCalled(); + }); - describe("unselectImpossibleNodes", function() { - it("unselects machines for which an action cannot be done", function() { - makeController(); - var machinePossible = makeObject('machines'); - var machineImpossible = makeObject('machines'); - var tab = $scope.tabs.machines; - - tab.actionOption = { name: 'commission' }; - machinePossible.actions = ['commission']; - machineImpossible.actions = ['deploy']; - MachinesManager._items.push(machinePossible, machineImpossible); - MachinesManager._selectedItems.push( - machinePossible, machineImpossible - ); + it("clears commissionOptions when successfully complete", function() { + makeController(); + var defer = $q.defer(); + spyOn(MachinesManager, "performAction").and.returnValue(defer.promise); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var object = makeObject("machines"); + MachinesManager._items.push(object); + MachinesManager._selectedItems.push(object); + $scope.tabs.machines.actionOption = { name: "commission" }; + $scope.tabs.machines.commissionOptions.enableSSH = true; + $scope.tabs.machines.commissionOptions.skipNetworking = true; + $scope.tabs.machines.commissionOptions.skipStorage = true; + $scope.tabs.machines.commissionOptions.updateFirmware = true; + $scope.tabs.machines.commissionOptions.configureHBA = true; + $scope.tabs.machines.commissioningSelection = [ + { + id: makeInteger(0, 100), + name: makeName("script_name") + } + ]; + $scope.tabs.machines.testSelection = [ + { + id: makeInteger(0, 100), + name: makeName("script_name") + } + ]; - $scope.unselectImpossibleNodes('machines'); + $scope.actionGo("machines"); + defer.resolve(); + $scope.$digest(); + expect($scope.tabs.machines.commissionOptions).toEqual({ + enableSSH: false, + skipBMCConfig: false, + skipNetworking: false, + skipStorage: false, + updateFirmware: false, + configureHBA: false + }); + expect($scope.tabs.machines.commissioningSelection).toEqual([]); + expect($scope.tabs.machines.testSelection).toEqual([]); + }); + }); + }); + + describe("tab(pools)", function() { + it("sets the actionOption when addPool is called", function() { + makeController(); + var poolsTab = $scope.tabs.pools; + expect(poolsTab.actionOption).toBe(false); + poolsTab.addPool(); + expect(poolsTab.actionOption).toBe(true); + }); + + it(`resets actionOption and newPool when + cancelAddPool is called`, function() { + makeController(); + var poolsTab = $scope.tabs.pools; + poolsTab.addPool(); + (poolsTab.newPool = { name: "mypool" }), poolsTab.cancelAddPool(); + expect(poolsTab.actionOption).toBe(false); + expect(poolsTab.newPool).toEqual({}); + }); + }); + + describe("addHardwareOptionChanged", function() { + it("calls show in addHardwareScope", function() { + makeController(); + $scope.addHardwareScope = { + show: jasmine.createSpy("show") + }; + $scope.addHardwareOption = { + name: "hardware" + }; + $scope.addHardwareOptionChanged(); + expect($scope.addHardwareScope.show).toHaveBeenCalledWith("hardware"); + }); + }); + + describe("addDevice", function() { + it("calls show in addDeviceScope", function() { + makeController(); + $scope.addDeviceScope = { + show: jasmine.createSpy("show") + }; + $scope.addDevice(); + expect($scope.addDeviceScope.show).toHaveBeenCalled(); + }); + }); + + describe("cancelAddDevice", function() { + it("calls cancel in addDeviceScope", function() { + makeController(); + $scope.addDeviceScope = { + cancel: jasmine.createSpy("cancel") + }; + $scope.cancelAddDevice(); + expect($scope.addDeviceScope.cancel).toHaveBeenCalled(); + }); + }); + + describe("getDeviceIPAssignment", function() { + it("returns 'External' for external assignment", function() { + makeController(); + expect($scope.getDeviceIPAssignment("external")).toBe("External"); + }); + + it("returns 'Dynamic' for dynamic assignment", function() { + makeController(); + expect($scope.getDeviceIPAssignment("dynamic")).toBe("Dynamic"); + }); + + it("returns 'Static' for static assignment", function() { + makeController(); + expect($scope.getDeviceIPAssignment("static")).toBe("Static"); + }); + }); + + describe("hasCustomCommissioningScripts", function() { + it("returns true with custom commissioning scripts", function() { + makeController(); + ScriptsManager._items.push({ script_type: 0 }); + expect($scope.hasCustomCommissioningScripts()).toBe(true); + }); + it("returns false without custom commissioning scripts", function() { + makeController(); + expect($scope.hasCustomCommissioningScripts()).toBe(false); + }); + }); + + describe("showswitches", function() { + it("is true if switches=on", function() { + $routeParams.switches = "on"; + makeController(); + expect($scope.showswitches).toBe(true); + }); + it("is false if switches=off", function() { + $routeParams.switches = "off"; + makeController(); + expect($scope.showswitches).toBe(false); + }); + it("is false if switches is not specified", function() { + makeController(); + expect($scope.showswitches).toBe(false); + }); + }); + + describe("resource pools listing", function() { + it("sets active target with initiatePoolAction", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 1, name: "foo" }; + tab.initiatePoolAction(pool, "action"); + expect(tab.activeTargetAction).toEqual("action"); + expect(tab.activeTarget).toEqual(pool); + }); + + it("unsets target with cancelPoolAction", function() { + makeController(); + var tab = $scope.tabs.pools; + tab.initiatePoolAction({ id: 1, name: "foo" }, "action"); + tab.cancelPoolAction(); + expect(tab.activeTargetAction).toBe(null); + expect(tab.activeTarget).toBe(null); + }); + + it("reports isPoolAction false when no action", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 1, name: "foo" }; + expect(tab.isPoolAction(pool, "action")).toBe(false); + }); + + it("reports isPoolAction true when action on the pool", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 1, name: "foo" }; + tab.initiatePoolAction(pool, "action"); + expect(tab.isPoolAction(pool, "action")).toBe(true); + }); + + it("reports isPoolAction true with any action", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 1, name: "foo" }; + tab.initiatePoolAction(pool, "action"); + expect(tab.isPoolAction(pool)).toBe(true); + }); + + it("reports isPoolAction false when action on other pool", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool1 = { id: 1, name: "foo" }; + var pool2 = { id: 2, name: "bar" }; + tab.initiatePoolAction(pool1, "action"); + expect(tab.isPoolAction(pool2, "action")).toBe(false); + }); + + it("reports isPoolAction false when different action", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 1, name: "foo" }; + tab.initiatePoolAction(pool, "action"); + expect(tab.isPoolAction(pool, "other-action")).toBe(false); + }); + + it("reports isDefaultPool false", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 1, name: "foo" }; + expect(tab.isDefaultPool(pool)).toBe(false); + }); + + it("reports isDefaultPool true", function() { + makeController(); + var tab = $scope.tabs.pools; + var pool = { id: 0, name: "foo" }; + expect(tab.isDefaultPool(pool)).toBe(true); + }); + + it("switches to the machine tab with pool filter", function() { + makeController(); + var machinesTab = $scope.tabs.machines; + var pool = { id: 10, name: "foo" }; + $scope.tabs.pools.goToPoolMachines(pool); + expect(machinesTab.search).toEqual("pool:(=foo)"); + expect($location.path()).toEqual("/machines"); + }); + }); + + describe("unselectImpossibleNodes", function() { + it("unselects machines for which an action cannot be done", function() { + makeController(); + var machinePossible = makeObject("machines"); + var machineImpossible = makeObject("machines"); + var tab = $scope.tabs.machines; + + tab.actionOption = { name: "commission" }; + machinePossible.actions = ["commission"]; + machineImpossible.actions = ["deploy"]; + MachinesManager._items.push(machinePossible, machineImpossible); + MachinesManager._selectedItems.push(machinePossible, machineImpossible); + + $scope.unselectImpossibleNodes("machines"); + + expect(tab.selectedItems).toEqual([machinePossible]); + }); + }); + + describe("updateFailedActionSentence", () => { + it("correctly sets $scope.failedActionSentence", () => { + makeController(); + const tab = $scope.tabs.machines; + tab.actionOption = { name: "override-failed-testing" }; + tab.actionErrorCount = 2; + + expect($scope.failedActionSentence).toEqual( + "Action cannot be performed." + ); + $scope.updateFailedActionSentence("machines"); + expect($scope.failedActionSentence).toEqual( + "Cannot override failed tests on 2 machines." + ); + }); + }); + + describe("getHardwareTestErrorText", function() { + it( + "returns correct string if 'Unable to run destructive" + + " test while deployed!'", + function() { + makeController(); + var tab = "machines"; + $scope.tabs[tab].selectedItems = ["foo", "bar", "baz"]; + expect( + $scope.getHardwareTestErrorText( + "Unable to run destructive test while deployed!", + tab + ) + ).toBe( + "3 machines cannot run hardware testing. The selected hardware" + + " tests contain one or more destructive tests." + + " Destructive tests cannot run on deployed machines." + ); + } + ); + + it("returns singular error string if only one selectedItem", function() { + makeController(); + var tab = "machines"; + $scope.tabs[tab].selectedItems = ["foo"]; + expect( + $scope.getHardwareTestErrorText( + "Unable to run destructive test while deployed!", + tab + ) + ).toBe( + "1 machine cannot run hardware testing. The selected hardware tests" + + " contain one or more destructive tests. Destructive tests cannot" + + " run on deployed machines." + ); + }); + + it( + "returns error string if not 'Unable to run destructive " + + "test while deployed!'", + function() { + makeController(); + var tab = "machines"; + $scope.tabs[tab].selectedItems = []; + var errorString = "There was an error"; + expect($scope.getHardwareTestErrorText(errorString, tab)).toBe( + errorString + ); + } + ); + }); + + describe("getFailedTests", () => { + it("calls MachinesManager.getLatestFailedTests", () => { + makeController(); + const nodes = [makeObject("machines"), makeObject("machines")]; + const tab = $scope.tabs.machines; + tab.selectedItems = nodes; + const defer = $q.defer(); + spyOn(MachinesManager, "getLatestFailedTests").and.returnValue( + defer.promise + ); + $scope.getFailedTests("machines"); + + expect(MachinesManager.getLatestFailedTests).toHaveBeenCalledWith(nodes); + }); + }); + + describe("getFailedTestCount", () => { + it("correctly sums failed tests for each node", () => { + makeController(); + const nodes = [makeObject("machines"), makeObject("machines")]; + const tab = $scope.tabs.machines; + tab.selectedItems = nodes; + tab.failedTests = { + [nodes[0].system_id]: [ + { name: makeName("script") }, + { name: makeName("script") } + ], + [nodes[1].system_id]: [ + { name: makeName("script") }, + { name: makeName("script") } + ] + }; - expect(tab.selectedItems).toEqual([machinePossible]); - }); + expect($scope.getFailedTestCount("machines")).toEqual(4); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_pod_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -1,991 +1,987 @@ -/* Copyright 2017-2018 Canonical Ltd. This software is licensed under the +/* Copyright 2017-2019 Canonical Ltd. This software is licensed under the * GNU Affero General Public License version 3 (see the file LICENSE). * * Unit tests for PodDetailsController. */ -// Make a fake user. -var userId = 0; -function makeUser() { - return { - id: userId++, - username: makeName("username"), - first_name: makeName("first_name"), - last_name: makeName("last_name"), - email: makeName("email"), - is_superuser: false, - sshkeys_count: 0 - }; -} +import { makeName } from "testing/utils"; describe("PodDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load the required managers. - var PodsManager, UsersManager, GeneralManager, DomainsManager; - var ZonesManager, ManagerHelperService, ErrorService; - var SubnetsManager, VLANsManager, FabricsManager, SpacesManager; - var ResourcePoolsManager; - beforeEach(inject(function($injector) { - PodsManager = $injector.get("PodsManager"); - UsersManager = $injector.get("UsersManager"); - GeneralManager = $injector.get("GeneralManager"); - DomainsManager = $injector.get("DomainsManager"); - ZonesManager = $injector.get("ZonesManager"); - MachinesManager = $injector.get("MachinesManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - SubnetsManager = $injector.get("SubnetsManager"); - VLANsManager = $injector.get("VLANsManager"); - FabricsManager = $injector.get("FabricsManager"); - SpacesManager = $injector.get("SpacesManager"); - ResourcePoolsManager = $injector.get("ResourcePoolsManager"); - })); - - // Mock the websocket connection to the region - var RegionConnection, webSocket; - beforeEach(inject(function($injector) { - RegionConnection = $injector.get("RegionConnection"); - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Makes a fake node/device. - var podId = 0; - function makePod() { - var pod = { - id: podId++, - default_pool: 0, - $selected: false, - capabilities: [], - permissions: [] - }; - PodsManager._items.push(pod); - return pod; - } - - // Create the pod that will be used and set the routeParams. - var pod, $routeParams; - beforeEach(function() { - pod = makePod(); - domain = {id: 0}; - DomainsManager._items.push(domain); - zone = {id: 0}; - ZonesManager._items.push(domain); - $routeParams = { - id: pod.id - }; - }); - - // Makes the PodsListController - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - // Create the controller. - var controller = $controller("PodDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $location: $location, - $routeParams: $routeParams, - PodsManager: PodsManager, - UsersManager: UsersManager, - DomainsManager: DomainsManager, - ZonesManager: ZonesManager, - MachinesManager: MachinesManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService, - SubnetsManager: SubnetsManager, - VLANsManager: VLANsManager, - FabricsManager: FabricsManager, - SpacesManager: SpacesManager, - ResourcePoolsManager: ResourcePoolsManager - }); - - return controller; - } - - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(PodsManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(pod); - $rootScope.$digest(); + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load the required managers. + var PodsManager, UsersManager, GeneralManager, DomainsManager; + var ZonesManager, ManagerHelperService, ErrorService; + var SubnetsManager, VLANsManager, FabricsManager, SpacesManager; + var ResourcePoolsManager, MachinesManager; + beforeEach(inject(function($injector) { + PodsManager = $injector.get("PodsManager"); + UsersManager = $injector.get("UsersManager"); + GeneralManager = $injector.get("GeneralManager"); + DomainsManager = $injector.get("DomainsManager"); + ZonesManager = $injector.get("ZonesManager"); + MachinesManager = $injector.get("MachinesManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + SubnetsManager = $injector.get("SubnetsManager"); + VLANsManager = $injector.get("VLANsManager"); + FabricsManager = $injector.get("FabricsManager"); + SpacesManager = $injector.get("SpacesManager"); + ResourcePoolsManager = $injector.get("ResourcePoolsManager"); + })); + + // Mock the websocket connection to the region + var RegionConnection, webSocket; + beforeEach(inject(function($injector) { + RegionConnection = $injector.get("RegionConnection"); + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Makes a fake node/device. + var podId = 0; + function makePod() { + var pod = { + id: podId++, + default_pool: 0, + $selected: false, + capabilities: [], + permissions: [] + }; + PodsManager._items.push(pod); + return pod; + } + + // Create the pod that will be used and set the routeParams. + var pod, $routeParams; + beforeEach(function() { + pod = makePod(); + const domain = { id: 0 }; + DomainsManager._items.push(domain); + ZonesManager._items.push(domain); + $routeParams = { + id: pod.id + }; + }); - return controller; + // Makes the PodsListController + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("pods"); - }); - - it("sets initial values on $scope", function() { - // tab-independent variables. - makeController(); - expect($scope.pod).toBeNull(); - expect($scope.loaded).toBe(false); - expect($scope.action.option).toBeNull(); - expect($scope.action.inProgress).toBe(false); - expect($scope.action.error).toBeNull(); - expect($scope.compose).toEqual({ - action: { - name: 'compose', - title: 'Compose', - sentence: 'compose' - }, - obj: { - storage: [{ - type: 'local', - size: 8, - tags: [], - pool: {}, - boot: true - }], - interfaces: [{ - name: 'default' - }], - requests: [] + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + // Create the controller. + var controller = $controller("PodDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $location: $location, + $routeParams: $routeParams, + PodsManager: PodsManager, + UsersManager: UsersManager, + DomainsManager: DomainsManager, + ZonesManager: ZonesManager, + MachinesManager: MachinesManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService, + SubnetsManager: SubnetsManager, + VLANsManager: VLANsManager, + FabricsManager: FabricsManager, + SpacesManager: SpacesManager, + ResourcePoolsManager: ResourcePoolsManager + }); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(PodsManager, "setActiveItem").and.returnValue(setActiveDefer.promise); + var defer = $q.defer(); + var controller = makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(pod); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("pods"); + }); + + it("sets initial values on $scope", function() { + // tab-independent variables. + makeController(); + expect($scope.pod).toBeNull(); + expect($scope.loaded).toBe(false); + expect($scope.action.option).toBeNull(); + expect($scope.action.inProgress).toBe(false); + expect($scope.action.error).toBeNull(); + expect($scope.compose).toEqual({ + action: { + name: "compose", + title: "Compose", + sentence: "compose" + }, + obj: { + storage: [ + { + type: "local", + size: 8, + tags: [], + pool: {}, + boot: true } - }); - expect($scope.power_types).toBe(GeneralManager.getData('power_types')); - expect($scope.domains).toBe(DomainsManager.getItems()); - expect($scope.zones).toBe(ZonesManager.getItems()); - expect($scope.pools).toBe(ResourcePoolsManager.getItems()); - expect($scope.editing).toBe(false); - }); + ], + interfaces: [ + { + name: "default" + } + ], + requests: [] + } + }); + expect($scope.power_types).toBe(GeneralManager.getData("power_types")); + expect($scope.domains).toBe(DomainsManager.getItems()); + expect($scope.zones).toBe(ZonesManager.getItems()); + expect($scope.pools).toBe(ResourcePoolsManager.getItems()); + expect($scope.editing).toBe(false); + }); - it("calls loadManagers with PodsManager, UsersManager, GeneralManager, \ + it("calls loadManagers with PodsManager, UsersManager, GeneralManager, \ DomainsManager, ZonesManager, SubnetsManager, VLANsManager, \ FabricsManager, SpacesManager, MachinesManager", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, - [ - PodsManager, - GeneralManager, - UsersManager, - DomainsManager, - ZonesManager, - MachinesManager, - ResourcePoolsManager, - SubnetsManager, - VLANsManager, - FabricsManager, - SpacesManager - ]); - }); - - it("sets loaded and title when loadManagers resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.loaded).toBe(true); - expect($scope.title).toBe('Pod ' + pod.name); - }); - - describe("stripTrailingZero", function() { - it("removes decimal point if zero", function() { - makeController(); - expect($scope.stripTrailingZero(41.0)).toBe('41'); - }); - - it("doesn't strip decimal point if not zero", function() { - makeController(); - expect($scope.stripTrailingZero(42.2)).toBe('42.2'); - }); - }); - - describe("isRackControllerConnected", function() { - it("returns false no power_types", function() { - makeController(); - $scope.power_types = []; - expect($scope.isRackControllerConnected()).toBe(false); - }); - - it("returns true if power_types", function() { - makeController(); - $scope.power_types = [{}]; - expect($scope.isRackControllerConnected()).toBe(true); - }); - }); - - describe("canEdit", function() { - it("returns false if no pod", function() { - makeController(); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canEdit()).toBe(false); - }); - - it("returns false if no pod permissions", function() { - makeController(); - $scope.pod = makePod(); - delete $scope.pod.permissions; - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canEdit()).toBe(false); - }); - - it("returns false if no edit permission", function() { - makeController(); - $scope.pod = makePod(); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canEdit()).toBe(false); - }); - - it("returns false if rack disconnected", function() { - makeController(); - $scope.pod = makePod(); - $scope.pod.permissions.push('edit'); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(false); - expect($scope.canEdit()).toBe(false); - }); - - it("returns true if super user, rack connected", function() { - makeController(); - $scope.pod = makePod(); - $scope.pod.permissions.push('edit'); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canEdit()).toBe(true); - }); - }); - - describe("editName", function() { - - it("doesnt set editing true", - function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(false); - $scope.name.editing = false; - $scope.editName(); - expect($scope.name.editing).toBe(false); - }); - - it("sets editing to true", - function() { - makeController(); - $scope.pod = pod; - spyOn($scope, "canEdit").and.returnValue(true); - $scope.name.editing = false; - $scope.editName(); - expect($scope.name.editing).toBe(true); - }); - - it("sets name.value to pod name", function() { - makeController(); - $scope.pod = pod; - spyOn($scope, "canEdit").and.returnValue(true); - $scope.editName(); - expect($scope.name.value).toBe(pod.name); - }); - - it("doesnt reset name.value on multiple calls", function() { - makeController(); - $scope.pod = pod; - spyOn($scope, "canEdit").and.returnValue(true); - $scope.editName(); - var updatedName = makeName("name"); - $scope.name.value = updatedName; - $scope.editName(); - expect($scope.name.value).toBe(updatedName); - }); - }); - - describe("editNameInvalid", function() { - - it("returns false if not editing", function() { - makeController(); - $scope.name.editing = false; - $scope.name.value = "abc_invalid.local"; - expect($scope.editNameInvalid()).toBe(false); - }); - - it("returns true for bad values", function() { - makeController(); - $scope.name.editing = true; - var values = [ - { - input: "aB0-z", - output: false - }, - { - input: "abc_alpha", - output: true - }, - { - input: "ab^&c", - output: true - }, - { - input: "abc.local", - output: true - } - ]; - angular.forEach(values, function(value) { - $scope.name.value = value.input; - expect($scope.editNameInvalid()).toBe(value.output); - }); - }); - }); - - describe("cancelEditName", function() { - - it("sets editing to false for name section", - function() { - makeController(); - $scope.pod = pod; - $scope.name.editing = true; - $scope.cancelEditName(); - expect($scope.name.editing).toBe(false); - }); - - it("sets name.value back to original", function() { - makeController(); - $scope.pod = pod; - $scope.name.editing = true; - $scope.name.value = makeName("name"); - $scope.cancelEditName(); - expect($scope.name.value).toBe(pod.name); - }); - }); - - describe("saveEditName", function() { - - it("does nothing if value is invalid", function() { - makeController(); - $scope.pod = pod; - spyOn($scope, "editNameInvalid").and.returnValue(true); - var sentinel = {}; - $scope.name.editing = sentinel; - $scope.saveEditName(); - expect($scope.name.editing).toBe(sentinel); - }); - - it("sets editing to false", function() { - makeController(); - spyOn(PodsManager, "updateItem").and.returnValue( - $q.defer().promise); - spyOn($scope, "editNameInvalid").and.returnValue(false); - - $scope.pod = pod; - $scope.name.editing = true; - $scope.name.value = makeName("name"); - $scope.saveEditName(); - - expect($scope.name.editing).toBe(false); - }); - - it("calls updateItem with copy of pod", function() { - makeController(); - spyOn(PodsManager, "updateItem").and.returnValue( - $q.defer().promise); - spyOn($scope, "editNameInvalid").and.returnValue(false); - - $scope.pod = pod; - $scope.name.editing = true; - $scope.name.value = makeName("name"); - $scope.saveEditName(); - - var calledWithPod = PodsManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithPod).not.toBe(pod); - }); - - it("calls updateItem with new name on pod", function() { - makeController(); - spyOn(PodsManager, "updateItem").and.returnValue( - $q.defer().promise); - spyOn($scope, "editNameInvalid").and.returnValue(false); - - var newName = makeName("name"); - $scope.pod = pod; - $scope.name.editing = true; - $scope.name.value = newName; - $scope.saveEditName(); - - var calledWithPod = PodsManager.updateItem.calls.argsFor(0)[0]; - expect(calledWithPod.name).toBe(newName); - }); - - it("calls updateName once updateItem resolves", function() { - makeController(); - var defer = $q.defer(); - spyOn(PodsManager, "updateItem").and.returnValue( - defer.promise); - spyOn($scope, "editNameInvalid").and.returnValue(false); - - $scope.pod = pod; - $scope.name.editing = true; - $scope.name.value = makeName("name"); - $scope.saveEditName(); - - defer.resolve(pod); - $rootScope.$digest(); - - // Since updateName is private in the controller, check - // that the name.value is set to the pod's name. - expect($scope.name.value).toBe(pod.name); - }); - }); - - describe("editPodConfiguration", function() { - it("sets editing to true if can edit", - function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(true); - $scope.editing = false; - $scope.editPodConfiguration(); - expect($scope.editing).toBe(true); - }); - - it("doesnt set editing to true if cannot", - function() { - makeController(); - spyOn($scope, "canEdit").and.returnValue(false); - $scope.editing = false; - $scope.editPodConfiguration(); - expect($scope.editing).toBe(false); - }); - }); - - describe("exitEditPodConfiguration", function() { - it("sets editing to false on exiting pod configuration", - function() { - makeController(); - $scope.editing = true; - $scope.exitEditPodConfiguration(); - expect($scope.editing).toBe(false); - }); - }); - - describe("isActionError", function() { - - it("returns false if not action error", function() { - makeController(); - expect($scope.isActionError()).toBe(false); - }); - - it("returns true if action error", function() { - makeController(); - $scope.action.error = makeName("error"); - expect($scope.isActionError()).toBe(true); - }); - }); - - describe("actionOptionChanged", function() { - - it("clears action error", function() { - makeController(); - $scope.action.error = makeName("error"); - $scope.actionOptionChanged(); - expect($scope.action.error).toBeNull(); - }); - }); - - describe("actionCancel", function() { - - it("clears action error and option", function() { - makeController(); - $scope.action.error = makeName("error"); - $scope.action.option = {}; - $scope.actionCancel(); - expect($scope.action.error).toBeNull(); - expect($scope.action.option).toBeNull(); - }); - }); - - describe("actionGo", function() { - - it("performs action and sets and clears inProgress", function() { - makeControllerResolveSetActiveItem(); - var defer = $q.defer(); - var refresh = jasmine.createSpy('refresh'); - refresh.and.returnValue(defer.promise); - $scope.action.option = { - operation: refresh - }; - $scope.action.error = makeName("error"); - $scope.actionGo(); - expect($scope.action.inProgress).toBe(true); - expect(refresh).toHaveBeenCalledWith(pod); - - defer.resolve(); - $scope.$digest(); - expect($scope.action.inProgress).toBe(false); - expect($scope.action.option).toBeNull(); - expect($scope.action.error).toBeNull(); - }); - - it("performs action and sets error", function() { - makeControllerResolveSetActiveItem(); - var defer = $q.defer(); - var refresh = jasmine.createSpy('refresh'); - refresh.and.returnValue(defer.promise); - $scope.action.option = { - operation: refresh - }; - $scope.actionGo(); - expect($scope.action.inProgress).toBe(true); - expect(refresh).toHaveBeenCalledWith(pod); - - var error = makeName("error"); - defer.reject(error); - $scope.$digest(); - expect($scope.action.inProgress).toBe(false); - expect($scope.action.option).not.toBeNull(); - expect($scope.action.error).toBe(error); - }); - - it("changes path to pods listing on delete", function() { - makeControllerResolveSetActiveItem(); - var defer = $q.defer(); - var refresh = jasmine.createSpy('refresh'); - refresh.and.returnValue(defer.promise); - $scope.action.option = { - name: 'delete', - operation: refresh - }; - - spyOn($location, "path"); - $scope.actionGo(); - defer.resolve(); - $rootScope.$digest(); - expect($location.path).toHaveBeenCalledWith("/pods"); - }); - }); - - describe("totalStoragePercentage", function() { - it("returns the correct percentage", function() { - makeController(); - var storage_pool = { - 'used': 40, - 'total': 100 + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + PodsManager, + GeneralManager, + UsersManager, + DomainsManager, + ZonesManager, + MachinesManager, + ResourcePoolsManager, + SubnetsManager, + VLANsManager, + FabricsManager, + SpacesManager + ]); + }); + + it("sets loaded and title when loadManagers resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.loaded).toBe(true); + expect($scope.title).toBe("Pod " + pod.name); + }); + + describe("stripTrailingZero", function() { + it("removes decimal point if zero", function() { + makeController(); + expect($scope.stripTrailingZero(41.0)).toBe("41"); + }); + + it("doesn't strip decimal point if not zero", function() { + makeController(); + expect($scope.stripTrailingZero(42.2)).toBe("42.2"); + }); + }); + + describe("isRackControllerConnected", function() { + it("returns false no power_types", function() { + makeController(); + $scope.power_types = []; + expect($scope.isRackControllerConnected()).toBe(false); + }); + + it("returns true if power_types", function() { + makeController(); + $scope.power_types = [{}]; + expect($scope.isRackControllerConnected()).toBe(true); + }); + }); + + describe("canEdit", function() { + it("returns false if no pod", function() { + makeController(); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canEdit()).toBe(false); + }); + + it("returns false if no pod permissions", function() { + makeController(); + $scope.pod = makePod(); + delete $scope.pod.permissions; + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canEdit()).toBe(false); + }); + + it("returns false if no edit permission", function() { + makeController(); + $scope.pod = makePod(); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canEdit()).toBe(false); + }); + + it("returns false if rack disconnected", function() { + makeController(); + $scope.pod = makePod(); + $scope.pod.permissions.push("edit"); + spyOn($scope, "isRackControllerConnected").and.returnValue(false); + expect($scope.canEdit()).toBe(false); + }); + + it("returns true if super user, rack connected", function() { + makeController(); + $scope.pod = makePod(); + $scope.pod.permissions.push("edit"); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canEdit()).toBe(true); + }); + }); + + describe("editName", function() { + it("doesnt set editing true", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(false); + $scope.name.editing = false; + $scope.editName(); + expect($scope.name.editing).toBe(false); + }); + + it("sets editing to true", function() { + makeController(); + $scope.pod = pod; + spyOn($scope, "canEdit").and.returnValue(true); + $scope.name.editing = false; + $scope.editName(); + expect($scope.name.editing).toBe(true); + }); + + it("sets name.value to pod name", function() { + makeController(); + $scope.pod = pod; + spyOn($scope, "canEdit").and.returnValue(true); + $scope.editName(); + expect($scope.name.value).toBe(pod.name); + }); + + it("doesnt reset name.value on multiple calls", function() { + makeController(); + $scope.pod = pod; + spyOn($scope, "canEdit").and.returnValue(true); + $scope.editName(); + var updatedName = makeName("name"); + $scope.name.value = updatedName; + $scope.editName(); + expect($scope.name.value).toBe(updatedName); + }); + }); + + describe("editNameInvalid", function() { + it("returns false if not editing", function() { + makeController(); + $scope.name.editing = false; + $scope.name.value = "abc_invalid.local"; + expect($scope.editNameInvalid()).toBe(false); + }); + + it("returns true for bad values", function() { + makeController(); + $scope.name.editing = true; + var values = [ + { + input: "aB0-z", + output: false + }, + { + input: "abc_alpha", + output: true + }, + { + input: "ab^&c", + output: true + }, + { + input: "abc.local", + output: true + } + ]; + angular.forEach(values, function(value) { + $scope.name.value = value.input; + expect($scope.editNameInvalid()).toBe(value.output); + }); + }); + }); + + describe("cancelEditName", function() { + it("sets editing to false for name section", function() { + makeController(); + $scope.pod = pod; + $scope.name.editing = true; + $scope.cancelEditName(); + expect($scope.name.editing).toBe(false); + }); + + it("sets name.value back to original", function() { + makeController(); + $scope.pod = pod; + $scope.name.editing = true; + $scope.name.value = makeName("name"); + $scope.cancelEditName(); + expect($scope.name.value).toBe(pod.name); + }); + }); + + describe("saveEditName", function() { + it("does nothing if value is invalid", function() { + makeController(); + $scope.pod = pod; + spyOn($scope, "editNameInvalid").and.returnValue(true); + var sentinel = {}; + $scope.name.editing = sentinel; + $scope.saveEditName(); + expect($scope.name.editing).toBe(sentinel); + }); + + it("sets editing to false", function() { + makeController(); + spyOn(PodsManager, "updateItem").and.returnValue($q.defer().promise); + spyOn($scope, "editNameInvalid").and.returnValue(false); + + $scope.pod = pod; + $scope.name.editing = true; + $scope.name.value = makeName("name"); + $scope.saveEditName(); + + expect($scope.name.editing).toBe(false); + }); + + it("calls updateItem with copy of pod", function() { + makeController(); + spyOn(PodsManager, "updateItem").and.returnValue($q.defer().promise); + spyOn($scope, "editNameInvalid").and.returnValue(false); + + $scope.pod = pod; + $scope.name.editing = true; + $scope.name.value = makeName("name"); + $scope.saveEditName(); + + var calledWithPod = PodsManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithPod).not.toBe(pod); + }); + + it("calls updateItem with new name on pod", function() { + makeController(); + spyOn(PodsManager, "updateItem").and.returnValue($q.defer().promise); + spyOn($scope, "editNameInvalid").and.returnValue(false); + + var newName = makeName("name"); + $scope.pod = pod; + $scope.name.editing = true; + $scope.name.value = newName; + $scope.saveEditName(); + + var calledWithPod = PodsManager.updateItem.calls.argsFor(0)[0]; + expect(calledWithPod.name).toBe(newName); + }); + + it("calls updateName once updateItem resolves", function() { + makeController(); + var defer = $q.defer(); + spyOn(PodsManager, "updateItem").and.returnValue(defer.promise); + spyOn($scope, "editNameInvalid").and.returnValue(false); + + $scope.pod = pod; + $scope.name.editing = true; + $scope.name.value = makeName("name"); + $scope.saveEditName(); + + defer.resolve(pod); + $rootScope.$digest(); + + // Since updateName is private in the controller, check + // that the name.value is set to the pod's name. + expect($scope.name.value).toBe(pod.name); + }); + }); + + describe("editPodConfiguration", function() { + it("sets editing to true if can edit", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(true); + $scope.editing = false; + $scope.editPodConfiguration(); + expect($scope.editing).toBe(true); + }); + + it("doesnt set editing to true if cannot", function() { + makeController(); + spyOn($scope, "canEdit").and.returnValue(false); + $scope.editing = false; + $scope.editPodConfiguration(); + expect($scope.editing).toBe(false); + }); + }); + + describe("exitEditPodConfiguration", function() { + it("sets editing to false on exiting pod configuration", function() { + makeController(); + $scope.editing = true; + $scope.exitEditPodConfiguration(); + expect($scope.editing).toBe(false); + }); + }); + + describe("isActionError", function() { + it("returns false if not action error", function() { + makeController(); + expect($scope.isActionError()).toBe(false); + }); + + it("returns true if action error", function() { + makeController(); + $scope.action.error = makeName("error"); + expect($scope.isActionError()).toBe(true); + }); + }); + + describe("actionOptionChanged", function() { + it("clears action error", function() { + makeController(); + $scope.action.error = makeName("error"); + $scope.actionOptionChanged(); + expect($scope.action.error).toBeNull(); + }); + }); + + describe("actionCancel", function() { + it("clears action error and option", function() { + makeController(); + $scope.action.error = makeName("error"); + $scope.action.option = {}; + $scope.actionCancel(); + expect($scope.action.error).toBeNull(); + expect($scope.action.option).toBeNull(); + }); + }); + + describe("actionGo", function() { + it("performs action and sets and clears inProgress", function() { + makeControllerResolveSetActiveItem(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh"); + refresh.and.returnValue(defer.promise); + $scope.action.option = { + operation: refresh + }; + $scope.action.error = makeName("error"); + $scope.actionGo(); + expect($scope.action.inProgress).toBe(true); + expect(refresh).toHaveBeenCalledWith(pod); + + defer.resolve(); + $scope.$digest(); + expect($scope.action.inProgress).toBe(false); + expect($scope.action.option).toBeNull(); + expect($scope.action.error).toBeNull(); + }); + + it("performs action and sets error", function() { + makeControllerResolveSetActiveItem(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh"); + refresh.and.returnValue(defer.promise); + $scope.action.option = { + operation: refresh + }; + $scope.actionGo(); + expect($scope.action.inProgress).toBe(true); + expect(refresh).toHaveBeenCalledWith(pod); + + var error = makeName("error"); + defer.reject(error); + $scope.$digest(); + expect($scope.action.inProgress).toBe(false); + expect($scope.action.option).not.toBeNull(); + expect($scope.action.error).toBe(error); + }); + + it("changes path to pods listing on delete", function() { + makeControllerResolveSetActiveItem(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh"); + refresh.and.returnValue(defer.promise); + $scope.action.option = { + name: "delete", + operation: refresh + }; + + spyOn($location, "path"); + $scope.actionGo(); + defer.resolve(); + $rootScope.$digest(); + expect($location.path).toHaveBeenCalledWith("/pods"); + }); + }); + + describe("validateMachineCompose", function() { + it("returns true for empty string", function() { + makeController(); + $scope.compose.obj.hostname = ""; + expect($scope.validateMachineCompose()).toBe(true); + }); + + it("returns true for undefined", function() { + makeController(); + $scope.compose.obj.hostname = undefined; + expect($scope.validateMachineCompose()).toBe(true); + }); + + it("returns true for valid hostname", function() { + makeController(); + $scope.compose.obj.hostname = "testing-hostname"; + expect($scope.validateMachineCompose()).toBe(true); + }); + + it("returns false for invalid hostname", function() { + makeController(); + $scope.compose.obj.hostname = "testing_hostname"; + expect($scope.validateMachineCompose()).toBe(false); + }); + }); + + describe("totalStoragePercentage", function() { + it("returns the correct percentage", function() { + makeController(); + var storage_pool = { + used: 40, + total: 100 + }; + var storage = 10; + expect($scope.totalStoragePercentage(storage_pool, storage)).toBe(50); + }); + + it("returns the overcommitted percentage", function() { + makeController(); + var storage_pool = { + used: 90, + total: 100 + }; + var storage = 60; + expect($scope.totalStoragePercentage(storage_pool, storage)).toBe(150); + }); + }); + + describe("canCompose", function() { + it("returns false when no pod", function() { + makeController(); + expect($scope.canCompose()).toBe(false); + }); + + it("returns false when no compose permission", function() { + makeControllerResolveSetActiveItem(); + expect($scope.canCompose()).toBe(false); + }); + + it("returns false when not composable", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.permissions.push("compose"); + expect($scope.canCompose()).toBe(false); + }); + + it("returns true when composable", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.permissions.push("compose"); + $scope.pod.capabilities.push("composable"); + expect($scope.canCompose()).toBe(true); + }); + }); + + describe("composeMachine", function() { + it("sets action.options to compose.action", function() { + makeController(); + $scope.composeMachine(); + expect($scope.action.option).toBe($scope.compose.action); + }); + + it("sets action.options to compose.action", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.default_pool = 42; + $scope.composeMachine(); + $scope.$digest(); + expect($scope.compose.obj.pool).toBe(42); + }); + }); + + describe("composePreProcess", function() { + it("sets id to pod id", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.type = "rsd"; + expect($scope.composePreProcess({})).toEqual({ + id: $scope.pod.id, + storage: "0:8(local)", + interfaces: "" + }); + }); + + it("sets rsd storage based on compose.obj.storage", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.type = "rsd"; + $scope.compose.obj.storage = [ + { + type: "iscsi", + size: 20, + tags: [ + { + text: "one" + }, + { + text: "two" } - var storage = 10; - expect($scope.totalStoragePercentage( - storage_pool, storage)).toBe(50); - }); - - it("returns the overcommitted percentage", function() { - makeController(); - var storage_pool = { - 'used': 90, - 'total': 100 + ], + boot: false + }, + { + type: "local", + size: 50, + tags: [ + { + text: "happy" + }, + { + text: "days" } - var storage = 60; - expect($scope.totalStoragePercentage( - storage_pool, storage)).toBe(150); - }); - }); - - describe("canCompose", function() { - - it("returns false when no pod", function() { - makeController(); - expect($scope.canCompose()).toBe(false); - }); - - it("returns false when no compose permission", function() { - makeControllerResolveSetActiveItem(); - expect($scope.canCompose()).toBe(false); - }); - - it("returns false when not composable", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.permissions.push('compose'); - expect($scope.canCompose()).toBe(false); - }); - - it("returns true when composable", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.permissions.push('compose'); - $scope.pod.capabilities.push('composable'); - expect($scope.canCompose()).toBe(true); - }); - }); - - describe("composeMachine", function() { - - it("sets action.options to compose.action", function() { - makeController(); - $scope.composeMachine(); - expect($scope.action.option).toBe($scope.compose.action); - }); - - it("sets action.options to compose.action", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.default_pool = 42; - $scope.composeMachine(); - $scope.$digest(); - expect($scope.compose.obj.pool).toBe(42); - }); - }); - - describe("composePreProcess", function() { - - it("sets id to pod id", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.type = 'rsd'; - expect($scope.composePreProcess({})).toEqual({ - id: $scope.pod.id, - storage: '0:8(local)', - interfaces: '' - }); - }); - - it("sets rsd storage based on compose.obj.storage", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.type = 'rsd'; - $scope.compose.obj.storage = [ - { - type: 'iscsi', - size: 20, - tags: [{ - text: 'one' - }, { - text: 'two' - }], - boot: false - }, - { - type: 'local', - size: 50, - tags: [{ - text: 'happy' - }, { - text: 'days' - }], - boot: true - }, - { - type: 'local', - size: 60, - tags: [{ - text: 'other' - }], - boot: false - } - ]; - expect($scope.composePreProcess({})).toEqual({ - id: $scope.pod.id, - storage: ( - '0:50(local,happy,days),' + - '1:20(iscsi,one,two),2:60(local,other)'), - interfaces: '' - }); - }); - - it("sets the interface constraint for subnets", function() { - makeControllerResolveSetActiveItem(); - $scope.compose.obj.interfaces = [ - { - name: 'eth0', - subnet: {cidr: '172.16.4.0/24'} - }, - { - name: 'eth1', - subnet: {cidr: '192.168.1.0/24'} - } - ]; - var expectedInterfaces = [ - 'eth0:subnet_cidr=172.16.4.0/24', - 'eth1:subnet_cidr=192.168.1.0/24' - ].join(';') - expect($scope.composePreProcess({})).toEqual({ - id: $scope.pod.id, - storage: '0:8()', - interfaces: expectedInterfaces - }); - }); - - it("sets the interface constraint favouring ip addresses", function() { - makeControllerResolveSetActiveItem(); - $scope.compose.obj.interfaces = [ - { - name: 'eth0', - ipaddress: '172.16.4.2', - subnet: {cidr: '172.16.4.0/24'} - }, - { - name: 'eth1', - ipaddress: '192.168.1.5', - subnet: {cidr: '192.168.1.0/24'} - }, - { - name: 'eth2', - subnet: {cidr: '192.168.2.0/24'} - } - ]; - var expectedInterfaces = [ - 'eth0:ip=172.16.4.2', - 'eth1:ip=192.168.1.5', - 'eth2:subnet_cidr=192.168.2.0/24' - ].join(';') - expect($scope.composePreProcess({})).toEqual({ - id: $scope.pod.id, - storage: '0:8()', - interfaces: expectedInterfaces - }); - }); - - it("sets virsh storage based on compose.obj.storage", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.type = 'virsh'; - $scope.compose.obj.storage = [ - { - size: 20, - pool: { - name: 'pool1' - }, - tags: [{ - text: 'one' - }, { - text: 'two' - }], - boot: false - }, - { - size: 50, - pool: { - name: 'pool2' - }, - tags: [{ - text: 'happy' - }, { - text: 'days' - }], - boot: true - }, - { - size: 60, - pool: { - name: 'pool3' - }, - tags: [{ - text: 'other' - }], - boot: false - } - ]; - expect($scope.composePreProcess({})).toEqual({ - id: $scope.pod.id, - storage: ( - '0:50(pool2,happy,days),' + - '1:20(pool1,one,two),2:60(pool3,other)'), - interfaces: '' - }); - }); - - it("sets virsh storage based on compose.obj.storage", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.type = 'virsh'; - $scope.compose.obj.storage = [ - { - size: 20, - pool: { - name: 'pool1' - }, - tags: [{ - text: 'one' - }, { - text: 'two' - }], - boot: false - }, - { - size: 50, - pool: { - name: 'pool2' - }, - tags: [{ - text: 'happy' - }, { - text: 'days' - }], - boot: true - }, - { - size: 60, - pool: { - name: 'pool3' - }, - tags: [{ - text: 'other' - }], - boot: false - } - ]; - expect($scope.composePreProcess({})).toEqual({ - id: $scope.pod.id, - storage: ( - '0:50(pool2,happy,days),' + - '1:20(pool1,one,two),2:60(pool3,other)'), - interfaces: '' - }); - }); - }); - - describe("cancelCompose", function() { - - it("resets obj and action.option", function() { - makeControllerResolveSetActiveItem(); - var otherObj = {}; - $scope.compose.obj = otherObj; - $scope.action.option = {}; - $scope.cancelCompose(); - expect($scope.compose.obj).not.toBe(otherObj); - expect($scope.compose.obj).toEqual({ - storage: [{ - type: 'local', - size: 8, - tags: [], - pool: {}, - boot: true - }], - interfaces: [{ - name: 'default' - }], - requests: [] - }); - expect($scope.action.option).toBeNull(); - }); - }); - - describe("composeAddStorage", function() { - - it("adds a new local storage item", function() { - makeControllerResolveSetActiveItem(); - expect($scope.compose.obj.storage.length).toBe(1); - $scope.composeAddStorage(); - expect($scope.compose.obj.storage.length).toBe(2); - expect($scope.compose.obj.storage[1]).toEqual({ - type: 'local', - size: 8, - tags: [], - pool: {}, - boot: false - }); - }); - - it("adds a new iscsi storage item", function() { - makeControllerResolveSetActiveItem(); - $scope.pod.capabilities.push('iscsi_storage'); - expect($scope.compose.obj.storage.length).toBe(1); - $scope.composeAddStorage(); - expect($scope.compose.obj.storage.length).toBe(2); - expect($scope.compose.obj.storage[1]).toEqual({ - type: 'iscsi', - size: 8, - tags: [], - pool: {}, - boot: false - }); - }); - }); - - describe("composeSetBootDisk", function() { - - it("sets a new boot disk", function() { - makeControllerResolveSetActiveItem(); - $scope.composeAddStorage(); - $scope.composeAddStorage(); - $scope.composeAddStorage(); - var newBoot = $scope.compose.obj.storage[3]; - $scope.composeSetBootDisk(newBoot); - expect($scope.compose.obj.storage[0].boot).toBe(false); - expect(newBoot.boot).toBe(true); - }); - }); - - describe("composeRemoveDisk", function() { - - it("removes disk from storage", function() { - makeControllerResolveSetActiveItem(); - $scope.composeAddStorage(); - $scope.composeAddStorage(); - $scope.composeAddStorage(); - var deleteStorage = $scope.compose.obj.storage[3]; - $scope.composeRemoveDisk(deleteStorage); - expect($scope.compose.obj.storage.indexOf(deleteStorage)).toBe(-1); - }); - }); - - describe("composeAddInterface", function() { - - it("adds a new interface item and removes the default", function() { - makeControllerResolveSetActiveItem(); - expect($scope.compose.obj.interfaces.length).toBe(1); - expect($scope.compose.obj.interfaces[0]).toEqual({ - name: 'default' - }); - $scope.composeAddInterface(); - expect($scope.compose.obj.interfaces.length).toBe(1); - expect($scope.compose.obj.interfaces[0]).toEqual({ - name: 'eth0' - }); - }); - - it("increments the default interface name", function() { - makeControllerResolveSetActiveItem(); - $scope.composeAddInterface(); - $scope.composeAddInterface(); - expect($scope.compose.obj.interfaces[0]).toEqual({ - name: 'eth0' - }); - expect($scope.compose.obj.interfaces[1]).toEqual({ - name: 'eth1' - }); - }); - }); - - describe("composeRemoveInterface", function() { - - it("removes interface from interfaces table", function() { - makeControllerResolveSetActiveItem(); - $scope.composeAddInterface(); - $scope.composeAddInterface(); - $scope.composeAddInterface(); - var deletedIface = $scope.compose.obj.interfaces[3]; - $scope.composeRemoveInterface(deletedIface); - - expect($scope.compose.obj.interfaces.indexOf( - deletedIface)).toBe(-1); - }); - }); - + ], + boot: true + }, + { + type: "local", + size: 60, + tags: [ + { + text: "other" + } + ], + boot: false + } + ]; + expect($scope.composePreProcess({})).toEqual({ + id: $scope.pod.id, + storage: + "0:50(local,happy,days)," + "1:20(iscsi,one,two),2:60(local,other)", + interfaces: "" + }); + }); + + it("sets the interface constraint for subnets", function() { + makeControllerResolveSetActiveItem(); + $scope.compose.obj.interfaces = [ + { + name: "eth0", + subnet: { cidr: "172.16.4.0/24" } + }, + { + name: "eth1", + subnet: { cidr: "192.168.1.0/24" } + } + ]; + var expectedInterfaces = [ + "eth0:subnet_cidr=172.16.4.0/24", + "eth1:subnet_cidr=192.168.1.0/24" + ].join(";"); + expect($scope.composePreProcess({})).toEqual({ + id: $scope.pod.id, + storage: "0:8()", + interfaces: expectedInterfaces + }); + }); + + it("sets the interface constraint favouring ip addresses", function() { + makeControllerResolveSetActiveItem(); + $scope.compose.obj.interfaces = [ + { + name: "eth0", + ipaddress: "172.16.4.2", + subnet: { cidr: "172.16.4.0/24" } + }, + { + name: "eth1", + ipaddress: "192.168.1.5", + subnet: { cidr: "192.168.1.0/24" } + }, + { + name: "eth2", + subnet: { cidr: "192.168.2.0/24" } + } + ]; + var expectedInterfaces = [ + "eth0:ip=172.16.4.2", + "eth1:ip=192.168.1.5", + "eth2:subnet_cidr=192.168.2.0/24" + ].join(";"); + expect($scope.composePreProcess({})).toEqual({ + id: $scope.pod.id, + storage: "0:8()", + interfaces: expectedInterfaces + }); + }); + + it("sets virsh storage based on compose.obj.storage", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.type = "virsh"; + $scope.compose.obj.storage = [ + { + size: 20, + pool: { + name: "pool1" + }, + tags: [ + { + text: "one" + }, + { + text: "two" + } + ], + boot: false + }, + { + size: 50, + pool: { + name: "pool2" + }, + tags: [ + { + text: "happy" + }, + { + text: "days" + } + ], + boot: true + }, + { + size: 60, + pool: { + name: "pool3" + }, + tags: [ + { + text: "other" + } + ], + boot: false + } + ]; + expect($scope.composePreProcess({})).toEqual({ + id: $scope.pod.id, + storage: + "0:50(pool2,happy,days)," + "1:20(pool1,one,two),2:60(pool3,other)", + interfaces: "" + }); + }); + + it("sets virsh storage based on compose.obj.storage", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.type = "virsh"; + $scope.compose.obj.storage = [ + { + size: 20, + pool: { + name: "pool1" + }, + tags: [ + { + text: "one" + }, + { + text: "two" + } + ], + boot: false + }, + { + size: 50, + pool: { + name: "pool2" + }, + tags: [ + { + text: "happy" + }, + { + text: "days" + } + ], + boot: true + }, + { + size: 60, + pool: { + name: "pool3" + }, + tags: [ + { + text: "other" + } + ], + boot: false + } + ]; + expect($scope.composePreProcess({})).toEqual({ + id: $scope.pod.id, + storage: + "0:50(pool2,happy,days)," + "1:20(pool1,one,two),2:60(pool3,other)", + interfaces: "" + }); + }); + }); + + describe("cancelCompose", function() { + it("resets obj and action.option", function() { + makeControllerResolveSetActiveItem(); + var otherObj = {}; + $scope.compose.obj = otherObj; + $scope.action.option = {}; + $scope.cancelCompose(); + expect($scope.compose.obj).not.toBe(otherObj); + expect($scope.compose.obj).toEqual({ + storage: [ + { + type: "local", + size: 8, + tags: [], + pool: {}, + boot: true + } + ], + interfaces: [ + { + name: "default" + } + ], + requests: [] + }); + expect($scope.action.option).toBeNull(); + }); + }); + + describe("composeAddStorage", function() { + it("adds a new local storage item", function() { + makeControllerResolveSetActiveItem(); + expect($scope.compose.obj.storage.length).toBe(1); + $scope.composeAddStorage(); + expect($scope.compose.obj.storage.length).toBe(2); + expect($scope.compose.obj.storage[1]).toEqual({ + type: "local", + size: 8, + tags: [], + pool: {}, + boot: false + }); + }); + + it("adds a new iscsi storage item", function() { + makeControllerResolveSetActiveItem(); + $scope.pod.capabilities.push("iscsi_storage"); + expect($scope.compose.obj.storage.length).toBe(1); + $scope.composeAddStorage(); + expect($scope.compose.obj.storage.length).toBe(2); + expect($scope.compose.obj.storage[1]).toEqual({ + type: "iscsi", + size: 8, + tags: [], + pool: {}, + boot: false + }); + }); + }); + + describe("composeSetBootDisk", function() { + it("sets a new boot disk", function() { + makeControllerResolveSetActiveItem(); + $scope.composeAddStorage(); + $scope.composeAddStorage(); + $scope.composeAddStorage(); + var newBoot = $scope.compose.obj.storage[3]; + $scope.composeSetBootDisk(newBoot); + expect($scope.compose.obj.storage[0].boot).toBe(false); + expect(newBoot.boot).toBe(true); + }); + }); + + describe("composeRemoveDisk", function() { + it("removes disk from storage", function() { + makeControllerResolveSetActiveItem(); + $scope.composeAddStorage(); + $scope.composeAddStorage(); + $scope.composeAddStorage(); + var deleteStorage = $scope.compose.obj.storage[3]; + $scope.composeRemoveDisk(deleteStorage); + expect($scope.compose.obj.storage.indexOf(deleteStorage)).toBe(-1); + }); + }); + + describe("composeAddInterface", function() { + it("adds a new interface item and removes the default", function() { + makeControllerResolveSetActiveItem(); + expect($scope.compose.obj.interfaces.length).toBe(1); + expect($scope.compose.obj.interfaces[0]).toEqual({ + name: "default" + }); + $scope.composeAddInterface(); + expect($scope.compose.obj.interfaces.length).toBe(1); + expect($scope.compose.obj.interfaces[0]).toEqual({ + name: "eth0" + }); + }); + + it("increments the default interface name", function() { + makeControllerResolveSetActiveItem(); + $scope.composeAddInterface(); + $scope.composeAddInterface(); + expect($scope.compose.obj.interfaces[0]).toEqual({ + name: "eth0" + }); + expect($scope.compose.obj.interfaces[1]).toEqual({ + name: "eth1" + }); + }); + }); + + describe("composeRemoveInterface", function() { + it("removes interface from interfaces table", function() { + makeControllerResolveSetActiveItem(); + $scope.composeAddInterface(); + $scope.composeAddInterface(); + $scope.composeAddInterface(); + var deletedIface = $scope.compose.obj.interfaces[3]; + $scope.composeRemoveInterface(deletedIface); + expect($scope.compose.obj.interfaces.indexOf(deletedIface)).toBe(-1); + }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_pods_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_pods_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_pods_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_pods_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,578 +4,531 @@ * Unit tests for NodesListController. */ -// Make a fake user. -var userId = 0; -function makeUser() { - return { - id: userId++, - username: makeName("username"), - first_name: makeName("first_name"), - last_name: makeName("last_name"), - email: makeName("email"), - is_superuser: false, - sshkeys_count: 0 - }; -} +import { makeInteger, makeName } from "testing/utils"; describe("PodsListController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load the required managers. - var PodsManager, UsersManager, GeneralManager; - var ZonesManager, ManagerHelperService, ResourcePoolsManager; - beforeEach(inject(function($injector) { - PodsManager = $injector.get("PodsManager"); - UsersManager = $injector.get("UsersManager"); - GeneralManager = $injector.get("GeneralManager"); - ZonesManager = $injector.get("ZonesManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ResourcePoolsManager = $injector.get("ResourcePoolsManager"); - })); - - // Mock the websocket connection to the region - var RegionConnection, webSocket; - beforeEach(inject(function($injector) { - RegionConnection = $injector.get("RegionConnection"); - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Makes the PodsListController - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Start the connection so a valid websocket is created in the - // RegionConnection. - RegionConnection.connect(""); - - // Create the controller. - var controller = $controller("PodsListController", { - $scope: $scope, - $rootScope: $rootScope, - PodsManager: PodsManager, - UsersManager: UsersManager, - ZonesManager: ZonesManager, - ManagerHelperService: ManagerHelperService - }); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load the required managers. + var PodsManager, UsersManager, GeneralManager; + var ZonesManager, ManagerHelperService, ResourcePoolsManager; + beforeEach(inject(function($injector) { + PodsManager = $injector.get("PodsManager"); + UsersManager = $injector.get("UsersManager"); + GeneralManager = $injector.get("GeneralManager"); + ZonesManager = $injector.get("ZonesManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ResourcePoolsManager = $injector.get("ResourcePoolsManager"); + })); + + // Mock the websocket connection to the region + var RegionConnection, webSocket; + beforeEach(inject(function($injector) { + RegionConnection = $injector.get("RegionConnection"); + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Makes the PodsListController + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - // Makes a fake node/device. - var podId = 0; - function makePod() { - var pod = { - id: podId++, - $selected: false, - permissions: [] - }; - PodsManager._items.push(pod); - return pod; - } + // Start the connection so a valid websocket is created in the + // RegionConnection. + RegionConnection.connect(""); + + // Create the controller. + var controller = $controller("PodsListController", { + $scope: $scope, + $rootScope: $rootScope, + PodsManager: PodsManager, + UsersManager: UsersManager, + ZonesManager: ZonesManager, + ManagerHelperService: ManagerHelperService + }); + + return controller; + } + + // Makes a fake node/device. + var podId = 0; + function makePod() { + var pod = { + id: podId++, + $selected: false, + permissions: [] + }; + PodsManager._items.push(pod); + return pod; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Pods"); + expect($rootScope.page).toBe("pods"); + }); + + it("sets initial values on $scope", function() { + // tab-independent variables. + makeController(); + expect($scope.pods).toBe(PodsManager.getItems()); + expect($scope.loading).toBe(true); + expect($scope.filteredItems).toEqual([]); + expect($scope.selectedItems).toBe(PodsManager.getSelectedItems()); + expect($scope.predicate).toBe("name"); + expect($scope.allViewableChecked).toBe(false); + expect($scope.action.option).toBeNull(); + expect($scope.add.open).toBe(false); + expect($scope.powerTypes).toBe(GeneralManager.getData("power_types")); + expect($scope.zones).toBe(ZonesManager.getItems()); + expect($scope.pools).toBe(ResourcePoolsManager.getItems()); + }); - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Pods"); - expect($rootScope.page).toBe("pods"); - }); + it("calls loadManagers with PodsManager, UsersManager, \ + GeneralManager, ZonesManager", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + PodsManager, + UsersManager, + GeneralManager, + ZonesManager, + ResourcePoolsManager + ]); + }); + + it("sets loading to false with loadManagers resolves", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.loading).toBe(false); + }); + + describe("isRackControllerConnected", function() { + it("returns false no powerTypes", function() { + makeController(); + $scope.powerTypes = []; + expect($scope.isRackControllerConnected()).toBe(false); + }); + + it("returns true if powerTypes", function() { + makeController(); + $scope.powerTypes = [{}]; + expect($scope.isRackControllerConnected()).toBe(true); + }); + }); + + describe("canAddPod", function() { + it("returns false if not global permission", function() { + makeController(); + spyOn(UsersManager, "hasGlobalPermission").and.returnValue(false); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canAddPod()).toBe(false); + expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( + "pod_create" + ); + }); + + it("returns false if rack disconnected", function() { + makeController(); + spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); + spyOn($scope, "isRackControllerConnected").and.returnValue(false); + expect($scope.canAddPod()).toBe(false); + }); + + it("returns true if super user, rack connected", function() { + makeController(); + spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); + spyOn($scope, "isRackControllerConnected").and.returnValue(true); + expect($scope.canAddPod()).toBe(true); + expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( + "pod_create" + ); + }); + }); + + describe("showActions", function() { + it("returns false if no permissions on pods", function() { + makeController(); + var pod = makePod(); + PodsManager._items.push(pod); + expect($scope.showActions()).toBe(false); + }); + + it("returns false if compose permissions on pods", function() { + makeController(); + var pod = makePod(); + pod.permissions.push("compose"); + PodsManager._items.push(pod); + expect($scope.showActions()).toBe(false); + }); + + it("returns true if edit permissions on pods", function() { + makeController(); + var pod = makePod(); + pod.permissions.push("edit"); + PodsManager._items.push(pod); + expect($scope.showActions()).toBe(true); + }); + }); + + describe("toggleChecked", function() { + var pod; + beforeEach(function() { + makeController(); + pod = makePod(); + $scope.filteredItems = $scope.pods; + }); + + it("selects object", function() { + $scope.toggleChecked(pod); + expect(pod.$selected).toBe(true); + }); + + it("deselects object", function() { + PodsManager.selectItem(pod.id); + $scope.toggleChecked(pod); + expect(pod.$selected).toBe(false); + }); + + it("sets allViewableChecked to true when all objects selected", function() { + $scope.toggleChecked(pod); + expect($scope.allViewableChecked).toBe(true); + }); + + it( + "sets allViewableChecked to false when not all objects " + "selected", + function() { + var pod2 = makePod(); + $scope.toggleChecked(pod); + expect($scope.allViewableChecked).toBe(false); + } + ); - it("sets initial values on $scope", function() { - // tab-independent variables. - makeController(); - expect($scope.pods).toBe(PodsManager.getItems()); - expect($scope.loading).toBe(true); - expect($scope.filteredItems).toEqual([]); - expect($scope.selectedItems).toBe(PodsManager.getSelectedItems()); - expect($scope.predicate).toBe('name'); + it( + "sets allViewableChecked to false when selected and " + "deselected", + function() { + $scope.toggleChecked(pod); + $scope.toggleChecked(pod); expect($scope.allViewableChecked).toBe(false); - expect($scope.action.option).toBeNull(); - expect($scope.add.open).toBe(false); - expect($scope.powerTypes).toBe(GeneralManager.getData('power_types')); - expect($scope.zones).toBe(ZonesManager.getItems()); - expect($scope.pools).toBe(ResourcePoolsManager.getItems()); - }); + } + ); - it("calls loadManagers with PodsManager, UsersManager, \ - GeneralManager, ZonesManager", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - PodsManager, UsersManager, GeneralManager, ZonesManager, - ResourcePoolsManager]); - }); - - it("sets loading to false with loadManagers resolves", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.loading).toBe(false); - }); - - describe("isRackControllerConnected", function() { - it("returns false no powerTypes", function() { - makeController(); - $scope.powerTypes = []; - expect($scope.isRackControllerConnected()).toBe(false); - }); - - it("returns true if powerTypes", function() { - makeController(); - $scope.powerTypes = [{}]; - expect($scope.isRackControllerConnected()).toBe(true); - }); - }); - - describe("canAddPod", function() { - it("returns false if not global permission", function() { - makeController(); - spyOn(UsersManager, "hasGlobalPermission").and.returnValue(false); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canAddPod()).toBe(false); - expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( - 'pod_create'); - }); - - it("returns false if rack disconnected", function() { - makeController(); - spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(false); - expect($scope.canAddPod()).toBe(false); - }); - - it("returns true if super user, rack connected", function() { - makeController(); - spyOn(UsersManager, "hasGlobalPermission").and.returnValue(true); - spyOn( - $scope, - "isRackControllerConnected").and.returnValue(true); - expect($scope.canAddPod()).toBe(true); - expect(UsersManager.hasGlobalPermission).toHaveBeenCalledWith( - 'pod_create'); - }); - }); - - describe("showActions", function() { - it("returns false if no permissions on pods", function() { - makeController(); - var pod = makePod(); - PodsManager._items.push(pod); - expect($scope.showActions()).toBe(false); - }); - - it("returns false if compose permissions on pods", function() { - makeController(); - var pod = makePod(); - pod.permissions.push('compose'); - PodsManager._items.push(pod); - expect($scope.showActions()).toBe(false); - }); - - it("returns true if edit permissions on pods", function() { - makeController(); - var pod = makePod(); - pod.permissions.push('edit'); - PodsManager._items.push(pod); - expect($scope.showActions()).toBe(true); - }); - }); - - describe("toggleChecked", function() { - - var pod; - beforeEach(function() { - makeController(); - pod = makePod(); - $scope.filteredItems = $scope.pods; - }); - - it("selects object", function() { - $scope.toggleChecked(pod); - expect(pod.$selected).toBe(true); - }); - - it("deselects object", function() { - PodsManager.selectItem(pod.id); - $scope.toggleChecked(pod); - expect(pod.$selected).toBe(false); - }); - - it("sets allViewableChecked to true when all objects selected", - function() { - $scope.toggleChecked(pod); - expect($scope.allViewableChecked).toBe(true); - }); - - it( - "sets allViewableChecked to false when not all objects " + - "selected", - function() { - var pod2 = makePod(); - $scope.toggleChecked(pod); - expect($scope.allViewableChecked).toBe(false); - }); - - it("sets allViewableChecked to false when selected and " + - "deselected", - function() { - $scope.toggleChecked(pod); - $scope.toggleChecked(pod); - expect($scope.allViewableChecked).toBe(false); - }); - - it("clears action option when none selected", function() { - $scope.action.option = {}; - $scope.toggleChecked(pod); - $scope.toggleChecked(pod); - expect($scope.action.option).toBeNull(); - }); - }); - - describe("toggleCheckAll", function() { - - var pod1, pod2; - beforeEach(function() { - makeController(); - pod1 = makePod(); - pod2 = makePod(); - $scope.filteredItems = $scope.pods; - }); - - it("selects all objects", function() { - $scope.toggleCheckAll(); - expect(pod1.$selected).toBe(true); - expect(pod2.$selected).toBe(true); - }); - - it("deselects all objects", function() { - $scope.toggleCheckAll(); - $scope.toggleCheckAll(); - expect(pod1.$selected).toBe(false); - expect(pod2.$selected).toBe(false); - }); - - it("clears action option when none selected", function() { - $scope.action.option = {}; - $scope.toggleCheckAll(); - $scope.toggleCheckAll(); - expect($scope.action.option).toBeNull(); - }); - }); - - describe("sortTable", function() { - - it("sets predicate", function() { - makeController(); - var predicate = makeName('predicate'); - $scope.sortTable(predicate); - expect($scope.predicate).toBe(predicate); - }); - - it("reverses reverse", function() { - makeController(); - $scope.reverse = true; - $scope.sortTable(makeName('predicate')); - expect($scope.reverse).toBe(false); - }); - }); - - describe("actionCancel", function() { - - it("sets actionOption to null", function() { - makeController(); - $scope.action.option = {}; - $scope.actionCancel(); - expect($scope.action.option).toBeNull(); - }); - - it("resets actionProgress", function() { - makeController(); - $scope.action.progress.total = makeInteger(1, 10); - $scope.action.progress.completed = - makeInteger(1, 10); - $scope.action.progress.errors = - makeInteger(1, 10); - $scope.actionCancel(); - expect($scope.action.progress.total).toBe(0); - expect($scope.action.progress.completed).toBe(0); - expect($scope.action.progress.errors).toBe(0); - }); - }); - - describe("actionGo", function() { - - it("sets action.progress.total to the number of selectedItems", - function() { - makeController(); - makePod(); - $scope.action.option = { name: "refresh" }; - $scope.action.selectedItems = [ - makePod(), - makePod(), - makePod() - ]; - $scope.actionGo(); - expect($scope.action.progress.total).toBe( - $scope.selectedItems.length); - }); - - it("calls operation for selected action", function() { - makeController(); - var pod = makePod(); - var spy = spyOn( - PodsManager, - "refresh").and.returnValue($q.defer().promise); - $scope.action.option = { name: "refresh", operation: spy }; - $scope.selectedItems = [pod]; - $scope.actionGo(); - expect(spy).toHaveBeenCalledWith(pod); - }); - - it("calls unselectItem after failed action", function() { - makeController(); - var pod = makePod(); - pod.action_failed = false; - spyOn( - $scope, 'hasActionsFailed').and.returnValue(true); - var defer = $q.defer(); - var refresh = jasmine.createSpy( - 'refresh').and.returnValue(defer.promise); - var spy = spyOn(PodsManager, "unselectItem"); - $scope.action.option = { - name: "refresh", operation: refresh }; - $scope.selectedItems = [pod]; - $scope.actionGo(); - defer.resolve(); - $scope.$digest(); - expect(spy).toHaveBeenCalled(); - }); - - it("keeps items selected after success", function() { - makeController(); - var pod = makePod(); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(false); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - var defer = $q.defer(); - var refresh = jasmine.createSpy( - 'refresh').and.returnValue(defer.promise); - var spy = spyOn(PodsManager, "unselectItem"); - $scope.action.option = { name: "refresh", operation: refresh }; - $scope.selectedItems = [pod]; - $scope.actionGo(); - defer.resolve(); - $scope.$digest(); - expect($scope.selectedItems).toEqual([pod]); - }); - - it("increments action.progress.completed after action complete", - function() { - makeController(); - var pod = makePod(); - var defer = $q.defer(); - var refresh = jasmine.createSpy( - 'refresh').and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(true); - $scope.action.option = { name: "start", operation: refresh }; - $scope.selectedItems = [pod]; - $scope.actionGo(); - defer.resolve(); - $scope.$digest(); - expect($scope.action.progress.completed).toBe(1); - }); - - it("clears action option when complete", function() { - makeController(); - var pod = makePod(); - var defer = $q.defer(); - var refresh = jasmine.createSpy( - 'refresh').and.returnValue(defer.promise); - spyOn( - $scope, 'hasActionsFailed').and.returnValue(true); - spyOn( - $scope, 'hasActionsInProgress').and.returnValue(false); - PodsManager._items.push(pod); - PodsManager._selectedItems.push(pod); - $scope.action.option = { name: "refresh", operation: refresh }; - $scope.actionGo(); - defer.resolve(); - $scope.$digest(); - expect($scope.action.option).toBeNull(); - }); - - it("increments action.progress.errors after action error", - function() { - makeController(); - var pod = makePod(); - var defer = $q.defer(); - var refresh = jasmine.createSpy( - 'refresh').and.returnValue(defer.promise); - $scope.action.option = { name: "refresh", operation: refresh }; - $scope.selectedItems = [pod]; - $scope.actionGo(); - defer.reject(makeName("error")); - $scope.$digest(); - expect( - $scope.action.progress.errors).toBe(1); - }); - - it("adds error to action.progress.errors on action error", - function() { - makeController(); - var pod = makePod(); - var defer = $q.defer(); - var refresh = jasmine.createSpy( - 'refresh').and.returnValue(defer.promise); - $scope.action.option = { name: "refresh", operation: refresh }; - $scope.selectedItems = [pod]; - $scope.actionGo(); - var error = makeName("error"); - defer.reject(error); - $scope.$digest(); - expect(pod.action_error).toBe(error); - expect(pod.action_failed).toBe(true); - }); - }); - - describe("hasActionsInProgress", function() { - - it("returns false if action.progress.total not > 0", function() { - makeController(); - $scope.action.progress.total = 0; - expect($scope.hasActionsInProgress()).toBe(false); - }); - - it("returns true if action.progress total != completed", - function() { - makeController(); - $scope.action.progress.total = 1; - $scope.action.progress.completed = 0; - expect($scope.hasActionsInProgress()).toBe(true); - }); - - it("returns false if actionProgress total == completed", - function() { - makeController(); - $scope.action.progress.total = 1; - $scope.action.progress.completed = 1; - expect($scope.hasActionsInProgress()).toBe(false); - }); - }); - - describe("hasActionsFailed", function() { - - it("returns false if no errors", function() { - makeController(); - $scope.action.progress.errors = 0; - expect($scope.hasActionsFailed()).toBe(false); - }); - - it("returns true if errors", function() { - makeController(); - $scope.action.progress.errors = 1; - expect($scope.hasActionsFailed()).toBe(true); - }); - }); - - describe("addPod", function() { - - function makeZone(id) { - var zone = { - name: makeName("name") - }; - if(angular.isDefined(id)) { - zone.id = id; - } else { - zone.id = makeInteger(1, 100); - } - return zone; - } + it("clears action option when none selected", function() { + $scope.action.option = {}; + $scope.toggleChecked(pod); + $scope.toggleChecked(pod); + expect($scope.action.option).toBeNull(); + }); + }); + + describe("toggleCheckAll", function() { + var pod1, pod2; + beforeEach(function() { + makeController(); + pod1 = makePod(); + pod2 = makePod(); + $scope.filteredItems = $scope.pods; + }); + + it("selects all objects", function() { + $scope.toggleCheckAll(); + expect(pod1.$selected).toBe(true); + expect(pod2.$selected).toBe(true); + }); + + it("deselects all objects", function() { + $scope.toggleCheckAll(); + $scope.toggleCheckAll(); + expect(pod1.$selected).toBe(false); + expect(pod2.$selected).toBe(false); + }); + + it("clears action option when none selected", function() { + $scope.action.option = {}; + $scope.toggleCheckAll(); + $scope.toggleCheckAll(); + expect($scope.action.option).toBeNull(); + }); + }); + + describe("sortTable", function() { + it("sets predicate", function() { + makeController(); + var predicate = makeName("predicate"); + $scope.sortTable(predicate); + expect($scope.predicate).toBe(predicate); + }); + + it("reverses reverse", function() { + makeController(); + $scope.reverse = true; + $scope.sortTable(makeName("predicate")); + expect($scope.reverse).toBe(false); + }); + }); + + describe("actionCancel", function() { + it("sets actionOption to null", function() { + makeController(); + $scope.action.option = {}; + $scope.actionCancel(); + expect($scope.action.option).toBeNull(); + }); + + it("resets actionProgress", function() { + makeController(); + $scope.action.progress.total = makeInteger(1, 10); + $scope.action.progress.completed = makeInteger(1, 10); + $scope.action.progress.errors = makeInteger(1, 10); + $scope.actionCancel(); + expect($scope.action.progress.total).toBe(0); + expect($scope.action.progress.completed).toBe(0); + expect($scope.action.progress.errors).toBe(0); + }); + }); + + describe("actionGo", function() { + it("sets action.progress.total to the number of selectedItems", function() { + makeController(); + makePod(); + $scope.action.option = { name: "refresh" }; + $scope.action.selectedItems = [makePod(), makePod(), makePod()]; + $scope.actionGo(); + expect($scope.action.progress.total).toBe($scope.selectedItems.length); + }); + + it("calls operation for selected action", function() { + makeController(); + var pod = makePod(); + var spy = spyOn(PodsManager, "refresh").and.returnValue( + $q.defer().promise + ); + $scope.action.option = { name: "refresh", operation: spy }; + $scope.selectedItems = [pod]; + $scope.actionGo(); + expect(spy).toHaveBeenCalledWith(pod); + }); + + it("calls unselectItem after failed action", function() { + makeController(); + var pod = makePod(); + pod.action_failed = false; + spyOn($scope, "hasActionsFailed").and.returnValue(true); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh").and.returnValue(defer.promise); + var spy = spyOn(PodsManager, "unselectItem"); + $scope.action.option = { + name: "refresh", + operation: refresh + }; + $scope.selectedItems = [pod]; + $scope.actionGo(); + defer.resolve(); + $scope.$digest(); + expect(spy).toHaveBeenCalled(); + }); + + it("keeps items selected after success", function() { + makeController(); + var pod = makePod(); + spyOn($scope, "hasActionsFailed").and.returnValue(false); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh").and.returnValue(defer.promise); + var spy = spyOn(PodsManager, "unselectItem"); + $scope.action.option = { name: "refresh", operation: refresh }; + $scope.selectedItems = [pod]; + $scope.actionGo(); + defer.resolve(); + $scope.$digest(); + expect($scope.selectedItems).toEqual([pod]); + }); + + it(`increments action.progress.completed + after action complete`, function() { + makeController(); + var pod = makePod(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh").and.returnValue(defer.promise); + spyOn($scope, "hasActionsFailed").and.returnValue(true); + $scope.action.option = { name: "start", operation: refresh }; + $scope.selectedItems = [pod]; + $scope.actionGo(); + defer.resolve(); + $scope.$digest(); + expect($scope.action.progress.completed).toBe(1); + }); + + it("clears action option when complete", function() { + makeController(); + var pod = makePod(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh").and.returnValue(defer.promise); + spyOn($scope, "hasActionsFailed").and.returnValue(true); + spyOn($scope, "hasActionsInProgress").and.returnValue(false); + PodsManager._items.push(pod); + PodsManager._selectedItems.push(pod); + $scope.action.option = { name: "refresh", operation: refresh }; + $scope.actionGo(); + defer.resolve(); + $scope.$digest(); + expect($scope.action.option).toBeNull(); + }); + + it("increments action.progress.errors after action error", function() { + makeController(); + var pod = makePod(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh").and.returnValue(defer.promise); + $scope.action.option = { name: "refresh", operation: refresh }; + $scope.selectedItems = [pod]; + $scope.actionGo(); + defer.reject(makeName("error")); + $scope.$digest(); + expect($scope.action.progress.errors).toBe(1); + }); + + it("adds error to action.progress.errors on action error", function() { + makeController(); + var pod = makePod(); + var defer = $q.defer(); + var refresh = jasmine.createSpy("refresh").and.returnValue(defer.promise); + $scope.action.option = { name: "refresh", operation: refresh }; + $scope.selectedItems = [pod]; + $scope.actionGo(); + var error = makeName("error"); + defer.reject(error); + $scope.$digest(); + expect(pod.action_error).toBe(error); + expect(pod.action_failed).toBe(true); + }); + }); + + describe("hasActionsInProgress", function() { + it("returns false if action.progress.total not > 0", function() { + makeController(); + $scope.action.progress.total = 0; + expect($scope.hasActionsInProgress()).toBe(false); + }); + + it("returns true if action.progress total != completed", function() { + makeController(); + $scope.action.progress.total = 1; + $scope.action.progress.completed = 0; + expect($scope.hasActionsInProgress()).toBe(true); + }); + + it("returns false if actionProgress total == completed", function() { + makeController(); + $scope.action.progress.total = 1; + $scope.action.progress.completed = 1; + expect($scope.hasActionsInProgress()).toBe(false); + }); + }); + + describe("hasActionsFailed", function() { + it("returns false if no errors", function() { + makeController(); + $scope.action.progress.errors = 0; + expect($scope.hasActionsFailed()).toBe(false); + }); + + it("returns true if errors", function() { + makeController(); + $scope.action.progress.errors = 1; + expect($scope.hasActionsFailed()).toBe(true); + }); + }); + + describe("addPod", function() { + function makeZone(id) { + var zone = { + name: makeName("name") + }; + if (angular.isDefined(id)) { + zone.id = id; + } else { + zone.id = makeInteger(1, 100); + } + return zone; + } + + function makePool(id) { + var pool = { + name: makeName("pool") + }; + if (angular.isDefined(id)) { + pool.id = id; + } else { + pool.id = makeInteger(1, 100); + } + return pool; + } - function makePool(id) { - var pool = { - name: makeName("pool") - }; - if(angular.isDefined(id)) { - pool.id = id; - } else { - pool.id = makeInteger(1, 100); - } - return pool; + it("sets add.open to true", function() { + makeController(); + var zero = makeZone(0); + ZonesManager._items.push(makeZone()); + ZonesManager._items.push(zero); + var defaultPool = makePool(0); + ResourcePoolsManager._items.push(makePool()); + ResourcePoolsManager._items.push(defaultPool); + $scope.addPod(); + expect($scope.add.open).toBe(true); + expect($scope.add.obj.cpu_over_commit_ratio).toBe(1); + expect($scope.add.obj.memory_over_commit_ratio).toBe(1); + expect($scope.add.obj.default_pool).toBe(0); + expect(ZonesManager.getDefaultZone()).toBe(zero); + expect(ResourcePoolsManager.getDefaultPool()).toBe(defaultPool); + }); + }); + + describe("cancelAddPod", function() { + it("set add.open to false and clears add.obj", function() { + makeController(); + var obj = {}; + $scope.add.obj = obj; + $scope.add.open = true; + $scope.cancelAddPod(); + expect($scope.add.open).toBe(false); + expect($scope.add.obj).toEqual({}); + expect($scope.add.obj).not.toBe(obj); + }); + }); + + describe("getPowerTypeTitle", function() { + it("returns power_type description", function() { + makeController(); + $scope.powerTypes = [ + { + name: "power_type", + description: "Power type" } + ]; + expect($scope.getPowerTypeTitle("power_type")).toBe("Power type"); + }); - it("sets add.open to true", function() { - makeController(); - var zero = makeZone(0); - ZonesManager._items.push(makeZone()); - ZonesManager._items.push(zero); - var defaultPool = makePool(0); - ResourcePoolsManager._items.push(makePool()); - ResourcePoolsManager._items.push(defaultPool); - $scope.addPod(); - expect($scope.add.open).toBe(true); - expect($scope.add.obj.cpu_over_commit_ratio).toBe(1); - expect($scope.add.obj.memory_over_commit_ratio).toBe(1); - expect($scope.add.obj.default_pool).toBe(0); - expect(ZonesManager.getDefaultZone()).toBe(zero); - expect(ResourcePoolsManager.getDefaultPool()).toBe(defaultPool); - }); - }); - - describe("cancelAddPod", function() { - - it("set add.open to false and clears add.obj", function() { - makeController(); - var obj = {}; - $scope.add.obj = obj; - $scope.add.open = true; - $scope.cancelAddPod(); - expect($scope.add.open).toBe(false); - expect($scope.add.obj).toEqual({}); - expect($scope.add.obj).not.toBe(obj); - }); - }); - - describe("getPowerTypeTitle", function() { - - it("returns power_type description", function() { - makeController(); - $scope.powerTypes = [ - { - name: 'power_type', - description: 'Power type' - } - ]; - expect($scope.getPowerTypeTitle('power_type')).toBe('Power type'); - }); - - it("returns power_type passed in", function() { - makeController(); - $scope.powerTypes = []; - expect($scope.getPowerTypeTitle('power_type')).toBe('power_type'); - }); + it("returns power_type passed in", function() { + makeController(); + $scope.powerTypes = []; + expect($scope.getPowerTypeTitle("power_type")).toBe("power_type"); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_prefs.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_prefs.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_prefs.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_prefs.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,61 +5,62 @@ */ describe("PreferencesController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load any injected managers and services. - var UsersManager, ManagerHelperService; - beforeEach(inject(function($injector) { - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Makes the PreferencesController - function makeController(loadManagerDefer) { - var loadManager = spyOn(ManagerHelperService, "loadManager"); - if(angular.isObject(loadManagerDefer)) { - loadManager.and.returnValue(loadManagerDefer.promise); - } else { - loadManager.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("PreferencesController", { - $scope: $scope, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService - }); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load any injected managers and services. + var UsersManager, ManagerHelperService; + beforeEach(inject(function($injector) { + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Makes the PreferencesController + function makeController(loadManagerDefer) { + var loadManager = spyOn(ManagerHelperService, "loadManager"); + if (angular.isObject(loadManagerDefer)) { + loadManager.and.returnValue(loadManagerDefer.promise); + } else { + loadManager.and.returnValue($q.defer().promise); } - it("calls loadManager with correct managers", function() { - makeController(); - expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( - $scope, UsersManager); + // Create the controller. + var controller = $controller("PreferencesController", { + $scope: $scope, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService }); - it("sets initial $scope", function() { - makeController(); - expect($scope.loading).toBe(true); - }); + return controller; + } - it("clears loading", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $scope.$digest(); - expect($scope.loading).toBe(false); - }); + it("calls loadManager with correct managers", function() { + makeController(); + expect(ManagerHelperService.loadManager).toHaveBeenCalledWith( + $scope, + UsersManager + ); + }); + + it("sets initial $scope", function() { + makeController(); + expect($scope.loading).toBe(true); + }); + + it("clears loading", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $scope.$digest(); + expect($scope.loading).toBe(false); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_settings.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_settings.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_settings.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_settings.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,611 +4,605 @@ * Unit tests for SettingsController. */ -describe("SettingsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $q = $injector.get("$q"); - $scope = $rootScope.$new(); - })); - - // Load the required dependencies for the SettingsController and - // mock the websocket connection. - var DHCPSnippetsManager, SubnetsManager, MachinesManager, GeneralManager; - var DevicesManager, ControllersManager, ManagerHelperService; - var PackageRepositoriesManager, RegionConnection, webSocket; - beforeEach(inject(function($injector) { - PackageRepositoriesManager = $injector.get( - "PackageRepositoriesManager"); - DHCPSnippetsManager = $injector.get("DHCPSnippetsManager"); - SubnetsManager = $injector.get("SubnetsManager"); - MachinesManager = $injector.get("MachinesManager"); - DevicesManager = $injector.get("DevicesManager"); - ControllersManager = $injector.get("ControllersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - RegionConnection = $injector.get("RegionConnection"); - GeneralManager = $injector.get("GeneralManager"); - - // Mock buildSocket so an actual connection is not made. - webSocket = new MockWebSocket(); - spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); - })); - - // Setup the routeParams. - var $routeParams; - beforeEach(function() { - $routeParams = {}; - }); - - // Make a fake snippet. - var _nextId = 0; - function makeSnippet() { - return { - id: _nextId++, - name: makeName("snippet"), - enabled: true, - value: makeName("value") - }; - } +import { makeInteger, makeName } from "testing/utils"; - // Make a fake repository. - var _nextRepoId = 0; - function makeRepo() { - return { - id: _nextRepoId++, - name: makeName("repo"), - enabled: true, - url: makeName("url"), - key: makeName("key"), - arches: [makeName("arch"), makeName("arch")], - distributions: [makeName("dist"), makeName("dist")], - components: [makeName("comp"), makeName("comp")] - }; - } +describe("SettingsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Makes the SettingsController - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - return $controller("SettingsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - PackageRepositoriesManager: PackageRepositoriesManager, - DHCPSnippetsManager: DHCPSnippetsManager, - SubnetsManager: SubnetsManager, - MachinesManager: MachinesManager, - DevicesManager: DevicesManager, - ControllersManager: ControllersManager, - GeneralManager: GeneralManager, - ManagerHelperService: ManagerHelperService - }); + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $q = $injector.get("$q"); + $scope = $rootScope.$new(); + })); + + // Load the required dependencies for the SettingsController and + // mock the websocket connection. + var DHCPSnippetsManager, SubnetsManager, MachinesManager, GeneralManager; + var DevicesManager, ControllersManager, ManagerHelperService; + var PackageRepositoriesManager, RegionConnection, webSocket; + beforeEach(inject(function($injector) { + PackageRepositoriesManager = $injector.get("PackageRepositoriesManager"); + DHCPSnippetsManager = $injector.get("DHCPSnippetsManager"); + SubnetsManager = $injector.get("SubnetsManager"); + MachinesManager = $injector.get("MachinesManager"); + DevicesManager = $injector.get("DevicesManager"); + ControllersManager = $injector.get("ControllersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + RegionConnection = $injector.get("RegionConnection"); + GeneralManager = $injector.get("GeneralManager"); + + // Mock buildSocket so an actual connection is not made. + webSocket = new MockWebSocket(); + spyOn(RegionConnection, "buildSocket").and.returnValue(webSocket); + })); + + // Setup the routeParams. + var $routeParams; + beforeEach(function() { + $routeParams = {}; + }); + + // Make a fake snippet. + var _nextId = 0; + function makeSnippet() { + return { + id: _nextId++, + name: makeName("snippet"), + enabled: true, + value: makeName("value") + }; + } + + // Make a fake repository. + var _nextRepoId = 0; + function makeRepo() { + return { + id: _nextRepoId++, + name: makeName("repo"), + enabled: true, + url: makeName("url"), + key: makeName("key"), + arches: [makeName("arch"), makeName("arch")], + distributions: [makeName("dist"), makeName("dist")], + components: [makeName("comp"), makeName("comp")] + }; + } + + // Makes the SettingsController + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title to loading and page to settings", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("settings"); - }); - - it("sets initial values", function() { - makeController(); - expect($scope.loading).toBe(true); - expect($scope.loading).toBe(true); - expect($scope.snippetsManager).toBe(DHCPSnippetsManager); - expect($scope.snippets).toBe(DHCPSnippetsManager.getItems()); - expect($scope.subnets).toBe(SubnetsManager.getItems()); - expect($scope.machines).toBe(MachinesManager.getItems()); - expect($scope.devices).toBe(DevicesManager.getItems()); - expect($scope.controllers).toBe(ControllersManager.getItems()); - expect($scope.known_architectures).toBe( - GeneralManager.getData("known_architectures")); - expect($scope.pockets_to_disable).toBe( - GeneralManager.getData("pockets_to_disable")); - expect($scope.components_to_disable).toBe( - GeneralManager.getData("components_to_disable")); - expect($scope.packageRepositoriesManager).toBe( - PackageRepositoriesManager); - expect($scope.repositories).toBe( - PackageRepositoriesManager.getItems()); - expect($scope.newSnippet).toBeNull(); - expect($scope.editSnippet).toBeNull(); - expect($scope.deleteSnippet).toBeNull(); - expect($scope.snippetTypes).toEqual(["Global", "Subnet", "Node"]); - expect($scope.newRepository).toBeNull(); - expect($scope.editRepository).toBeNull(); - expect($scope.deleteRepository).toBeNull(); - }); - - it("sets the values for 'dhcp' section", function() { - $routeParams.section = "dhcp"; - makeController(); - expect($scope.title).toBe("DHCP snippets"); - expect($scope.currentpage).toBe("dhcp"); - }); - - it("sets the values for 'repositories' section", function() { - $routeParams.section = "repositories"; - makeController(); - expect($scope.title).toBe("Package repositories"); - expect($scope.currentpage).toBe("repositories"); - }); - - it("calls loadManagers with all needed managers", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - PackageRepositoriesManager, DHCPSnippetsManager, - MachinesManager, DevicesManager, ControllersManager, - SubnetsManager, GeneralManager]); - }); - - it("sets loading to false", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $scope.$digest(); - expect($scope.loading).toBe(false); - }); - - describe("repositoryEnabledToggle", function() { - - it("calls updateItem", function() { - makeController(); - var repository = makeRepo(); - spyOn(PackageRepositoriesManager, "updateItem"); - $scope.repositoryEnabledToggle(repository); - expect(PackageRepositoriesManager.updateItem).toHaveBeenCalledWith( - repository); - }); - }); - - describe("repositoryEnterRemove", function() { - - it("clears new and edit and sets delete", function() { - makeController(); - var repository = makeRepo(); - $scope.newRepository = {}; - $scope.editRepository = {}; - $scope.repositoryEnterRemove(repository); - expect($scope.newRepository).toBeNull(); - expect($scope.editRepository).toBeNull(); - expect($scope.deleteRepository).toBe(repository); - }); - }); - - describe("repositoryExitRemove", function() { - - it("clears deleteRepository", function() { - makeController(); - $scope.deleteRepository = {}; - $scope.repositoryExitRemove(); - expect($scope.deleteRepository).toBeNull(); - }); - }); - - describe("repositoryConfirmRemove", function() { - - it("calls deleteItem and then repositoryExitRemove", function() { - makeController(); - var repository = makeRepo(); - var defer = $q.defer(); - spyOn(PackageRepositoriesManager, "deleteItem").and.returnValue( - defer.promise); - spyOn($scope, "repositoryExitRemove"); - $scope.deleteRepository = repository; - $scope.repositoryConfirmRemove(); - expect(PackageRepositoriesManager.deleteItem).toHaveBeenCalledWith( - repository); - defer.resolve(); - $scope.$digest(); - expect($scope.repositoryExitRemove).toHaveBeenCalled(); - }); - }); - - describe("isPPA", function() { - - it("false when not object", function() { - makeController(); - expect($scope.isPPA(null)).toBe(false); - }); - - it("false when no url", function() { - makeController(); - expect($scope.isPPA({ - url: null - })).toBe(false); - }); - - it("true when url startswith", function() { - makeController(); - expect($scope.isPPA({ - url: "ppa:" - })).toBe(true); - }); - - it("true when url contains ppa.launchpad.net", function() { - makeController(); - expect($scope.isPPA({ - url: "http://ppa.launchpad.net/" - })).toBe(true); - }); - }); - - describe("isMirror", function() { - - it("false when not object", function() { - makeController(); - expect($scope.isMirror(null)).toBe(false); - }); - - it("false when no name", function() { - makeController(); - expect($scope.isMirror({ - name: null - })).toBe(false); - }); - - it("true when name is 'main_archive'", function() { - makeController(); - expect($scope.isMirror({ - name: "main_archive" - })).toBe(true); - }); - - it("true when name is 'ports_archive'", function() { - makeController(); - expect($scope.isMirror({ - name: "ports_archive" - })).toBe(true); - }); - }); - - describe("repositoryEnterEdit", function() { - - it("clears new and delete and sets edit", function() { - makeController(); - var repository = makeRepo(); - $scope.newRepository = {}; - $scope.deleteRepository = {}; - $scope.repositoryEnterEdit(repository); - expect($scope.editRepository).toBe(repository); - expect($scope.newRepository).toBeNull(); - expect($scope.deleteRepository).toBeNull(); - }); - }); - - describe("repositoryExitEdit", function() { - - it("clears edit", function() { - makeController(); - $scope.editRepository = {}; - $scope.repositoryExitEdit(); - expect($scope.editRepository).toBeNull(); - }); - }); - - describe("repositoryAdd", function() { - - it("sets newRepository for ppa", function() { - makeController(); - $scope.repositoryAdd(true); - expect($scope.newRepository).toEqual({ - name: "", - enabled: true, - url: "ppa:", - key: "", - arches: ["i386", "amd64"], - distributions: [], - components: [] - }); - }); - - it("sets newRepository not for ppa", function() { - makeController(); - $scope.repositoryAdd(false); - expect($scope.newRepository).toEqual({ - name: "", - enabled: true, - url: "", - key: "", - arches: ["i386", "amd64"], - distributions: [], - components: [] - }); - }); - }); - - describe("repositoryAddCancel", function() { - - it("newRepository gets cleared", function() { - makeController(); - $scope.newRepository = {}; - $scope.repositoryAddCancel(); - expect($scope.newRepository).toBeNull(); - }); - }); - - describe("getSubnetName", function() { - - it("calls SubnetsManager.getName", function() { - makeController(); - var subnet = {}; - var subnetsName = {}; - spyOn(SubnetsManager, "getName").and.returnValue(subnetsName); - expect($scope.getSubnetName(subnet)).toBe(subnetsName); - expect(SubnetsManager.getName).toHaveBeenCalledWith(subnet); - }); - }); - - describe("getSnippetTypeText", function() { - - it("returns 'Node'", function() { - makeController(); - var snippet = makeSnippet(); - snippet.node = makeName("system_id"); - expect($scope.getSnippetTypeText(snippet)).toBe("Node"); - }); - - it("returns 'Subnet'", function() { - makeController(); - var snippet = makeSnippet(); - snippet.subnet = makeInteger(); - expect($scope.getSnippetTypeText(snippet)).toBe("Subnet"); - }); - - it("returns 'Global'", function() { - makeController(); - var snippet = makeSnippet(); - expect($scope.getSnippetTypeText(snippet)).toBe("Global"); - }); - }); - - describe("getSnippetAppliesToObject", function() { - - it("returns node from MachinesManager", function() { - makeController(); - var system_id = makeName("system_id"); - var node = { - system_id: system_id - }; - var snippet = makeSnippet(); - snippet.node = system_id; - MachinesManager._items = [node]; - expect($scope.getSnippetAppliesToObject(snippet)).toBe(node); - }); - - it("returns device from DevicesManager", function() { - makeController(); - var system_id = makeName("system_id"); - var device = { - system_id: system_id - }; - var snippet = makeSnippet(); - snippet.node = system_id; - DevicesManager._items = [device]; - expect($scope.getSnippetAppliesToObject(snippet)).toBe(device); - }); - - it("returns controller from ControllersManager", function() { - makeController(); - var system_id = makeName("system_id"); - var controller = { - system_id: system_id - }; - var snippet = makeSnippet(); - snippet.node = system_id; - ControllersManager._items = [controller]; - expect($scope.getSnippetAppliesToObject(snippet)).toBe(controller); - }); - - it("returns subnet from SubnetsManager", function() { - makeController(); - var subnet_id = makeInteger(0, 100); - var subnet = { - id: subnet_id - }; - var snippet = makeSnippet(); - snippet.subnet = subnet_id; - SubnetsManager._items = [subnet]; - expect($scope.getSnippetAppliesToObject(snippet)).toBe(subnet); - }); - }); - - describe("getSnippetAppliesToText", function() { - - it("returns node.fqdn from MachinesManager", function() { - makeController(); - var system_id = makeName("system_id"); - var fqdn = makeName("fqdn"); - var node = { - system_id: system_id, - fqdn: fqdn - }; - var snippet = makeSnippet(); - snippet.node = system_id; - MachinesManager._items = [node]; - expect($scope.getSnippetAppliesToText(snippet)).toBe(fqdn); - }); - - it("returns device.fqdn from DevicesManager", function() { - makeController(); - var system_id = makeName("system_id"); - var fqdn = makeName("fqdn"); - var device = { - system_id: system_id, - fqdn: fqdn - }; - var snippet = makeSnippet(); - snippet.node = system_id; - DevicesManager._items = [device]; - expect($scope.getSnippetAppliesToText(snippet)).toBe(fqdn); - }); - - it("returns controller.fqdn from ControllersManager", function() { - makeController(); - var system_id = makeName("system_id"); - var fqdn = makeName("fqdn"); - var controller = { - system_id: system_id, - fqdn: fqdn - }; - var snippet = makeSnippet(); - snippet.node = system_id; - ControllersManager._items = [controller]; - expect($scope.getSnippetAppliesToText(snippet)).toBe(fqdn); - }); - - it("returns subnet from SubnetsManager", function() { - makeController(); - var subnet_id = makeInteger(0, 100); - var cidr = makeName("cidr"); - var subnet = { - id: subnet_id, - cidr: cidr - }; - var snippet = makeSnippet(); - snippet.subnet = subnet_id; - SubnetsManager._items = [subnet]; - expect($scope.getSnippetAppliesToText(snippet)).toBe(cidr); - }); - }); - - describe("snippetEnterRemove", function() { - - it("clears new and edit and sets delete", function() { - makeController(); - var snippet = makeSnippet(); - $scope.newSnippet = {}; - $scope.editSnippet = {}; - $scope.snippetEnterRemove(snippet); - expect($scope.deleteSnippet).toBe(snippet); - expect($scope.newSnippet).toBeNull(); - expect($scope.editSnippet).toBeNull(); - }); - }); - - describe("snippetExitRemove", function() { - - it("sets delete to null", function() { - makeController(); - $scope.deleteSnippet = {}; - $scope.snippetExitRemove(); - expect($scope.deleteSnippet).toBeNull(); - }); - }); - - describe("snippetConfirmRemove", function() { - - it("calls deleteItem and then snippetExitRemove", function() { - makeController(); - var snippet = makeSnippet(); - var defer = $q.defer(); - spyOn(DHCPSnippetsManager, "deleteItem").and.returnValue( - defer.promise); - spyOn($scope, "snippetExitRemove"); - $scope.deleteSnippet = snippet; - $scope.snippetConfirmRemove(snippet); - expect(DHCPSnippetsManager.deleteItem).toHaveBeenCalledWith( - snippet); - defer.resolve(); - $scope.$digest(); - expect($scope.snippetExitRemove).toHaveBeenCalled(); - }); - }); - - describe("snippetEnterEdit", function() { - - it("clears new and delete and sets edit", function() { - $q.defer(); - makeController(); - var snippet = makeSnippet(); - - $scope.newSnippet = {}; - $scope.deleteSnippet = {}; - $scope.snippetEnterEdit(snippet); - expect($scope.editSnippet).toBe(snippet); - expect($scope.editSnippet.type).toBe( - $scope.getSnippetTypeText(snippet)); - expect($scope.newSnippet).toBeNull(); - expect($scope.deleteSnippet).toBeNull(); - }); - }); - - describe("snippetExitEdit", function() { - - it("clears editSnippet", function() { - makeController(); - $scope.editSnippet = {}; - - $scope.snippetExitEdit(); - expect($scope.editSnippet).toBeNull(); - }); - }); - - describe("snippetToggle", function() { - - it("calls updateItem", function() { - makeController(); - var snippet = makeSnippet(); - spyOn(DHCPSnippetsManager, "updateItem").and.returnValue( - $q.defer().promise); - $scope.snippetToggle(snippet); - expect(DHCPSnippetsManager.updateItem).toHaveBeenCalledWith( - snippet); - }); - - it("updateItem reject resets enabled", function() { - makeController(); - var snippet = makeSnippet(); - defer = $q.defer(); - spyOn(DHCPSnippetsManager, "updateItem").and.returnValue( - defer.promise); - spyOn(console, "log"); - $scope.snippetToggle(snippet); - var errorMsg = makeName("error"); - defer.reject(errorMsg); - $scope.$digest(); - expect(snippet.enabled).toBe(false); - expect(console.log).toHaveBeenCalledWith(errorMsg); - }); - }); - - describe("snippetAdd", function() { - - it("sets newSnippet", function() { - makeController(); - $scope.editSnippet = {}; - $scope.deleteSnippet = {}; - $scope.snippetAdd(); - expect($scope.newSnippet).toEqual({ - name: "", - type: "Global", - enabled: true - }); - expect($scope.editSnippet).toBeNull(); - expect($scope.deleteSnippet).toBeNull(); - }); - }); - - describe("snippetAddCancel", function() { - - it("newSnippet gets cleared", function() { - makeController(); - $scope.newSnippet = {}; - $scope.snippetAddCancel(); - expect($scope.newSnippet).toBeNull(); - }); + return $controller("SettingsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + PackageRepositoriesManager: PackageRepositoriesManager, + DHCPSnippetsManager: DHCPSnippetsManager, + SubnetsManager: SubnetsManager, + MachinesManager: MachinesManager, + DevicesManager: DevicesManager, + ControllersManager: ControllersManager, + GeneralManager: GeneralManager, + ManagerHelperService: ManagerHelperService + }); + } + + it("sets title to loading and page to settings", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("settings"); + }); + + it("sets initial values", function() { + makeController(); + expect($scope.loading).toBe(true); + expect($scope.loading).toBe(true); + expect($scope.snippetsManager).toBe(DHCPSnippetsManager); + expect($scope.snippets).toBe(DHCPSnippetsManager.getItems()); + expect($scope.subnets).toBe(SubnetsManager.getItems()); + expect($scope.machines).toBe(MachinesManager.getItems()); + expect($scope.devices).toBe(DevicesManager.getItems()); + expect($scope.controllers).toBe(ControllersManager.getItems()); + expect($scope.known_architectures).toBe( + GeneralManager.getData("known_architectures") + ); + expect($scope.pockets_to_disable).toBe( + GeneralManager.getData("pockets_to_disable") + ); + expect($scope.components_to_disable).toBe( + GeneralManager.getData("components_to_disable") + ); + expect($scope.packageRepositoriesManager).toBe(PackageRepositoriesManager); + expect($scope.repositories).toBe(PackageRepositoriesManager.getItems()); + expect($scope.newSnippet).toBeNull(); + expect($scope.editSnippet).toBeNull(); + expect($scope.deleteSnippet).toBeNull(); + expect($scope.snippetTypes).toEqual(["Global", "Subnet", "Node"]); + expect($scope.newRepository).toBeNull(); + expect($scope.editRepository).toBeNull(); + expect($scope.deleteRepository).toBeNull(); + }); + + it("sets the values for 'dhcp' section", function() { + $routeParams.section = "dhcp"; + makeController(); + expect($scope.title).toBe("DHCP snippets"); + expect($scope.currentpage).toBe("dhcp"); + }); + + it("sets the values for 'repositories' section", function() { + $routeParams.section = "repositories"; + makeController(); + expect($scope.title).toBe("Package repositories"); + expect($scope.currentpage).toBe("repositories"); + }); + + it("calls loadManagers with all needed managers", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + PackageRepositoriesManager, + DHCPSnippetsManager, + MachinesManager, + DevicesManager, + ControllersManager, + SubnetsManager, + GeneralManager + ]); + }); + + it("sets loading to false", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $scope.$digest(); + expect($scope.loading).toBe(false); + }); + + describe("repositoryEnabledToggle", function() { + it("calls updateItem", function() { + makeController(); + var repository = makeRepo(); + spyOn(PackageRepositoriesManager, "updateItem"); + $scope.repositoryEnabledToggle(repository); + expect(PackageRepositoriesManager.updateItem).toHaveBeenCalledWith( + repository + ); + }); + }); + + describe("repositoryEnterRemove", function() { + it("clears new and edit and sets delete", function() { + makeController(); + var repository = makeRepo(); + $scope.newRepository = {}; + $scope.editRepository = {}; + $scope.repositoryEnterRemove(repository); + expect($scope.newRepository).toBeNull(); + expect($scope.editRepository).toBeNull(); + expect($scope.deleteRepository).toBe(repository); + }); + }); + + describe("repositoryExitRemove", function() { + it("clears deleteRepository", function() { + makeController(); + $scope.deleteRepository = {}; + $scope.repositoryExitRemove(); + expect($scope.deleteRepository).toBeNull(); + }); + }); + + describe("repositoryConfirmRemove", function() { + it("calls deleteItem and then repositoryExitRemove", function() { + makeController(); + var repository = makeRepo(); + var defer = $q.defer(); + spyOn(PackageRepositoriesManager, "deleteItem").and.returnValue( + defer.promise + ); + spyOn($scope, "repositoryExitRemove"); + $scope.deleteRepository = repository; + $scope.repositoryConfirmRemove(); + expect(PackageRepositoriesManager.deleteItem).toHaveBeenCalledWith( + repository + ); + defer.resolve(); + $scope.$digest(); + expect($scope.repositoryExitRemove).toHaveBeenCalled(); + }); + }); + + describe("isPPA", function() { + it("false when not object", function() { + makeController(); + expect($scope.isPPA(null)).toBe(false); + }); + + it("false when no url", function() { + makeController(); + expect( + $scope.isPPA({ + url: null + }) + ).toBe(false); + }); + + it("true when url startswith", function() { + makeController(); + expect( + $scope.isPPA({ + url: "ppa:" + }) + ).toBe(true); + }); + + it("true when url contains ppa.launchpad.net", function() { + makeController(); + expect( + $scope.isPPA({ + url: "http://ppa.launchpad.net/" + }) + ).toBe(true); + }); + }); + + describe("isMirror", function() { + it("false when not object", function() { + makeController(); + expect($scope.isMirror(null)).toBe(false); + }); + + it("false when no name", function() { + makeController(); + expect( + $scope.isMirror({ + name: null + }) + ).toBe(false); + }); + + it("true when name is 'main_archive'", function() { + makeController(); + expect( + $scope.isMirror({ + name: "main_archive" + }) + ).toBe(true); + }); + + it("true when name is 'ports_archive'", function() { + makeController(); + expect( + $scope.isMirror({ + name: "ports_archive" + }) + ).toBe(true); + }); + }); + + describe("repositoryEnterEdit", function() { + it("clears new and delete and sets edit", function() { + makeController(); + var repository = makeRepo(); + $scope.newRepository = {}; + $scope.deleteRepository = {}; + $scope.repositoryEnterEdit(repository); + expect($scope.editRepository).toBe(repository); + expect($scope.newRepository).toBeNull(); + expect($scope.deleteRepository).toBeNull(); + }); + }); + + describe("repositoryExitEdit", function() { + it("clears edit", function() { + makeController(); + $scope.editRepository = {}; + $scope.repositoryExitEdit(); + expect($scope.editRepository).toBeNull(); + }); + }); + + describe("repositoryAdd", function() { + it("sets newRepository for ppa", function() { + makeController(); + $scope.repositoryAdd(true); + expect($scope.newRepository).toEqual({ + name: "", + enabled: true, + url: "ppa:", + key: "", + arches: ["i386", "amd64"], + distributions: [], + components: [] + }); + }); + + it("sets newRepository not for ppa", function() { + makeController(); + $scope.repositoryAdd(false); + expect($scope.newRepository).toEqual({ + name: "", + enabled: true, + url: "", + key: "", + arches: ["i386", "amd64"], + distributions: [], + components: [] + }); + }); + }); + + describe("repositoryAddCancel", function() { + it("newRepository gets cleared", function() { + makeController(); + $scope.newRepository = {}; + $scope.repositoryAddCancel(); + expect($scope.newRepository).toBeNull(); + }); + }); + + describe("getSubnetName", function() { + it("calls SubnetsManager.getName", function() { + makeController(); + var subnet = {}; + var subnetsName = {}; + spyOn(SubnetsManager, "getName").and.returnValue(subnetsName); + expect($scope.getSubnetName(subnet)).toBe(subnetsName); + expect(SubnetsManager.getName).toHaveBeenCalledWith(subnet); + }); + }); + + describe("getSnippetTypeText", function() { + it("returns 'Node'", function() { + makeController(); + var snippet = makeSnippet(); + snippet.node = makeName("system_id"); + expect($scope.getSnippetTypeText(snippet)).toBe("Node"); + }); + + it("returns 'Subnet'", function() { + makeController(); + var snippet = makeSnippet(); + snippet.subnet = makeInteger(); + expect($scope.getSnippetTypeText(snippet)).toBe("Subnet"); + }); + + it("returns 'Global'", function() { + makeController(); + var snippet = makeSnippet(); + expect($scope.getSnippetTypeText(snippet)).toBe("Global"); + }); + }); + + describe("getSnippetAppliesToObject", function() { + it("returns node from MachinesManager", function() { + makeController(); + var system_id = makeName("system_id"); + var node = { + system_id: system_id + }; + var snippet = makeSnippet(); + snippet.node = system_id; + MachinesManager._items = [node]; + expect($scope.getSnippetAppliesToObject(snippet)).toBe(node); + }); + + it("returns device from DevicesManager", function() { + makeController(); + var system_id = makeName("system_id"); + var device = { + system_id: system_id + }; + var snippet = makeSnippet(); + snippet.node = system_id; + DevicesManager._items = [device]; + expect($scope.getSnippetAppliesToObject(snippet)).toBe(device); + }); + + it("returns controller from ControllersManager", function() { + makeController(); + var system_id = makeName("system_id"); + var controller = { + system_id: system_id + }; + var snippet = makeSnippet(); + snippet.node = system_id; + ControllersManager._items = [controller]; + expect($scope.getSnippetAppliesToObject(snippet)).toBe(controller); + }); + + it("returns subnet from SubnetsManager", function() { + makeController(); + var subnet_id = makeInteger(0, 100); + var subnet = { + id: subnet_id + }; + var snippet = makeSnippet(); + snippet.subnet = subnet_id; + SubnetsManager._items = [subnet]; + expect($scope.getSnippetAppliesToObject(snippet)).toBe(subnet); + }); + }); + + describe("getSnippetAppliesToText", function() { + it("returns node.fqdn from MachinesManager", function() { + makeController(); + var system_id = makeName("system_id"); + var fqdn = makeName("fqdn"); + var node = { + system_id: system_id, + fqdn: fqdn + }; + var snippet = makeSnippet(); + snippet.node = system_id; + MachinesManager._items = [node]; + expect($scope.getSnippetAppliesToText(snippet)).toBe(fqdn); + }); + + it("returns device.fqdn from DevicesManager", function() { + makeController(); + var system_id = makeName("system_id"); + var fqdn = makeName("fqdn"); + var device = { + system_id: system_id, + fqdn: fqdn + }; + var snippet = makeSnippet(); + snippet.node = system_id; + DevicesManager._items = [device]; + expect($scope.getSnippetAppliesToText(snippet)).toBe(fqdn); + }); + + it("returns controller.fqdn from ControllersManager", function() { + makeController(); + var system_id = makeName("system_id"); + var fqdn = makeName("fqdn"); + var controller = { + system_id: system_id, + fqdn: fqdn + }; + var snippet = makeSnippet(); + snippet.node = system_id; + ControllersManager._items = [controller]; + expect($scope.getSnippetAppliesToText(snippet)).toBe(fqdn); + }); + + it("returns subnet from SubnetsManager", function() { + makeController(); + var subnet_id = makeInteger(0, 100); + var cidr = makeName("cidr"); + var subnet = { + id: subnet_id, + cidr: cidr + }; + var snippet = makeSnippet(); + snippet.subnet = subnet_id; + SubnetsManager._items = [subnet]; + expect($scope.getSnippetAppliesToText(snippet)).toBe(cidr); + }); + }); + + describe("snippetEnterRemove", function() { + it("clears new and edit and sets delete", function() { + makeController(); + var snippet = makeSnippet(); + $scope.newSnippet = {}; + $scope.editSnippet = {}; + $scope.snippetEnterRemove(snippet); + expect($scope.deleteSnippet).toBe(snippet); + expect($scope.newSnippet).toBeNull(); + expect($scope.editSnippet).toBeNull(); + }); + }); + + describe("snippetExitRemove", function() { + it("sets delete to null", function() { + makeController(); + $scope.deleteSnippet = {}; + $scope.snippetExitRemove(); + expect($scope.deleteSnippet).toBeNull(); + }); + }); + + describe("snippetConfirmRemove", function() { + it("calls deleteItem and then snippetExitRemove", function() { + makeController(); + var snippet = makeSnippet(); + var defer = $q.defer(); + spyOn(DHCPSnippetsManager, "deleteItem").and.returnValue(defer.promise); + spyOn($scope, "snippetExitRemove"); + $scope.deleteSnippet = snippet; + $scope.snippetConfirmRemove(snippet); + expect(DHCPSnippetsManager.deleteItem).toHaveBeenCalledWith(snippet); + defer.resolve(); + $scope.$digest(); + expect($scope.snippetExitRemove).toHaveBeenCalled(); + }); + }); + + describe("snippetEnterEdit", function() { + it("clears new and delete and sets edit", function() { + $q.defer(); + makeController(); + var snippet = makeSnippet(); + + $scope.newSnippet = {}; + $scope.deleteSnippet = {}; + $scope.snippetEnterEdit(snippet); + expect($scope.editSnippet).toBe(snippet); + expect($scope.editSnippet.type).toBe($scope.getSnippetTypeText(snippet)); + expect($scope.newSnippet).toBeNull(); + expect($scope.deleteSnippet).toBeNull(); + }); + }); + + describe("snippetExitEdit", function() { + it("clears editSnippet", function() { + makeController(); + $scope.editSnippet = {}; + + $scope.snippetExitEdit(); + expect($scope.editSnippet).toBeNull(); + }); + }); + + describe("snippetToggle", function() { + it("calls updateItem", function() { + makeController(); + var snippet = makeSnippet(); + spyOn(DHCPSnippetsManager, "updateItem").and.returnValue( + $q.defer().promise + ); + $scope.snippetToggle(snippet); + expect(DHCPSnippetsManager.updateItem).toHaveBeenCalledWith(snippet); + }); + + it("updateItem reject resets enabled", function() { + makeController(); + var snippet = makeSnippet(); + let defer = $q.defer(); + spyOn(DHCPSnippetsManager, "updateItem").and.returnValue(defer.promise); + spyOn(console, "log"); + $scope.snippetToggle(snippet); + var errorMsg = makeName("error"); + defer.reject(errorMsg); + $scope.$digest(); + expect(snippet.enabled).toBe(false); + expect(console.log).toHaveBeenCalledWith(errorMsg); + }); + }); + + describe("snippetAdd", function() { + it("sets newSnippet", function() { + makeController(); + $scope.editSnippet = {}; + $scope.deleteSnippet = {}; + $scope.snippetAdd(); + expect($scope.newSnippet).toEqual({ + name: "", + type: "Global", + enabled: true + }); + expect($scope.editSnippet).toBeNull(); + expect($scope.deleteSnippet).toBeNull(); + }); + }); + + describe("snippetAddCancel", function() { + it("newSnippet gets cleared", function() { + makeController(); + $scope.newSnippet = {}; + $scope.snippetAddCancel(); + expect($scope.newSnippet).toBeNull(); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_space_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_space_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_space_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_space_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,260 +4,255 @@ * Unit tests for SpacesListController. */ -describe("SpaceDetailsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Make a fake space - function makeSpace() { - var space = { - id: makeInteger(1, 10000), - name: makeName("space") - }; - SpacesManager._items.push(space); - return space; - } - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $routeParams; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load any injected managers and services. - var SpacesManager, VLANsManager, SubnetsManager, FabricsManager; - var ControllersManager, UsersManager, ManagerHelperService, ErrorService; - beforeEach(inject(function($injector) { - SpacesManager = $injector.get("SpacesManager"); - VLANsManager = $injector.get("VLANsManager"); - SubnetsManager = $injector.get("SubnetsManager"); - FabricsManager = $injector.get("FabricsManager"); - ControllersManager = $injector.get("ControllersManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - })); - - var space; - beforeEach(function() { - space = makeSpace(); - }); +import { makeInteger, makeName } from "testing/utils"; - // Makes the NodesListController - function makeController(loadManagerDefer) { - spyOn(UsersManager, "isSuperUser").and.returnValue(true); - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("SpaceDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - SpacesManager: SpacesManager, - VLANsManager: VLANsManager, - SubnetsManager: SubnetsManager, - FabricsManager: FabricsManager, - ControllersManager: ControllersManager, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); - - return controller; - } - - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(SpacesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - $routeParams.space_id = space.id; - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(space); - $rootScope.$digest(); +describe("SpaceDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - return controller; + // Make a fake space + function makeSpace() { + var space = { + id: makeInteger(1, 10000), + name: makeName("space") + }; + SpacesManager._items.push(space); + return space; + } + + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load any injected managers and services. + var SpacesManager, VLANsManager, SubnetsManager, FabricsManager; + var ControllersManager, UsersManager, ManagerHelperService, ErrorService; + beforeEach(inject(function($injector) { + SpacesManager = $injector.get("SpacesManager"); + VLANsManager = $injector.get("VLANsManager"); + SubnetsManager = $injector.get("SubnetsManager"); + FabricsManager = $injector.get("FabricsManager"); + ControllersManager = $injector.get("ControllersManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + })); + + var space; + beforeEach(function() { + space = makeSpace(); + }); + + // Makes the NodesListController + function makeController(loadManagerDefer) { + spyOn(UsersManager, "isSuperUser").and.returnValue(true); + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { + // Create the controller. + var controller = $controller("SpaceDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + SpacesManager: SpacesManager, + VLANsManager: VLANsManager, + SubnetsManager: SubnetsManager, + FabricsManager: FabricsManager, + ControllersManager: ControllersManager, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(SpacesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + var controller = makeController(defer); + $routeParams.space_id = space.id; + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(space); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("networks"); + }); + + it( + "calls loadManagers with correct managers" + + function() { makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("networks"); - }); - - it("calls loadManagers with correct managers" + - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - SpacesManager, VLANsManager, SubnetsManager, - FabricsManager, UsersManager]); - }); - - it("raises error if space identifier is invalid", function() { - spyOn(SpacesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ErrorService, "raiseError").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.space_id = 'xyzzy'; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.space).toBe(null); - expect($scope.loaded).toBe(false); - expect(SpacesManager.setActiveItem).not.toHaveBeenCalled(); - expect(ErrorService.raiseError).toHaveBeenCalled(); - }); - - it("doesn't call setActiveItem if space is loaded", function() { - spyOn(SpacesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - SpacesManager._activeItem = space; - $routeParams.space_id = space.id; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.space).toBe(space); - expect($scope.loaded).toBe(true); - expect(SpacesManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if space is not active", function() { - spyOn(SpacesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.space_id = space.id; - - defer.resolve(); - $rootScope.$digest(); - - expect(SpacesManager.setActiveItem).toHaveBeenCalledWith( - space.id); - }); - - it("sets space and loaded once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.space).toBe(space); - expect($scope.loaded).toBe(true); - }); - - it("title is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(space.name); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + SpacesManager, + VLANsManager, + SubnetsManager, + FabricsManager, + UsersManager + ]); + } + ); + + it("raises error if space identifier is invalid", function() { + spyOn(SpacesManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ErrorService, "raiseError").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.space_id = "xyzzy"; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.space).toBe(null); + expect($scope.loaded).toBe(false); + expect(SpacesManager.setActiveItem).not.toHaveBeenCalled(); + expect(ErrorService.raiseError).toHaveBeenCalled(); + }); + + it("doesn't call setActiveItem if space is loaded", function() { + spyOn(SpacesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + SpacesManager._activeItem = space; + $routeParams.space_id = space.id; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.space).toBe(space); + expect($scope.loaded).toBe(true); + expect(SpacesManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if space is not active", function() { + spyOn(SpacesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.space_id = space.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(SpacesManager.setActiveItem).toHaveBeenCalledWith(space.id); + }); + + it("sets space and loaded once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.space).toBe(space); + expect($scope.loaded).toBe(true); + }); + + it("title is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(space.name); + }); + + it("default space title is not special", function() { + space.id = 0; + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(space.name); + }); + + describe("enterEditSummary", function() { + it("sets editSummary", function() { + makeController(); + $scope.enterEditSummary(); + expect($scope.editSummary).toBe(true); + }); + }); + + describe("exitEditSummary", function() { + it("sets editSummary", function() { + makeController(); + $scope.enterEditSummary(); + $scope.exitEditSummary(); + expect($scope.editSummary).toBe(false); + }); + }); + + describe("canBeDeleted", function() { + it("returns false if space is null", function() { + makeControllerResolveSetActiveItem(); + $scope.space = null; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns false if space has subnets", function() { + makeControllerResolveSetActiveItem(); + $scope.space.subnet_ids = [makeInteger()]; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns true if space has no subnets", function() { + makeControllerResolveSetActiveItem(); + $scope.space.subnet_ids = []; + expect($scope.canBeDeleted()).toBe(true); + }); + }); + + describe("deleteButton", function() { + it("confirms delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + expect($scope.confirmingDelete).toBe(true); + }); + + it("clears error", function() { + makeControllerResolveSetActiveItem(); + $scope.error = makeName("error"); + $scope.deleteButton(); + expect($scope.error).toBeNull(); + }); + }); + + describe("cancelDeleteButton", function() { + it("cancels delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + $scope.cancelDeleteButton(); + expect($scope.confirmingDelete).toBe(false); + }); + }); + + describe("deleteSpace", function() { + it("calls deleteSpace", function() { + $location = {}; + $location.path = jasmine.createSpy("path"); + $location.search = jasmine.createSpy("search"); + makeController(); + var deleteSpace = spyOn(SpacesManager, "deleteSpace"); + var defer = $q.defer(); + deleteSpace.and.returnValue(defer.promise); + $scope.deleteConfirmButton(); + defer.resolve(); + $rootScope.$apply(); + expect(deleteSpace).toHaveBeenCalled(); + expect($location.path).toHaveBeenCalledWith("/networks"); + expect($location.search).toHaveBeenCalledWith("by", "space"); }); - - it("default space title is not special", function() { - space.id = 0; - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(space.name); - }); - - describe("enterEditSummary", function() { - - it("sets editSummary", function() { - makeController(); - $scope.enterEditSummary(); - expect($scope.editSummary).toBe(true); - }); - }); - - describe("exitEditSummary", function() { - - it("sets editSummary", function() { - makeController(); - $scope.enterEditSummary(); - $scope.exitEditSummary(); - expect($scope.editSummary).toBe(false); - }); - }); - - describe("canBeDeleted", function() { - - it("returns false if space is null", function() { - makeControllerResolveSetActiveItem(); - $scope.space = null; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns false if space has subnets", function() { - makeControllerResolveSetActiveItem(); - $scope.space.subnet_ids = [makeInteger()]; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns true if space has no subnets", function() { - makeControllerResolveSetActiveItem(); - $scope.space.subnet_ids = []; - expect($scope.canBeDeleted()).toBe(true); - }); - }); - - describe("deleteButton", function() { - - it("confirms delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - expect($scope.confirmingDelete).toBe(true); - }); - - it("clears error", function() { - makeControllerResolveSetActiveItem(); - $scope.error = makeName("error"); - $scope.deleteButton(); - expect($scope.error).toBeNull(); - }); - }); - - describe("cancelDeleteButton", function() { - - it("cancels delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - $scope.cancelDeleteButton(); - expect($scope.confirmingDelete).toBe(false); - }); - }); - - describe("deleteSpace", function() { - - it("calls deleteSpace", function() { - $location = {}; - $location.path = jasmine.createSpy('path'); - $location.search = jasmine.createSpy('search'); - makeController(); - var deleteSpace = spyOn(SpacesManager, "deleteSpace"); - var defer = $q.defer(); - deleteSpace.and.returnValue(defer.promise); - $scope.deleteConfirmButton(); - defer.resolve(); - $rootScope.$apply(); - expect(deleteSpace).toHaveBeenCalled(); - expect($location.path).toHaveBeenCalledWith("/networks"); - expect($location.search).toHaveBeenCalledWith("by", "space"); - }); - }); - + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_subnet_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,740 +4,710 @@ * Unit tests for SubnetsListController. */ -describe("SubnetDetailsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Make a fake fabric - function makeFabric() { - var fabric = { - id: 0, - name: "fabric-0" - }; - FabricsManager._items.push(fabric); - } - - function makeVLAN() { - var vlan = { - id: 0, - fabric: 0, - vid: 0 - }; - VLANsManager._items.push(vlan); - } - - function makeSpace() { - var space = { - id: 0, - name: "default" - }; - SpacesManager._items.push(space); - } - - // Make a fake subnet - function makeSubnet() { - var subnet = { - id: makeInteger(1, 10000), - cidr: '169.254.0.0/24', - name: 'Link Local', - vlan: 0, - dns_servers: [] - }; - SubnetsManager._items.push(subnet); - return subnet; - } - - function makeIPRange() { - var iprange = { - id: makeInteger(1, 10000), - subnet: subnet.id - }; - IPRangesManager._items.push(iprange); - return iprange; - } - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $routeParams, $filter; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - $location = $injector.get("$filter"); - })); - - // Load any injected managers and services. - var ConfigsManager, SubnetsManager, IPRangesManager, StaticRoutesManager; - var SpacesManager, VLANsManager, FabricsManager, UsersManager; - var HelperService, ErrorService, ConverterService; - beforeEach(inject(function($injector) { - ConfigsManager = $injector.get("ConfigsManager"); - SubnetsManager = $injector.get("SubnetsManager"); - IPRangesManager = $injector.get("IPRangesManager"); - StaticRoutesManager = $injector.get("StaticRoutesManager"); - SpacesManager = $injector.get("SpacesManager"); - VLANsManager = $injector.get("VLANsManager"); - FabricsManager = $injector.get("FabricsManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - ConverterService = $injector.get("ConverterService"); - })); - - var fabric, vlan, space, subnet; - beforeEach(function() { - fabric = makeFabric(); - vlan = makeVLAN(); - space = makeSpace(); - subnet = makeSubnet(); - }); - - // Makes the SubnetDetailsController - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } +import { makeInteger, makeName } from "testing/utils"; - // Create the controller. - var controller = $controller("SubnetDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - ConfigsManager: ConfigsManager, - SubnetsManager: SubnetsManager, - IPRangesManager: IPRangesManager, - SpacesManager: SpacesManager, - VLANsManager: VLANsManager, - FabricsManager: FabricsManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); - - return controller; - } - - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(SubnetsManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - spyOn(ConfigsManager, "getItemFromList").and.returnValue( - {'value': "", 'choices': []}); - var defer = $q.defer(); - var controller = makeController(defer); - $routeParams.subnet_id = subnet.id; - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(subnet); - $rootScope.$digest(); +describe("SubnetDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - return controller; + // Make a fake fabric + function makeFabric() { + var fabric = { + id: 0, + name: "fabric-0" + }; + FabricsManager._items.push(fabric); + } + + function makeVLAN() { + var vlan = { + id: 0, + fabric: 0, + vid: 0 + }; + VLANsManager._items.push(vlan); + } + + function makeSpace() { + var space = { + id: 0, + name: "default" + }; + SpacesManager._items.push(space); + } + + // Make a fake subnet + function makeSubnet() { + var subnet = { + id: makeInteger(1, 10000), + cidr: "169.254.0.0/24", + name: "Link Local", + vlan: 0, + dns_servers: [] + }; + SubnetsManager._items.push(subnet); + return subnet; + } + + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + $location = $injector.get("$filter"); + })); + + // Load any injected managers and services. + var ConfigsManager, SubnetsManager, IPRangesManager, StaticRoutesManager; + var SpacesManager, VLANsManager, FabricsManager; + var ErrorService, ConverterService, ManagerHelperService; + beforeEach(inject(function($injector) { + ConfigsManager = $injector.get("ConfigsManager"); + SubnetsManager = $injector.get("SubnetsManager"); + IPRangesManager = $injector.get("IPRangesManager"); + StaticRoutesManager = $injector.get("StaticRoutesManager"); + SpacesManager = $injector.get("SpacesManager"); + VLANsManager = $injector.get("VLANsManager"); + FabricsManager = $injector.get("FabricsManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + ConverterService = $injector.get("ConverterService"); + })); + + var subnet; + beforeEach(function() { + makeFabric(); + makeVLAN(); + makeSpace(); + subnet = makeSubnet(); + }); + + // Makes the SubnetDetailsController + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { + // Create the controller. + var controller = $controller("SubnetDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + ConfigsManager: ConfigsManager, + SubnetsManager: SubnetsManager, + IPRangesManager: IPRangesManager, + SpacesManager: SpacesManager, + VLANsManager: VLANsManager, + FabricsManager: FabricsManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(SubnetsManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + spyOn(ConfigsManager, "getItemFromList").and.returnValue({ + value: "", + choices: [] + }); + var defer = $q.defer(); + var controller = makeController(defer); + $routeParams.subnet_id = subnet.id; + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(subnet); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("networks"); + }); + + it( + "calls loadManagers with required managers" + + function() { makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("networks"); - }); - - it("calls loadManagers with required managers" + - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - ConfigsManager, SubnetsManager, IPRangesManager, - SpacesManager, VLANsManager, FabricsManager - ]); - }); - - it("raises error if subnet identifier is invalid", function() { - spyOn(SubnetsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ConfigsManager, "getItemFromList").and.returnValue( - {'value': "", 'choices': []}); - spyOn(ErrorService, "raiseError").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.subnet_id = 'xyzzy'; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.subnet).toBe(null); - expect($scope.loaded).toBe(false); - expect(SubnetsManager.setActiveItem).not.toHaveBeenCalled(); - expect(ErrorService.raiseError).toHaveBeenCalled(); - }); - - it("doesn't call setActiveItem if subnet is loaded", function() { - spyOn(SubnetsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ConfigsManager, "getItemFromList").and.returnValue( - {'value': "", 'choices': []}); - var defer = $q.defer(); - makeController(defer); - SubnetsManager._activeItem = subnet; - $routeParams.subnet_id = subnet.id; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.subnet).toBe(subnet); - expect($scope.loaded).toBe(true); - expect(SubnetsManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if subnet is not active", function() { - spyOn(SubnetsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ConfigsManager, "getItemFromList").and.returnValue( - {'value': "", 'choices': []}); - var defer = $q.defer(); - makeController(defer); - $routeParams.subnet_id = subnet.id; - - defer.resolve(); - $rootScope.$digest(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ConfigsManager, + SubnetsManager, + IPRangesManager, + SpacesManager, + VLANsManager, + FabricsManager + ]); + } + ); + + it("raises error if subnet identifier is invalid", function() { + spyOn(SubnetsManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ConfigsManager, "getItemFromList").and.returnValue({ + value: "", + choices: [] + }); + spyOn(ErrorService, "raiseError").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.subnet_id = "xyzzy"; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.subnet).toBe(null); + expect($scope.loaded).toBe(false); + expect(SubnetsManager.setActiveItem).not.toHaveBeenCalled(); + expect(ErrorService.raiseError).toHaveBeenCalled(); + }); + + it("doesn't call setActiveItem if subnet is loaded", function() { + spyOn(SubnetsManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ConfigsManager, "getItemFromList").and.returnValue({ + value: "", + choices: [] + }); + var defer = $q.defer(); + makeController(defer); + SubnetsManager._activeItem = subnet; + $routeParams.subnet_id = subnet.id; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.subnet).toBe(subnet); + expect($scope.loaded).toBe(true); + expect(SubnetsManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if subnet is not active", function() { + spyOn(SubnetsManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ConfigsManager, "getItemFromList").and.returnValue({ + value: "", + choices: [] + }); + var defer = $q.defer(); + makeController(defer); + $routeParams.subnet_id = subnet.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(SubnetsManager.setActiveItem).toHaveBeenCalledWith(subnet.id); + }); + + it("sets subnet and loaded once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.subnet).toBe(subnet); + expect($scope.loaded).toBe(true); + }); + + it("title is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(subnet.cidr + " (" + subnet.name + ")"); + }); + + describe("ipSort", function() { + it("calls ipv4ToInteger when ipVersion == 4", function() { + makeControllerResolveSetActiveItem(); + $scope.ipVersion = 4; + var expected = {}; + spyOn(ConverterService, "ipv4ToInteger").and.returnValue(expected); + var ipAddress = { + ip: {} + }; + var observed = $scope.ipSort(ipAddress); + expect(ConverterService.ipv4ToInteger).toHaveBeenCalledWith(ipAddress.ip); + expect(observed).toBe(expected); + }); + + it("calls ipv6Expand when ipVersion == 6", function() { + makeControllerResolveSetActiveItem(); + $scope.ipVersion = 6; + var expected = {}; + spyOn(ConverterService, "ipv6Expand").and.returnValue(expected); + var ipAddress = { + ip: {} + }; + var observed = $scope.ipSort(ipAddress); + expect(ConverterService.ipv6Expand).toHaveBeenCalledWith(ipAddress.ip); + expect(observed).toBe(expected); + }); + + it("is predicate default", function() { + makeControllerResolveSetActiveItem(); + expect($scope.predicate).toBe($scope.ipSort); + }); + }); + + describe("getAllocType", function() { + var scenarios = { + 0: "Automatic", + 1: "Static", + 4: "User reserved", + 5: "DHCP", + 6: "Observed", + 7: "Unknown" + }; - expect(SubnetsManager.setActiveItem).toHaveBeenCalledWith( - subnet.id); - }); - - it("sets subnet and loaded once setActiveItem resolves", function() { + angular.forEach(scenarios, function(expected, allocType) { + it("allocType( " + allocType + ") = " + expected, function() { makeControllerResolveSetActiveItem(); - expect($scope.subnet).toBe(subnet); - expect($scope.loaded).toBe(true); + expect($scope.getAllocType(allocType)).toBe(expected); + }); }); + }); - it("title is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(subnet.cidr + " (" + subnet.name + ")"); - }); + describe("allocTypeSort", function() { + it("calls getAllocType", function() { + makeControllerResolveSetActiveItem(); + var expected = {}; + spyOn($scope, "getAllocType").and.returnValue(expected); + var ipAddress = { + alloc_type: {} + }; + var observed = $scope.allocTypeSort(ipAddress); + expect($scope.getAllocType).toHaveBeenCalledWith(ipAddress.alloc_type); + expect(observed).toBe(expected); + }); + }); + + describe("getUsageForIP", function() { + var scenarios = { + 0: "Machine", + 1: "Device", + 2: "Rack controller", + 3: "Region controller", + 4: "Rack and region controller", + 5: "Chassis", + 6: "Storage", + 7: "Unknown" + }; - describe("ipSort", function() { - - it("calls ipv4ToInteger when ipVersion == 4", function() { - makeControllerResolveSetActiveItem(); - $scope.ipVersion = 4; - var expected = {}; - spyOn(ConverterService, "ipv4ToInteger").and.returnValue(expected); - var ipAddress = { - ip: {} - }; - var observed = $scope.ipSort(ipAddress); - expect(ConverterService.ipv4ToInteger).toHaveBeenCalledWith( - ipAddress.ip); - expect(observed).toBe(expected); - }); - - it("calls ipv6Expand when ipVersion == 6", function() { - makeControllerResolveSetActiveItem(); - $scope.ipVersion = 6; - var expected = {}; - spyOn(ConverterService, "ipv6Expand").and.returnValue(expected); - var ipAddress = { - ip: {} - }; - var observed = $scope.ipSort(ipAddress); - expect(ConverterService.ipv6Expand).toHaveBeenCalledWith( - ipAddress.ip); - expect(observed).toBe(expected); - }); - - it("is predicate default", function() { - makeControllerResolveSetActiveItem(); - expect($scope.predicate).toBe($scope.ipSort); - }); - }); - - describe("getAllocType", function() { - - var scenarios = { - 0: 'Automatic', - 1: 'Static', - 4: 'User reserved', - 5: 'DHCP', - 6: 'Observed', - 7: 'Unknown' - }; - - angular.forEach(scenarios, function(expected, allocType) { - it("allocType( " + allocType + ") = " + expected, function() { - makeControllerResolveSetActiveItem(); - expect($scope.getAllocType(allocType)).toBe(expected); - }); - }); - }); - - describe("allocTypeSort", function() { - - it("calls getAllocType", function() { - makeControllerResolveSetActiveItem(); - var expected = {}; - spyOn($scope, "getAllocType").and.returnValue(expected); - var ipAddress = { - alloc_type: {} - }; - var observed = $scope.allocTypeSort(ipAddress); - expect($scope.getAllocType).toHaveBeenCalledWith( - ipAddress.alloc_type); - expect(observed).toBe(expected); - }); - }); - - describe("getUsageForIP", function() { - - var scenarios = { - 0: 'Machine', - 1: 'Device', - 2: 'Rack controller', - 3: 'Region controller', - 4: 'Rack and region controller', - 5: 'Chassis', - 6: 'Storage', - 7: 'Unknown' - }; - - angular.forEach(scenarios, function(expected, nodeType) { - it("nodeType( " + nodeType + ") = " + expected, function() { - makeControllerResolveSetActiveItem(); - var ip = {}; - ip.node_summary = {}; - ip.node_summary.node_type = nodeType; - expect($scope.getUsageForIP(ip)).toBe(expected); - }); - }); - - it("handles BMCs", function() { - makeControllerResolveSetActiveItem(); - var ipAddress = { - bmcs: [] - }; - expect($scope.getUsageForIP(ipAddress)).toBe('BMC'); - }); - - it("handles containers", function() { - makeControllerResolveSetActiveItem(); - var ip = {}; - ip.node_summary = {}; - ip.node_summary.node_type = 1; - ip.node_summary.is_container = true; - expect($scope.getUsageForIP(ip)).toBe('Container'); - }); - - it("handles DNS records", function() { - makeControllerResolveSetActiveItem(); - var ipAddress = { - dns_records: [] - }; - expect($scope.getUsageForIP(ipAddress)).toBe('DNS'); - }); - - it("handles the unknown", function() { - makeControllerResolveSetActiveItem(); - var ipAddress = {}; - expect($scope.getUsageForIP(ipAddress)).toBe('Unknown'); - }); - }); - - describe("nodeTypeSort", function() { - - it("calls getUsageForIP", function() { - makeControllerResolveSetActiveItem(); - var expected = {}; - spyOn($scope, "getUsageForIP").and.returnValue(expected); - var ipAddress = { - node_summary: { - node_type: {} - } - }; - var observed = $scope.nodeTypeSort(ipAddress); - expect($scope.getUsageForIP).toHaveBeenCalledWith(ipAddress); - expect(observed).toBe(expected); - }); - }); - - describe("ownerSort", function() { - - it("returns owner", function() { - makeControllerResolveSetActiveItem(); - var ipAddress = { - user: makeName("owner") - }; - var observed = $scope.ownerSort(ipAddress); - expect(observed).toBe(ipAddress.user); - }); - - it("returns MAAS for empty string", function() { - makeControllerResolveSetActiveItem(); - var ipAddress = { - user: "" - }; - var observed = $scope.ownerSort(ipAddress); - expect(observed).toBe("MAAS"); - }); - - it("returns MAAS for null", function() { - makeControllerResolveSetActiveItem(); - var ipAddress = { - user: null - }; - var observed = $scope.ownerSort(ipAddress); - expect(observed).toBe("MAAS"); - }); - }); - - describe("sortIPTable", function() { - - it("sets predicate and inverts reverse", function() { - makeControllerResolveSetActiveItem(); - $scope.reverse = true; - var predicate = {}; - $scope.sortIPTable(predicate); - expect($scope.predicate).toBe(predicate); - expect($scope.reverse).toBe(false); - $scope.sortIPTable(predicate); - expect($scope.reverse).toBe(true); - }); - }); - - describe("subnetPreSave", function() { - - it("updates vlan when fabric changed", function() { - makeController(); - var vlan = { - id: makeInteger(0, 100) - }; - var fabric = { - id: makeInteger(1, 100), - default_vlan_id: vlan.id, - vlan_ids: [vlan.id] - }; - FabricsManager._items.push(fabric); - var subnet = { - fabric: fabric.id - }; - var updatedSubnet = $scope.subnetPreSave(subnet, ['fabric']); - expect(updatedSubnet.vlan).toBe(vlan.id); - }); - }); - - describe("editSubnetSummary", function() { - - it("enters edit mode for summary", function() { - makeController(); - $scope.editSummary = false; - $scope.enterEditSummary(); - expect($scope.editSummary).toBe(true); - }); - }); - - describe("exitEditSubnetSummary", function() { - - it("enters edit mode for summary", function() { - makeController(); - $scope.editSummary = true; - $scope.exitEditSummary(); - expect($scope.editSummary).toBe(false); - }); - }); - - describe("addStaticRoute", function() { - - it("set newStaticRoute", function() { - makeController(); - $scope.subnet = { - id: makeInteger(0, 100) - }; - $scope.addStaticRoute(); - expect($scope.newStaticRoute).toEqual({ - source: $scope.subnet.id, - gateway_ip: "", - destination: null, - metric: 0 - }); - }); - - it("clear editStaticRoute", function() { - makeController(); - $scope.subnet = { - id: makeInteger(0, 100) - }; - $scope.editStaticRoute = {}; - $scope.addStaticRoute(); - expect($scope.editStaticRoute).toBeNull(); - }); - - it("clear deleteStaticRoute", function() { - makeController(); - $scope.subnet = { - id: makeInteger(0, 100) - }; - $scope.deleteStaticRoute = {}; - $scope.addStaticRoute(); - expect($scope.deleteStaticRoute).toBeNull(); - }); - }); - - describe("cancelAddStaticRoute", function() { - - it("clears newStaticRoute", function() { - makeController(); - $scope.newStaticRoute = {}; - $scope.cancelAddStaticRoute(); - expect($scope.newStaticRoute).toBeNull(); - }); - }); - - describe("isStaticRouteInEditMode", function() { - - it("returns true when editStaticRoute", function() { - makeController(); - var route = {}; - $scope.editStaticRoute = route; - expect($scope.isStaticRouteInEditMode(route)).toBe(true); - }); - - it("returns false when editIPRange", function() { - makeController(); - var route = {}; - $scope.editStaticRoute = route; - expect($scope.isStaticRouteInEditMode({})).toBe(false); - }); - }); - - describe("staticRouteToggleEditMode", function() { - - it("clears newStaticRoute", function() { - makeController(); - $scope.newStaticRoute = {}; - $scope.staticRouteToggleEditMode({}); - expect($scope.newStaticRoute).toBeNull(); - }); - - it("clears deleteStaticRoute", function() { - makeController(); - $scope.deleteStaticRoute = {}; - $scope.staticRouteToggleEditMode({}); - expect($scope.deleteStaticRoute).toBeNull(); - }); - - it("clears editStaticRoute when already set", function() { - makeController(); - var route = {}; - $scope.editStaticRoute = route; - $scope.staticRouteToggleEditMode(route); - expect($scope.editStaticRoute).toBeNull(); - }); - - it("sets editStaticRoute when different range", function() { - makeController(); - var route = {}; - var otherRoute = {}; - $scope.editStaticRoute = otherRoute; - $scope.staticRouteToggleEditMode(route); - expect($scope.editStaticRoute).toBe(route); - }); - }); - - describe("isStaticRouteInDeleteMode", function() { - - it("return true when deleteStaticRoute is same", function() { - makeController(); - var route = {}; - $scope.deleteStaticRoute = route; - expect($scope.isStaticRouteInDeleteMode(route)).toBe(true); - }); - - it("return false when deleteIPRange is different", function() { - makeController(); - var route = {}; - $scope.deleteStaticRoute = route; - expect($scope.isStaticRouteInDeleteMode({})).toBe(false); - }); - }); - - describe("staticRouteEnterDeleteMode", function() { - - it("clears edit and new and sets deleteStaticRoute", function() { - makeController(); - var route = {}; - $scope.newStaticRoute = {}; - $scope.editStaticRoute = {}; - $scope.staticRouteEnterDeleteMode(route); - expect($scope.newStaticRoute).toBeNull(); - expect($scope.editStaticRoute).toBeNull(); - expect($scope.deleteStaticRoute).toBe(route); - }); - }); - - describe("staticRouteCancelDelete", function() { - - it("clears deleteStaticRoute", function() { - makeController(); - $scope.deleteStaticRoute = {}; - $scope.staticRouteCancelDelete(); - expect($scope.deleteStaticRoute).toBeNull(); - }); - }); - - describe("staticRouteConfirmDelete", function() { - - it("calls deleteItem and clears deleteStaticRoute on resolve", - function() { - makeController(); - var route = {}; - $scope.deleteStaticRoute = route; - - var defer = $q.defer(); - spyOn(StaticRoutesManager, "deleteItem").and.returnValue( - defer.promise); - $scope.staticRouteConfirmDelete(); - - expect(StaticRoutesManager.deleteItem).toHaveBeenCalledWith( - route); - defer.resolve(); - $scope.$digest(); - - expect($scope.deleteStaticRoute).toBeNull(); - }); - }); - - describe("actionRetry", function() { - - it("clears actionError", function() { - makeController(); - $scope.actionError = {}; - $scope.actionRetry(); - expect($scope.actionError).toBeNull(); - }); - }); - - describe("actionGo", function() { - - it("map_subnet action calls scanSubnet", function() { - makeControllerResolveSetActiveItem(); - var scanSubnet = spyOn(SubnetsManager, "scanSubnet"); - var defer = $q.defer(); - result = { - result: "Error message from scan.", - scan_started_on: ['not empty'] - }; - scanSubnet.and.returnValue(defer.promise); - $scope.actionOption = { - name: "map_subnet", - title: "Map subnet" - }; - $scope.actionGo(); - defer.resolve(result); - $scope.$digest(); - expect(scanSubnet).toHaveBeenCalled(); - expect($scope.actionOption).toBeNull(); - expect($scope.actionError).toBeNull(); - }); - - it("actionError populated on scans not started", function() { - makeControllerResolveSetActiveItem(); - var scanSubnet = spyOn(SubnetsManager, "scanSubnet"); - var defer = $q.defer(); - result = { - result: "Error message from scan.", - scan_started_on: [] - }; - scanSubnet.and.returnValue(defer.promise); - $scope.actionOption = { - name: "map_subnet", - title: "Map subnet" - }; - $scope.actionGo(); - defer.resolve(result); - $scope.$digest(); - expect(scanSubnet).toHaveBeenCalled(); - expect($scope.actionError).toBe("Error message from scan."); - }); - - it("actionError populated on map_subnet action failure", function() { - makeControllerResolveSetActiveItem(); - $scope.actionOption = { - name: "map_subnet", - title: "Map subnet" - }; - var defer = $q.defer(); - spyOn(SubnetsManager, "scanSubnet").and.returnValue( - defer.promise); - $scope.actionGo(); - error = 'errorString'; - $scope.actionOption = null; - defer.reject(error); - $scope.$digest(); - expect($scope.actionError).toBe(error); - }); - - it("delete action calls deleteSubnet", function() { - $location.path = jasmine.createSpy('path'); - makeControllerResolveSetActiveItem(); - var deleteSubnet = spyOn(SubnetsManager, "deleteSubnet"); - var defer = $q.defer(); - deleteSubnet.and.returnValue(defer.promise); - $scope.actionOption = { - name: "delete", - title: "Delete" - }; - $scope.actionGo(); - defer.resolve(); - $scope.$digest(); - expect(deleteSubnet).toHaveBeenCalled(); - expect($location.path).toHaveBeenCalledWith("/networks"); - expect($scope.actionOption).toBeNull(); - expect($scope.actionError).toBeNull(); - }); - - it("actionError populated on delete action failure", function() { - makeControllerResolveSetActiveItem(); - $scope.actionOption = { - name: "delete", - title: "Delete" - }; - var defer = $q.defer(); - spyOn(SubnetsManager, "deleteSubnet").and.returnValue( - defer.promise); - $scope.actionGo(); - error = 'errorString'; - $scope.actionOption = null; - defer.reject(error); - $scope.$digest(); - expect($scope.actionError).toBe(error); - }); - }); - - describe("actionChanged", function() { - - it("clears actionError", function() { - makeController(); - $scope.actionError = {}; - $scope.actionChanged(); - expect($scope.actionError).toBeNull(); - }); - }); - - describe("cancelAction", function() { - - it("clears actionOption and actionError", function() { - makeController(); - $scope.actionOption = {}; - $scope.actionError = {}; - $scope.cancelAction(); - expect($scope.actionOption).toBeNull(); - expect($scope.actionError).toBeNull(); - }); + angular.forEach(scenarios, function(expected, nodeType) { + it("nodeType( " + nodeType + ") = " + expected, function() { + makeControllerResolveSetActiveItem(); + var ip = {}; + ip.node_summary = {}; + ip.node_summary.node_type = nodeType; + expect($scope.getUsageForIP(ip)).toBe(expected); + }); + }); + + it("handles BMCs", function() { + makeControllerResolveSetActiveItem(); + var ipAddress = { + bmcs: [] + }; + expect($scope.getUsageForIP(ipAddress)).toBe("BMC"); + }); + + it("handles containers", function() { + makeControllerResolveSetActiveItem(); + var ip = {}; + ip.node_summary = {}; + ip.node_summary.node_type = 1; + ip.node_summary.is_container = true; + expect($scope.getUsageForIP(ip)).toBe("Container"); + }); + + it("handles DNS records", function() { + makeControllerResolveSetActiveItem(); + var ipAddress = { + dns_records: [] + }; + expect($scope.getUsageForIP(ipAddress)).toBe("DNS"); + }); + + it("handles the unknown", function() { + makeControllerResolveSetActiveItem(); + var ipAddress = {}; + expect($scope.getUsageForIP(ipAddress)).toBe("Unknown"); + }); + }); + + describe("nodeTypeSort", function() { + it("calls getUsageForIP", function() { + makeControllerResolveSetActiveItem(); + var expected = {}; + spyOn($scope, "getUsageForIP").and.returnValue(expected); + var ipAddress = { + node_summary: { + node_type: {} + } + }; + var observed = $scope.nodeTypeSort(ipAddress); + expect($scope.getUsageForIP).toHaveBeenCalledWith(ipAddress); + expect(observed).toBe(expected); + }); + }); + + describe("ownerSort", function() { + it("returns owner", function() { + makeControllerResolveSetActiveItem(); + var ipAddress = { + user: makeName("owner") + }; + var observed = $scope.ownerSort(ipAddress); + expect(observed).toBe(ipAddress.user); + }); + + it("returns MAAS for empty string", function() { + makeControllerResolveSetActiveItem(); + var ipAddress = { + user: "" + }; + var observed = $scope.ownerSort(ipAddress); + expect(observed).toBe("MAAS"); + }); + + it("returns MAAS for null", function() { + makeControllerResolveSetActiveItem(); + var ipAddress = { + user: null + }; + var observed = $scope.ownerSort(ipAddress); + expect(observed).toBe("MAAS"); + }); + }); + + describe("sortIPTable", function() { + it("sets predicate and inverts reverse", function() { + makeControllerResolveSetActiveItem(); + $scope.reverse = true; + var predicate = {}; + $scope.sortIPTable(predicate); + expect($scope.predicate).toBe(predicate); + expect($scope.reverse).toBe(false); + $scope.sortIPTable(predicate); + expect($scope.reverse).toBe(true); + }); + }); + + describe("subnetPreSave", function() { + it("updates vlan when fabric changed", function() { + makeController(); + var vlan = { + id: makeInteger(0, 100) + }; + var fabric = { + id: makeInteger(1, 100), + default_vlan_id: vlan.id, + vlan_ids: [vlan.id] + }; + FabricsManager._items.push(fabric); + var subnet = { + fabric: fabric.id + }; + var updatedSubnet = $scope.subnetPreSave(subnet, ["fabric"]); + expect(updatedSubnet.vlan).toBe(vlan.id); + }); + }); + + describe("editSubnetSummary", function() { + it("enters edit mode for summary", function() { + makeController(); + $scope.editSummary = false; + $scope.enterEditSummary(); + expect($scope.editSummary).toBe(true); + }); + }); + + describe("exitEditSubnetSummary", function() { + it("enters edit mode for summary", function() { + makeController(); + $scope.editSummary = true; + $scope.exitEditSummary(); + expect($scope.editSummary).toBe(false); + }); + }); + + describe("addStaticRoute", function() { + it("set newStaticRoute", function() { + makeController(); + $scope.subnet = { + id: makeInteger(0, 100) + }; + $scope.addStaticRoute(); + expect($scope.newStaticRoute).toEqual({ + source: $scope.subnet.id, + gateway_ip: "", + destination: null, + metric: 0 + }); + }); + + it("clear editStaticRoute", function() { + makeController(); + $scope.subnet = { + id: makeInteger(0, 100) + }; + $scope.editStaticRoute = {}; + $scope.addStaticRoute(); + expect($scope.editStaticRoute).toBeNull(); + }); + + it("clear deleteStaticRoute", function() { + makeController(); + $scope.subnet = { + id: makeInteger(0, 100) + }; + $scope.deleteStaticRoute = {}; + $scope.addStaticRoute(); + expect($scope.deleteStaticRoute).toBeNull(); + }); + }); + + describe("cancelAddStaticRoute", function() { + it("clears newStaticRoute", function() { + makeController(); + $scope.newStaticRoute = {}; + $scope.cancelAddStaticRoute(); + expect($scope.newStaticRoute).toBeNull(); + }); + }); + + describe("isStaticRouteInEditMode", function() { + it("returns true when editStaticRoute", function() { + makeController(); + var route = {}; + $scope.editStaticRoute = route; + expect($scope.isStaticRouteInEditMode(route)).toBe(true); + }); + + it("returns false when editIPRange", function() { + makeController(); + var route = {}; + $scope.editStaticRoute = route; + expect($scope.isStaticRouteInEditMode({})).toBe(false); + }); + }); + + describe("staticRouteToggleEditMode", function() { + it("clears newStaticRoute", function() { + makeController(); + $scope.newStaticRoute = {}; + $scope.staticRouteToggleEditMode({}); + expect($scope.newStaticRoute).toBeNull(); + }); + + it("clears deleteStaticRoute", function() { + makeController(); + $scope.deleteStaticRoute = {}; + $scope.staticRouteToggleEditMode({}); + expect($scope.deleteStaticRoute).toBeNull(); + }); + + it("clears editStaticRoute when already set", function() { + makeController(); + var route = {}; + $scope.editStaticRoute = route; + $scope.staticRouteToggleEditMode(route); + expect($scope.editStaticRoute).toBeNull(); + }); + + it("sets editStaticRoute when different range", function() { + makeController(); + var route = {}; + var otherRoute = {}; + $scope.editStaticRoute = otherRoute; + $scope.staticRouteToggleEditMode(route); + expect($scope.editStaticRoute).toBe(route); + }); + }); + + describe("isStaticRouteInDeleteMode", function() { + it("return true when deleteStaticRoute is same", function() { + makeController(); + var route = {}; + $scope.deleteStaticRoute = route; + expect($scope.isStaticRouteInDeleteMode(route)).toBe(true); + }); + + it("return false when deleteIPRange is different", function() { + makeController(); + var route = {}; + $scope.deleteStaticRoute = route; + expect($scope.isStaticRouteInDeleteMode({})).toBe(false); + }); + }); + + describe("staticRouteEnterDeleteMode", function() { + it("clears edit and new and sets deleteStaticRoute", function() { + makeController(); + var route = {}; + $scope.newStaticRoute = {}; + $scope.editStaticRoute = {}; + $scope.staticRouteEnterDeleteMode(route); + expect($scope.newStaticRoute).toBeNull(); + expect($scope.editStaticRoute).toBeNull(); + expect($scope.deleteStaticRoute).toBe(route); + }); + }); + + describe("staticRouteCancelDelete", function() { + it("clears deleteStaticRoute", function() { + makeController(); + $scope.deleteStaticRoute = {}; + $scope.staticRouteCancelDelete(); + expect($scope.deleteStaticRoute).toBeNull(); + }); + }); + + describe("staticRouteConfirmDelete", function() { + it("calls deleteItem and clears deleteStaticRoute on resolve", function() { + makeController(); + var route = {}; + $scope.deleteStaticRoute = route; + + var defer = $q.defer(); + spyOn(StaticRoutesManager, "deleteItem").and.returnValue(defer.promise); + $scope.staticRouteConfirmDelete(); + + expect(StaticRoutesManager.deleteItem).toHaveBeenCalledWith(route); + defer.resolve(); + $scope.$digest(); + + expect($scope.deleteStaticRoute).toBeNull(); + }); + }); + + describe("actionRetry", function() { + it("clears actionError", function() { + makeController(); + $scope.actionError = {}; + $scope.actionRetry(); + expect($scope.actionError).toBeNull(); + }); + }); + + describe("actionGo", function() { + it("map_subnet action calls scanSubnet", function() { + makeControllerResolveSetActiveItem(); + var scanSubnet = spyOn(SubnetsManager, "scanSubnet"); + var defer = $q.defer(); + const result = { + result: "Error message from scan.", + scan_started_on: ["not empty"] + }; + scanSubnet.and.returnValue(defer.promise); + $scope.actionOption = { + name: "map_subnet", + title: "Map subnet" + }; + $scope.actionGo(); + defer.resolve(result); + $scope.$digest(); + expect(scanSubnet).toHaveBeenCalled(); + expect($scope.actionOption).toBeNull(); + expect($scope.actionError).toBeNull(); + }); + + it("actionError populated on scans not started", function() { + makeControllerResolveSetActiveItem(); + var scanSubnet = spyOn(SubnetsManager, "scanSubnet"); + var defer = $q.defer(); + const result = { + result: "Error message from scan.", + scan_started_on: [] + }; + scanSubnet.and.returnValue(defer.promise); + $scope.actionOption = { + name: "map_subnet", + title: "Map subnet" + }; + $scope.actionGo(); + defer.resolve(result); + $scope.$digest(); + expect(scanSubnet).toHaveBeenCalled(); + expect($scope.actionError).toBe("Error message from scan."); + }); + + it("actionError populated on map_subnet action failure", function() { + makeControllerResolveSetActiveItem(); + $scope.actionOption = { + name: "map_subnet", + title: "Map subnet" + }; + var defer = $q.defer(); + spyOn(SubnetsManager, "scanSubnet").and.returnValue(defer.promise); + $scope.actionGo(); + const error = "errorString"; + $scope.actionOption = null; + defer.reject(error); + $scope.$digest(); + expect($scope.actionError).toBe(error); + }); + + it("delete action calls deleteSubnet", function() { + $location.path = jasmine.createSpy("path"); + makeControllerResolveSetActiveItem(); + var deleteSubnet = spyOn(SubnetsManager, "deleteSubnet"); + var defer = $q.defer(); + deleteSubnet.and.returnValue(defer.promise); + $scope.actionOption = { + name: "delete", + title: "Delete" + }; + $scope.actionGo(); + defer.resolve(); + $scope.$digest(); + expect(deleteSubnet).toHaveBeenCalled(); + expect($location.path).toHaveBeenCalledWith("/networks"); + expect($scope.actionOption).toBeNull(); + expect($scope.actionError).toBeNull(); + }); + + it("actionError populated on delete action failure", function() { + makeControllerResolveSetActiveItem(); + $scope.actionOption = { + name: "delete", + title: "Delete" + }; + var defer = $q.defer(); + spyOn(SubnetsManager, "deleteSubnet").and.returnValue(defer.promise); + $scope.actionGo(); + const error = "errorString"; + $scope.actionOption = null; + defer.reject(error); + $scope.$digest(); + expect($scope.actionError).toBe(error); + }); + }); + + describe("actionChanged", function() { + it("clears actionError", function() { + makeController(); + $scope.actionError = {}; + $scope.actionChanged(); + expect($scope.actionError).toBeNull(); + }); + }); + + describe("cancelAction", function() { + it("clears actionOption and actionError", function() { + makeController(); + $scope.actionOption = {}; + $scope.actionError = {}; + $scope.cancelAction(); + expect($scope.actionOption).toBeNull(); + expect($scope.actionError).toBeNull(); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_vlan_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,989 +4,1035 @@ * Unit tests for SubentsListController. */ -describe("VLANDetailsController", function() { +import { makeInteger, makeName } from "testing/utils"; - // Load the MAAS module. - beforeEach(module("MAAS")); +describe("VLANDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - var VLAN_ID = makeInteger(5000, 6000); + var VLAN_ID = makeInteger(5000, 6000); - // Make a fake VLAN - function makeVLAN() { - var vlan = { - id: VLAN_ID, - vid: makeInteger(1,4094), - fabric: 1, - name: null, - dhcp_on: true, - space_ids: [2001], - primary_rack: primaryController.system_id, - secondary_rack: secondaryController.system_id, - rack_sids: [] - }; - VLANsManager._items.push(vlan); - return vlan; + // Make a fake VLAN + function makeVLAN() { + var vlan = { + id: VLAN_ID, + vid: makeInteger(1, 4094), + fabric: 1, + name: null, + dhcp_on: true, + space_ids: [2001], + primary_rack: primaryController.system_id, + secondary_rack: secondaryController.system_id, + rack_sids: [] + }; + VLANsManager._items.push(vlan); + return vlan; + } + + // Make a fake fabric + function makeFabric(id) { + if (id === undefined) { + id = 1; } - - // Make a fake fabric - function makeFabric(id) { - if(id === undefined) { - id = 1; - } - var fabric = { - id: id, - name: 'fabric-' + id, - default_vlan_id: 5000 - }; - FabricsManager._items.push(fabric); - return fabric; + var fabric = { + id: id, + name: "fabric-" + id, + default_vlan_id: 5000 + }; + FabricsManager._items.push(fabric); + return fabric; + } + + // Make a fake space + function makeSpace(id) { + if (id === undefined) { + id = 2001; } - - // Make a fake space - function makeSpace(id) { - if(id === undefined) { - id = 2001; - } - var space = { - id: id, - name: 'space-' + id - }; - SpacesManager._items.push(space); - return space; + var space = { + id: id, + name: "space-" + id + }; + SpacesManager._items.push(space); + return space; + } + + // Make a fake subnet + function makeSubnet(id, spaceId) { + if (id === undefined) { + id = 6001; } - - // Make a fake subnet - function makeSubnet(id, spaceId) { - if(id === undefined) { - id = 6001; - } - if(!spaceId) { - spaceId = 2001; - } - var subnet = { - id: id, - name: null, - cidr: '192.168.0.1/24', - space: spaceId, - vlan: VLAN_ID, - statistics: { ranges: [] } - }; - SubnetsManager._items.push(subnet); - return subnet; + if (!spaceId) { + spaceId = 2001; } - - // Make a fake controller - function makeRackController(id, name, sid, vlan) { - var rack = { - id: id, - system_id: sid, - hostname: name, - node_type: 2, - default_vlan_id: VLAN_ID, - vlan_ids: [VLAN_ID] - }; - ControllersManager._items.push(rack); - if(angular.isObject(vlan)) { - VLANsManager.addRackController(vlan, rack); - } - return rack; + var subnet = { + id: id, + name: null, + cidr: "192.168.0.1/24", + space: spaceId, + vlan: VLAN_ID, + statistics: { ranges: [] } + }; + SubnetsManager._items.push(subnet); + return subnet; + } + + // Make a fake controller + function makeRackController(id, name, sid, vlan) { + var rack = { + id: id, + system_id: sid, + hostname: name, + node_type: 2, + default_vlan_id: VLAN_ID, + vlan_ids: [VLAN_ID] + }; + ControllersManager._items.push(rack); + if (angular.isObject(vlan)) { + VLANsManager.addRackController(vlan, rack); } + return rack; + } - // Grab the needed angular pieces. - var $controller, $rootScope, $filter, $location, $scope, $q; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $filter = $injector.get("$filter"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - })); - - // Load any injected managers and services. - var VLANsManager, SubnetsManager, SpacesManager, FabricsManager; - var ControllersManager, UsersManager, ManagerHelperService, ErrorService; - beforeEach(inject(function($injector) { - VLANsManager = $injector.get("VLANsManager"); - SubnetsManager = $injector.get("SubnetsManager"); - SpacesManager = $injector.get("SpacesManager"); - FabricsManager = $injector.get("FabricsManager"); - ControllersManager = $injector.get("ControllersManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - })); - - var vlan, fabric, fabric2, primaryController, secondaryController; - var space, subnet, $routeParams; - beforeEach(function() { - primaryController = makeRackController(1, "primary", "p1"); - secondaryController = makeRackController(2, "secondary", "p2"); - vlan = makeVLAN(); - VLANsManager.addRackController(vlan, primaryController); - VLANsManager.addRackController(vlan, secondaryController); - fabric = makeFabric(1); - fabric2 = makeFabric(2); - space = makeSpace(); - subnet = makeSubnet(); - $routeParams = { - vlan_id: vlan.id - }; - }); - - function makeController(loadManagersDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagersDefer)) { - loadManagers.and.returnValue(loadManagersDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("VLANDetailsController as vlanDetails", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $filter: $filter, - $location: $location, - VLANsManager: VLANsManager, - SubnetsManager: SubnetsManager, - SpacesManager: SpacesManager, - FabricsManager: FabricsManager, - ControllersManager: ControllersManager, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $filter, $location, $scope, $q; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $filter = $injector.get("$filter"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + })); + + // Load any injected managers and services. + var VLANsManager, SubnetsManager, SpacesManager, FabricsManager; + var ControllersManager, UsersManager, ManagerHelperService, ErrorService; + beforeEach(inject(function($injector) { + VLANsManager = $injector.get("VLANsManager"); + SubnetsManager = $injector.get("SubnetsManager"); + SpacesManager = $injector.get("SpacesManager"); + FabricsManager = $injector.get("FabricsManager"); + ControllersManager = $injector.get("ControllersManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + })); + + var vlan, fabric, fabric2, primaryController, secondaryController; + var subnet, $routeParams; + beforeEach(function() { + primaryController = makeRackController(1, "primary", "p1"); + secondaryController = makeRackController(2, "secondary", "p2"); + vlan = makeVLAN(); + VLANsManager.addRackController(vlan, primaryController); + VLANsManager.addRackController(vlan, secondaryController); + fabric = makeFabric(1); + fabric2 = makeFabric(2); + makeSpace(); + subnet = makeSubnet(); + $routeParams = { + vlan_id: vlan.id + }; + }); + + function makeController(loadManagersDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagersDefer)) { + loadManagers.and.returnValue(loadManagersDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(VLANsManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(vlan); - $rootScope.$digest(); - - return controller; + // Create the controller. + var controller = $controller("VLANDetailsController as vlanDetails", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $filter: $filter, + $location: $location, + VLANsManager: VLANsManager, + SubnetsManager: SubnetsManager, + SpacesManager: SpacesManager, + FabricsManager: FabricsManager, + ControllersManager: ControllersManager, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(VLANsManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + var controller = makeController(defer); + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(vlan); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("networks"); + }); + + it("calls loadManagers with required managers", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + VLANsManager, + SubnetsManager, + SpacesManager, + FabricsManager, + ControllersManager, + UsersManager + ]); + }); + + it("raises error if vlan identifier is invalid", function() { + spyOn(VLANsManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ErrorService, "raiseError").and.returnValue($q.defer().promise); + var defer = $q.defer(); + var controller = makeController(defer); + $routeParams.vlan_id = "xyzzy"; + + defer.resolve(); + $rootScope.$digest(); + + expect(controller.vlan).toBe(null); + expect(controller.loaded).toBe(false); + expect(VLANsManager.setActiveItem).not.toHaveBeenCalled(); + expect(ErrorService.raiseError).toHaveBeenCalled(); + }); + + it("doesn't call setActiveItem if vlan is loaded", function() { + spyOn(VLANsManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + var controller = makeController(defer); + VLANsManager._activeItem = vlan; + $routeParams.vlan_id = vlan.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(controller.vlan).toBe(vlan); + expect(controller.loaded).toBe(true); + expect(VLANsManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if vlan is not active", function() { + spyOn(VLANsManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.vlan_id = vlan.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(VLANsManager.setActiveItem).toHaveBeenCalledWith(vlan.id); + }); + + it("sets vlan and loaded once setActiveItem resolves", function() { + var controller = makeControllerResolveSetActiveItem(); + expect(controller.vlan).toBe(vlan); + expect(controller.loaded).toBe(true); + }); + + it("title is updated once setActiveItem resolves", function() { + var controller = makeControllerResolveSetActiveItem(); + expect(controller.title).toBe("VLAN " + vlan.vid + " in " + fabric.name); + }); + + it("default VLAN title is special", function() { + vlan.vid = 0; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.title).toBe("Default VLAN in " + fabric.name); + }); + + it("custom VLAN name renders in title", function() { + vlan.name = "Super Awesome VLAN"; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); + }); + + it("changes title when VLAN name changes", function() { + vlan.vid = 0; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.title).toBe("Default VLAN in " + fabric.name); + vlan.name = "Super Awesome VLAN"; + $scope.$digest(); + expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); + }); + + it("changes title when fabric name changes", function() { + vlan.name = "Super Awesome VLAN"; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); + fabric.name = "space"; + $scope.$digest(); + expect(controller.title).toBe("Super Awesome VLAN in space"); + }); + + it("updates VLAN when fabric changes", function() { + vlan.name = "Super Awesome VLAN"; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); + fabric2.name = "space"; + vlan.fabric = fabric2.id; + $scope.$digest(); + expect(controller.title).toBe("Super Awesome VLAN in space"); + }); + + it("updates primaryRack variable when controller changes", function() { + vlan.primary_rack = 0; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.primaryRack).toBe(null); + expect(controller.secondaryRack).toBe(secondaryController); + vlan.primary_rack = primaryController.system_id; + $scope.$digest(); + expect(controller.primaryRack).toBe(primaryController); + }); + + it("updates secondaryRack variable when controller changes", function() { + vlan.secondary_rack = 0; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.primaryRack).toBe(primaryController); + expect(controller.secondaryRack).toBe(null); + vlan.secondary_rack = secondaryController.system_id; + $scope.$digest(); + expect(controller.secondaryRack).toBe(secondaryController); + }); + + it("updates reatedControllers when controllers list changes", function() { + var controller = makeControllerResolveSetActiveItem(); + expect(controller.controllers.length).toBe(2); + expect(controller.relatedControllers.length).toBe(2); + makeRackController(3, "three", "t3", vlan); + expect(controller.relatedControllers.length).toBe(2); + expect(controller.controllers.length).toBe(3); + $scope.$digest(); + expect(controller.relatedControllers.length).toBe(3); + }); + + it("updates relatedSubnets when subnets list changes", function() { + var controller = makeControllerResolveSetActiveItem(); + makeSubnet(6002); + expect(controller.relatedSubnets.length).toBe(1); + expect(controller.subnets.length).toBe(2); + $scope.$digest(); + expect(controller.relatedSubnets.length).toBe(2); + }); + + it("updates relatedSubnets when spaces list changes", function() { + var controller = makeControllerResolveSetActiveItem(); + expect(controller.spaces.length).toBe(1); + makeSpace(2002); + vlan.space_ids.push(2002); + makeSubnet(6002, 2002); + expect(controller.controllers.length).toBe(2); + $scope.$digest(); + }); + + it("actionOption cleared on action success", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.actionOption = controller.DELETE_ACTION; + var defer = $q.defer(); + spyOn(VLANsManager, "deleteVLAN").and.returnValue(defer.promise); + controller.actionGo(); + defer.resolve(); + $scope.$digest(); + expect(controller.actionOption).toBe(null); + expect(controller.actionError).toBe(null); + }); + + it( + "prepares provideDHCPAction on actionOptionChanged " + + "and populates suggested gateway", + function() { + var controller = makeControllerResolveSetActiveItem(); + controller.subnets[0].gateway_ip = null; + controller.subnets[0].statistics = { + suggested_gateway: "192.168.0.1" + }; + // to avoid side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.updateSubnet(); + controller.openDHCPPanel(); + expect(controller.provideDHCPAction).toEqual({ + subnet: subnet.id, + relayVLAN: null, + primaryRack: "p1", + secondaryRack: "p2", + maxIPs: 0, + startIP: "", + startPlaceholder: "(no available IPs)", + endIP: "", + endPlaceholder: "(no available IPs)", + gatewayIP: "192.168.0.1", + gatewayPlaceholder: "192.168.0.1", + needsGatewayIP: true, + subnetMissingGatewayIP: true, + needsDynamicRange: false + }); } + ); - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("networks"); - }); - - it("calls loadManagers with required managers", function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ - VLANsManager, SubnetsManager, SpacesManager, FabricsManager, - ControllersManager, UsersManager]); - }); - - it("raises error if vlan identifier is invalid", function() { - spyOn(VLANsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ErrorService, "raiseError").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - var controller = makeController(defer); - $routeParams.vlan_id = 'xyzzy'; - - defer.resolve(); - $rootScope.$digest(); - - expect(controller.vlan).toBe(null); - expect(controller.loaded).toBe(false); - expect(VLANsManager.setActiveItem).not.toHaveBeenCalled(); - expect(ErrorService.raiseError).toHaveBeenCalled(); - }); - - it("doesn't call setActiveItem if vlan is loaded", function() { - spyOn(VLANsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - var controller = makeController(defer); - VLANsManager._activeItem = vlan; - $routeParams.vlan_id = vlan.id; - - defer.resolve(); - $rootScope.$digest(); - - expect(controller.vlan).toBe(vlan); - expect(controller.loaded).toBe(true); - expect(VLANsManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if vlan is not active", function() { - spyOn(VLANsManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.vlan_id = vlan.id; - - defer.resolve(); - $rootScope.$digest(); - - expect(VLANsManager.setActiveItem).toHaveBeenCalledWith( - vlan.id); - }); - - it("sets vlan and loaded once setActiveItem resolves", function() { - var controller = makeControllerResolveSetActiveItem(); - expect(controller.vlan).toBe(vlan); - expect(controller.loaded).toBe(true); - }); - - it("title is updated once setActiveItem resolves", function() { - var controller = makeControllerResolveSetActiveItem(); - expect(controller.title).toBe( - "VLAN " + vlan.vid + " in " + fabric.name); - }); - - it("default VLAN title is special", function() { - vlan.vid = 0; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.title).toBe("Default VLAN in " + fabric.name); - }); - - it("custom VLAN name renders in title", function() { - vlan.name = "Super Awesome VLAN"; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); - }); - - it("changes title when VLAN name changes", function() { - vlan.vid = 0; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.title).toBe("Default VLAN in " + fabric.name); - vlan.name = "Super Awesome VLAN"; - $scope.$digest(); - expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); - }); - - it("changes title when fabric name changes", function() { - vlan.name = "Super Awesome VLAN"; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); - fabric.name = "space"; - $scope.$digest(); - expect(controller.title).toBe("Super Awesome VLAN in space"); - }); - - it("updates VLAN when fabric changes", function() { - vlan.name = "Super Awesome VLAN"; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.title).toBe("Super Awesome VLAN in " + fabric.name); - fabric2.name = "space"; - vlan.fabric = fabric2.id; - $scope.$digest(); - expect(controller.title).toBe("Super Awesome VLAN in space"); - }); - - it("updates primaryRack variable when controller changes", function() { - vlan.primary_rack = 0; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.primaryRack).toBe(null); - expect(controller.secondaryRack).toBe(secondaryController); - vlan.primary_rack = primaryController.system_id; - $scope.$digest(); - expect(controller.primaryRack).toBe(primaryController); - }); - - it("updates secondaryRack variable when controller changes", function() { - vlan.secondary_rack = 0; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.primaryRack).toBe(primaryController); - expect(controller.secondaryRack).toBe(null); - vlan.secondary_rack = secondaryController.system_id; - $scope.$digest(); - expect(controller.secondaryRack).toBe(secondaryController); - }); - - it("updates reatedControllers when controllers list changes", function() { - var controller = makeControllerResolveSetActiveItem(); - expect(controller.controllers.length).toBe(2); - expect(controller.relatedControllers.length).toBe(2); - makeRackController(3, "three", "t3", vlan); - expect(controller.relatedControllers.length).toBe(2); - expect(controller.controllers.length).toBe(3); - $scope.$digest(); - expect(controller.relatedControllers.length).toBe(3); - }); - - it("updates relatedSubnets when subnets list changes", function() { - var controller = makeControllerResolveSetActiveItem(); - makeSubnet(6002); - expect(controller.relatedSubnets.length).toBe(1); - expect(controller.subnets.length).toBe(2); - $scope.$digest(); - expect(controller.relatedSubnets.length).toBe(2); - }); - - it("updates relatedSubnets when spaces list changes", - function() { - var controller = makeControllerResolveSetActiveItem(); - expect(controller.spaces.length).toBe(1); - makeSpace(2002); - vlan.space_ids.push(2002); - makeSubnet(6002, 2002); - expect(controller.controllers.length).toBe(2); - $scope.$digest(); - }); - - it("actionOption cleared on action success", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.actionOption = controller.DELETE_ACTION; - var defer = $q.defer(); - spyOn(VLANsManager, "deleteVLAN").and.returnValue( - defer.promise); - controller.actionGo(); - defer.resolve(); - $scope.$digest(); - expect(controller.actionOption).toBe(null); - expect(controller.actionError).toBe(null); - }); - - it("prepares provideDHCPAction on actionOptionChanged " + - "and populates suggested gateway", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.subnets[0].gateway_ip = null; - controller.subnets[0].statistics = { - suggested_gateway: "192.168.0.1" - }; - // to avoid side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.updateSubnet(); - controller.openDHCPPanel(); - expect(controller.provideDHCPAction).toEqual({ - subnet: subnet.id, - relayVLAN: null, - primaryRack: "p1", - secondaryRack: "p2", - maxIPs: 0, - startIP: "", - startPlaceholder: "(no available IPs)", - endIP: "", - endPlaceholder: "(no available IPs)", - gatewayIP: '192.168.0.1', - gatewayPlaceholder: '192.168.0.1', - needsGatewayIP: true, - subnetMissingGatewayIP: true, - needsDynamicRange: false - }); - }); - - it("prevents selection of a duplicate rack controller", function() { - var controller = makeControllerResolveSetActiveItem(); - // to avoid side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - controller.provideDHCPAction.primaryRack = "p2"; - controller.updatePrimaryRack(); - expect(controller.provideDHCPAction.primaryRack).toEqual("p2"); - // This should automatically select p1 by default; the user has to - // clear it out manually if desired. (this is done via an extra option - // in the view.) - expect(controller.provideDHCPAction.secondaryRack).toBe("p1"); - controller.provideDHCPAction.secondaryRack = "p2"; - controller.updateSecondaryRack(); - expect(controller.provideDHCPAction.primaryRack).toBe(null); - expect(controller.provideDHCPAction.secondaryRack).toBe(null); - }); - - describe("filterPrimaryRack", function() { - - it("filters out the currently-selected primary rack", - function() { - var controller = makeControllerResolveSetActiveItem(); - // to avoid side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - // The filter should return false if the item is to be excluded. - // So the primary rack should match, as this filter is used from - // the secondary rack drop-down to exclude the primary. - expect(controller.filterPrimaryRack( - primaryController)).toBe(false); - expect(controller.filterPrimaryRack( - secondaryController)).toBe(true); - }); - }); - - describe("enterEditSummary", function() { - - it("sets editSummary", function() { - var controller = makeController(); - controller.enterEditSummary(); - expect(controller.editSummary).toBe(true); - }); - }); + it("prevents selection of a duplicate rack controller", function() { + var controller = makeControllerResolveSetActiveItem(); + // to avoid side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + controller.provideDHCPAction.primaryRack = "p2"; + controller.updatePrimaryRack(); + expect(controller.provideDHCPAction.primaryRack).toEqual("p2"); + // This should automatically select p1 by default; the user has to + // clear it out manually if desired. (this is done via an extra option + // in the view.) + expect(controller.provideDHCPAction.secondaryRack).toBe( + controller.secondaryRack + ); + controller.provideDHCPAction.secondaryRack = "p2"; + controller.updateSecondaryRack(); + expect(controller.provideDHCPAction.primaryRack).toBe(null); + expect(controller.provideDHCPAction.secondaryRack).toBe(null); + }); + + describe("filterPrimaryRack", function() { + it("filters out the currently-selected primary rack", function() { + var controller = makeControllerResolveSetActiveItem(); + // to avoid side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + // The filter should return false if the item is to be excluded. + // So the primary rack should match, as this filter is used from + // the secondary rack drop-down to exclude the primary. + expect(controller.filterPrimaryRack(primaryController)).toBe(false); + expect(controller.filterPrimaryRack(secondaryController)).toBe(true); + }); + }); + + describe("enterEditSummary", function() { + it("sets editSummary", function() { + var controller = makeController(); + controller.enterEditSummary(); + expect(controller.editSummary).toBe(true); + }); + }); + + describe("exitEditSummary", function() { + it("sets editSummary", function() { + var controller = makeController(); + controller.enterEditSummary(); + controller.exitEditSummary(); + expect(controller.editSummary).toBe(false); + }); + }); + + describe("getSpaceName", function() { + it("returns space name", function() { + var controller = makeController(); + var spaceName = makeName("space"); + SpacesManager._items = [ + { + id: 1, + name: spaceName + } + ]; + controller.vlan = { + space: 1 + }; + expect(controller.getSpaceName()).toBe(spaceName); + }); + + it("returns space (undefined)", function() { + var controller = makeController(); + controller.vlan = {}; + expect(controller.getSpaceName()).toBe("(undefined)"); + }); + }); + + describe("updatePossibleActions", function() { + // Note: updatePossibleActions() is called indirectly by these tests + // after all the managers load. + + it("returns an empty actions list for a non-superuser", function() { + vlan.dhcp_on = true; + UsersManager._authUser = { is_superuser: false }; + var controller = makeControllerResolveSetActiveItem(); + $scope.$digest(); + expect(controller.actionOptions).toEqual([]); + }); + + it("returns delete when dhcp is off", function() { + vlan.dhcp_on = false; + UsersManager._authUser = { is_superuser: true }; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.actionOptions).toEqual([controller.DELETE_ACTION]); + }); + + it("returns delete when dhcp is on", function() { + vlan.dhcp_on = true; + UsersManager._authUser = { is_superuser: true }; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.actionOptions).toEqual([controller.DELETE_ACTION]); + }); + + it("returns delete when relay_vlan is set", function() { + vlan.relay_vlan = 5001; + UsersManager._authUser = { is_superuser: true }; + var controller = makeControllerResolveSetActiveItem(); + expect(controller.actionOptions).toEqual([controller.DELETE_ACTION]); + }); + }); + + describe("openDHCPPanel", function() { + it("opens DHCP panel", function() { + var controller = makeControllerResolveSetActiveItem(); + // to avoid side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + expect(controller.showDHCPPanel).toBe(true); + }); + + it("calls `initProvideDHCP` and `setSuggestedRange`", function() { + var controller = makeControllerResolveSetActiveItem(); + spyOn(controller, "initProvideDHCP"); + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + expect(controller.initProvideDHCP).toHaveBeenCalled(); + expect(controller.setSuggestedRange).toHaveBeenCalled(); + }); + }); + + describe("closeDHCPPanel", function() { + it("closes DHCP Panel", function() { + var controller = makeController(); + controller.showDHCPPanel = true; + controller.closeDHCPPanel(); + expect(controller.showDHCPPanel).toBe(false); + }); + + it("unsets `suggestedRange`", function() { + var controller = makeController(); + controller.suggestedRange = { + subnet: 1, + type: "dynamic", + comment: "Dynamic", + start_ip: "127.168.0.1", + end_ip: "127.168.0.2", + gateway_ip: "127.168.0.0" + }; + controller.closeDHCPPanel(); + expect(controller.suggestedRange).toBe(null); + }); + + it("unsets `isProvidingDHCP`", function() { + var controller = makeController(); + controller.isProvidingDHCP = true; + controller.closeDHCPPanel(); + expect(controller.isProvidingDHCP).toBe(false); + }); + + it("unsets `DHCPError`", function() { + var controller = makeController(); + controller.DHCPError = "Lorem ipsum dolor sit amet"; + controller.closeDHCPPanel(); + expect(controller.DHCPError).toBe(null); + }); + + it("sets `MAASProvidesDHCP`", function() { + var controller = makeController(); + controller.MAASProvidesDHCP = false; + controller.closeDHCPPanel(); + expect(controller.MAASProvidesDHCP).toBe(true); + }); + }); + + describe("getDHCPButtonText", function() { + it("reads 'Enable' if status is disabled", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.vlan.dhcp_on = false; + expect(controller.getDHCPButtonText()).toBe("Enable DHCP"); + }); + + it("reads 'Reconfigure' if status is enabled", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.vlan.dhcp_on = true; + expect(controller.getDHCPButtonText()).toBe("Reconfigure DHCP"); + }); + + it("reads 'Reconfigure relay' if status is relayed", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.vlan.relay_vlan = 2; + expect(controller.getDHCPButtonText()).toBe("Reconfigure DHCP relay"); + }); + }); + + describe("showGatewayCol", function() { + it("returns `true` if one of more subnets have no gateway", function() { + var controller = makeController(); + controller.relatedSubnets = [ + { subnet: { gateway_ip: "127.0.0.1" } }, + { subnet: { gateway_ip: null } }, + { subnet: { gateway_ip: "127.0.0.2" } } + ]; + expect(controller.showGatewayCol()).toBe(true); + }); + + it("returns `false` if all subnets have gateway", function() { + var controller = makeController(); + controller.relatedSubnets = [ + { subnet: { gateway_ip: "127.0.0.1" } }, + { subnet: { gateway_ip: "127.0.0.2" } } + ]; + expect(controller.showGatewayCol()).toBe(false); + }); + }); + + describe("setSuggestedRange", function() { + it("sets a suggested IP range", function() { + var controller = makeController(); + controller.suggestedRange = null; + controller.relatedSubnets = [ + { + subnet: { + id: 1, + gateway_ip: "127.168.0.0", + statistics: { + ranges: [ + { + num_addresses: 2, + purpose: ["unused"], + start: "127.168.0.1", + end: "127.168.0.2" + } + ] + } + } + } + ]; - describe("exitEditSummary", function() { + controller.setSuggestedRange(); - it("sets editSummary", function() { - var controller = makeController(); - controller.enterEditSummary(); - controller.exitEditSummary(); - expect(controller.editSummary).toBe(false); + expect(controller.suggestedRange).toEqual({ + type: "dynamic", + comment: "Dynamic", + start_ip: "127.168.0.1", + end_ip: "127.168.0.2", + subnet: 1, + gateway_ip: "127.168.0.0" }); }); - describe("getSpaceName", function() { - - it("returns space name", function() { - var controller = makeController(); - var spaceName = makeName("space"); - SpacesManager._items = [{ + it("sets placeholders if relay VLAN is set", function() { + var controller = makeController(); + controller.suggestedRange = null; + controller.relayVLAN = true; + controller.relatedSubnets = [ + { + subnet: { id: 1, - name: spaceName - }]; - controller.vlan = { - space: 1 - }; - expect(controller.getSpaceName()).toBe(spaceName); - }); - - it("returns space (undefined)", function() { - var controller = makeController(); - controller.vlan = {}; - expect(controller.getSpaceName()).toBe("(undefined)"); - }); - }); - - describe("updatePossibleActions", function() { - - // Note: updatePossibleActions() is called indirectly by these tests - // after all the managers load. - - it("returns an empty actions list for a non-superuser", - function() { - vlan.dhcp_on = true; - UsersManager._authUser = {is_superuser: false}; - var controller = makeControllerResolveSetActiveItem(); - $scope.$digest(); - expect(controller.actionOptions).toEqual([]); - }); - - it("returns delete when dhcp is off", - function() { - vlan.dhcp_on = false; - UsersManager._authUser = {is_superuser: true}; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.actionOptions).toEqual([ - controller.DELETE_ACTION - ]); - }); - - it("returns delete when dhcp is on", function() { - vlan.dhcp_on = true; - UsersManager._authUser = {is_superuser: true}; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.actionOptions).toEqual([ - controller.DELETE_ACTION - ]); - }); - - it("returns delete when relay_vlan is set", function() { - vlan.relay_vlan = 5001; - UsersManager._authUser = {is_superuser: true}; - var controller = makeControllerResolveSetActiveItem(); - expect(controller.actionOptions).toEqual([ - controller.DELETE_ACTION - ]); - }); - }); - - describe("openDHCPPanel", function() { - it("opens DHCP panel", function() { - var controller = makeControllerResolveSetActiveItem(); - // to avoid side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - expect(controller.showDHCPPanel).toBe(true); - }); - - it("calls `initProvideDHCP` and `setSuggestedRange`", function() { - var controller = makeControllerResolveSetActiveItem(); - spyOn(controller, 'initProvideDHCP'); - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - expect(controller.initProvideDHCP).toHaveBeenCalled(); - expect(controller.setSuggestedRange).toHaveBeenCalled(); - }); - }); - - describe("closeDHCPPanel", function() { - it("closes DHCP Panel", function() { - var controller = makeController(); - controller.showDHCPPanel = true; - controller.closeDHCPPanel(); - expect(controller.showDHCPPanel).toBe(false); - }); - - it("unsets `suggestedRange`", function() { - var controller = makeController(); - controller.suggestedRange = { - subnet: 1, - type: "dynamic", - comment: "Dynamic", - start_ip: "127.168.0.1", - end_ip: "127.168.0.2", - gateway_ip: "127.168.0.0" - }; - controller.closeDHCPPanel(); - expect(controller.suggestedRange).toBe(null); - }); - - it("unsets `isProvidingDHCP`", function() { - var controller = makeController(); - controller.isProvidingDHCP = true; - controller.closeDHCPPanel(); - expect(controller.isProvidingDHCP).toBe(false); - }); - - it("unsets `DHCPError`", function() { - var controller = makeController(); - controller.DHCPError = "Lorem ipsum dolor sit amet"; - controller.closeDHCPPanel(); - expect(controller.DHCPError).toBe(null); - }); - - it("sets `MAASProvidesDHCP`", function() { - var controller = makeController(); - controller.MAASProvidesDHCP = false; - controller.closeDHCPPanel(); - expect(controller.MAASProvidesDHCP).toBe(true); - }); - }); - - describe("getDHCPButtonText", function() { - it("reads 'Enable' if status is disabled", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.vlan.dhcp_on = false; - expect(controller.getDHCPButtonText()).toBe("Enable DHCP"); - }); - - it("reads 'Reconfigure' if status is enabled", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.vlan.dhcp_on = true; - expect(controller.getDHCPButtonText()).toBe("Reconfigure DHCP"); - }); - - it("reads 'Reconfigure relay' if status is relayed", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.vlan.relay_vlan = 2; - expect(controller.getDHCPButtonText()) - .toBe("Reconfigure DHCP relay"); - }); - }); - - describe("showGatewayCol", function() { - it("returns `true` if one of more subnets have no gateway", function() { - var controller = makeController(); - controller.relatedSubnets = [ - { subnet: { gateway_ip: "127.0.0.1" }}, - { subnet: { gateway_ip: null }}, - { subnet: { gateway_ip: "127.0.0.2" }} - ]; - expect(controller.showGatewayCol()).toBe(true); - }); - - it("returns `false` if all subnets have gateway", function() { - var controller = makeController(); - controller.relatedSubnets = [ - { subnet: { gateway_ip: "127.0.0.1" }}, - { subnet: { gateway_ip: "127.0.0.2" }} - ]; - expect(controller.showGatewayCol()).toBe(false); - }); - }); - - describe("setSuggestedRange", function() { - it("sets a suggested IP range", function() { - var controller = makeController(); - controller.suggestedRange = null; - controller.relatedSubnets = [{ - subnet: { - id: 1, - gateway_ip: "127.168.0.0", - statistics: { - ranges: [{ - num_addresses: 2, - purpose: ["unused"], - start: "127.168.0.1", - end: "127.168.0.2" - }] - } + gateway_ip: "127.168.0.0", + statistics: { + ranges: [ + { + num_addresses: 2, + purpose: ["unused"], + start: "127.168.0.1", + end: "127.168.0.2" } - }]; + ] + } + } + } + ]; - controller.setSuggestedRange(); + controller.setSuggestedRange(); - expect(controller.suggestedRange).toEqual({ - type: "dynamic", - comment: "Dynamic", - start_ip: "127.168.0.1", - end_ip: "127.168.0.2", - subnet: 1, - gateway_ip: "127.168.0.0" - }); - }); - - it("sets placeholders if relay VLAN is set", function() { - var controller = makeController(); - controller.suggestedRange = null; - controller.relayVLAN = true; - controller.relatedSubnets = [{ - subnet: { - id: 1, - gateway_ip: "127.168.0.0", - statistics: { - ranges: [{ - num_addresses: 2, - purpose: ["unused"], - start: "127.168.0.1", - end: "127.168.0.2" - }] - } - } - }]; + expect(controller.suggestedRange).toEqual({ + type: "dynamic", + comment: "Dynamic", + start_ip: "", + end_ip: "", + subnet: 1, + gateway_ip: "", + startPlaceholder: "127.168.0.1 (Optional)", + endPlaceholder: "127.168.0.2 (Optional)" + }); + }); + }); - controller.setSuggestedRange(); + describe("getDHCPPanelTitle", function() { + it("sets the panel title to 'Configure DHCP'", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: false }; + expect(controller.getDHCPPanelTitle()).toBe("Configure DHCP"); + }); + + it("sets the panel title to 'Reconfigure DHCP'", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: true }; + expect(controller.getDHCPPanelTitle()).toBe("Reconfigure DHCP"); + }); + + it("sets the panel title to 'Configure MAAS-managed DHCP", function() { + var controller = makeController(); + var VLAN_ID = makeInteger(5000, 6000); + var vlan = { + id: VLAN_ID, + vid: makeInteger(1, 4094), + fabric: 1, + name: null, + dhcp_on: true, + space_ids: [2001], + primary_rack: primaryController.system_id, + secondary_rack: secondaryController.system_id, + rack_sids: [], + external_dhcp: 1 + }; + VLANsManager._items.push(vlan); + controller.vlan = { external_dhcp: 1 }; + expect(controller.getDHCPPanelTitle()).toBe( + "Configure MAAS-managed DHCP" + ); + }); + }); + + describe("toggleMAASProvidesDHCP", function() { + it("sets `MAASProvidesDHCP` to `false`", function() { + var controller = makeController(); + controller.MAASProvidesDHCP = true; + controller.toggleMAASProvidesDHCP(); + expect(controller.MAASProvidesDHCP).toBe(false); + }); + + it("sets `MAASProvidesDHCP` to `true`", function() { + var controller = makeController(); + controller.MAASProvidesDHCP = false; + controller.toggleMAASProvidesDHCP(); + expect(controller.MAASProvidesDHCP).toBe(true); + }); + }); + + describe("setDHCPAction", function() { + it("sets `provideDHCP` to `true`", function() { + var controller = makeController(); + // To prevent side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.provideDHCP = false; + controller.relayVLAN = true; + controller.setDHCPAction("provideDHCP"); + expect(controller.provideDHCP).toBe(true); + expect(controller.relayVLAN).toBe(false); + }); + + it("sets `relayVLAN` to `false`", function() { + var controller = makeController(); + // To prevent side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.provideDHCP = true; + controller.relayVLAN = false; + controller.setDHCPAction("relayVLAN"); + expect(controller.relayVLAN).toBe(true); + expect(controller.provideDHCP).toBe(false); + }); + + it("calls `setSuggestedRange` for DHCP", function() { + var controller = makeControllerResolveSetActiveItem(); + spyOn(controller, "setSuggestedRange"); + controller.setDHCPAction("provideDHCP"); + expect(controller.setSuggestedRange).toHaveBeenCalled(); + }); + + it("calls `setSuggestedRange` for relay VLAN", function() { + var controller = makeController(); + spyOn(controller, "setSuggestedRange"); + controller.setDHCPAction("relayVLAN"); + expect(controller.setSuggestedRange).toHaveBeenCalled(); + }); + }); + + describe("enableDHCP", function() { + it("DHCPError populated on action failure", function() { + var controller = makeControllerResolveSetActiveItem(); + // To prevent side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + var defer = $q.defer(); + spyOn(VLANsManager, "configureDHCP").and.returnValue(defer.promise); + controller.enableDHCP(); + const result = { + error: "errorString", + request: { + params: { + action: "enable_dhcp" + } + } + }; + defer.reject(result); + $scope.$digest(); + expect(controller.DHCPError).toBe("errorString"); + }); + + it("performAction for enable_dhcp called with all params", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.actionOption = controller.PROVIDE_DHCP_ACTION; + // This will populate the default values for the racks with + // the current values from the mock objects. + // To prevent side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + controller.provideDHCPAction.subnet = 1; + controller.provideDHCPAction.gatewayIP = "192.168.0.1"; + controller.provideDHCPAction.startIP = "192.168.0.2"; + controller.provideDHCPAction.endIP = "192.168.0.254"; + var defer = $q.defer(); + spyOn(VLANsManager, "configureDHCP").and.returnValue(defer.promise); + controller.enableDHCP(); + defer.resolve(); + $scope.$digest(); + expect(VLANsManager.configureDHCP).toHaveBeenCalledWith( + controller.vlan, + [controller.primaryRack.system_id, controller.secondaryRack.system_id], + { + subnet: 1, + gateway: "192.168.0.1", + start: "192.168.0.2", + end: "192.168.0.254" + } + ); + expect(controller.DHCPError).toBe(null); + }); - expect(controller.suggestedRange).toEqual({ - type: "dynamic", - comment: "Dynamic", - start_ip: "", - end_ip: "", - subnet: 1, - gateway_ip: "", - startPlaceholder: "127.168.0.1 (Optional)", - endPlaceholder: "127.168.0.2 (Optional)" - }); - }); - }); - - describe("getDHCPPanelTitle", function() { - it("sets the panel title to 'Configure DHCP'", function() { - var controller = makeController(); - controller.vlan = { dhcp_on: false }; - expect(controller.getDHCPPanelTitle()).toBe('Configure DHCP'); - }); - - it("sets the panel title to 'Reconfigure DHCP'", function() { - var controller = makeController(); - controller.vlan = { dhcp_on: true }; - expect(controller.getDHCPPanelTitle()).toBe('Reconfigure DHCP'); - }); - - it("sets the panel title to 'Configure MAAS-managed DHCP", function() { - var controller = makeController(); - var VLAN_ID = makeInteger(5000, 6000); - var vlan = { - id: VLAN_ID, - vid: makeInteger(1, 4094), - fabric: 1, - name: null, - dhcp_on: true, - space_ids: [2001], - primary_rack: primaryController.system_id, - secondary_rack: secondaryController.system_id, - rack_sids: [], - external_dhcp: 1 - }; - VLANsManager._items.push(vlan); - controller.vlan = { external_dhcp: 1 }; - expect(controller.getDHCPPanelTitle()) - .toBe('Configure MAAS-managed DHCP'); - }); - }); - - describe("toggleMAASProvidesDHCP", function() { - it("sets `MAASProvidesDHCP` to `false`", function() { - var controller = makeController(); - controller.MAASProvidesDHCP = true; - controller.toggleMAASProvidesDHCP(); - expect(controller.MAASProvidesDHCP).toBe(false); - }); - - it("sets `MAASProvidesDHCP` to `true`", function() { - var controller = makeController(); - controller.MAASProvidesDHCP = false; - controller.toggleMAASProvidesDHCP(); - expect(controller.MAASProvidesDHCP).toBe(true); - }); - }); - - describe("setDHCPAction", function() { - it("sets `provideDHCP` to `true`", function() { - var controller = makeController(); - // To prevent side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.provideDHCP = false; - controller.relayVLAN = true; - controller.setDHCPAction('provideDHCP'); - expect(controller.provideDHCP).toBe(true); - expect(controller.relayVLAN).toBe(false); - }); - - it("sets `relayVLAN` to `false`", function() { - var controller = makeController(); - // To prevent side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.provideDHCP = true; - controller.relayVLAN = false; - controller.setDHCPAction('relayVLAN'); - expect(controller.relayVLAN).toBe(true); - expect(controller.provideDHCP).toBe(false); - }); - - it("calls `setSuggestedRange` for DHCP", function() { - var controller = makeControllerResolveSetActiveItem(); - spyOn(controller, "setSuggestedRange"); - controller.setDHCPAction("provideDHCP"); - expect(controller.setSuggestedRange).toHaveBeenCalled(); - }); - - it("calls `setSuggestedRange` for relay VLAN", function() { - var controller = makeController(); - spyOn(controller, "setSuggestedRange"); - controller.setDHCPAction("relayVLAN"); - expect(controller.setSuggestedRange).toHaveBeenCalled(); - }); - }); - - describe("enableDHCP", function() { - it("DHCPError populated on action failure", function () { - var controller = makeControllerResolveSetActiveItem(); - // To prevent side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - var defer = $q.defer(); - spyOn(VLANsManager, "configureDHCP").and.returnValue( - defer.promise); - controller.enableDHCP(); - result = { - error: 'errorString', request: { - params: { - action: 'enable_dhcp' - } - } - }; - defer.reject(result); - $scope.$digest(); - expect(controller.DHCPError).toBe('errorString'); - }); - - it("performAction for enable_dhcp called with all params", function () { - var controller = makeControllerResolveSetActiveItem(); - controller.actionOption = controller.PROVIDE_DHCP_ACTION; - // This will populate the default values for the racks with - // the current values from the mock objects. - // To prevent side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - controller.provideDHCPAction.subnet = 1; - controller.provideDHCPAction.gatewayIP = "192.168.0.1"; - controller.provideDHCPAction.startIP = "192.168.0.2"; - controller.provideDHCPAction.endIP = "192.168.0.254"; - var defer = $q.defer(); - spyOn(VLANsManager, "configureDHCP").and.returnValue( - defer.promise); - controller.enableDHCP(); - defer.resolve(); - $scope.$digest(); - expect(VLANsManager.configureDHCP).toHaveBeenCalledWith( - controller.vlan, - [ - controller.primaryRack.system_id, - controller.secondaryRack.system_id - ], + it(`performAction for enable_dhcp not called + if racks are missing`, function() { + vlan.primary_rack = 0; + vlan.secondary_rack = 0; + var controller = makeControllerResolveSetActiveItem(); + controller.actionOption = controller.PROVIDE_DHCP_ACTION; + // This will populate the default values for the racks with + // the current values from the mock objects. + // To prevent side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + controller.provideDHCPAction.primaryRack = null; + controller.provideDHCPAction.secondaryRack = null; + controller.provideDHCPAction.subnet = 1; + controller.provideDHCPAction.gatewayIP = "192.168.0.1"; + controller.provideDHCPAction.startIP = "192.168.0.2"; + controller.provideDHCPAction.endIP = "192.168.0.254"; + var defer = $q.defer(); + spyOn(VLANsManager, "configureDHCP").and.returnValue(defer.promise); + controller.enableDHCP(); + defer.resolve(); + $scope.$digest(); + expect(VLANsManager.configureDHCP).not.toHaveBeenCalled(); + expect(controller.DHCPError).toBe( + "A primary rack controller must be specified." + ); + }); + }); + + describe("relayDHCP", function() { + it("performAction for relay_dhcp called with all params", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.actionOption = controller.RELAY_DHCP_ACTION; + // This will populate the default values for the racks with + // the current values from the mock objects. + controller.relatedSubnets = [ + { + subnet: { + id: 1, + gateway_ip: "192.168.0.1", + statistics: { + ranges: [ { - subnet: 1, - gateway: "192.168.0.1", - start: "192.168.0.2", - end: "192.168.0.254" - } - ); - expect(controller.DHCPError).toBe(null); - }); - - it("performAction for enable_dhcp not called if racks are missing", - function () { - vlan.primary_rack = 0; - vlan.secondary_rack = 0; - var controller = makeControllerResolveSetActiveItem(); - controller.actionOption = controller.PROVIDE_DHCP_ACTION; - // This will populate the default values for the racks with - // the current values from the mock objects. - // To prevent side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - controller.provideDHCPAction.primaryRack = null; - controller.provideDHCPAction.secondaryRack = null; - controller.provideDHCPAction.subnet = 1; - controller.provideDHCPAction.gatewayIP = "192.168.0.1"; - controller.provideDHCPAction.startIP = "192.168.0.2"; - controller.provideDHCPAction.endIP = "192.168.0.254"; - var defer = $q.defer(); - spyOn(VLANsManager, "configureDHCP").and.returnValue( - defer.promise); - controller.enableDHCP(); - defer.resolve(); - $scope.$digest(); - expect(VLANsManager.configureDHCP).not.toHaveBeenCalled(); - expect(controller.DHCPError).toBe( - "A primary rack controller must be specified."); - }); - }); - - describe("relayDHCP", function() { - it("performAction for relay_dhcp called with all params", function () { - var controller = makeControllerResolveSetActiveItem(); - controller.actionOption = controller.RELAY_DHCP_ACTION; - // This will populate the default values for the racks with - // the current values from the mock objects. - controller.relatedSubnets = [{ - subnet: { - id: 1, - gateway_ip: "192.168.0.1", - statistics: { - ranges: [{ - num_addresses: 2, - purpose: ["unused"], - start: "192.168.0.2", - end: "192.168.0.254" - }] - } + num_addresses: 2, + purpose: ["unused"], + start: "192.168.0.2", + end: "192.168.0.254" } - }]; - controller.openDHCPPanel(); - controller.provideDHCPAction.subnet = 1; - controller.provideDHCPAction.gatewayIP = "192.168.0.1"; - controller.provideDHCPAction.startIP = "192.168.0.2"; - controller.provideDHCPAction.endIP = "192.168.0.254"; - var relay = { - id: makeInteger(5001, 6000) - }; - VLANsManager._items = [relay]; - controller.provideDHCPAction.relayVLAN = relay; - var defer = $q.defer(); - spyOn(VLANsManager, "configureDHCP").and.returnValue( - defer.promise); - controller.relayDHCP(); - defer.resolve(); - $scope.$digest(); - expect(VLANsManager.configureDHCP).toHaveBeenCalledWith( - controller.vlan, - [], - { - subnet: 1, - gateway: "192.168.0.1", - start: "192.168.0.2", - end: "192.168.0.254" - }, - relay.id - ); - expect(controller.DHCPError).toBe(null); - }); - }); - - describe("disableDHCP", function() { - it("performAction for disable_dhcp called with all params", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.actionOption = controller.DISABLE_DHCP_ACTION; - // This will populate the default values for the racks with - // the current values from the mock objects. - // To prevent side effects of calling `openDHCPPanel` - spyOn(controller, "setSuggestedRange"); - controller.openDHCPPanel(); - var defer = $q.defer(); - spyOn(VLANsManager, "disableDHCP").and.returnValue( - defer.promise); - controller.disableDHCP(); - defer.resolve(); - $scope.$digest(); - expect(VLANsManager.disableDHCP) - .toHaveBeenCalledWith(controller.vlan); - expect(controller.DHCPError).toBe(null); - }); - }); - - describe("getAvailableVLANS", function() { - it("doesn't return current VLAN", function() { - var controller = makeControllerResolveSetActiveItem(); - var vlan = { - id: 5259, - vid: 525, - fabric: 1, - name: null, - dhcp_on: true, - space_ids: [2001], - primary_rack: primaryController.system_id, - secondary_rack: secondaryController.system_id, - rack_sids: [], - external_dhcp: 1 - }; - controller.vlans = [vlan]; - controller.vlan = vlan; - expect(controller.getAvailableVLANS()).toBe(0); - }); - - it("doesn't return vlan with no dhcp", function() { - var controller = makeControllerResolveSetActiveItem(); - var vlan = { - id: 5259, - vid: 525, - fabric: 1, - name: null, - dhcp_on: false, - space_ids: [2001], - primary_rack: primaryController.system_id, - secondary_rack: secondaryController.system_id, - rack_sids: [], - external_dhcp: 1 - }; - controller.vlans = [vlan]; - controller.vlan = vlan; - expect(controller.getAvailableVLANS()).toBe(0); - }); - - it("returns if not current vlan and has dhcp", function() { - var controller = makeControllerResolveSetActiveItem(); - controller.vlans = [{ - id: 5259, - vid: 525, - fabric: 1, - name: null, - dhcp_on: true, - space_ids: [2001], - primary_rack: primaryController.system_id, - secondary_rack: secondaryController.system_id, - rack_sids: [], - external_dhcp: 1 - }]; - controller.vlan = { - id: 5239, - vid: 525, - fabric: 1, - name: null, - dhcp_on: true, - space_ids: [2001], - primary_rack: primaryController.system_id, - secondary_rack: secondaryController.system_id, - rack_sids: [], - external_dhcp: 1 - }; - expect(controller.getAvailableVLANS()).toBe(1); - }); + ] + } + } + } + ]; + controller.openDHCPPanel(); + controller.provideDHCPAction.subnet = 1; + controller.provideDHCPAction.gatewayIP = "192.168.0.1"; + controller.provideDHCPAction.startIP = "192.168.0.2"; + controller.provideDHCPAction.endIP = "192.168.0.254"; + var relay = { + id: makeInteger(5001, 6000) + }; + VLANsManager._items = [relay]; + controller.provideDHCPAction.relayVLAN = relay; + var defer = $q.defer(); + spyOn(VLANsManager, "configureDHCP").and.returnValue(defer.promise); + controller.relayDHCP(); + defer.resolve(); + $scope.$digest(); + expect(VLANsManager.configureDHCP).toHaveBeenCalledWith( + controller.vlan, + [], + { + subnet: 1, + gateway: "192.168.0.1", + start: "192.168.0.2", + end: "192.168.0.254" + }, + relay.id + ); + expect(controller.DHCPError).toBe(null); + }); + }); + + describe("disableDHCP", function() { + it("performAction for disable_dhcp called with all params", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.actionOption = controller.DISABLE_DHCP_ACTION; + // This will populate the default values for the racks with + // the current values from the mock objects. + // To prevent side effects of calling `openDHCPPanel` + spyOn(controller, "setSuggestedRange"); + controller.openDHCPPanel(); + var defer = $q.defer(); + spyOn(VLANsManager, "disableDHCP").and.returnValue(defer.promise); + controller.disableDHCP(); + defer.resolve(); + $scope.$digest(); + expect(VLANsManager.disableDHCP).toHaveBeenCalledWith(controller.vlan); + expect(controller.DHCPError).toBe(null); + }); + }); + + describe("dismissHighAvailabilityNotification", function() { + it("sets hideHighAvailabilityNotification to true", function() { + var controller = makeController(); + controller.vlan = { id: 5001 }; + controller.hideHighAvailabilityNotification = false; + controller.dismissHighAvailabilityNotification(); + expect(controller.hideHighAvailabilityNotification).toBe(true); + }); + }); + + describe("showHighAvailabilityNotification", function() { + it("returns true if has DHCP, no secondary rack but could", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: true }; + controller.provideDHCPAction.secondaryRack = null; + controller.relatedControllers = [{ id: 1 }, { id: 2 }]; + controller.hideHighAvailabilityNotification = false; + expect(controller.showHighAvailabilityNotification()).toBe(true); + }); + + it("returns false if no DHCP", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: false }; + controller.relatedControllers = [{ id: 1 }, { id: 2 }]; + controller.hideHighAvailabilityNotification = false; + expect(controller.showHighAvailabilityNotification()).toBe(false); + }); + + it("returns false if has secondary rack", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: true }; + controller.hideHighAvailabilityNotification = false; + expect(controller.showHighAvailabilityNotification()).toBe(false); + }); + + it("returns false if has no available racks", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: true }; + controller.hideHighAvailabilityNotification = false; + expect(controller.showHighAvailabilityNotification()).toBe(false); + }); + + it("returns false if hideHighAvailabilityNotification if true", function() { + var controller = makeController(); + controller.vlan = { dhcp_on: true }; + controller.hideHighAvailabilityNotification = true; + expect(controller.showHighAvailabilityNotification()).toBe(false); + }); + }); + + describe("getAvailableVLANS", function() { + it("doesn't return current VLAN", function() { + var controller = makeControllerResolveSetActiveItem(); + var vlan = { + id: 5259, + vid: 525, + fabric: 1, + name: null, + dhcp_on: true, + space_ids: [2001], + primary_rack: primaryController.system_id, + secondary_rack: secondaryController.system_id, + rack_sids: [], + external_dhcp: 1 + }; + controller.vlans = [vlan]; + controller.vlan = vlan; + expect(controller.getAvailableVLANS()).toBe(0); + }); + + it("doesn't return vlan with no dhcp", function() { + var controller = makeControllerResolveSetActiveItem(); + var vlan = { + id: 5259, + vid: 525, + fabric: 1, + name: null, + dhcp_on: false, + space_ids: [2001], + primary_rack: primaryController.system_id, + secondary_rack: secondaryController.system_id, + rack_sids: [], + external_dhcp: 1 + }; + controller.vlans = [vlan]; + controller.vlan = vlan; + expect(controller.getAvailableVLANS()).toBe(0); + }); + + it("returns if not current vlan and has dhcp", function() { + var controller = makeControllerResolveSetActiveItem(); + controller.vlans = [ + { + id: 5259, + vid: 525, + fabric: 1, + name: null, + dhcp_on: true, + space_ids: [2001], + primary_rack: primaryController.system_id, + secondary_rack: secondaryController.system_id, + rack_sids: [], + external_dhcp: 1 + } + ]; + controller.vlan = { + id: 5239, + vid: 525, + fabric: 1, + name: null, + dhcp_on: true, + space_ids: [2001], + primary_rack: primaryController.system_id, + secondary_rack: secondaryController.system_id, + rack_sids: [], + external_dhcp: 1 + }; + expect(controller.getAvailableVLANS()).toBe(1); }); + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_zone_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_zone_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_zone_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_zone_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,217 +4,213 @@ * Unit tests for ZonesListController. */ -describe("ZoneDetailsController", function() { - - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Make a fake zone - function makeZone() { - var zone = { - id: makeInteger(1, 10000), - name: makeName("zone") - }; - ZonesManager._items.push(zone); - return zone; - } - - // Grab the needed angular pieces. - var $controller, $rootScope, $location, $scope, $q, $routeParams; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $location = $injector.get("$location"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load any injected managers and services. - var ZonesManager, UsersManager, ManagerHelperService, ErrorService; - beforeEach(inject(function($injector) { - ZonesManager = $injector.get("ZonesManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - ErrorService = $injector.get("ErrorService"); - })); - - var zone; - beforeEach(function() { - zone = makeZone(); - }); - - // Makes the NodesListController - function makeController(loadManagerDefer) { - spyOn(UsersManager, "isSuperUser").and.returnValue(true); - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("ZoneDetailsController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - $location: $location, - ZonesManager: ZonesManager, - UsersManager: UsersManager, - ManagerHelperService: ManagerHelperService, - ErrorService: ErrorService - }); +import { makeInteger, makeName } from "testing/utils"; - return controller; - } - - // Make the controller and resolve the setActiveItem call. - function makeControllerResolveSetActiveItem() { - var setActiveDefer = $q.defer(); - spyOn(ZonesManager, "setActiveItem").and.returnValue( - setActiveDefer.promise); - var defer = $q.defer(); - var controller = makeController(defer); - $routeParams.zone_id = zone.id; - - defer.resolve(); - $rootScope.$digest(); - setActiveDefer.resolve(zone); - $rootScope.$digest(); +describe("ZoneDetailsController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - return controller; + // Make a fake zone + function makeZone() { + var zone = { + id: makeInteger(1, 10000), + name: makeName("zone") + }; + ZonesManager._items.push(zone); + return zone; + } + + // Grab the needed angular pieces. + var $controller, $rootScope, $location, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $location = $injector.get("$location"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load any injected managers and services. + var ZonesManager, UsersManager, ManagerHelperService, ErrorService; + beforeEach(inject(function($injector) { + ZonesManager = $injector.get("ZonesManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + ErrorService = $injector.get("ErrorService"); + })); + + var zone; + beforeEach(function() { + zone = makeZone(); + }); + + // Makes the NodesListController + function makeController(loadManagerDefer) { + spyOn(UsersManager, "isSuperUser").and.returnValue(true); + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { + // Create the controller. + var controller = $controller("ZoneDetailsController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + $location: $location, + ZonesManager: ZonesManager, + UsersManager: UsersManager, + ManagerHelperService: ManagerHelperService, + ErrorService: ErrorService + }); + + return controller; + } + + // Make the controller and resolve the setActiveItem call. + function makeControllerResolveSetActiveItem() { + var setActiveDefer = $q.defer(); + spyOn(ZonesManager, "setActiveItem").and.returnValue( + setActiveDefer.promise + ); + var defer = $q.defer(); + var controller = makeController(defer); + $routeParams.zone_id = zone.id; + + defer.resolve(); + $rootScope.$digest(); + setActiveDefer.resolve(zone); + $rootScope.$digest(); + + return controller; + } + + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Loading..."); + expect($rootScope.page).toBe("zones"); + }); + + it( + "calls loadManagers with [ZonesManager, UsersManager]" + + function() { makeController(); - expect($rootScope.title).toBe("Loading..."); - expect($rootScope.page).toBe("zones"); - }); - - it("calls loadManagers with [ZonesManager, UsersManager]" + - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ZonesManager, UsersManager]); - }); - - it("raises error if zone identifier is invalid", function() { - spyOn(ZonesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - spyOn(ErrorService, "raiseError").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.zone_id = 'xyzzy'; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.zone).toBe(null); - expect($scope.loaded).toBe(false); - expect(ZonesManager.setActiveItem).not.toHaveBeenCalled(); - expect(ErrorService.raiseError).toHaveBeenCalled(); - }); - - it("doesn't call setActiveItem if zone is loaded", function() { - spyOn(ZonesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - ZonesManager._activeItem = zone; - $routeParams.zone_id = zone.id; - - defer.resolve(); - $rootScope.$digest(); - - expect($scope.zone).toBe(zone); - expect($scope.loaded).toBe(true); - expect(ZonesManager.setActiveItem).not.toHaveBeenCalled(); - }); - - it("calls setActiveItem if zone is not active", function() { - spyOn(ZonesManager, "setActiveItem").and.returnValue( - $q.defer().promise); - var defer = $q.defer(); - makeController(defer); - $routeParams.zone_id = zone.id; - - defer.resolve(); - $rootScope.$digest(); - - expect(ZonesManager.setActiveItem).toHaveBeenCalledWith( - zone.id); - }); - - it("sets zone and loaded once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($scope.zone).toBe(zone); - expect($scope.loaded).toBe(true); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + UsersManager + ]); + } + ); + + it("raises error if zone identifier is invalid", function() { + spyOn(ZonesManager, "setActiveItem").and.returnValue($q.defer().promise); + spyOn(ErrorService, "raiseError").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.zone_id = "xyzzy"; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.zone).toBe(null); + expect($scope.loaded).toBe(false); + expect(ZonesManager.setActiveItem).not.toHaveBeenCalled(); + expect(ErrorService.raiseError).toHaveBeenCalled(); + }); + + it("doesn't call setActiveItem if zone is loaded", function() { + spyOn(ZonesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + ZonesManager._activeItem = zone; + $routeParams.zone_id = zone.id; + + defer.resolve(); + $rootScope.$digest(); + + expect($scope.zone).toBe(zone); + expect($scope.loaded).toBe(true); + expect(ZonesManager.setActiveItem).not.toHaveBeenCalled(); + }); + + it("calls setActiveItem if zone is not active", function() { + spyOn(ZonesManager, "setActiveItem").and.returnValue($q.defer().promise); + var defer = $q.defer(); + makeController(defer); + $routeParams.zone_id = zone.id; + + defer.resolve(); + $rootScope.$digest(); + + expect(ZonesManager.setActiveItem).toHaveBeenCalledWith(zone.id); + }); + + it("sets zone and loaded once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($scope.zone).toBe(zone); + expect($scope.loaded).toBe(true); + }); + + it("title is updated once setActiveItem resolves", function() { + makeControllerResolveSetActiveItem(); + expect($rootScope.title).toBe(zone.name); + }); + + describe("canBeDeleted", function() { + it("returns false if zone is null", function() { + makeControllerResolveSetActiveItem(); + $scope.zone = null; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns false if zone id is 0", function() { + makeControllerResolveSetActiveItem(); + $scope.zone.id = 0; + expect($scope.canBeDeleted()).toBe(false); + }); + + it("returns true if zone id > 0", function() { + makeControllerResolveSetActiveItem(); + $scope.zone.id = 1; + expect($scope.canBeDeleted()).toBe(true); + }); + }); + + describe("deleteButton", function() { + it("confirms delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + expect($scope.confirmingDelete).toBe(true); + }); + + it("clears error", function() { + makeControllerResolveSetActiveItem(); + $scope.error = makeName("error"); + $scope.deleteButton(); + expect($scope.error).toBeNull(); + }); + }); + + describe("cancelDeleteButton", function() { + it("cancels delete", function() { + makeControllerResolveSetActiveItem(); + $scope.deleteButton(); + $scope.cancelDeleteButton(); + expect($scope.confirmingDelete).toBe(false); + }); + }); + + describe("deleteZone", function() { + it("calls deleteItem", function() { + makeController(); + var deleteItem = spyOn(ZonesManager, "deleteItem"); + var defer = $q.defer(); + deleteItem.and.returnValue(defer.promise); + $scope.deleteConfirmButton(); + expect(deleteItem).toHaveBeenCalled(); }); - - it("title is updated once setActiveItem resolves", function() { - makeControllerResolveSetActiveItem(); - expect($rootScope.title).toBe(zone.name); - }); - - describe("canBeDeleted", function() { - - it("returns false if zone is null", function() { - makeControllerResolveSetActiveItem(); - $scope.zone = null; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns false if zone id is 0", function() { - makeControllerResolveSetActiveItem(); - $scope.zone.id = 0; - expect($scope.canBeDeleted()).toBe(false); - }); - - it("returns true if zone id > 0", function() { - makeControllerResolveSetActiveItem(); - $scope.zone.id = 1; - expect($scope.canBeDeleted()).toBe(true); - }); - }); - - describe("deleteButton", function() { - - it("confirms delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - expect($scope.confirmingDelete).toBe(true); - }); - - it("clears error", function() { - makeControllerResolveSetActiveItem(); - $scope.error = makeName("error"); - $scope.deleteButton(); - expect($scope.error).toBeNull(); - }); - }); - - describe("cancelDeleteButton", function() { - - it("cancels delete", function() { - makeControllerResolveSetActiveItem(); - $scope.deleteButton(); - $scope.cancelDeleteButton(); - expect($scope.confirmingDelete).toBe(false); - }); - }); - - describe("deleteZone", function() { - - it("calls deleteItem", function() { - makeController(); - var deleteItem = spyOn(ZonesManager, "deleteItem"); - var defer = $q.defer(); - deleteItem.and.returnValue(defer.promise); - $scope.deleteConfirmButton(); - expect(deleteItem).toHaveBeenCalled(); - }); - }); - + }); }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_zones_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_zones_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/tests/test_zones_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/tests/test_zones_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,115 +5,113 @@ */ describe("ZonesListController", function() { + // Load the MAAS module. + beforeEach(angular.mock.module("MAAS")); - // Load the MAAS module. - beforeEach(module("MAAS")); - - // Grab the needed angular pieces. - var $controller, $rootScope, $scope, $q, $routeParams; - beforeEach(inject(function($injector) { - $controller = $injector.get("$controller"); - $rootScope = $injector.get("$rootScope"); - $scope = $rootScope.$new(); - $q = $injector.get("$q"); - $routeParams = {}; - })); - - // Load the managers and services. - var ZonesManager, UsersManager; - var ManagerHelperService, RegionConnection; - beforeEach(inject(function($injector) { - ZonesManager = $injector.get("ZonesManager"); - UsersManager = $injector.get("UsersManager"); - ManagerHelperService = $injector.get("ManagerHelperService"); - })); - - // Makes the ZonesListController - function makeController(loadManagerDefer, defaultConnectDefer) { - var loadManagers = spyOn(ManagerHelperService, "loadManagers"); - if(angular.isObject(loadManagerDefer)) { - loadManagers.and.returnValue(loadManagerDefer.promise); - } else { - loadManagers.and.returnValue($q.defer().promise); - } - - // Create the controller. - var controller = $controller("ZonesListController", { - $scope: $scope, - $rootScope: $rootScope, - $routeParams: $routeParams, - ZonesManager: ZonesManager, - ManagerHelperService: ManagerHelperService - }); - - return controller; + // Grab the needed angular pieces. + var $controller, $rootScope, $scope, $q, $routeParams; + beforeEach(inject(function($injector) { + $controller = $injector.get("$controller"); + $rootScope = $injector.get("$rootScope"); + $scope = $rootScope.$new(); + $q = $injector.get("$q"); + $routeParams = {}; + })); + + // Load the managers and services. + var ZonesManager, UsersManager; + var ManagerHelperService, RegionConnection; + beforeEach(inject(function($injector) { + ZonesManager = $injector.get("ZonesManager"); + UsersManager = $injector.get("UsersManager"); + ManagerHelperService = $injector.get("ManagerHelperService"); + })); + + // Makes the ZonesListController + function makeController(loadManagerDefer, defaultConnectDefer) { + var loadManagers = spyOn(ManagerHelperService, "loadManagers"); + if (angular.isObject(loadManagerDefer)) { + loadManagers.and.returnValue(loadManagerDefer.promise); + } else { + loadManagers.and.returnValue($q.defer().promise); } - it("sets title and page on $rootScope", function() { - makeController(); - expect($rootScope.title).toBe("Zones"); - expect($rootScope.page).toBe("zones"); + // Create the controller. + var controller = $controller("ZonesListController", { + $scope: $scope, + $rootScope: $rootScope, + $routeParams: $routeParams, + ZonesManager: ZonesManager, + ManagerHelperService: ManagerHelperService }); - it("sets initial values on $scope", function() { - // tab-independent variables. - makeController(); - expect($scope.zones).toBe(ZonesManager.getItems()); - expect($scope.loading).toBe(true); - }); + return controller; + } - it("calls loadManagers with [ZonesManager, UsersManager]", - function() { - makeController(); - expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith( - $scope, [ZonesManager, UsersManager]); - }); - - it("sets loading to false when loadManagers resolves", function() { - var defer = $q.defer(); - makeController(defer); - defer.resolve(); - $rootScope.$digest(); - expect($scope.loading).toBe(false); + it("sets title and page on $rootScope", function() { + makeController(); + expect($rootScope.title).toBe("Zones"); + expect($rootScope.page).toBe("zones"); + }); + + it("sets initial values on $scope", function() { + // tab-independent variables. + makeController(); + expect($scope.zones).toBe(ZonesManager.getItems()); + expect($scope.loading).toBe(true); + }); + + it("calls loadManagers with [ZonesManager, UsersManager]", function() { + makeController(); + expect(ManagerHelperService.loadManagers).toHaveBeenCalledWith($scope, [ + ZonesManager, + UsersManager + ]); + }); + + it("sets loading to false when loadManagers resolves", function() { + var defer = $q.defer(); + makeController(defer); + defer.resolve(); + $rootScope.$digest(); + expect($scope.loading).toBe(false); + }); + + describe("addZone", function() { + it("sets action.open to true", function() { + makeController(); + $scope.addZone(); + expect($scope.action.open).toBe(true); }); + }); - describe("addZone", function() { - - it("sets action.open to true", function() { - makeController(); - $scope.addZone(); - expect($scope.action.open).toBe(true); - }); - }); - - describe("closeZone", function() { - - it("set action.open to false and clears action.obj", function() { - makeController(); - var obj = {}; - $scope.action.obj = obj; - $scope.action.open = true; - $scope.closeZone(); - expect($scope.action.open).toBe(false); - expect($scope.action.obj).toEqual({}); - expect($scope.action.obj).not.toBe(obj); - }); + describe("closeZone", function() { + it("set action.open to false and clears action.obj", function() { + makeController(); + var obj = {}; + $scope.action.obj = obj; + $scope.action.open = true; + $scope.closeZone(); + expect($scope.action.open).toBe(false); + expect($scope.action.obj).toEqual({}); + expect($scope.action.obj).not.toBe(obj); }); + }); - setupController = function(zones) { - var defer = $q.defer(); - var controller = makeController(defer); - $scope.zones = zones; - ZonesManager._items = zones; - defer.resolve(); - $rootScope.$digest(); - return controller; - }; - - testUpdates = function(controller, zones, expectedZonesData) { - $scope.zones = zones; - ZonesManager._items = zones; - $rootScope.$digest(); - expect($scope.data).toEqual(expectedZonesData); - }; + setupController = function(zones) { + var defer = $q.defer(); + var controller = makeController(defer); + $scope.zones = zones; + ZonesManager._items = zones; + defer.resolve(); + $rootScope.$digest(); + return controller; + }; + + testUpdates = function(controller, zones, expectedZonesData) { + $scope.zones = zones; + ZonesManager._items = zones; + $rootScope.$digest(); + expect($scope.data).toEqual(expectedZonesData); + }; }); diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/vlan_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/vlan_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/vlan_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/vlan_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -5,745 +5,800 @@ */ export function ignoreSelf() { - return function(objects, self) { - var filtered = []; - angular.forEach(objects, function(obj) { - if (obj !== self) { - filtered.push(obj); - } - }); - return filtered; - }; + return function(objects, self) { + var filtered = []; + angular.forEach(objects, function(obj) { + if (obj !== self) { + filtered.push(obj); + } + }); + return filtered; + }; } export function removeNoDHCP() { - return function(objects) { - var filtered = []; - angular.forEach(objects, function(obj) { - if (obj.dhcp_on) { - filtered.push(obj); - } - }); - return filtered; - } + return function(objects) { + var filtered = []; + angular.forEach(objects, function(obj) { + if (obj.dhcp_on) { + filtered.push(obj); + } + }); + return filtered; + }; } /* @ngInject */ export function VLANDetailsController( - $scope, $rootScope, $routeParams, $filter, $location, - VLANsManager, SubnetsManager, SpacesManager, FabricsManager, - ControllersManager, UsersManager, ManagerHelperService, ErrorService, - IPRangesManager) { - var vm = this; - - var filterByVLAN = $filter('filterByVLAN'); - - // Set title and page. - $rootScope.title = "Loading..."; - - // Note: this value must match the top-level tab, in order for - // highlighting to occur properly. - $rootScope.page = "networks"; - - vm.DELETE_ACTION = { - name: "delete", - title: "Delete" - }; - - vm.ipranges = IPRangesManager.getItems(); - - // Initial values. - vm.loaded = false; - vm.vlan = null; - vm.title = null; - vm.actionOption = null; - vm.actionOptions = []; - vm.vlanManager = VLANsManager; - vm.vlans = VLANsManager.getItems(); - vm.subnets = SubnetsManager.getItems(); - vm.spaces = SpacesManager.getItems(); - vm.fabrics = FabricsManager.getItems(); - vm.controllers = ControllersManager.getItems(); - vm.actionError = null; - vm.relatedSubnets = []; - vm.relatedControllers = []; - vm.provideDHCPAction = {}; - vm.primaryRack = null; - vm.secondaryRack = null; + $scope, + $rootScope, + $routeParams, + $filter, + $location, + $timeout, + VLANsManager, + SubnetsManager, + SpacesManager, + FabricsManager, + ControllersManager, + UsersManager, + ManagerHelperService, + ErrorService, + IPRangesManager +) { + var vm = this; + + var filterByVLAN = $filter("filterByVLAN"); + + // Set title and page. + $rootScope.title = "Loading..."; + + // Note: this value must match the top-level tab, in order for + // highlighting to occur properly. + $rootScope.page = "networks"; + + vm.DELETE_ACTION = { + name: "delete", + title: "Delete" + }; + + vm.ipranges = IPRangesManager.getItems(); + + // Initial values. + vm.loaded = false; + vm.vlan = null; + vm.title = null; + vm.actionOption = null; + vm.actionOptions = []; + vm.vlanManager = VLANsManager; + vm.vlans = VLANsManager.getItems(); + vm.subnets = SubnetsManager.getItems(); + vm.spaces = SpacesManager.getItems(); + vm.fabrics = FabricsManager.getItems(); + vm.controllers = ControllersManager.getItems(); + vm.actionError = null; + vm.relatedSubnets = []; + vm.relatedControllers = []; + vm.provideDHCPAction = {}; + vm.primaryRack = null; + vm.secondaryRack = null; + vm.editSummary = false; + vm.showDHCPPanel = false; + vm.MAASProvidesDHCP = true; + vm.provideDHCP = true; + vm.relayVLAN = false; + vm.filteredRelatedSubnets = []; + vm.iprangesInVLAN = []; + vm.selectedSubnet = null; + vm.suggestedRange = null; + vm.isProvidingDHCP = false; + vm.DHCPError = null; + vm.hideHighAvailabilityNotification = false; + + // Return true if the authenticated user is super user. + vm.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Called when the "edit" button is cliked in the vlan summary + vm.enterEditSummary = function() { + vm.editSummary = true; + }; + + // Called when the "cancel" button is cliked in the vlan summary + vm.exitEditSummary = function() { vm.editSummary = false; - vm.showDHCPPanel = false; - vm.MAASProvidesDHCP = true; - vm.provideDHCP = true; - vm.relayVLAN = false; - vm.filteredRelatedSubnets = []; - vm.iprangesInVLAN = []; - vm.selectedSubnet = null; - vm.suggestedRange = null; - vm.isProvidingDHCP = false; - vm.DHCPError = null; + }; - // Return true if the authenticated user is super user. - vm.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Called when the "edit" button is cliked in the vlan summary - vm.enterEditSummary = function() { - vm.editSummary = true; - }; - - // Called when the "cancel" button is cliked in the vlan summary - vm.exitEditSummary = function() { - vm.editSummary = false; - }; - - // Get the space name for the VLAN. - vm.getSpaceName = function() { - var space = SpacesManager.getItemFromList(vm.vlan.space); - if (space) { - return space.name; - } else { - return "(undefined)"; - } - }; + // Get the space name for the VLAN. + vm.getSpaceName = function() { + var space = SpacesManager.getItemFromList(vm.vlan.space); + if (space) { + return space.name; + } else { + return "(undefined)"; + } + }; - // Get the aciton structure for the action with the specified name. - vm.getActionByName = function(name) { - var i; - for (i = 0; i < vm.actionOptions.length; i++) { - if (vm.actionOptions[i].name === name) { - return vm.actionOptions[i]; - } - } - return null; - }; + // Get the aciton structure for the action with the specified name. + vm.getActionByName = function(name) { + var i; + for (i = 0; i < vm.actionOptions.length; i++) { + if (vm.actionOptions[i].name === name) { + return vm.actionOptions[i]; + } + } + return null; + }; - // Get the title for the DHCP panel - vm.getDHCPPanelTitle = function() { - var DHCPStatus = vm.getDHCPStatus(); + // Get the title for the DHCP panel + vm.getDHCPPanelTitle = function() { + var DHCPStatus = vm.getDHCPStatus(); - if (vm.vlan && vm.vlan.external_dhcp) { - return 'Configure MAAS-managed DHCP'; - } + if (vm.vlan && vm.vlan.external_dhcp) { + return "Configure MAAS-managed DHCP"; + } - if (DHCPStatus !== 'Disabled') { - return 'Reconfigure DHCP'; - } + if (DHCPStatus !== "Disabled") { + return "Reconfigure DHCP"; + } - return 'Configure DHCP'; - }; + return "Configure DHCP"; + }; - // Set DHCP action - vm.setDHCPAction = function(action) { - if (action === 'relayVLAN') { - vm.relayVLAN = true; - vm.provideDHCP = false; - vm.setSuggestedRange(); - } else { - vm.relayVLAN = false; - vm.provideDHCP = true; - vm.setSuggestedRange(); - } - }; + // Set DHCP action + vm.setDHCPAction = function(action) { + if (action === "relayVLAN") { + vm.relayVLAN = true; + vm.provideDHCP = false; + vm.setSuggestedRange(); + } else { + vm.relayVLAN = false; + vm.provideDHCP = true; + vm.setSuggestedRange(); + } + }; - // Toggle state of `MAASProvidesDHCP` - vm.toggleMAASProvidesDHCP = function() { - vm.MAASProvidesDHCP = !vm.MAASProvidesDHCP; - }; - - // Initialize the provideDHCPAction structure with the current primary - // and secondary rack, plus an indication regarding whether or not - // adding a dynamic IP range is required. - vm.initProvideDHCP = function(forRelay) { - vm.provideDHCPAction = {}; - var dhcp = vm.provideDHCPAction; - dhcp.subnet = null; - dhcp.relayVLAN = null; - if (angular.isNumber(vm.vlan.relay_vlan)) { - dhcp.relayVLAN = VLANsManager.getItemFromList( - vm.vlan.relay_vlan); - } - if (angular.isObject(vm.primaryRack)) { - dhcp.primaryRack = vm.primaryRack.system_id; - } else if (vm.relatedControllers.length > 0) { - // Select the primary controller arbitrarily by default. - dhcp.primaryRack = vm.relatedControllers[0].system_id; - } - if (angular.isObject(vm.secondaryRack)) { - dhcp.secondaryRack = vm.secondaryRack.system_id; - } else if (vm.relatedControllers.length > 1) { - // Select the secondary controller arbitrarily by default. - dhcp.secondaryRack = vm.relatedControllers[1].system_id; - } - dhcp.maxIPs = 0; - dhcp.startIP = null; - dhcp.endIP = null; - dhcp.gatewayIP = ""; - if (angular.isObject(vm.relatedSubnets)) { - // Select a subnet arbitrarily by default. - if (vm.relatedSubnets.length > 0 && - angular.isObject(vm.relatedSubnets[0].subnet)) { - dhcp.subnet = vm.relatedSubnets[0].subnet.id; - } - dhcp.needsDynamicRange = true; - var i, subnet; - for (i = 0; i < vm.relatedSubnets.length; i++) { - subnet = vm.relatedSubnets[i].subnet; - // If any related subnet already has a dynamic range, we - // cannot prompt the user to enter one here. If a - // suggestion does not exist, a range does not exist - // already. - var iprange = subnet.statistics.suggested_dynamic_range; - if (!angular.isObject(iprange)) { - // If there is already a dynamic range on one of the - // subnets, it's the "subnet of least surprise" if - // the user is choosing to reconfigure their rack - // controllers. (if they want DHCP on *another* subnet, - // they should need to be explicit about it.) - dhcp.subnet = subnet.id; - dhcp.needsDynamicRange = false; - break; - } - } - // We must prompt the user for a subnet and a gateway IP - // address if any subnet does not yet contain a gateway IP - // address. - dhcp.needsGatewayIP = false; - dhcp.subnetMissingGatewayIP = true; - for (i = 0; i < vm.relatedSubnets.length; i++) { - subnet = vm.relatedSubnets[i].subnet; - if (subnet.gateway_ip === null || - subnet.gateway_ip === '') { - dhcp.needsGatewayIP = true; - break; - } - } - } - // Since we are setting default values for these three options, - // ensure all the appropriate updates occur. - if (!forRelay) { - vm.updatePrimaryRack(); - vm.updateSecondaryRack(); + // Toggle state of `MAASProvidesDHCP` + vm.toggleMAASProvidesDHCP = function() { + vm.MAASProvidesDHCP = !vm.MAASProvidesDHCP; + }; + + // Initialize the provideDHCPAction structure with the current primary + // and secondary rack, plus an indication regarding whether or not + // adding a dynamic IP range is required. + vm.initProvideDHCP = function(forRelay) { + vm.provideDHCPAction = {}; + var dhcp = vm.provideDHCPAction; + dhcp.subnet = null; + dhcp.relayVLAN = null; + if (angular.isNumber(vm.vlan.relay_vlan)) { + dhcp.relayVLAN = VLANsManager.getItemFromList(vm.vlan.relay_vlan); + } + if (angular.isObject(vm.primaryRack)) { + dhcp.primaryRack = vm.primaryRack.system_id; + } else if (vm.relatedControllers.length > 0) { + // Select the primary controller arbitrarily by default. + dhcp.primaryRack = vm.relatedControllers[0].system_id; + } + if (angular.isObject(vm.secondaryRack)) { + dhcp.secondaryRack = vm.secondaryRack.system_id; + } else { + dhcp.secondaryRack = vm.secondaryRack; + } + dhcp.maxIPs = 0; + dhcp.startIP = null; + dhcp.endIP = null; + dhcp.gatewayIP = ""; + if (angular.isObject(vm.relatedSubnets)) { + // Select a subnet arbitrarily by default. + if ( + vm.relatedSubnets.length > 0 && + angular.isObject(vm.relatedSubnets[0].subnet) + ) { + dhcp.subnet = vm.relatedSubnets[0].subnet.id; + } + dhcp.needsDynamicRange = true; + var i, subnet; + for (i = 0; i < vm.relatedSubnets.length; i++) { + subnet = vm.relatedSubnets[i].subnet; + // If any related subnet already has a dynamic range, we + // cannot prompt the user to enter one here. If a + // suggestion does not exist, a range does not exist + // already. + var iprange = subnet.statistics.suggested_dynamic_range; + if (!angular.isObject(iprange)) { + // If there is already a dynamic range on one of the + // subnets, it's the "subnet of least surprise" if + // the user is choosing to reconfigure their rack + // controllers. (if they want DHCP on *another* subnet, + // they should need to be explicit about it.) + dhcp.subnet = subnet.id; + dhcp.needsDynamicRange = false; + break; + } + } + // We must prompt the user for a subnet and a gateway IP + // address if any subnet does not yet contain a gateway IP + // address. + dhcp.needsGatewayIP = false; + dhcp.subnetMissingGatewayIP = true; + for (i = 0; i < vm.relatedSubnets.length; i++) { + subnet = vm.relatedSubnets[i].subnet; + if (subnet.gateway_ip === null || subnet.gateway_ip === "") { + dhcp.needsGatewayIP = true; + break; } - vm.updateSubnet(forRelay); - }; + } + } + // Since we are setting default values for these three options, + // ensure all the appropriate updates occur. + if (!forRelay) { + vm.updatePrimaryRack(); + vm.updateSecondaryRack(); + } + vm.updateSubnet(forRelay); + }; - // Called when the actionOption has changed. - vm.actionOptionChanged = function() { - if (vm.actionOption.name === "enable_dhcp") { - vm.initProvideDHCP(false); - } else if (vm.actionOption.name === "relay_dhcp") { - vm.initProvideDHCP(true); - } - // Clear the action error. - vm.actionError = null; - }; - - // Cancel the action. - vm.actionCancel = function() { - // When the user wants to cancel an action, we need to clear out - // both the actionOption (so that the action screen will not be - // presented again) and the actionError (so that the error screen - // is hidden). - vm.actionOption = null; - vm.actionError = null; - }; - - // Called from the Provide DHCP form when the primary rack changes. - vm.updatePrimaryRack = function() { - var dhcp = vm.provideDHCPAction; - // If the user selects the secondary controller to be the primary, - // then the primary controller needs to be cleared out. - if (dhcp.primaryRack === dhcp.secondaryRack) { - dhcp.secondaryRack = null; - } - var i; - for (i = 0; i < vm.relatedControllers.length; i++) { - if (vm.relatedControllers[i].system_id !== dhcp.primaryRack) { - dhcp.secondaryRack = vm.relatedControllers[i].system_id; - break; - } - } - }; + // Called when the actionOption has changed. + vm.actionOptionChanged = function() { + if (vm.actionOption.name === "enable_dhcp") { + vm.initProvideDHCP(false); + } else if (vm.actionOption.name === "relay_dhcp") { + vm.initProvideDHCP(true); + } + // Clear the action error. + vm.actionError = null; + }; - // Called from the Provide DHCP form when the secondary rack changes. - vm.updateSecondaryRack = function() { - var dhcp = vm.provideDHCPAction; - // This should no longer be possible due to the filters on the - // drop-down boxes, but just in case. - if (dhcp.primaryRack === dhcp.secondaryRack) { - dhcp.primaryRack = null; - dhcp.secondaryRack = null; - } - }; + // Cancel the action. + vm.actionCancel = function() { + // When the user wants to cancel an action, we need to clear out + // both the actionOption (so that the action screen will not be + // presented again) and the actionError (so that the error screen + // is hidden). + vm.actionOption = null; + vm.actionError = null; + }; - // Called from the view to exclude the primary rack when selecting - // the secondary rack controller. - vm.filterPrimaryRack = function(rack) { - var dhcp = vm.provideDHCPAction; - return rack.system_id !== dhcp.primaryRack; - }; - - // Called from the Provide DHCP form when the subnet selection changes. - vm.updateSubnet = function(forRelay) { - var dhcp = vm.provideDHCPAction; - var subnet = SubnetsManager.getItemFromList(dhcp.subnet); - if (angular.isObject(subnet)) { - var suggested_gateway = null; - var iprange = null; - if (angular.isObject(subnet.statistics)) { - suggested_gateway = subnet.statistics.suggested_gateway; - iprange = subnet.statistics.suggested_dynamic_range; - } - if (angular.isObject(iprange) && iprange.num_addresses > 0) { - dhcp.maxIPs = iprange.num_addresses; - if (forRelay) { - dhcp.startIP = ""; - dhcp.endIP = ""; - dhcp.startPlaceholder = iprange.start + "(optional)"; - dhcp.endPlaceholder = iprange.end + " (optional)"; - } else { - dhcp.startIP = iprange.start; - dhcp.endIP = iprange.end; - dhcp.startPlaceholder = iprange.start; - dhcp.endPlaceholder = iprange.end; - } - } else { - // Need to add a dynamic range, but according to our data, - // there is no room on the subnet for a dynamic range. - dhcp.maxIPs = 0; - dhcp.startIP = ""; - dhcp.endIP = ""; - dhcp.startPlaceholder = "(no available IPs)"; - dhcp.endPlaceholder = "(no available IPs)"; - } - if (angular.isString(suggested_gateway)) { - if (forRelay) { - dhcp.gatewayIP = ""; - dhcp.gatewayPlaceholder = ( - suggested_gateway + " (optional)"); - } else { - dhcp.gatewayIP = suggested_gateway; - dhcp.gatewayPlaceholder = suggested_gateway; - } - } else { - // This means the subnet already has a gateway, so don't - // bother populating it. - dhcp.gatewayIP = ""; - dhcp.gatewayPlaceholder = ""; - } + // Called from the Provide DHCP form when the primary rack changes. + vm.updatePrimaryRack = function() { + var dhcp = vm.provideDHCPAction; + // If the user selects the secondary controller to be the primary, + // then the primary controller needs to be cleared out. + if (dhcp.primaryRack === dhcp.secondaryRack) { + dhcp.secondaryRack = null; + } + var i; + for (i = 0; i < vm.relatedControllers.length; i++) { + if ( + vm.relatedControllers[i].system_id !== dhcp.primaryRack && + !dhcp.secondaryRack + ) { + dhcp.secondaryRack = vm.secondaryRack; + break; + } + } + }; + + // Called from the Provide DHCP form when the secondary rack changes. + vm.updateSecondaryRack = function() { + var dhcp = vm.provideDHCPAction; + // This should no longer be possible due to the filters on the + // drop-down boxes, but just in case. + if (dhcp.primaryRack === dhcp.secondaryRack) { + dhcp.primaryRack = null; + dhcp.secondaryRack = null; + } + }; + + // Called from the view to exclude the primary rack when selecting + // the secondary rack controller. + vm.filterPrimaryRack = function(rack) { + var dhcp = vm.provideDHCPAction; + return rack.system_id !== dhcp.primaryRack; + }; + + // Called from the Provide DHCP form when the subnet selection changes. + vm.updateSubnet = function(forRelay) { + var dhcp = vm.provideDHCPAction; + var subnet = SubnetsManager.getItemFromList(dhcp.subnet); + if (angular.isObject(subnet)) { + var suggested_gateway = null; + var iprange = null; + if (angular.isObject(subnet.statistics)) { + suggested_gateway = subnet.statistics.suggested_gateway; + iprange = subnet.statistics.suggested_dynamic_range; + } + if (angular.isObject(iprange) && iprange.num_addresses > 0) { + dhcp.maxIPs = iprange.num_addresses; + if (forRelay) { + dhcp.startIP = ""; + dhcp.endIP = ""; + dhcp.startPlaceholder = iprange.start + "(optional)"; + dhcp.endPlaceholder = iprange.end + " (optional)"; } else { - // Don't need to add a dynamic range, so ensure these fields - // are cleared out. - dhcp.maxIPs = 0; - dhcp.startIP = null; - dhcp.endIP = null; - dhcp.gatewayIP = ""; - } - if (angular.isObject(subnet)) { - dhcp.subnetMissingGatewayIP = !angular.isString( - subnet.gateway_ip); + dhcp.startIP = iprange.start; + dhcp.endIP = iprange.end; + dhcp.startPlaceholder = iprange.start; + dhcp.endPlaceholder = iprange.end; + } + } else { + // Need to add a dynamic range, but according to our data, + // there is no room on the subnet for a dynamic range. + dhcp.maxIPs = 0; + dhcp.startIP = ""; + dhcp.endIP = ""; + dhcp.startPlaceholder = "(no available IPs)"; + dhcp.endPlaceholder = "(no available IPs)"; + } + if (angular.isString(suggested_gateway)) { + if (forRelay) { + dhcp.gatewayIP = ""; + dhcp.gatewayPlaceholder = suggested_gateway + " (optional)"; } else { - dhcp.subnetMissingGatewayIP = false; - } - if (dhcp.subnetMissingGatewayIP === false) { - dhcp.gatewayIP = null; + dhcp.gatewayIP = suggested_gateway; + dhcp.gatewayPlaceholder = suggested_gateway; } - }; + } else { + // This means the subnet already has a gateway, so don't + // bother populating it. + dhcp.gatewayIP = ""; + dhcp.gatewayPlaceholder = ""; + } + } else { + // Don't need to add a dynamic range, so ensure these fields + // are cleared out. + dhcp.maxIPs = 0; + dhcp.startIP = null; + dhcp.endIP = null; + dhcp.gatewayIP = ""; + } + if (angular.isObject(subnet)) { + dhcp.subnetMissingGatewayIP = !angular.isString(subnet.gateway_ip); + } else { + dhcp.subnetMissingGatewayIP = false; + } + if (dhcp.subnetMissingGatewayIP === false) { + dhcp.gatewayIP = null; + } + }; - vm.actionRetry = function() { - // When we clear actionError, the HTML will be re-rendered to - // hide the error message (and the user will be taken back to - // the previous action they were performing, since we reset - // the actionOption in the error handler. - vm.actionError = null; - }; - - // Return True if the current action can be performed. - vm.canPerformAction = function() { - if (vm.provideDHCP) { - return vm.relatedSubnets.length > 0; - } else if (vm.relayVLAN) { - return angular.isObject(vm.provideDHCPAction.relayVLAN); - } else { - return true; - } - }; + vm.actionRetry = function() { + // When we clear actionError, the HTML will be re-rendered to + // hide the error message (and the user will be taken back to + // the previous action they were performing, since we reset + // the actionOption in the error handler. + vm.actionError = null; + }; - // Perform the action. - vm.actionGo = function() { - // Do nothing if action cannot be performed. - if (!vm.canPerformAction()) { - return; - } + // Return True if the current action can be performed. + vm.canPerformAction = function() { + if (vm.provideDHCP) { + return vm.relatedSubnets.length > 0; + } else if (vm.relayVLAN) { + return angular.isObject(vm.provideDHCPAction.relayVLAN); + } else { + return true; + } + }; - if (vm.actionOption.name === "delete") { - VLANsManager.deleteVLAN(vm.vlan).then(function() { - $location.path("/networks"); - vm.actionOption = null; - vm.actionError = null; - }, function(result) { - vm.actionError = result.error; - vm.actionOption = vm.DELETE_ACTION; - }); - } - }; + // Perform the action. + vm.actionGo = function() { + // Do nothing if action cannot be performed. + if (!vm.canPerformAction()) { + return; + } - // Delete VLAN - vm.deleteVLAN = function() { - vm.actionOption = vm.DELETE_ACTION; - }; - - // Return true if there is an action error. - vm.isActionError = function() { - return vm.actionError !== null; - }; - - // Return the name of the VLAN. - vm.getFullVLANName = function(vlan_id) { - var vlan = VLANsManager.getItemFromList(vlan_id); - var fabric = FabricsManager.getItemFromList(vlan.fabric); - return ( - FabricsManager.getName(fabric) + "." + - VLANsManager.getName(vlan)); - }; - - // Return the current DHCP status. - vm.getDHCPStatus = function() { - if (vm.vlan) { - if (vm.vlan.dhcp_on) { - return "Enabled"; - } else if (vm.vlan.relay_vlan) { - return "Relayed via " + vm.getFullVLANName(vm.vlan.relay_vlan); - } else { - return "Disabled"; - } - } else { - return ""; + if (vm.actionOption.name === "delete") { + VLANsManager.deleteVLAN(vm.vlan).then( + function() { + $location.path("/networks"); + vm.actionOption = null; + vm.actionError = null; + }, + function(result) { + vm.actionError = result.error; + vm.actionOption = vm.DELETE_ACTION; } - }; + ); + } + }; - vm.getAvailableVLANS = function() { - var availableVLANS = vm.vlans.filter(function(vlan) { - return vlan !== vm.vlan && vlan.dhcp_on; - }); - return availableVLANS.length; - }; - - // Sets suggested IP range - vm.setSuggestedRange = function() { - vm.filteredRelatedSubnets = vm.relatedSubnets; - - vm.filteredRelatedSubnets.forEach(function(subnet) { - subnet.subnet.statistics.ranges = - subnet.subnet.statistics.ranges.filter(function(range) { - return range.num_addresses > 1 && - range.purpose[0] === "unused"; - }); - }); - - vm.iprangesInVLAN = vm.ipranges.filter(function(iprange) { - return iprange.vlan === vm.vlan.id; - }); - - if (!vm.iprangesInVLAN.length) { - vm.selectedSubnet = vm.filteredRelatedSubnets[0]; - var firstAvailableRange - = vm.selectedSubnet.subnet.statistics.ranges[0]; - vm.suggestedRange = { - type: 'dynamic', - comment: 'Dynamic', - start_ip: firstAvailableRange.start, - end_ip: firstAvailableRange.end, - subnet: vm.selectedSubnet.subnet.id, - gateway_ip: vm.selectedSubnet.subnet.gateway_ip - || vm.selectedSubnet.subnet.statistics.suggested_gateway - }; - - if (vm.relayVLAN) { - vm.suggestedRange.start_ip = ''; - vm.suggestedRange.end_ip = ''; - vm.suggestedRange.gateway_ip = ''; - vm.suggestedRange.startPlaceholder - = firstAvailableRange.start + " (Optional)"; - vm.suggestedRange.endPlaceholder - = firstAvailableRange.end + " (Optional)"; - } - } - }; + // Delete VLAN + vm.deleteVLAN = function() { + vm.actionOption = vm.DELETE_ACTION; + }; + + // Return true if there is an action error. + vm.isActionError = function() { + return vm.actionError !== null; + }; + + // Return the name of the VLAN. + vm.getFullVLANName = function(vlan_id) { + var vlan = VLANsManager.getItemFromList(vlan_id); + var fabric = FabricsManager.getItemFromList(vlan.fabric); + return FabricsManager.getName(fabric) + "." + VLANsManager.getName(vlan); + }; + + // Return the current DHCP status. + vm.getDHCPStatus = function() { + if (vm.vlan) { + if (vm.vlan.dhcp_on) { + return "Enabled"; + } else if (vm.vlan.relay_vlan) { + return "Relayed via " + vm.getFullVLANName(vm.vlan.relay_vlan); + } else { + return "Disabled"; + } + } else { + return ""; + } + }; - // Enables DHCP - vm.enableDHCP = function() { - vm.isProvidingDHCP = true; - vm.DHCPError = null; - var dhcp = vm.provideDHCPAction; - var controllers = []; - // These will be undefined if they don't exist, and the region - // will simply get an empty dictionary. - var extra = {}; - extra.subnet = dhcp.subnet; - extra.start = dhcp.startIP; - extra.end = dhcp.endIP; - extra.gateway = dhcp.gatewayIP; - if (angular.isString(dhcp.primaryRack)) { - controllers.push(dhcp.primaryRack); - } - if (angular.isString(dhcp.secondaryRack)) { - controllers.push(dhcp.secondaryRack); - } - // Abort the action without calling down to the region if - // the user didn't select a controller. - if (controllers.length === 0) { - vm.DHCPError = - "A primary rack controller must be specified."; - return; - } - VLANsManager.configureDHCP( - vm.vlan, controllers, extra).then(function() { - vm.DHCPError = null; - vm.closeDHCPPanel(); - }, function(result) { - vm.DHCPError = result.error; - vm.isProvidingDHCP = false; - }); - }; - - // Relays DHCP - vm.relayDHCP = function() { - vm.isProvidingDHCP = true; - vm.DHCPError = null; - // These will be undefined if they don't exist, and the region - // will simply get an empty dictionary. - var extraDHCP = {}; - - if (angular.isObject(vm.suggestedRange)) { - extraDHCP.subnet = vm.suggestedRange.subnet; - extraDHCP.start = vm.suggestedRange.start_ip; - extraDHCP.end = vm.suggestedRange.end_ip; - extraDHCP.gateway = vm.suggestedRange.gateway_ip; + vm.getAvailableVLANS = function() { + var availableVLANS = vm.vlans.filter(function(vlan) { + return vlan !== vm.vlan && vlan.dhcp_on; + }); + return availableVLANS.length; + }; + + // Sets suggested IP range + vm.setSuggestedRange = function() { + vm.filteredRelatedSubnets = vm.relatedSubnets; + + vm.filteredRelatedSubnets.forEach(function(subnet) { + subnet.subnet.statistics.ranges = subnet.subnet.statistics.ranges.filter( + function(range) { + return range.num_addresses > 1 && range.purpose[0] === "unused"; } + ); + }); - var relay = vm.provideDHCPAction.relayVLAN.id; + vm.iprangesInVLAN = vm.ipranges.filter(function(iprange) { + return iprange.vlan === vm.vlan.id; + }); - VLANsManager.configureDHCP( - vm.vlan, [], extraDHCP, relay).then(function() { - vm.closeDHCPPanel(); - }, function(result) { - vm.DHCPError = result.error; - vm.isProvidingDHCP = false; - }); - }; - - // Disables DHCP - vm.disableDHCP = function() { - vm.isProvidingDHCP = true; - VLANsManager.disableDHCP(vm.vlan).then(function() { - vm.closeDHCPPanel(); - }, function(result) { - vm.DHCPError = result.error; - }); - }; - - // Opens DHCP panel - vm.openDHCPPanel = function() { - vm.showDHCPPanel = true; - vm.initProvideDHCP(false); + if (!vm.iprangesInVLAN.length) { + vm.selectedSubnet = vm.filteredRelatedSubnets[0]; + var firstAvailableRange = vm.selectedSubnet.subnet.statistics.ranges[0]; + vm.suggestedRange = { + type: "dynamic", + comment: "Dynamic", + start_ip: firstAvailableRange.start, + end_ip: firstAvailableRange.end, + subnet: vm.selectedSubnet.subnet.id, + gateway_ip: + vm.selectedSubnet.subnet.gateway_ip || + vm.selectedSubnet.subnet.statistics.suggested_gateway + }; + + if (vm.relayVLAN) { + vm.suggestedRange.start_ip = ""; + vm.suggestedRange.end_ip = ""; + vm.suggestedRange.gateway_ip = ""; + vm.suggestedRange.startPlaceholder = + firstAvailableRange.start + " (Optional)"; + vm.suggestedRange.endPlaceholder = + firstAvailableRange.end + " (Optional)"; + } + } + }; - if (vm.vlan.relay_vlan) { - vm.setDHCPAction('relayVLAN'); - } + // Enables DHCP + vm.enableDHCP = function() { + vm.isProvidingDHCP = true; + vm.DHCPError = null; + var dhcp = vm.provideDHCPAction; + var controllers = []; + // These will be undefined if they don't exist, and the region + // will simply get an empty dictionary. + var extra = {}; + extra.subnet = dhcp.subnet; + extra.start = dhcp.startIP; + extra.end = dhcp.endIP; + extra.gateway = dhcp.gatewayIP; + if (angular.isString(dhcp.primaryRack)) { + controllers.push(dhcp.primaryRack); + } + if (angular.isString(dhcp.secondaryRack)) { + controllers.push(dhcp.secondaryRack); + } + // Abort the action without calling down to the region if + // the user didn't select a controller. + if (controllers.length === 0) { + vm.DHCPError = "A primary rack controller must be specified."; + return; + } + VLANsManager.configureDHCP(vm.vlan, controllers, extra).then( + function() { + vm.DHCPError = null; + vm.closeDHCPPanel(); + }, + function(result) { + vm.DHCPError = result.error; + vm.isProvidingDHCP = false; + } + ); + }; + + // Relays DHCP + vm.relayDHCP = function() { + vm.isProvidingDHCP = true; + vm.DHCPError = null; + // These will be undefined if they don't exist, and the region + // will simply get an empty dictionary. + var extraDHCP = {}; + + if (angular.isObject(vm.suggestedRange)) { + extraDHCP.subnet = vm.suggestedRange.subnet; + extraDHCP.start = vm.suggestedRange.start_ip; + extraDHCP.end = vm.suggestedRange.end_ip; + extraDHCP.gateway = vm.suggestedRange.gateway_ip; + } - vm.setSuggestedRange(); - }; + var relay = vm.provideDHCPAction.relayVLAN.id; - // Closes DHCP Panel - vm.closeDHCPPanel = function() { - vm.showDHCPPanel = false; - vm.suggestedRange = null; + VLANsManager.configureDHCP(vm.vlan, [], extraDHCP, relay).then( + function() { + vm.closeDHCPPanel(); + }, + function(result) { + vm.DHCPError = result.error; vm.isProvidingDHCP = false; - vm.DHCPError = null; - vm.MAASProvidesDHCP = true; - }; + } + ); + }; + + // Disables DHCP + vm.disableDHCP = function() { + vm.isProvidingDHCP = true; + VLANsManager.disableDHCP(vm.vlan).then( + function() { + vm.closeDHCPPanel(); + }, + function(result) { + vm.DHCPError = result.error; + } + ); + }; + + // Opens DHCP panel + vm.openDHCPPanel = function() { + vm.showDHCPPanel = true; + vm.initProvideDHCP(false); - // Get button text for DHCP button - vm.getDHCPButtonText = function() { - if (vm.vlan) { - if (vm.vlan.dhcp_on && !vm.vlan.relay_vlan) { - return "Reconfigure DHCP"; - } else if (vm.vlan.relay_vlan) { - return "Reconfigure DHCP relay"; - } else { - return "Enable DHCP"; - } - } - }; + if (vm.vlan.relay_vlan) { + vm.setDHCPAction("relayVLAN"); + } - // Checks if gateway col should be present in DHCP table - vm.showGatewayCol = function() { - var subnetsWithNoGateway - = vm.relatedSubnets.filter(function(subnet) { - return !subnet.subnet.gateway_ip; - }); - - return subnetsWithNoGateway.length ? true : false; - }; - - // Updates the page title. - function updateTitle() { - var vlan = vm.vlan; - var fabric = vm.fabric; - if (angular.isObject(vlan) && angular.isObject(fabric)) { - if (!vlan.name) { - if (vlan.vid === 0) { - vm.title = "Default VLAN"; - } else { - vm.title = "VLAN " + vlan.vid; - } - } else { - vm.title = vlan.name; - } - vm.title += " in " + fabric.name; - $rootScope.title = vm.title; - } + vm.setSuggestedRange(); + }; + + // Closes DHCP Panel + vm.closeDHCPPanel = function() { + vm.showDHCPPanel = false; + vm.suggestedRange = null; + vm.isProvidingDHCP = false; + vm.DHCPError = null; + vm.MAASProvidesDHCP = true; + }; + + // Get button text for DHCP button + vm.getDHCPButtonText = function() { + if (vm.vlan) { + if (vm.vlan.dhcp_on && !vm.vlan.relay_vlan) { + return "Reconfigure DHCP"; + } else if (vm.vlan.relay_vlan) { + return "Reconfigure DHCP relay"; + } else { + return "Enable DHCP"; + } } + }; - // Called from a $watch when the management racks are updated. - function updateManagementRacks() { - var vlan = vm.vlan; - if (!angular.isObject(vlan)) { - return; - } - if (vlan.primary_rack) { - vm.primaryRack = ControllersManager.getItemFromList( - vlan.primary_rack); - } else { - vm.primaryRack = null; - } - if (vlan.secondary_rack) { - vm.secondaryRack = ControllersManager.getItemFromList( - vlan.secondary_rack); + // Checks if gateway col should be present in DHCP table + vm.showGatewayCol = function() { + var subnetsWithNoGateway = vm.relatedSubnets.filter(function(subnet) { + return !subnet.subnet.gateway_ip; + }); + + return subnetsWithNoGateway.length ? true : false; + }; + + // Dismiss high availability notification + vm.dismissHighAvailabilityNotification = function() { + vm.hideHighAvailabilityNotification = true; + localStorage.setItem( + `hideHighAvailabilityNotification-${vm.vlan.id}`, + true + ); + }; + + vm.showHighAvailabilityNotification = function() { + if ( + vm.vlan.dhcp_on && + !vm.provideDHCPAction.secondaryRack && + vm.relatedControllers.length > 1 && + !vm.hideHighAvailabilityNotification + ) { + return true; + } else { + return false; + } + }; + + // Updates the page title. + function updateTitle() { + var vlan = vm.vlan; + var fabric = vm.fabric; + if (angular.isObject(vlan) && angular.isObject(fabric)) { + if (!vlan.name) { + if (vlan.vid === 0) { + vm.title = "Default VLAN"; } else { - vm.secondaryRack = null; + vm.title = "VLAN " + vlan.vid; } + } else { + vm.title = vlan.name; + } + vm.title += " in " + fabric.name; + $rootScope.title = vm.title; } + } - // Called from a $watch when the related controllers may have changed. - function updateRelatedControllers() { - var vlan = vm.vlan; - if (!angular.isObject(vlan)) { - return; - } - var racks = []; - angular.forEach(vlan.rack_sids, function(rack_sid) { - var rack = ControllersManager.getItemFromList(rack_sid); - if (angular.isObject(rack)) { - racks.push(rack); - } - }); - vm.relatedControllers = racks; - } - - // Called from a $watch when the related subnets or spaces may have - // changed. - function updateRelatedSubnets() { - var vlan = vm.vlan; - if (!angular.isObject(vlan)) { - return; - } - var subnets = []; - angular.forEach( - filterByVLAN(vm.subnets, vlan), function(subnet) { - var space = SpacesManager.getItemFromList(subnet.space); - if (!angular.isObject(space)) { - space = { name: "(undefined)" }; - } - var row = { - subnet: subnet, - space: space - }; - subnets.push(row); - }); - vm.relatedSubnets = subnets; - } - - function updatePossibleActions() { - var vlan = vm.vlan; - if (!angular.isObject(vlan)) { - return; - } - // Clear out the actionOptions array. (this needs to be the same - // object, since it's watched from $scope.) - vm.actionOptions.length = 0; - if (UsersManager.isSuperUser()) { - if (!vm.isFabricDefault) { - vm.actionOptions.push(vm.DELETE_ACTION); - } - } + // Called from a $watch when the management racks are updated. + function updateManagementRacks() { + var vlan = vm.vlan; + if (!angular.isObject(vlan)) { + return; + } + if (vlan.primary_rack) { + vm.primaryRack = ControllersManager.getItemFromList(vlan.primary_rack); + } else { + vm.primaryRack = null; + } + if (vlan.secondary_rack) { + vm.secondaryRack = ControllersManager.getItemFromList( + vlan.secondary_rack + ); + } else { + vm.secondaryRack = null; } + } - // Called when the vlan has been loaded. - function vlanLoaded(vlan) { - vm.vlan = vlan; - updateVLAN(); - vm.loaded = true; + // Called from a $watch when the related controllers may have changed. + function updateRelatedControllers() { + var vlan = vm.vlan; + if (!angular.isObject(vlan)) { + return; } + var racks = []; + angular.forEach(vlan.rack_sids, function(rack_sid) { + var rack = ControllersManager.getItemFromList(rack_sid); + if (angular.isObject(rack)) { + racks.push(rack); + } + }); + vm.relatedControllers = racks; + } - function updateVLAN() { - if (!vm.loaded) { - return; - } - var vlan = vm.vlan; - vm.fabric = FabricsManager.getItemFromList(vlan.fabric); - vm.isFabricDefault = vm.fabric.default_vlan_id === vm.vlan.id; - - updateTitle(); - updateManagementRacks(); - updateRelatedControllers(); - updateRelatedSubnets(); - updatePossibleActions(); - } - - // Load all the required managers. - ManagerHelperService.loadManagers($scope, [ - VLANsManager, SubnetsManager, SpacesManager, FabricsManager, - ControllersManager, UsersManager - ]).then(function() { - // Possibly redirected from another controller that already had - // this vlan set to active. Only call setActiveItem if not - // already the activeItem. - var activeVLAN = VLANsManager.getActiveItem(); - var requestedVLAN = parseInt($routeParams.vlan_id, 10); - if (isNaN(requestedVLAN)) { - ErrorService.raiseError("Invalid VLAN identifier."); - } else if (angular.isObject(activeVLAN) && - activeVLAN.id === requestedVLAN) { - vlanLoaded(activeVLAN); - } else { - VLANsManager.setActiveItem( - requestedVLAN).then(function(vlan) { - vlanLoaded(vlan); - }, function(error) { - ErrorService.raiseError(error); - }); + // Called from a $watch when the related subnets or spaces may have + // changed. + function updateRelatedSubnets() { + var vlan = vm.vlan; + if (!angular.isObject(vlan)) { + return; + } + var subnets = []; + angular.forEach(filterByVLAN(vm.subnets, vlan), function(subnet) { + var space = SpacesManager.getItemFromList(subnet.space); + if (!angular.isObject(space)) { + space = { name: "(undefined)" }; + } + var row = { + subnet: subnet, + space: space + }; + subnets.push(row); + }); + vm.relatedSubnets = subnets; + } + + function updatePossibleActions() { + var vlan = vm.vlan; + if (!angular.isObject(vlan)) { + return; + } + // Clear out the actionOptions array. (this needs to be the same + // object, since it's watched from $scope.) + vm.actionOptions.length = 0; + if (UsersManager.isSuperUser()) { + if (!vm.isFabricDefault) { + vm.actionOptions.push(vm.DELETE_ACTION); + } + } + } + + // Called when the vlan has been loaded. + function vlanLoaded(vlan) { + vm.vlan = vlan; + updateVLAN(); + vm.loaded = true; + if ($routeParams.provide_dhcp) { + // Put this at the end of event loop otherwise + // it doesn't have the data it needs + $timeout(function() { + vm.openDHCPPanel(); + }, 0); + } + if (localStorage.getItem(`hideHighAvailabilityNotification-${vlan.id}`)) { + vm.hideHighAvailabilityNotification = true; + } + } + + function updateVLAN() { + if (!vm.loaded) { + return; + } + var vlan = vm.vlan; + vm.fabric = FabricsManager.getItemFromList(vlan.fabric); + vm.isFabricDefault = vm.fabric.default_vlan_id === vm.vlan.id; + + updateTitle(); + updateManagementRacks(); + updateRelatedControllers(); + updateRelatedSubnets(); + updatePossibleActions(); + } + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ + VLANsManager, + SubnetsManager, + SpacesManager, + FabricsManager, + ControllersManager, + UsersManager + ]).then(function() { + // Possibly redirected from another controller that already had + // this vlan set to active. Only call setActiveItem if not + // already the activeItem. + var activeVLAN = VLANsManager.getActiveItem(); + var requestedVLAN = parseInt($routeParams.vlan_id, 10); + if (isNaN(requestedVLAN)) { + ErrorService.raiseError("Invalid VLAN identifier."); + } else if ( + angular.isObject(activeVLAN) && + activeVLAN.id === requestedVLAN + ) { + vlanLoaded(activeVLAN); + } else { + VLANsManager.setActiveItem(requestedVLAN).then( + function(vlan) { + vlanLoaded(vlan); + }, + function(error) { + ErrorService.raiseError(error); } + ); + } - $scope.$watch("vlanDetails.vlan.name", updateTitle); - $scope.$watch("vlanDetails.vlan.vid", updateTitle); - $scope.$watch("vlanDetails.vlan.fabric", updateVLAN); - $scope.$watch("vlanDetails.vlan.dhcp_on", updatePossibleActions); - $scope.$watch( - "vlanDetails.vlan.relay_vlan", updatePossibleActions); - $scope.$watch("vlanDetails.fabric.name", updateTitle); - $scope.$watch( - "vlanDetails.vlan.primary_rack", updateManagementRacks); - $scope.$watch( - "vlanDetails.vlan.secondary_rack", updateManagementRacks); - - $scope.$watchCollection( - "vlanDetails.subnets", updateRelatedSubnets); - $scope.$watchCollection( - "vlanDetails.spaces", updateRelatedSubnets); - $scope.$watchCollection( - "vlanDetails.controllers", updateRelatedControllers); - }); + $scope.$watch("vlanDetails.vlan.name", updateTitle); + $scope.$watch("vlanDetails.vlan.vid", updateTitle); + $scope.$watch("vlanDetails.vlan.fabric", updateVLAN); + $scope.$watch("vlanDetails.vlan.dhcp_on", updatePossibleActions); + $scope.$watch("vlanDetails.vlan.relay_vlan", updatePossibleActions); + $scope.$watch("vlanDetails.fabric.name", updateTitle); + $scope.$watch("vlanDetails.vlan.primary_rack", updateManagementRacks); + $scope.$watch("vlanDetails.vlan.secondary_rack", updateManagementRacks); + + $scope.$watchCollection("vlanDetails.subnets", updateRelatedSubnets); + $scope.$watchCollection("vlanDetails.spaces", updateRelatedSubnets); + $scope.$watchCollection( + "vlanDetails.controllers", + updateRelatedControllers + ); + }); } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/zone_details.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/zone_details.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/zone_details.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/zone_details.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,113 +6,124 @@ /* @ngInject */ function ZoneDetailsController( - $scope, $rootScope, $routeParams, $location, - ZonesManager, UsersManager, ManagerHelperService, - ErrorService) { - - // Set title and page. - $rootScope.title = "Loading..."; - - // Note: this value must match the top-level tab, in order for - // highlighting to occur properly. - $rootScope.page = "zones"; - - // Initial values. - $scope.loaded = false; - $scope.zone = null; - $scope.zoneManager = ZonesManager; + $scope, + $rootScope, + $routeParams, + $location, + ZonesManager, + UsersManager, + ManagerHelperService, + ErrorService +) { + // Set title and page. + $rootScope.title = "Loading..."; + + // Note: this value must match the top-level tab, in order for + // highlighting to occur properly. + $rootScope.page = "zones"; + + // Initial values. + $scope.loaded = false; + $scope.zone = null; + $scope.zoneManager = ZonesManager; + $scope.editSummary = false; + $scope.predicate = "name"; + $scope.reverse = false; + + // Updates the page title. + function updateTitle() { + $rootScope.title = $scope.zone.name; + } + + // Called when the zone has been loaded. + function zoneLoaded(zone) { + $scope.zone = zone; + $scope.loaded = true; + updateTitle(); + } + + // Called when the "edit" button is cliked in the zone summary + $scope.enterEditSummary = function() { + $scope.editSummary = true; + }; + + // Called when the "cancel" button is cliked in the zone summary + $scope.exitEditSummary = function() { $scope.editSummary = false; - $scope.predicate = "name"; - $scope.reverse = false; + }; - // Updates the page title. - function updateTitle() { - $rootScope.title = $scope.zone.name; + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Return true if this is the default zone. + $scope.isDefaultZone = function() { + if (angular.isObject($scope.zone)) { + return $scope.zone.id === 1; } + return false; + }; - // Called when the zone has been loaded. - function zoneLoaded(zone) { - $scope.zone = zone; - $scope.loaded = true; - updateTitle(); + // Called to check if the zone can be deleted. + $scope.canBeDeleted = function() { + if (angular.isObject($scope.zone)) { + return $scope.zone.id !== 0; } + return false; + }; - - // Called when the "edit" button is cliked in the zone summary - $scope.enterEditSummary = function() { - $scope.editSummary = true; - }; - - // Called when the "cancel" button is cliked in the zone summary - $scope.exitEditSummary = function() { - $scope.editSummary = false; - }; - - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Return true if this is the default zone. - $scope.isDefaultZone = function() { - if (angular.isObject($scope.zone)) { - return $scope.zone.id === 1; - } - return false; - }; - - // Called to check if the zone can be deleted. - $scope.canBeDeleted = function() { - if (angular.isObject($scope.zone)) { - return $scope.zone.id !== 0; - } - return false; - }; - - // Called when the delete zone button is pressed. - $scope.deleteButton = function() { - $scope.error = null; - $scope.confirmingDelete = true; - }; - - // Called when the cancel delete zone button is pressed. - $scope.cancelDeleteButton = function() { + // Called when the delete zone button is pressed. + $scope.deleteButton = function() { + $scope.error = null; + $scope.confirmingDelete = true; + }; + + // Called when the cancel delete zone button is pressed. + $scope.cancelDeleteButton = function() { + $scope.confirmingDelete = false; + }; + + // Called when the confirm delete space button is pressed. + $scope.deleteConfirmButton = function() { + ZonesManager.deleteItem($scope.zone).then( + function() { $scope.confirmingDelete = false; - }; - - // Called when the confirm delete space button is pressed. - $scope.deleteConfirmButton = function() { - ZonesManager.deleteItem($scope.zone).then(function() { - $scope.confirmingDelete = false; - $location.path("/zones"); - }, function(error) { - $scope.error = - ManagerHelperService.parseValidationError(error); - }); - }; - - // Load all the required managers. - ManagerHelperService.loadManagers( - $scope, [ZonesManager, UsersManager]).then(function() { - // Possibly redirected from another controller that already had - // this zone set to active. Only call setActiveItem if not - // already the activeItem. - var activeZone = ZonesManager.getActiveItem(); - var requestedZone = parseInt($routeParams.zone_id, 10); - if (isNaN(requestedZone)) { - ErrorService.raiseError("Invalid zone identifier."); - } else if (angular.isObject(activeZone) && - activeZone.id === requestedZone) { - zoneLoaded(activeZone); - } else { - ZonesManager.setActiveItem( - requestedZone).then(function(zone) { - zoneLoaded(zone); - }, function(error) { - ErrorService.raiseError(error); - }); - } - }); + $location.path("/zones"); + }, + function(error) { + $scope.error = ManagerHelperService.parseValidationError(error); + } + ); + }; + + // Load all the required managers. + ManagerHelperService.loadManagers($scope, [ZonesManager, UsersManager]).then( + function() { + // Possibly redirected from another controller that already had + // this zone set to active. Only call setActiveItem if not + // already the activeItem. + var activeZone = ZonesManager.getActiveItem(); + var requestedZone = parseInt($routeParams.zone_id, 10); + if (isNaN(requestedZone)) { + ErrorService.raiseError("Invalid zone identifier."); + } else if ( + angular.isObject(activeZone) && + activeZone.id === requestedZone + ) { + zoneLoaded(activeZone); + } else { + ZonesManager.setActiveItem(requestedZone).then( + function(zone) { + zoneLoaded(zone); + }, + function(error) { + ErrorService.raiseError(error); + } + ); + } + } + ); } export default ZoneDetailsController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/zones_list.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/zones_list.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/controllers/zones_list.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/controllers/zones_list.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,12 +4,14 @@ * MAAS Zones List Controller */ - /* @ngInject */ function ZonesListController( - $scope, $rootScope, ZonesManager, - UsersManager, ManagerHelperService) { - + $scope, + $rootScope, + ZonesManager, + UsersManager, + ManagerHelperService +) { // Set title and page. $rootScope.title = "Zones"; $rootScope.page = "zones"; @@ -42,11 +44,11 @@ return UsersManager.isSuperUser(); }; - ManagerHelperService.loadManagers( - $scope, [ZonesManager, UsersManager]).then( - function() { - $scope.loading = false; - }); + ManagerHelperService.loadManagers($scope, [ZonesManager, UsersManager]).then( + function() { + $scope.loading = false; + } + ); } export default ZonesListController; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/accordion.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/accordion.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/accordion.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/accordion.js 2019-06-01 02:18:13.000000000 +0000 @@ -10,34 +10,37 @@ */ function maasAccordion() { - return { - restrict: "C", - link: function(scope, element, attrs) { + return { + restrict: "C", + link: function(scope, element) { + // Called when accordion tabs are clicked. Removes active on + // all other tabs except to the tab that was clicked. + var clickHandler = function(evt) { + var tab = evt.data.tab; + angular.element(tab).toggleClass("is-selected"); + }; - // Called when accordion tabs are clicked. Removes active on - // all other tabs except to the tab that was clicked. - var clickHandler = function(evt) { - var tab = evt.data.tab; - angular.element(tab).toggleClass("is-selected"); - }; + // Listen for the click event on all tabs in the accordion. + var tabs = element.find(".maas-accordion-tab"); + angular.forEach(tabs, function(tab) { + tab = angular.element(tab); + tab.on( + "click", + { + tab: tab + }, + clickHandler + ); + }); - // Listen for the click event on all tabs in the accordion. - var tabs = element.find(".maas-accordion-tab"); - angular.forEach(tabs, function(tab) { - tab = angular.element(tab); - tab.on("click", { - tab: tab - }, clickHandler); - }); - - // Remove the handlers when the scope is destroyed. - scope.$on("$destroy", function() { - angular.forEach(tabs, function(tab) { - angular.element(tab).off("click", clickHandler); - }); - }); - } - }; + // Remove the handlers when the scope is destroyed. + scope.$on("$destroy", function() { + angular.forEach(tabs, function(tab) { + angular.element(tab).off("click", clickHandler); + }); + }); + } + }; } export default maasAccordion; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/action_button.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/action_button.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/action_button.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/action_button.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,24 +6,27 @@ /* @ngInject */ export function cacheActionButton($templateCache) { - // Inject action-button.html into the template cache. - $templateCache.put('directive/templates/action-button.html', [ - '' - ].join('')); + // Inject action-button.html into the template cache. + $templateCache.put( + "directive/templates/action-button.html", + [ + '" + ].join("") + ); } export function maasActionButton() { - return { - restrict: "E", - replace: true, - transclude: true, - scope: { - doneState: '<', - indeterminateState: '<', - }, - templateUrl: 'directive/templates/action-button.html', - }; + return { + restrict: "E", + replace: true, + transclude: true, + scope: { + doneState: "<", + indeterminateState: "<" + }, + templateUrl: "directive/templates/action-button.html" + }; } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/boot_images.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/boot_images.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/boot_images.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/boot_images.js 2019-06-01 02:18:13.000000000 +0000 @@ -2,1025 +2,1008 @@ * GNU Affero General Public License version 3 (see the file LICENSE). * * Boot images directive. -*/ - + */ /* @ngInject */ export function maasBootImagesStatus(BootResourcesManager) { - return { - restrict: "E", - scope: {}, - template: [ - '

', - '', - '', - 'Step 1/2: Region controller importing', - '', - '

', - '

', - '', - '', - 'Step 2/2: Rack controller(s) importing', - '', - '

' - ].join(''), - controller: BootImagesStatusController - }; - - /* @ngInject */ - function BootImagesStatusController($scope) { - // This controller doesn't start the polling. The - // `maasBootImages` controller should be used on the page to - // start the polling. This just presents a nice status spinner - // when the polling is enabled. - $scope.data = BootResourcesManager.getData(); - } - + return { + restrict: "E", + scope: {}, + template: [ + '

', + '', + '', + "Step 1/2: Region controller importing", + "", + "

", + '

', + '', + '', + "Step 2/2: Rack controller(s) importing", + "", + "

" + ].join(""), + controller: BootImagesStatusController + }; + + /* @ngInject */ + function BootImagesStatusController($scope) { + // This controller doesn't start the polling. The + // `maasBootImages` controller should be used on the page to + // start the polling. This just presents a nice status spinner + // when the polling is enabled. + $scope.data = BootResourcesManager.getData(); + } } /* @ngInject */ -export function maasBootImages($timeout, BootResourcesManager, - UsersManager, ManagerHelperService) { - return { - restrict: "E", - scope: { - design: "=?" - }, - templateUrl: ( - 'static/partials/boot-images.html?v=' + ( - MAAS_config.files_version)), - controller: BootImagesController - +export function maasBootImages( + $timeout, + BootResourcesManager, + UsersManager, + ManagerHelperService +) { + return { + restrict: "E", + scope: { + design: "=?" + }, + templateUrl: + "static/partials/boot-images.html?v=" + MAAS_config.files_version, + controller: BootImagesController + }; + + /* @ngInject */ + function BootImagesController($scope) { + $scope.loading = true; + $scope.saving = false; + $scope.saved = false; + $scope.stopping = false; + $scope.design = $scope.design || "page"; + $scope.bootResources = BootResourcesManager.getData(); + $scope.ubuntuImages = []; + $scope.source = { + isNew: false, + tooMany: false, + showingAdvanced: false, + connecting: false, + errorMessage: "", + source_type: "maas.io", + url: "", + keyring_filename: "", + keyring_data: "", + releases: [], + arches: [], + selections: { + changed: false, + releases: [], + arches: [] + } + }; + $scope.ubuntuCoreImages = []; + $scope.ubuntu_core = { + changed: false, + images: [] + }; + $scope.otherImages = []; + $scope.other = { + changed: false, + images: [] + }; + $scope.generatedImages = []; + $scope.customImages = []; + $scope.ubuntuDeleteId = null; + $scope.removingImage = null; + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); }; - /* @ngInject */ - function BootImagesController($scope) { - $scope.loading = true; - $scope.saving = false; - $scope.saved = false; - $scope.stopping = false; - $scope.design = $scope.design || 'page'; - $scope.bootResources = BootResourcesManager.getData(); - $scope.ubuntuImages = []; - $scope.source = { - isNew: false, - tooMany: false, - showingAdvanced: false, - connecting: false, - errorMessage: "", - source_type: 'maas.io', - url: '', - keyring_filename: '', - keyring_data: '', - releases: [], - arches: [], - selections: { - changed: false, - releases: [], - arches: [] - } - }; - $scope.ubuntuCoreImages = []; - $scope.ubuntu_core = { - changed: false, - images: [] - }; - $scope.otherImages = []; - $scope.other = { - changed: false, - images: [] - }; - $scope.generatedImages = []; - $scope.customImages = []; - $scope.ubuntuDeleteId = null; - $scope.removingImage = null; - - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Return the overall title icon. - $scope.getTitleIcon = function() { - if ($scope.bootResources.resources.length === 0) { - return 'p-icon--success-muted'; - } else { - return 'p-icon--success'; - } - }; - - // Return true if the mirror path section should be shown. - $scope.showMirrorPath = function() { - if ($scope.source.source_type === 'custom') { - return true; - } else { - return false; - } - }; - - // Return true if the advanced options are shown. - $scope.isShowingAdvancedOptions = function() { - return $scope.source.showingAdvanced; - }; - - // Toggle showing the advanced options. - $scope.toggleAdvancedOptions = function() { - $scope.source.showingAdvanced = ( - !$scope.source.showingAdvanced); - }; - - // Return true if both keyring options are set. - $scope.bothKeyringOptionsSet = function() { - return ( - $scope.source.keyring_filename.length > 0 && - $scope.source.keyring_data.length > 0); - }; - - // Return true when the connect button for the mirror path - // should be shown. - $scope.showConnectButton = function() { - return $scope.source.isNew; - }; - - // Called when the source radio changed. - $scope.sourceChanged = function() { - var currentSources = $scope.bootResources.ubuntu.sources; - if (currentSources.length === 0) { - $scope.source.isNew = true; - } else { - var prevNew = $scope.source.isNew; - $scope.source.isNew = ( - $scope.source.source_type !== - currentSources[0].source_type); - if ($scope.source.source_type === 'custom') { - $scope.source.isNew = $scope.source.isNew || ( - $scope.source.url !== - currentSources[0].url - ); - } - if (prevNew && !$scope.source.isNew) { - // No longer a new source set url and keyring to - // original. - $scope.source.url = currentSources[0].url; - $scope.source.keyring_filename = ( - currentSources[0].keyring_filename); - $scope.source.keyring_data = ( - currentSources[0].keyring_data); - } - $scope.source.releases = []; - $scope.source.arches = []; - $scope.source.selections = { - changed: false, - releases: [], - arches: [] - }; - } - $scope.updateSource(); - $scope.regenerateUbuntuImages(); - - // When the source is new and its maas.io automatically - // fetch the source information. - if ($scope.source.isNew && - $scope.source.source_type === 'maas.io') { - $scope.connect(); - } - }; - - // Return true when the connect button should be disabled. - $scope.isConnectButtonDisabled = function() { - if ($scope.source.source_type === 'maas.io') { - return false; - } else { - return ($scope.bothKeyringOptionsSet() || - $scope.source.url.length === 0 || - $scope.source.connecting); - } - }; - - // Return the source parameters. - $scope.getSourceParams = function() { - if ($scope.source.source_type === 'maas.io') { - return { - source_type: 'maas.io' - }; - } else { - return { - source_type: $scope.source.source_type, - url: $scope.source.url, - keyring_filename: $scope.source.keyring_filename, - keyring_data: $scope.source.keyring_data - }; - } - }; + // Return the overall title icon. + $scope.getTitleIcon = function() { + if ($scope.bootResources.resources.length === 0) { + return "p-icon--success-muted"; + } else { + return "p-icon--success"; + } + }; - // Select the default images that should be selected. Current - // defaults are '18.04 LTS' and 'amd64'. - $scope.selectDefaults = function() { - angular.forEach($scope.source.releases, function(release) { - if (release.name === "bionic") { - $scope.source.selections.releases.push(release); - } - }); - angular.forEach($scope.source.arches, function(arch) { - if (arch.name === "amd64") { - $scope.source.selections.arches.push(arch); - } - }); - }; + // Return true if the mirror path section should be shown. + $scope.showMirrorPath = function() { + if ($scope.source.source_type === "custom") { + return true; + } else { + return false; + } + }; - // Connected to the simplestreams endpoint. This only gets the - // information from the endpoint it does not save it into the - // database. - $scope.connect = function() { - if ($scope.isConnectButtonDisabled()) { - return; - } + // Return true if the advanced options are shown. + $scope.isShowingAdvancedOptions = function() { + return $scope.source.showingAdvanced; + }; - var source = $scope.getSourceParams(); - $scope.source.connecting = true; - $scope.source.releases = []; - $scope.source.arches = []; - $scope.source.selections.changed = true; - $scope.source.selections.releases = []; - $scope.source.selections.arches = []; - $scope.regenerateUbuntuImages(); - BootResourcesManager.fetch(source).then(function(data) { - $scope.source.connecting = false; - data = angular.fromJson(data); - $scope.source.releases = data.releases; - $scope.source.arches = data.arches; - $scope.selectDefaults(); - $scope.regenerateUbuntuImages(); - }, function(error) { - $scope.source.connecting = false; - $scope.source.errorMessage = error; - }); - }; + // Toggle showing the advanced options. + $scope.toggleAdvancedOptions = function() { + $scope.source.showingAdvanced = !$scope.source.showingAdvanced; + }; - // Return true if the connect block should be shown. - $scope.showConnectBlock = function() { - return $scope.source.tooMany || ( - $scope.source.isNew && !$scope.showSelections()); - }; + // Return true if both keyring options are set. + $scope.bothKeyringOptionsSet = function() { + return ( + $scope.source.keyring_filename.length > 0 && + $scope.source.keyring_data.length > 0 + ); + }; - // Return true if the release and architecture selection - // should be shown. - $scope.showSelections = function() { - return ( - $scope.source.releases.length > 0 && - $scope.source.arches.length > 0); - }; + // Return true when the connect button for the mirror path + // should be shown. + $scope.showConnectButton = function() { + return $scope.source.isNew; + }; - // Return the Ubuntu LTS releases. - $scope.getUbuntuLTSReleases = function() { - var releases = $scope.bootResources.ubuntu.releases; - if ($scope.source.isNew) { - releases = $scope.source.releases; - } - var filtered = []; - angular.forEach(releases, function(release) { - if (!release.deleted && - release.title.indexOf('LTS') !== -1) { - filtered.push(release); - } - }); - return filtered; - }; + // Called when the source radio changed. + $scope.sourceChanged = function() { + var currentSources = $scope.bootResources.ubuntu.sources; + if (currentSources.length === 0) { + $scope.source.isNew = true; + } else { + var prevNew = $scope.source.isNew; + $scope.source.isNew = + $scope.source.source_type !== currentSources[0].source_type; + if ($scope.source.source_type === "custom") { + $scope.source.isNew = + $scope.source.isNew || $scope.source.url !== currentSources[0].url; + } + if (prevNew && !$scope.source.isNew) { + // No longer a new source set url and keyring to + // original. + $scope.source.url = currentSources[0].url; + $scope.source.keyring_filename = currentSources[0].keyring_filename; + $scope.source.keyring_data = currentSources[0].keyring_data; + } + $scope.source.releases = []; + $scope.source.arches = []; + $scope.source.selections = { + changed: false, + releases: [], + arches: [] + }; + } + $scope.updateSource(); + $scope.regenerateUbuntuImages(); + + // When the source is new and its maas.io automatically + // fetch the source information. + if ($scope.source.isNew && $scope.source.source_type === "maas.io") { + $scope.connect(); + } + }; - // Return the Ubuntu non-LTS releases. - $scope.getUbuntuNonLTSReleases = function() { - var releases = $scope.bootResources.ubuntu.releases; - if ($scope.source.isNew) { - releases = $scope.source.releases; - } - var filtered = []; - angular.forEach(releases, function(release) { - if (!release.deleted && - release.title.indexOf('LTS') === -1) { - filtered.push(release); - } - }); - return filtered; - }; + // Return true when the connect button should be disabled. + $scope.isConnectButtonDisabled = function() { + if ($scope.source.source_type === "maas.io") { + return false; + } else { + return ( + $scope.bothKeyringOptionsSet() || + $scope.source.url.length === 0 || + $scope.source.connecting + ); + } + }; - // Return the available architectures. - $scope.getArchitectures = function() { - var arches = $scope.bootResources.ubuntu.arches; - if ($scope.source.isNew) { - arches = $scope.source.arches; - } - var filtered = []; - angular.forEach(arches, function(arch) { - if (!arch.deleted) { - filtered.push(arch); - } - }); - return filtered; + // Return the source parameters. + $scope.getSourceParams = function() { + if ($scope.source.source_type === "maas.io") { + return { + source_type: "maas.io" + }; + } else { + return { + source_type: $scope.source.source_type, + url: $scope.source.url, + keyring_filename: $scope.source.keyring_filename, + keyring_data: $scope.source.keyring_data }; + } + }; - // Return true if the source has this selected. - $scope.isSelected = function(type, obj) { - return $scope.source.selections[type].indexOf(obj) >= 0; - }; + // Select the default images that should be selected. Current + // defaults are '18.04 LTS' and 'amd64'. + $scope.selectDefaults = function() { + angular.forEach($scope.source.releases, function(release) { + if (release.name === "bionic") { + $scope.source.selections.releases.push(release); + } + }); + angular.forEach($scope.source.arches, function(arch) { + if (arch.name === "amd64") { + $scope.source.selections.arches.push(arch); + } + }); + }; - // Toggle the selection of the release or the architecture. - $scope.toggleSelection = function(type, obj) { - var idx = $scope.source.selections[type].indexOf(obj); - if (idx === -1) { - $scope.source.selections[type].push(obj); - } else { - $scope.source.selections[type].splice(idx, 1); - } - $scope.source.selections.changed = true; - $scope.regenerateUbuntuImages(); - }; + // Connected to the simplestreams endpoint. This only gets the + // information from the endpoint it does not save it into the + // database. + $scope.connect = function() { + if ($scope.isConnectButtonDisabled()) { + return; + } + + var source = $scope.getSourceParams(); + $scope.source.connecting = true; + $scope.source.releases = []; + $scope.source.arches = []; + $scope.source.selections.changed = true; + $scope.source.selections.releases = []; + $scope.source.selections.arches = []; + $scope.regenerateUbuntuImages(); + BootResourcesManager.fetch(source).then( + function(data) { + $scope.source.connecting = false; + data = angular.fromJson(data); + $scope.source.releases = data.releases; + $scope.source.arches = data.arches; + $scope.selectDefaults(); + $scope.regenerateUbuntuImages(); + }, + function(error) { + $scope.source.connecting = false; + $scope.source.errorMessage = error; + } + ); + }; - // Return true if the images table should be shown. - $scope.showImagesTable = function() { - if ($scope.ubuntuImages.length > 0) { - return true; - } else { - // Show the images table source has - // releases and architectures. - return ( - $scope.source.arches.length > 0 && - $scope.source.releases.length > 0); - } - }; + // Return true if the connect block should be shown. + $scope.showConnectBlock = function() { + return ( + $scope.source.tooMany || + ($scope.source.isNew && !$scope.showSelections()) + ); + }; - // Regenerates the Ubuntu images list for the directive. - $scope.regenerateUbuntuImages = function() { - var getResource = function() { return null; }; - var resources = $scope.bootResources.resources.filter( - function(resource) { - var name_split = resource.name.split('/'); - var resource_os = name_split[0]; - return ( - resource.rtype === 0 && - resource_os === 'ubuntu'); - }); - if (!$scope.source.isNew) { - getResource = function(release, arch) { - var i; - for (i = 0; i < resources.length; i++) { - // Only care about Ubuntu images. - var resource = (resources[i]); - var name_split = resource.name.split('/'); - var resource_release = name_split[1]; - if (resource_release === release && - resource.arch === arch) { - resources.splice(i, 1); - return resource; - } - } - return null; - }; - } + // Return true if the release and architecture selection + // should be shown. + $scope.showSelections = function() { + return ( + $scope.source.releases.length > 0 && $scope.source.arches.length > 0 + ); + }; - // Create the images based on the selections. - $scope.ubuntuImages.length = 0; - angular.forEach($scope.source.selections.releases, - function(release) { - angular.forEach($scope.source.selections.arches, - function(arch) { - var image = { - icon: 'p-icon--status-queued', - title: release.title, - arch: arch.title, - size: '-', - status: 'Selected for download', - beingDeleted: false, - name: release.name - }; - var resource = getResource( - release.name, arch.name); - if (angular.isObject(resource)) { - image.resourceId = resource.id; - image.icon = ( - 'p-icon--status-' + resource.icon); - image.size = resource.size; - image.status = resource.status; - if (resource.downloading) { - image.icon += ' u-animation--pulse'; - } - } - $scope.ubuntuImages.push(image); - }); - }); - - // If not a new source and images remain in resources, then - // those are set to be deleted. - if (!$scope.source.isNew) { - angular.forEach(resources, function(resource) { - var name_split = resource.name.split('/'); - var image = { - icon: 'p-icon--status-failed', - title: resource.title, - arch: resource.arch, - size: resource.size, - status: 'Will be deleted', - beingDeleted: true, - name: name_split[1] - }; - $scope.ubuntuImages.push(image); - }); - } - }; + // Return the Ubuntu LTS releases. + $scope.getUbuntuLTSReleases = function() { + var releases = $scope.bootResources.ubuntu.releases; + if ($scope.source.isNew) { + releases = $scope.source.releases; + } + var filtered = []; + angular.forEach(releases, function(release) { + if (!release.deleted && release.title.indexOf("LTS") !== -1) { + filtered.push(release); + } + }); + return filtered; + }; - // Regenerates the Ubuntu Core images list for the directive. - $scope.regenerateUbuntuCoreImages = function() { - var isUbuntuCore = function(resource) { - var name_split = resource.name.split('/'); - var resource_os = name_split[0]; - return ( - resource.rtype === 0 && - resource_os === 'ubuntu-core'); - }; - var resources = ( - $scope.bootResources.resources.filter(isUbuntuCore)); - var getResource = function(release, arch) { - var i; - for (i = 0; i < resources.length; i++) { - // Only care about other images. Removing custom, - // bootloaders, and Ubuntu images. - var resource = (resources[i]); - var name_split = resource.name.split('/'); - var resource_release = name_split[1]; - if (resource_release === release && - resource.arch === arch) { - resources.splice(i, 1); - return resource; - } - } - return null; - }; - - // Create the images based on the selections. - $scope.ubuntuCoreImages.length = 0; - angular.forEach($scope.ubuntu_core.images, - function(ubuntuCoreImage) { - if (ubuntuCoreImage.checked) { - var name_split = ubuntuCoreImage.name.split( - '/'); - var image = { - icon: 'p-icon--status-queued', - title: ubuntuCoreImage.title, - arch: name_split[1], - size: '-', - status: 'Selected for download', - beingDeleted: false - }; - var resource = getResource( - name_split[3], name_split[1]); - if (angular.isObject(resource)) { - image.icon = ( - 'p-icon--status-' + resource.icon); - image.size = resource.size; - image.status = resource.status; - if (resource.downloading) { - image.icon += ' u-animation--pulse'; - } - } - $scope.ubuntuCoreImages.push(image); - } - }); - - // If not a new source and images remain in resources, then - // those are set to be deleted. - angular.forEach(resources, function(resource) { - var image = { - icon: 'p-icon--status-failed', - title: resource.title, - arch: resource.arch, - size: resource.size, - status: 'Will be deleted', - beingDeleted: true - }; - $scope.ubuntuCoreImages.push(image); - }); - }; + // Return the Ubuntu non-LTS releases. + $scope.getUbuntuNonLTSReleases = function() { + var releases = $scope.bootResources.ubuntu.releases; + if ($scope.source.isNew) { + releases = $scope.source.releases; + } + var filtered = []; + angular.forEach(releases, function(release) { + if (!release.deleted && release.title.indexOf("LTS") === -1) { + filtered.push(release); + } + }); + return filtered; + }; - // Regenerates the other images list for the directive. - $scope.regenerateOtherImages = function() { - var isOther = function(resource) { - var name_split = resource.name.split('/'); - var resource_os = name_split[0]; - return ( - resource.rtype === 0 && - resource_os !== 'ubuntu' && - resource_os !== 'ubuntu-core' && - resource_os !== 'custom'); - }; - var resources = ( - $scope.bootResources.resources.filter(isOther)); - var getResource = function(release, arch) { - var i; - for (i = 0; i < resources.length; i++) { - // Only care about other images. Removing custom, - // bootloaders, and Ubuntu images. - var resource = (resources[i]); - var name_split = resource.name.split('/'); - var resource_release = name_split[1]; - if (resource_release === release && - resource.arch === arch) { - resources.splice(i, 1); - return resource; - } - } - return null; - }; - - // Create the images based on the selections. - $scope.otherImages.length = 0; - angular.forEach($scope.other.images, - function(otherImage) { - if (otherImage.checked) { - var name_split = otherImage.name.split('/'); - var image = { - icon: 'p-icon--status-queued', - title: otherImage.title, - arch: name_split[1], - size: '-', - status: 'Selected for download', - beingDeleted: false - }; - var resource = getResource( - name_split[3], name_split[1]); - if (angular.isObject(resource)) { - image.icon = ( - 'p-icon--status-' + resource.icon); - image.size = resource.size; - image.status = resource.status; - if (resource.downloading) { - image.icon += ' u-animation--pulse'; - } - } - $scope.otherImages.push(image); - } - }); - - // If not a new source and images remain in resources, then - // those are set to be deleted. - angular.forEach(resources, function(resource) { - var image = { - icon: 'p-icon--status-failed', - title: resource.title, - arch: resource.arch, - size: resource.size, - status: 'Will be deleted', - beingDeleted: true - }; - $scope.otherImages.push(image); - }); - }; + // Return the available architectures. + $scope.getArchitectures = function() { + var arches = $scope.bootResources.ubuntu.arches; + if ($scope.source.isNew) { + arches = $scope.source.arches; + } + var filtered = []; + angular.forEach(arches, function(arch) { + if (!arch.deleted) { + filtered.push(arch); + } + }); + return filtered; + }; - // Helper for basic image generation. - $scope._regenerateImages = function(rtype, images) { - // Create the generated images list. - images.length = 0; - angular.forEach($scope.bootResources.resources, - function(resource) { - if (resource.rtype === rtype) { - var image = { - icon: 'p-icon--status-' + resource.icon, - title: resource.title, - arch: resource.arch, - image_id: resource.id, - size: resource.size, - status: resource.status - }; - if (resource.downloading) { - image.icon += ' u-animation--pulse'; - } - images.push(image); - } - }); - }; + // Return true if the source has this selected. + $scope.isSelected = function(type, obj) { + return $scope.source.selections[type].indexOf(obj) >= 0; + }; - // Regenerates the generated images list for the directive. - $scope.regenerateGeneratedImages = function() { - $scope._regenerateImages(1, $scope.generatedImages); - }; + // Toggle the selection of the release or the architecture. + $scope.toggleSelection = function(type, obj) { + var idx = $scope.source.selections[type].indexOf(obj); + if (idx === -1) { + $scope.source.selections[type].push(obj); + } else { + $scope.source.selections[type].splice(idx, 1); + } + $scope.source.selections.changed = true; + $scope.regenerateUbuntuImages(); + }; - // Regenerates the custom images list for the directive. - $scope.regenerateCustomImages = function() { - $scope._regenerateImages(2, $scope.customImages); - }; + // Return true if the images table should be shown. + $scope.showImagesTable = function() { + if ($scope.ubuntuImages.length > 0) { + return true; + } else { + // Show the images table source has + // releases and architectures. + return ( + $scope.source.arches.length > 0 && $scope.source.releases.length > 0 + ); + } + }; - // Returns true if at least one LTS is selected. - $scope.ltsIsSelected = function() { - var i; - for (i = 0; i < $scope.ubuntuImages.length; i++) { - // Must have LTS in the title and not being deleted. - if (!$scope.ubuntuImages[i].beingDeleted && - $scope.ubuntuImages[i].title.indexOf('LTS') >= 0) { - // Must be greater than Ubuntu series 14. - var series = parseInt( - $scope.ubuntuImages[i].title.split('.')[0], 10); - if (series >= 14) { - return true; - } - } + // Regenerates the Ubuntu images list for the directive. + $scope.regenerateUbuntuImages = function() { + var getResource = function() { + return null; + }; + var resources = $scope.bootResources.resources.filter(function(resource) { + var name_split = resource.name.split("/"); + var resource_os = name_split[0]; + return resource.rtype === 0 && resource_os === "ubuntu"; + }); + if (!$scope.source.isNew) { + getResource = function(release, arch) { + var i; + for (i = 0; i < resources.length; i++) { + // Only care about Ubuntu images. + var resource = resources[i]; + var name_split = resource.name.split("/"); + var resource_release = name_split[1]; + if (resource_release === release && resource.arch === arch) { + resources.splice(i, 1); + return resource; + } + } + return null; + }; + } + + // Create the images based on the selections. + $scope.ubuntuImages.length = 0; + angular.forEach($scope.source.selections.releases, function(release) { + angular.forEach($scope.source.selections.arches, function(arch) { + var image = { + icon: "p-icon--status-queued", + title: release.title, + arch: arch.title, + size: "-", + status: "Selected for download", + beingDeleted: false, + name: release.name + }; + var resource = getResource(release.name, arch.name); + if (angular.isObject(resource)) { + image.resourceId = resource.id; + image.icon = "p-icon--status-" + resource.icon; + image.size = resource.size; + image.status = resource.status; + if (resource.downloading) { + image.icon += " u-animation--pulse"; } - return false; - }; + } + $scope.ubuntuImages.push(image); + }); + }); - // Returns true if the commission series is selected - $scope.commissioningSeriesSelected = function() { - var i; - for (i = 0; i < $scope.ubuntuImages.length; i++) { - if (!$scope.ubuntuImages[i].beingDeleted && - $scope.ubuntuImages[i].name === - $scope.bootResources.ubuntu.commissioning_series) { - return true; - } - } - return false; - }; + // If not a new source and images remain in resources, then + // those are set to be deleted. + if (!$scope.source.isNew) { + angular.forEach(resources, function(resource) { + var name_split = resource.name.split("/"); + var image = { + icon: "p-icon--status-failed", + title: resource.title, + arch: resource.arch, + size: resource.size, + status: "Will be deleted", + beingDeleted: true, + name: name_split[1] + }; + $scope.ubuntuImages.push(image); + }); + } + }; - // Return if we are asking about deleting this image. - $scope.isShowingDeleteConfirm = function(image) { - return image === $scope.removingImage; + // Regenerates the Ubuntu Core images list for the directive. + $scope.regenerateUbuntuCoreImages = function() { + var isUbuntuCore = function(resource) { + var name_split = resource.name.split("/"); + var resource_os = name_split[0]; + return resource.rtype === 0 && resource_os === "ubuntu-core"; + }; + var resources = $scope.bootResources.resources.filter(isUbuntuCore); + var getResource = function(release, arch) { + var i; + for (i = 0; i < resources.length; i++) { + // Only care about other images. Removing custom, + // bootloaders, and Ubuntu images. + var resource = resources[i]; + var name_split = resource.name.split("/"); + var resource_release = name_split[1]; + if (resource_release === release && resource.arch === arch) { + resources.splice(i, 1); + return resource; + } + } + return null; + }; + + // Create the images based on the selections. + $scope.ubuntuCoreImages.length = 0; + angular.forEach($scope.ubuntu_core.images, function(ubuntuCoreImage) { + if (ubuntuCoreImage.checked) { + var name_split = ubuntuCoreImage.name.split("/"); + var image = { + icon: "p-icon--status-queued", + title: ubuntuCoreImage.title, + arch: name_split[1], + size: "-", + status: "Selected for download", + beingDeleted: false + }; + var resource = getResource(name_split[3], name_split[1]); + if (angular.isObject(resource)) { + image.icon = "p-icon--status-" + resource.icon; + image.size = resource.size; + image.status = resource.status; + if (resource.downloading) { + image.icon += " u-animation--pulse"; + } + } + $scope.ubuntuCoreImages.push(image); + } + }); + + // If not a new source and images remain in resources, then + // those are set to be deleted. + angular.forEach(resources, function(resource) { + var image = { + icon: "p-icon--status-failed", + title: resource.title, + arch: resource.arch, + size: resource.size, + status: "Will be deleted", + beingDeleted: true }; + $scope.ubuntuCoreImages.push(image); + }); + }; - // Mark the image for deletion. - $scope.quickRemove = function(image) { - $scope.removingImage = image; + // Regenerates the other images list for the directive. + $scope.regenerateOtherImages = function() { + var isOther = function(resource) { + var name_split = resource.name.split("/"); + var resource_os = name_split[0]; + return ( + resource.rtype === 0 && + resource_os !== "ubuntu" && + resource_os !== "ubuntu-core" && + resource_os !== "custom" + ); + }; + var resources = $scope.bootResources.resources.filter(isOther); + var getResource = function(release, arch) { + var i; + for (i = 0; i < resources.length; i++) { + // Only care about other images. Removing custom, + // bootloaders, and Ubuntu images. + var resource = resources[i]; + var name_split = resource.name.split("/"); + var resource_release = name_split[1]; + if (resource_release === release && resource.arch === arch) { + resources.splice(i, 1); + return resource; + } + } + return null; + }; + + // Create the images based on the selections. + $scope.otherImages.length = 0; + angular.forEach($scope.other.images, function(otherImage) { + if (otherImage.checked) { + var name_split = otherImage.name.split("/"); + var image = { + icon: "p-icon--status-queued", + title: otherImage.title, + arch: name_split[1], + size: "-", + status: "Selected for download", + beingDeleted: false + }; + var resource = getResource(name_split[3], name_split[1]); + if (angular.isObject(resource)) { + image.icon = "p-icon--status-" + resource.icon; + image.size = resource.size; + image.status = resource.status; + if (resource.downloading) { + image.icon += " u-animation--pulse"; + } + } + $scope.otherImages.push(image); + } + }); + + // If not a new source and images remain in resources, then + // those are set to be deleted. + angular.forEach(resources, function(resource) { + var image = { + icon: "p-icon--status-failed", + title: resource.title, + arch: resource.arch, + size: resource.size, + status: "Will be deleted", + beingDeleted: true }; + $scope.otherImages.push(image); + }); + }; - // Cancel image deletion. - $scope.cancelRemove = function() { - $scope.removingImage = null; - }; + // Helper for basic image generation. + $scope._regenerateImages = function(rtype, images) { + // Create the generated images list. + images.length = 0; + angular.forEach($scope.bootResources.resources, function(resource) { + if (resource.rtype === rtype) { + var image = { + icon: "p-icon--status-" + resource.icon, + title: resource.title, + arch: resource.arch, + image_id: resource.id, + size: resource.size, + status: resource.status + }; + if (resource.downloading) { + image.icon += " u-animation--pulse"; + } + images.push(image); + } + }); + }; - // Mark the image for deletion. - $scope.confirmRemove = function(image) { - if (image === $scope.removingImage) { - BootResourcesManager.deleteImage( - { id: image.image_id }); - } - $scope.cancelRemove(); - }; + // Regenerates the generated images list for the directive. + $scope.regenerateGeneratedImages = function() { + $scope._regenerateImages(1, $scope.generatedImages); + }; - // Return true if the stop import button should be shown. - $scope.showStopImportButton = function() { - return $scope.bootResources.region_import_running; - }; + // Regenerates the custom images list for the directive. + $scope.regenerateCustomImages = function() { + $scope._regenerateImages(2, $scope.customImages); + }; - // Return true if should show save selection button, this - // doesn't mean it can actually be clicked. - $scope.showSaveSelection = function() { - return $scope.showImagesTable(); - }; + // Returns true if at least one LTS is selected. + $scope.ltsIsSelected = function() { + var i; + for (i = 0; i < $scope.ubuntuImages.length; i++) { + // Must have LTS in the title and not being deleted. + if ( + !$scope.ubuntuImages[i].beingDeleted && + $scope.ubuntuImages[i].title.indexOf("LTS") >= 0 + ) { + // Must be greater than Ubuntu series 14. + var series = parseInt($scope.ubuntuImages[i].title.split(".")[0], 10); + if (series >= 14) { + return true; + } + } + } + return false; + }; - // Return true if can save the current selection. - $scope.canSaveSelection = function() { - var commissioning_series_being_deleted = false; - var commissioning_series_arches = 0; - var i; - for (i = 0; i < $scope.ubuntuImages.length; i++) { - if ($scope.ubuntuImages[i].name === - $scope.bootResources.ubuntu.commissioning_series) { - commissioning_series_arches++; - } - } - // Only prevent the current commissioning series from - // being deleted if it isn't the commissioning series isn't - // available on another architecture.. If the current - // commissioning series isn't currently selected another - // LTS may be choosen, downloaded, and configured as the - // commissioning series. - for (i = 0; i < $scope.ubuntuImages.length; i++) { - if ($scope.ubuntuImages[i].beingDeleted && - $scope.ubuntuImages[i].name === - $scope.bootResources.ubuntu.commissioning_series && - commissioning_series_arches === 1) { - commissioning_series_being_deleted = true; - break; - } - } - return ( - !commissioning_series_being_deleted && - !$scope.saving && - !$scope.stopping && - $scope.ltsIsSelected()); - }; + // Returns true if the commission series is selected + $scope.commissioningSeriesSelected = function() { + var i; + for (i = 0; i < $scope.ubuntuImages.length; i++) { + if ( + !$scope.ubuntuImages[i].beingDeleted && + $scope.ubuntuImages[i].name === + $scope.bootResources.ubuntu.commissioning_series + ) { + return true; + } + } + return false; + }; - // Return the text for the save selection button. - $scope.getSaveSelectionText = function() { - if ($scope.saving) { - return "Saving..."; - } else if ($scope.saved) { - return "Selection updated"; - } else { - return "Update selection"; - } - }; + // Return if we are asking about deleting this image. + $scope.isShowingDeleteConfirm = function(image) { + return image === $scope.removingImage; + }; - // Return true if can stop current import. - $scope.canStopImport = function() { - return !$scope.saving && !$scope.stopping; - }; + // Mark the image for deletion. + $scope.quickRemove = function(image) { + $scope.removingImage = image; + }; - // Return the text for the stop import button. - $scope.getStopImportText = function() { - if ($scope.stopping) { - return "Stopping..."; - } else { - return "Stop import"; - } - }; + // Cancel image deletion. + $scope.cancelRemove = function() { + $scope.removingImage = null; + }; - // Called to stop the import. - $scope.stopImport = function() { - if (!$scope.canStopImport()) { - return; - } + // Mark the image for deletion. + $scope.confirmRemove = function(image) { + if (image === $scope.removingImage) { + BootResourcesManager.deleteImage({ id: image.image_id }); + } + $scope.cancelRemove(); + }; - $scope.stopping = true; - BootResourcesManager.stopImport().then(function() { - $scope.stopping = false; - }); - }; + // Return true if the stop import button should be shown. + $scope.showStopImportButton = function() { + return $scope.bootResources.region_import_running; + }; - // Save the selections into boot selections. - $scope.saveSelection = function() { - if (!$scope.canSaveSelection()) { - return; - } + // Return true if should show save selection button, this + // doesn't mean it can actually be clicked. + $scope.showSaveSelection = function() { + return $scope.showImagesTable(); + }; - var params = $scope.getSourceParams(); - params.releases = ( - $scope.source.selections.releases.map(function(obj) { - return obj.name; - })); - params.arches = ( - $scope.source.selections.arches.map(function(obj) { - return obj.name; - })); - $scope.saving = true; - BootResourcesManager.saveUbuntu(params).then(function() { - $scope.saving = false; - $scope.source.isNew = false; - $scope.source.selections.changed = false; - $scope.savedTimeout(); - $scope.updateSource(); - }); - }; + // Return true if can save the current selection. + $scope.canSaveSelection = function() { + var commissioning_series_being_deleted = false; + var commissioning_series_arches = 0; + var i; + for (i = 0; i < $scope.ubuntuImages.length; i++) { + if ( + $scope.ubuntuImages[i].name === + $scope.bootResources.ubuntu.commissioning_series + ) { + commissioning_series_arches++; + } + } + // Only prevent the current commissioning series from + // being deleted if it isn't the commissioning series isn't + // available on another architecture.. If the current + // commissioning series isn't currently selected another + // LTS may be choosen, downloaded, and configured as the + // commissioning series. + for (i = 0; i < $scope.ubuntuImages.length; i++) { + if ( + $scope.ubuntuImages[i].beingDeleted && + $scope.ubuntuImages[i].name === + $scope.bootResources.ubuntu.commissioning_series && + commissioning_series_arches === 1 + ) { + commissioning_series_being_deleted = true; + break; + } + } + return ( + !commissioning_series_being_deleted && + !$scope.saving && + !$scope.stopping && + $scope.ltsIsSelected() + ); + }; - $scope.savedTimeout = function() { - $scope.saved = true; - $timeout(() => $scope.saved = false, 3000); - }; + // Return the text for the save selection button. + $scope.getSaveSelectionText = function() { + if ($scope.saving) { + return "Saving..."; + } else if ($scope.saved) { + return "Selection updated"; + } else { + return "Update selection"; + } + }; - // Re-create an array with the new objects using the old - // selected array. - $scope.getNewSelections = function(newObjs, oldSelections) { - var newSelections = []; - angular.forEach(newObjs, function(obj) { - angular.forEach(oldSelections, function(selection) { - if (obj.name === selection.name) { - newSelections.push(obj); - } - }); - }); - return newSelections; - }; + // Return true if can stop current import. + $scope.canStopImport = function() { + return !$scope.saving && !$scope.stopping; + }; - // Update the source information. - $scope.updateSource = function() { - // Do not update if the source is new. - if ($scope.source.isNew) { - return; - } + // Return the text for the stop import button. + $scope.getStopImportText = function() { + if ($scope.stopping) { + return "Stopping..."; + } else { + return "Stop import"; + } + }; - var source_len = $scope.bootResources.ubuntu.sources.length; - if (source_len === 0) { - $scope.source.isNew = true; - $scope.source.source_type = 'custom'; - $scope.source.errorMessage = ( - "Currently no source exists."); - } else if (source_len === 1) { - var source = $scope.bootResources.ubuntu.sources[0]; - $scope.source.source_type = source.source_type; - if (source.source_type === "maas.io") { - $scope.source.url = ""; - $scope.source.keyring_filename = ""; - $scope.source.keyring_data = ""; - } else { - $scope.source.url = source.url; - $scope.source.keyring_filename = ( - source.keyring_filename); - $scope.source.keyring_data = source.keyring_data; - } - $scope.source.releases = ( - $scope.bootResources.ubuntu.releases); - $scope.source.arches = ( - $scope.bootResources.ubuntu.arches); - if (!$scope.source.selections.changed) { - // User didn't make a change update to the - // current selections server side. - $scope.source.selections.releases = ( - $scope.source.releases.filter(function(obj) { - return obj.checked; - })); - $scope.source.selections.arches = ( - $scope.source.arches.filter(function(obj) { - return obj.checked; - })); - } else { - // Update the objects to be the new objects, with - // the same selections. - $scope.source.selections.releases = ( - $scope.getNewSelections( - $scope.source.releases, - $scope.source.selections.releases)); - $scope.source.selections.arches = ( - $scope.getNewSelections( - $scope.source.arches, - $scope.source.selections.arches)); - } - $scope.regenerateUbuntuImages(); - } else { - // Having more than one source prevents modification - // of the sources. - $scope.source.tooMany = true; - $scope.source.releases = ( - $scope.bootResources.ubuntu.releases); - $scope.source.arches = ( - $scope.bootResources.ubuntu.arches); - $scope.source.selections.releases = ( - $scope.source.releases.filter(function(obj) { - return obj.checked; - })); - $scope.source.selections.arches = ( - $scope.source.arches.filter(function(obj) { - return obj.checked; - })); - $scope.source.errorMessage = ( - "More than one image source exists. UI does not " + - "support modification of sources when more than " + - "one has been defined. Used the API to adjust " + - "your sources."); - $scope.regenerateUbuntuImages(); - } - }; + // Called to stop the import. + $scope.stopImport = function() { + if (!$scope.canStopImport()) { + return; + } - // Toggle the selection of Ubuntu Core images. - $scope.toggleUbuntuCoreSelection = function(image) { - $scope.ubuntu_core.changed = true; - image.checked = !image.checked; - $scope.regenerateUbuntuCoreImages(); - }; + $scope.stopping = true; + BootResourcesManager.stopImport().then(function() { + $scope.stopping = false; + }); + }; - // Save the Ubuntu Core image selections into boot selections. - $scope.saveUbuntuCoreSelection = function() { - var params = { - images: $scope.ubuntu_core.images.filter(function( - image) { - return image.checked; - }).map(function(image) { - return image.name; - }) - }; - $scope.saving = true; - BootResourcesManager.saveUbuntuCore(params).then( - function() { - $scope.saving = false; - }); - }; + // Save the selections into boot selections. + $scope.saveSelection = function() { + if (!$scope.canSaveSelection()) { + return; + } + + var params = $scope.getSourceParams(); + params.releases = $scope.source.selections.releases.map(function(obj) { + return obj.name; + }); + params.arches = $scope.source.selections.arches.map(function(obj) { + return obj.name; + }); + $scope.saving = true; + BootResourcesManager.saveUbuntu(params).then(function() { + $scope.saving = false; + $scope.source.isNew = false; + $scope.source.selections.changed = false; + $scope.savedTimeout(); + $scope.updateSource(); + }); + }; - // Toggle the selection of other images. - $scope.toggleOtherSelection = function(image) { - $scope.other.changed = true; - image.checked = !image.checked; - $scope.regenerateOtherImages(); - }; + $scope.savedTimeout = function() { + $scope.saved = true; + $timeout(() => ($scope.saved = false), 3000); + }; - // Save the other image selections into boot selections. - $scope.saveOtherSelection = function() { - var params = { - images: $scope.other.images.filter(function(image) { - return image.checked; - }).map(function(image) { - return image.name; - }) - }; - $scope.saving = true; - BootResourcesManager.saveOther(params).then(function() { - $scope.saving = false; - }); - }; + // Re-create an array with the new objects using the old + // selected array. + $scope.getNewSelections = function(newObjs, oldSelections) { + var newSelections = []; + angular.forEach(newObjs, function(obj) { + angular.forEach(oldSelections, function(selection) { + if (obj.name === selection.name) { + newSelections.push(obj); + } + }); + }); + return newSelections; + }; - // Return True if the Ubuntu image can be removed. - $scope.canBeRemoved = function(image) { - // Image must have a resourceId to be able to be removed. - if (!angular.isNumber(image.resourceId)) { - return false; - } + // Update the source information. + $scope.updateSource = function() { + // Do not update if the source is new. + if ($scope.source.isNew) { + return; + } + + var source_len = $scope.bootResources.ubuntu.sources.length; + if (source_len === 0) { + $scope.source.isNew = true; + $scope.source.source_type = "custom"; + $scope.source.errorMessage = "Currently no source exists."; + } else if (source_len === 1) { + var source = $scope.bootResources.ubuntu.sources[0]; + $scope.source.source_type = source.source_type; + if (source.source_type === "maas.io") { + $scope.source.url = ""; + $scope.source.keyring_filename = ""; + $scope.source.keyring_data = ""; + } else { + $scope.source.url = source.url; + $scope.source.keyring_filename = source.keyring_filename; + $scope.source.keyring_data = source.keyring_data; + } + $scope.source.releases = $scope.bootResources.ubuntu.releases; + $scope.source.arches = $scope.bootResources.ubuntu.arches; + if (!$scope.source.selections.changed) { + // User didn't make a change update to the + // current selections server side. + $scope.source.selections.releases = $scope.source.releases.filter( + function(obj) { + return obj.checked; + } + ); + $scope.source.selections.arches = $scope.source.arches.filter( + function(obj) { + return obj.checked; + } + ); + } else { + // Update the objects to be the new objects, with + // the same selections. + $scope.source.selections.releases = $scope.getNewSelections( + $scope.source.releases, + $scope.source.selections.releases + ); + $scope.source.selections.arches = $scope.getNewSelections( + $scope.source.arches, + $scope.source.selections.arches + ); + } + $scope.regenerateUbuntuImages(); + } else { + // Having more than one source prevents modification + // of the sources. + $scope.source.tooMany = true; + $scope.source.releases = $scope.bootResources.ubuntu.releases; + $scope.source.arches = $scope.bootResources.ubuntu.arches; + $scope.source.selections.releases = $scope.source.releases.filter( + function(obj) { + return obj.checked; + } + ); + $scope.source.selections.arches = $scope.source.arches.filter(function( + obj + ) { + return obj.checked; + }); + $scope.source.errorMessage = + "More than one image source exists. UI does not " + + "support modification of sources when more than " + + "one has been defined. Used the API to adjust " + + "your sources."; + $scope.regenerateUbuntuImages(); + } + }; - // If the release or architecture is set to deleted - // then this image can be deleted. - var i; - var releases = $scope.bootResources.ubuntu.releases; - var arches = $scope.bootResources.ubuntu.arches; - for (i = 0; i < releases.length; i++) { - var release = releases[i]; - if (release.deleted && image.title === release.title) { - return true; - } - } - for (i = 0; i < arches.length; i++) { - var arch = arches[i]; - if (arch.deleted && image.arch === arch.name) { - return true; - } - } - return false; - }; + // Toggle the selection of Ubuntu Core images. + $scope.toggleUbuntuCoreSelection = function(image) { + $scope.ubuntu_core.changed = true; + image.checked = !image.checked; + $scope.regenerateUbuntuCoreImages(); + }; - // Deletes the give image. - $scope.deleteImage = function(image) { - if (angular.isObject(image)) { - $scope.ubuntuDeleteId = image.resourceId; - } else { - $scope.ubuntuDeleteId = null; - } - }; + // Save the Ubuntu Core image selections into boot selections. + $scope.saveUbuntuCoreSelection = function() { + var params = { + images: $scope.ubuntu_core.images + .filter(function(image) { + return image.checked; + }) + .map(function(image) { + return image.name; + }) + }; + $scope.saving = true; + BootResourcesManager.saveUbuntuCore(params).then(function() { + $scope.saving = false; + }); + }; - // Deletes the give image. - $scope.confirmDeleteImage = function() { - // Delete the image by its resourceId. - BootResourcesManager.deleteImage( - { id: $scope.ubuntuDeleteId }); - $scope.ubuntuDeleteId = null; - }; + // Toggle the selection of other images. + $scope.toggleOtherSelection = function(image) { + $scope.other.changed = true; + image.checked = !image.checked; + $scope.regenerateOtherImages(); + }; - // Start polling now that the directive is viewable and ensure - // the UserManager is loaded. - var ready = 2; - BootResourcesManager.startPolling().then(function() { - ready -= 1; - if (ready === 0) { - $scope.loading = false; - } - }); - ManagerHelperService.loadManager( - $scope, UsersManager).then(function() { - ready -= 1; - if (ready === 0) { - $scope.loading = false; - } - }); - - // Update the source information with the ubuntu source - // information changes. - $scope.$watch("bootResources.ubuntu", function() { - if (!angular.isObject($scope.bootResources.ubuntu)) { - return; - } - $scope.updateSource(); - }); + // Save the other image selections into boot selections. + $scope.saveOtherSelection = function() { + var params = { + images: $scope.other.images + .filter(function(image) { + return image.checked; + }) + .map(function(image) { + return image.name; + }) + }; + $scope.saving = true; + BootResourcesManager.saveOther(params).then(function() { + $scope.saving = false; + }); + }; - // Regenerate the images array when the resources change. - $scope.$watch("bootResources.resources", function() { - if (!angular.isArray($scope.bootResources.resources)) { - return; - } - $scope.regenerateUbuntuImages(); - $scope.regenerateUbuntuCoreImages(); - $scope.regenerateOtherImages(); - $scope.regenerateGeneratedImages(); - $scope.regenerateCustomImages(); - }); + // Return True if the Ubuntu image can be removed. + $scope.canBeRemoved = function(image) { + // Image must have a resourceId to be able to be removed. + if (!angular.isNumber(image.resourceId)) { + return false; + } + + // If the release or architecture is set to deleted + // then this image can be deleted. + var i; + var releases = $scope.bootResources.ubuntu.releases; + var arches = $scope.bootResources.ubuntu.arches; + for (i = 0; i < releases.length; i++) { + var release = releases[i]; + if (release.deleted && image.title === release.title) { + return true; + } + } + for (i = 0; i < arches.length; i++) { + var arch = arches[i]; + if (arch.deleted && image.arch === arch.name) { + return true; + } + } + return false; + }; - $scope.$watch("bootResources.ubuntu_core_images", function() { - var images = $scope.bootResources.ubuntu_core_images; - if (!angular.isArray(images)) { - return; - } - if (!$scope.ubuntu_core.changed) { - $scope.ubuntu_core.images = images; - } - $scope.regenerateUbuntuCoreImages(); - }); + // Deletes the give image. + $scope.deleteImage = function(image) { + if (angular.isObject(image)) { + $scope.ubuntuDeleteId = image.resourceId; + } else { + $scope.ubuntuDeleteId = null; + } + }; - $scope.$watch("bootResources.other_images", function() { - if (!angular.isArray($scope.bootResources.other_images)) { - return; - } - if (!$scope.other.changed) { - $scope.other.images = $scope.bootResources.other_images; - } - $scope.regenerateOtherImages(); - }); + // Deletes the give image. + $scope.confirmDeleteImage = function() { + // Delete the image by its resourceId. + BootResourcesManager.deleteImage({ id: $scope.ubuntuDeleteId }); + $scope.ubuntuDeleteId = null; + }; - // Stop polling when the directive is destroyed. - $scope.$on('$destroy', function() { - BootResourcesManager.stopPolling(); - }); - } + // Start polling now that the directive is viewable and ensure + // the UserManager is loaded. + var ready = 2; + BootResourcesManager.startPolling().then(function() { + ready -= 1; + if (ready === 0) { + $scope.loading = false; + } + }); + ManagerHelperService.loadManager($scope, UsersManager).then(function() { + ready -= 1; + if (ready === 0) { + $scope.loading = false; + } + }); + + // Update the source information with the ubuntu source + // information changes. + $scope.$watch("bootResources.ubuntu", function() { + if (!angular.isObject($scope.bootResources.ubuntu)) { + return; + } + $scope.updateSource(); + }); + + // Regenerate the images array when the resources change. + $scope.$watch("bootResources.resources", function() { + if (!angular.isArray($scope.bootResources.resources)) { + return; + } + $scope.regenerateUbuntuImages(); + $scope.regenerateUbuntuCoreImages(); + $scope.regenerateOtherImages(); + $scope.regenerateGeneratedImages(); + $scope.regenerateCustomImages(); + }); + + $scope.$watch("bootResources.ubuntu_core_images", function() { + var images = $scope.bootResources.ubuntu_core_images; + if (!angular.isArray(images)) { + return; + } + if (!$scope.ubuntu_core.changed) { + $scope.ubuntu_core.images = images; + } + $scope.regenerateUbuntuCoreImages(); + }); + + $scope.$watch("bootResources.other_images", function() { + if (!angular.isArray($scope.bootResources.other_images)) { + return; + } + if (!$scope.other.changed) { + $scope.other.images = $scope.bootResources.other_images; + } + $scope.regenerateOtherImages(); + }); + + // Stop polling when the directive is destroyed. + $scope.$on("$destroy", function() { + BootResourcesManager.stopPolling(); + }); + } } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/call_to_action.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/call_to_action.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/call_to_action.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/call_to_action.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,158 +6,160 @@ /* @ngInject */ export function cacheCta($templateCache) { - // Inject the cta.html into the template cache. - $templateCache.put('directive/templates/cta.html', [ - '
', - '', - '
', - '', - '', - '', - '
', - '
', - ].join('')); + // Inject the cta.html into the template cache. + $templateCache.put( + "directive/templates/cta.html", + [ + '
', + '", + '
", + '', + '", + "", + "
", + "
" + ].join("") + ); } export function maasCta() { - return { - restrict: "A", - replace: true, - require: "ngModel", - scope: { - maasCta: '=', - ngModel: '=', - selectedItems: '=', - }, - templateUrl: 'directive/templates/cta.html', - link: link, - controller: CtaController - }; - - /* @ngInject */ - function link(scope, element, attrs, ngModelCtrl) { - // Use the link function to grab the ngModel controller. - - // Title of the button when not active. - var defaultTitle = "Take action"; - if (angular.isString(attrs.defaultTitle) && - attrs.defaultTitle !== "") { - defaultTitle = attrs.defaultTitle; + return { + restrict: "A", + replace: true, + require: "ngModel", + scope: { + maasCta: "=", + ngModel: "=", + selectedItems: "=" + }, + templateUrl: "directive/templates/cta.html", + link: link, + controller: CtaController + }; + + /* @ngInject */ + function link(scope, element, attrs, ngModelCtrl) { + // Use the link function to grab the ngModel controller. + + // Title of the button when not active. + var defaultTitle = "Take action"; + if (angular.isString(attrs.defaultTitle) && attrs.defaultTitle !== "") { + defaultTitle = attrs.defaultTitle; + } + + // When an item is selected in the list set the title, hide the + // dropdown, and set the value to the given model. + scope.selectItem = function(action) { + scope.shown = false; + ngModelCtrl.$setViewValue(action); + }; + + // Return the title of the dropdown button. + scope.getTitle = function() { + if (angular.isObject(ngModelCtrl.$modelValue)) { + let option = ngModelCtrl.$modelValue; + scope.secondary = true; + // Some designs have the requirement that the title of + // the menu option change if it is selected. + if (angular.isString(option.selectedTitle)) { + return option.selectedTitle; } + return option.title; + } else { + scope.secondary = false; + return defaultTitle; + } + }; - // When an item is selected in the list set the title, hide the - // dropdown, and set the value to the given model. - scope.selectItem = function(action) { - scope.shown = false; - ngModelCtrl.$setViewValue(action); - }; - - // Return the title of the dropdown button. - scope.getTitle = function() { - if (angular.isObject(ngModelCtrl.$modelValue)) { - let option = ngModelCtrl.$modelValue; - scope.secondary = true; - // Some designs have the requirement that the title of - // the menu option change if it is selected. - if (angular.isString(option.selectedTitle)) { - return option.selectedTitle; - } - return option.title; - } else { - scope.secondary = false; - return defaultTitle; - } - }; - - // Called to get the title for each option. (Sometimes the title - // of an option is different when it is selected.) - scope.getOptionTitle = function(option) { - if (!scope.secondary) { - return option.title; - } else { - if (angular.isString(option.selectedTitle)) { - return option.selectedTitle; - } else { - return option.title; - } - } - }; - - scope.getActionTypes = function() { - var actions = scope.maasCta || []; - var types = []; - actions.forEach(function(action) { - if (types.indexOf(action.type) === -1) { - types.push(action.type); - } - }); - - return types; - }; - - scope.showAction = function(action) { - return !(scope.selectedItems - && action.available === 0 - && action.type !== 'lifecycle'); - }; - - scope.showCount = function(action) { - return (scope.selectedItems > 1 - && action.available > 0); - }; - - // When the model changes in the above selectItem function this - // function will be called causing the ngChange directive to be - // fired. - ngModelCtrl.$viewChangeListeners.push(function() { - scope.$eval(attrs.ngChange); - }); - } + // Called to get the title for each option. (Sometimes the title + // of an option is different when it is selected.) + scope.getOptionTitle = function(option) { + if (!scope.secondary) { + return option.title; + } else { + if (angular.isString(option.selectedTitle)) { + return option.selectedTitle; + } else { + return option.title; + } + } + }; - /* @ngInject */ - function CtaController($scope, $rootScope, $element, $document) { - // Default dropdown is hidden. - $scope.shown = false; - $scope.secondary = false; - - // Don't propagate the element click. This stops the click event - // from firing on the body element. - $element.bind('click', function(evt) { - evt.stopPropagation(); - }); - - // If a click makes it to the body element then hide the dropdown. - $document.find('body').bind('click', function() { - // Use $apply because this function will be called outside - // of the digest cycle. - $rootScope.$apply($scope.shown = false); - }); + scope.getActionTypes = function() { + var actions = scope.maasCta || []; + var types = []; + actions.forEach(function(action) { + if (types.indexOf(action.type) === -1) { + types.push(action.type); + } + }); - } + return types; + }; + + scope.showAction = function(action) { + return !( + scope.selectedItems && + action.available === 0 && + action.type !== "lifecycle" + ); + }; + + scope.showCount = function(action) { + return scope.selectedItems > 1 && action.available > 0; + }; + + // When the model changes in the above selectItem function this + // function will be called causing the ngChange directive to be + // fired. + ngModelCtrl.$viewChangeListeners.push(function() { + scope.$eval(attrs.ngChange); + }); + } + + /* @ngInject */ + function CtaController($scope, $rootScope, $element, $document) { + // Default dropdown is hidden. + $scope.shown = false; + $scope.secondary = false; + + // Don't propagate the element click. This stops the click event + // from firing on the body element. + $element.bind("click", function(evt) { + evt.stopPropagation(); + }); + + // If a click makes it to the body element then hide the dropdown. + $document.find("body").bind("click", function() { + // Use $apply because this function will be called outside + // of the digest cycle. + $rootScope.$apply(($scope.shown = false)); + }); + } } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/card_loader.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/card_loader.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/card_loader.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/card_loader.js 2019-06-01 02:18:13.000000000 +0000 @@ -9,11 +9,11 @@ return { restrict: "A", link: function(scope, element, attrs) { - var templateUrl = ( - 'static/partials/cards/' + attrs.maasCardLoader + ( - '.html?v=' + MAAS_config.files_version)); - var include = ( - ''); + var templateUrl = + "static/partials/cards/" + + attrs.maasCardLoader + + (".html?v=" + MAAS_config.files_version); + var include = ""; element.html(include); $compile(element.contents())(scope); } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/code_lines.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/code_lines.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/code_lines.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/code_lines.js 2019-06-01 02:18:13.000000000 +0000 @@ -10,43 +10,39 @@ */ function maasCodeLines() { - return { - restrict: "A", - scope: { - maasCodeLines: '&' - }, - link: function(scope, element, attributes) { - - function insertContent() { - - // Empty the element contents and include again, this assures - // its the most up-to-date content - element.empty(); - element.text(scope.maasCodeLines); - - // Count the line contents - var lines = element.html().split('\n'), - newLine = '', - insert = ""; - - // Each line is to be wrapped by a span which is style & given - // its appropriate line number - $.each(lines, function() { - insert += newLine + '' + - this + ''; - newLine = '\n'; - }); - insert += ""; - - // Re-insert the contents - element.html(insert); - } - - // Watch the contents of the element so when it changes to - // re-add the line numbers. - scope.$watch(scope.maasCodeLines, insertContent); - } - }; + return { + restrict: "A", + scope: { + maasCodeLines: "&" + }, + link: function(scope, element) { + function insertContent() { + // Empty the element contents and include again, this assures + // its the most up-to-date content + element.empty(); + element.text(scope.maasCodeLines); + + // Count the line contents + var lines = element.html().split("\n"), + newLine = "", + insert = ""; + + // Each line is to be wrapped by a span which is style & given + // its appropriate line number + angular.forEach(lines, function(line) { + insert += newLine + '' + line + ""; + }); + insert += ""; + + // Re-insert the contents + element.html(insert); + } + + // Watch the contents of the element so when it changes to + // re-add the line numbers. + scope.$watch(scope.maasCodeLines, insertContent); + } + }; } export default maasCodeLines; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/contenteditable.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/contenteditable.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/contenteditable.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/contenteditable.js 2019-06-01 02:18:13.000000000 +0000 @@ -10,52 +10,51 @@ */ function contenteditable() { - return { - restrict: "A", - require: "ngModel", - scope: { - ngDisabled: "&", - maasEditing: "&" - }, - link: function(scope, element, attrs, ngModel) { - - // If the element is disabled then make the element lose focus. - var focusHandler = function() { - if(scope.ngDisabled()) { - element.blur(); - } else { - // Didn't lose focus, so its now editing. - scope.$apply(scope.maasEditing()); - } - }; - element.bind("focus", focusHandler); - - // Update the value of the model when events occur that - // can change the value of the model. - var changeHandler = function() { - scope.$apply(ngModel.$setViewValue(element.text())); - }; - element.bind("blur keyup change", changeHandler); - - // When the model changes set the html content for that element. - ngModel.$render = function() { - element.html(ngModel.$viewValue || ""); - }; - - // When the model changes this function will be called causing the - // ngChange directive to be fired. - ngModel.$viewChangeListeners.push(function() { - scope.$eval(attrs.ngChange); - }); - - // Remove the event handler on the element when the scope is - // destroyed. - scope.$on("$destroy", function() { - element.unbind("blur keyup change", changeHandler); - element.unbind("focus", focusHandler); - }); + return { + restrict: "A", + require: "ngModel", + scope: { + ngDisabled: "&", + maasEditing: "&" + }, + link: function(scope, element, attrs, ngModel) { + // If the element is disabled then make the element lose focus. + var focusHandler = function() { + if (scope.ngDisabled()) { + element.blur(); + } else { + // Didn't lose focus, so its now editing. + scope.$apply(scope.maasEditing()); } - }; + }; + element.bind("focus", focusHandler); + + // Update the value of the model when events occur that + // can change the value of the model. + var changeHandler = function() { + scope.$apply(ngModel.$setViewValue(element.text())); + }; + element.bind("blur keyup change", changeHandler); + + // When the model changes set the html content for that element. + ngModel.$render = function() { + element.html(ngModel.$viewValue || ""); + }; + + // When the model changes this function will be called causing the + // ngChange directive to be fired. + ngModel.$viewChangeListeners.push(function() { + scope.$eval(attrs.ngChange); + }); + + // Remove the event handler on the element when the scope is + // destroyed. + scope.$on("$destroy", function() { + element.unbind("blur keyup change", changeHandler); + element.unbind("focus", focusHandler); + }); + } + }; } export default contenteditable; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/controller_image_status.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/controller_image_status.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/controller_image_status.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/controller_image_status.js 2019-06-01 02:18:13.000000000 +0000 @@ -8,151 +8,153 @@ /* @ngInject */ export function ControllerImageStatusService( - $timeout, $interval, ControllersManager) { - var self = this; - - // How often to check the sync status of a controller in seconds. - var CHECK_INTERVAL = 30; - - // List of controllers that need to have the image status updated. - this.controllers = []; - - // List of current controller statues. - this.statuses = {}; - - // Interval function that is called to update the statuses. - this.updateStatuses = function() { - var controllerIds = []; - angular.forEach(self.controllers, function(system_id) { - controllerIds.push({ system_id: system_id }); - }); - - // Check the image states. - ControllersManager.checkImageStates(controllerIds).then( - function(results) { - angular.forEach(controllerIds, function(controller) { - var status = results[controller.system_id]; - if (status) { - self.statuses[controller.system_id] = status; - } else { - self.statuses[controller.system_id] = "Unknown"; - } - }); - }); - }; - - // Register this controller system_id. - this.register = function(system_id) { - var known = self.controllers.indexOf(system_id) >= 0; - if (!known) { - self.controllers.push(system_id); - } - - // When the interval is already running and its a new controller then - // the interval needs to be reset. When it already exists it doesn't - // need to be reset. - if (angular.isDefined(self.runningInterval)) { - if (known) { - return; - } else { - $interval.cancel(self.runningInterval); - self.runningInterval = undefined; - } - } - - // If its not running and the timeout has been started we re-create - // the timeout. This delays the start of the interval until the - // all directives on the page have been fully loaded. - if (angular.isDefined(self.startTimeout)) { - $timeout.cancel(self.startTimeout); - } - self.startTimeout = $timeout(function() { - self.startTimeout = undefined; - self.runningInterval = $interval(function() { - self.updateStatuses(); - }, CHECK_INTERVAL * 1000); - self.updateStatuses(); - }, 100); - }; - - // Unregister the controller. - this.unregister = function(system_id) { - var idx = self.controllers.indexOf(system_id); - if (idx > -1) { - self.controllers.splice(idx, 1); - } - - // If no controllers are left stop all intervals and timeouts. - if (self.controllers.length === 0) { - if (angular.isDefined(self.startTimeout)) { - $timeout.cancel(self.startTimeout); - self.startTimeout = undefined; - } - if (angular.isDefined(self.runningInterval)) { - $interval.cancel(self.runningInterval); - self.runningInterval = undefined; - } - } - }; - - // Return true if the spinner should be shown. - this.showSpinner = function(system_id) { - var status = self.statuses[system_id]; - if (angular.isString(status) && status !== "Syncing") { - return false; + $timeout, + $interval, + ControllersManager +) { + var self = this; + + // How often to check the sync status of a controller in seconds. + var CHECK_INTERVAL = 30; + + // List of controllers that need to have the image status updated. + this.controllers = []; + + // List of current controller statues. + this.statuses = {}; + + // Interval function that is called to update the statuses. + this.updateStatuses = function() { + var controllerIds = []; + angular.forEach(self.controllers, function(system_id) { + controllerIds.push({ system_id: system_id }); + }); + + // Check the image states. + ControllersManager.checkImageStates(controllerIds).then(function(results) { + angular.forEach(controllerIds, function(controller) { + var status = results[controller.system_id]; + if (status) { + self.statuses[controller.system_id] = status; } else { - return true; + self.statuses[controller.system_id] = "Unknown"; } - }; - - // Get the image status. - this.getImageStatus = function(system_id) { - var status = self.statuses[system_id]; - if (angular.isString(status)) { - return status; - } else { - return "Asking for status..."; - } - }; + }); + }); + }; + + // Register this controller system_id. + this.register = function(system_id) { + var known = self.controllers.indexOf(system_id) >= 0; + if (!known) { + self.controllers.push(system_id); + } + + // When the interval is already running and its a new controller then + // the interval needs to be reset. When it already exists it doesn't + // need to be reset. + if (angular.isDefined(self.runningInterval)) { + if (known) { + return; + } else { + $interval.cancel(self.runningInterval); + self.runningInterval = undefined; + } + } + + // If its not running and the timeout has been started we re-create + // the timeout. This delays the start of the interval until the + // all directives on the page have been fully loaded. + if (angular.isDefined(self.startTimeout)) { + $timeout.cancel(self.startTimeout); + } + self.startTimeout = $timeout(function() { + self.startTimeout = undefined; + self.runningInterval = $interval(function() { + self.updateStatuses(); + }, CHECK_INTERVAL * 1000); + self.updateStatuses(); + }, 100); + }; + + // Unregister the controller. + this.unregister = function(system_id) { + var idx = self.controllers.indexOf(system_id); + if (idx > -1) { + self.controllers.splice(idx, 1); + } + + // If no controllers are left stop all intervals and timeouts. + if (self.controllers.length === 0) { + if (angular.isDefined(self.startTimeout)) { + $timeout.cancel(self.startTimeout); + self.startTimeout = undefined; + } + if (angular.isDefined(self.runningInterval)) { + $interval.cancel(self.runningInterval); + self.runningInterval = undefined; + } + } + }; + + // Return true if the spinner should be shown. + this.showSpinner = function(system_id) { + var status = self.statuses[system_id]; + if (angular.isString(status) && status !== "Syncing") { + return false; + } else { + return true; + } + }; + + // Get the image status. + this.getImageStatus = function(system_id) { + var status = self.statuses[system_id]; + if (angular.isString(status)) { + return status; + } else { + return "Asking for status..."; + } + }; } /* @ngInject */ export function maasControllerImageStatus(ControllerImageStatusService) { - return { - restrict: "E", - scope: { - systemId: "=" - }, - template: [ - ' ', - '{$ getImageStatus() $}'].join(''), - link: function(scope, element, attrs) { - // Don't register until the systemId is set. - var unwatch, registered = false; - unwatch = scope.$watch("systemId", function() { - if (angular.isDefined(scope.systemId) && !registered) { - ControllerImageStatusService.register(scope.systemId); - registered = true; - unwatch(); - } - }); - - scope.showSpinner = function() { - return ControllerImageStatusService.showSpinner( - scope.systemId); - }; - scope.getImageStatus = function() { - return ControllerImageStatusService.getImageStatus( - scope.systemId); - }; - - // Unregister when destroyed. - scope.$on("$destroy", function() { - if (registered) { - ControllerImageStatusService.unregister(scope.systemId); - } - }); - } - }; + return { + restrict: "E", + scope: { + systemId: "=" + }, + template: [ + ' ', + "{$ getImageStatus() $}" + ].join(""), + link: function(scope) { + // Don't register until the systemId is set. + var unwatch, + registered = false; + unwatch = scope.$watch("systemId", function() { + if (angular.isDefined(scope.systemId) && !registered) { + ControllerImageStatusService.register(scope.systemId); + registered = true; + unwatch(); + } + }); + + scope.showSpinner = function() { + return ControllerImageStatusService.showSpinner(scope.systemId); + }; + scope.getImageStatus = function() { + return ControllerImageStatusService.getImageStatus(scope.systemId); + }; + + // Unregister when destroyed. + scope.$on("$destroy", function() { + if (registered) { + ControllerImageStatusService.unregister(scope.systemId); + } + }); + } + }; } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/controller_status.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/controller_status.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/controller_status.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/controller_status.js 2019-06-01 02:18:13.000000000 +0000 @@ -6,96 +6,95 @@ /* @ngInject */ export function cacheControllerStatus($templateCache) { - // Inject the controller-status.html into the template cache. - $templateCache.put('directive/templates/controller-status.html', [ - '', - '', - '', - '', - '' - ].join('')); + // Inject the controller-status.html into the template cache. + $templateCache.put( + "directive/templates/controller-status.html", + [ + "", + '', + "", + '', + "" + ].join("") + ); } /* @ngInject */ export function maasControllerStatus(ControllersManager, ServicesManager) { - return { /* @ngInject */ - restrict: "A", - scope: { - controller: '=maasControllerStatus', - textOnly: '=?maasTextOnly' - }, - templateUrl: 'directive/templates/controller-status.html', - controller: maasControllerStatusController - - }; - + return { /* @ngInject */ - function maasControllerStatusController($scope) { - $scope.serviceClass = "unknown"; - $scope.services = ServicesManager.getItems(); - $scope.serviceText = ""; - if ($scope.textOnly) { - $scope.textOnly = true; - } else { - $scope.textOnly = false; - } + restrict: "A", + scope: { + controller: "=maasControllerStatus", + textOnly: "=?maasTextOnly" + }, + templateUrl: "directive/templates/controller-status.html", + controller: maasControllerStatusController + }; + + /* @ngInject */ + function maasControllerStatusController($scope) { + $scope.serviceClass = "unknown"; + $scope.services = ServicesManager.getItems(); + $scope.serviceText = ""; + if ($scope.textOnly) { + $scope.textOnly = true; + } else { + $scope.textOnly = false; + } - // Return the status class for the service. - function getClass(service) { - if (service.status === "running") { - return "success"; - } else if (service.status === "degraded") { - return "warning"; - } else if (service.status === "dead") { - return "error"; - } else { - return "unknown"; - } - } + // Return the status class for the service. + function getClass(service) { + if (service.status === "running") { + return "success"; + } else if (service.status === "degraded") { + return "warning"; + } else if (service.status === "dead") { + return "error"; + } else { + return "unknown"; + } + } - // Return the number of times class is displayed. - function countClass(classes, class_name) { - var counter = 0; - angular.forEach(classes, function(name) { - if (name === class_name) { - counter++; - } - }); - return counter; + // Return the number of times class is displayed. + function countClass(classes, class_name) { + var counter = 0; + angular.forEach(classes, function(name) { + if (name === class_name) { + counter++; } + }); + return counter; + } - // Update the class based on status of the services on the - // controller. - function updateStatusClass() { - $scope.serviceClass = "unknown"; - if (angular.isObject($scope.controller)) { - var services = ControllersManager.getServices( - $scope.controller); - if (services.length > 0) { - var classes = services.map(getClass); - if (classes.indexOf("error") !== -1) { - $scope.serviceClass = "power-error"; - $scope.serviceText = countClass( - classes, "error") + " dead"; - } else if (classes.indexOf("warning") !== -1) { - $scope.serviceClass = "warning"; - $scope.serviceText = countClass( - classes, "warning") + " degraded"; - } else { - $scope.serviceClass = "success"; - $scope.serviceText = countClass( - classes, "success") + " running"; - } - } - } + // Update the class based on status of the services on the + // controller. + function updateStatusClass() { + $scope.serviceClass = "unknown"; + if (angular.isObject($scope.controller)) { + var services = ControllersManager.getServices($scope.controller); + if (services.length > 0) { + var classes = services.map(getClass); + if (classes.indexOf("error") !== -1) { + $scope.serviceClass = "power-error"; + $scope.serviceText = countClass(classes, "error") + " dead"; + } else if (classes.indexOf("warning") !== -1) { + $scope.serviceClass = "warning"; + $scope.serviceText = countClass(classes, "warning") + " degraded"; + } else { + $scope.serviceClass = "success"; + $scope.serviceText = countClass(classes, "success") + " running"; + } } - - // Watch the services array and the services on the controller, - // if any changes then update the status. - $scope.$watch("controller.service_ids", updateStatusClass); - $scope.$watch("services", updateStatusClass, true); - - // Update on creation. - updateStatusClass(); + } } + + // Watch the services array and the services on the controller, + // if any changes then update the status. + $scope.$watch("controller.service_ids", updateStatusClass); + $scope.$watch("services", updateStatusClass, true); + + // Update on creation. + updateStatusClass(); + } } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/dbl_click_overlay.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/dbl_click_overlay.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/dbl_click_overlay.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/dbl_click_overlay.js 2019-06-01 02:18:13.000000000 +0000 @@ -11,117 +11,133 @@ /* @ngInject */ export function cacheDoubleClickOverlay($templateCache) { - // Inject the style for the maas-dbl-overlay class. We inject the style - // instead of placing it in maas-styles.css because it is required for - // this directive to work at all. - var styleElement = document.createElement('style'); - styleElement.innerHTML = [ - '.maas-dbl-overlay {', - 'display: inline-block;', - 'position: relative;', - '}', - '.maas-dbl-overlay--overlay {', - 'position: absolute;', - 'left: 0;', - 'right: 0;', - 'top: 0;', - 'bottom: 0;', - '-webkit-touch-callout: none;', - '-webkit-user-select: none;', - '-khtml-user-select: none;', - '-moz-user-select: none;', - '-ms-user-select: none;', - 'user-select: none;', - '}' - ].join(''); - document.getElementsByTagName('head')[0].appendChild(styleElement); - - // Inject the double_click_overlay.html into the template cache. - $templateCache.put('directive/templates/double_click_overlay.html', [ - '
', - '', - '
', - '
' - ].join('')); + // Inject the style for the maas-dbl-overlay class. We inject the style + // instead of placing it in maas-styles.css because it is required for + // this directive to work at all. + var styleElement = document.createElement("style"); + styleElement.innerHTML = [ + ".maas-dbl-overlay {", + "display: inline-block;", + "position: relative;", + "}", + ".maas-dbl-overlay--overlay {", + "position: absolute;", + "left: 0;", + "right: 0;", + "top: 0;", + "bottom: 0;", + "-webkit-touch-callout: none;", + "-webkit-user-select: none;", + "-khtml-user-select: none;", + "-moz-user-select: none;", + "-ms-user-select: none;", + "user-select: none;", + "}" + ].join(""); + document.getElementsByTagName("head")[0].appendChild(styleElement); + + // Inject the double_click_overlay.html into the template cache. + $templateCache.put( + "directive/templates/double_click_overlay.html", + [ + '
', + "", + '
', + "
" + ].join("") + ); } /* @ngInject */ export function maasDblClickOverlay(BrowserService) { - return { - restrict: "A", - transclude: true, - replace: true, - scope: { - maasDblClickOverlay: '&' - }, - templateUrl: 'directive/templates/double_click_overlay.html', - link: function(scope, element, attrs) { - // Create the click function that will be called when the - // overlay is clicked. This changes based on the element that - // is transcluded into this directive. - var overlay = element.find(".maas-dbl-overlay--overlay"); - var transclude = element.find( - "span[data-ng-transclude]").children()[0]; - var clickElement; - if (transclude.tagName === "SELECT") { - clickElement = function() { - // Have to create a custom mousedown event for the - // select click to be handled. Using 'click()' or - //'focus()' will not work. - var evt = document.createEvent('MouseEvents'); - evt.initMouseEvent( - 'mousedown', true, true, window, 0, 0, 0, 0, 0, - false, false, false, false, 0, null); - transclude.dispatchEvent(evt); - }; - - // Selects use a pointer for the cursor. - overlay.css({ cursor: "pointer" }); - } else if (transclude.tagName === "INPUT") { - clickElement = function() { - // An input will become in focus when clicked. - angular.element(transclude).triggerHandler('focus'); - }; - - // Inputs use a text for the cursor. - overlay.css({ cursor: "text" }); - } else { - clickElement = function() { - // Standard element just call click on that element. - angular.element(transclude).triggerHandler('click'); - }; - - // Don't set cursor on other element types. - } - - // Add the click and double click handlers. - var overlayClick = function(evt) { - clickElement(); - evt.preventDefault(); - evt.stopPropagation(); - }; - var overlayDblClick = function(evt) { - // Call the double click handler with in the scope. - scope.$apply(scope.maasDblClickOverlay); - evt.preventDefault(); - evt.stopPropagation(); - }; - - // Enable the handlers if not Firefox. It firefox, then hide - // the overlay as Firefox does not allow sending click events - // to select elements. - if (BrowserService.browser !== "firefox") { - overlay.on("click", overlayClick); - overlay.on("dblclick", overlayDblClick); - } else { - overlay.addClass("ng-hide"); - } - - // Remove the handlers when the scope is destroyed. - scope.$on("$destroy", function() { - overlay.off("click", overlayClick); - overlay.off("dblclick", overlayDblClick); - }); - } - }; + return { + restrict: "A", + transclude: true, + replace: true, + scope: { + maasDblClickOverlay: "&" + }, + templateUrl: "directive/templates/double_click_overlay.html", + link: function(scope, element) { + // Create the click function that will be called when the + // overlay is clicked. This changes based on the element that + // is transcluded into this directive. + var overlay = element.find(".maas-dbl-overlay--overlay"); + var transclude = element.find("span[data-ng-transclude]").children()[0]; + var clickElement; + if (transclude.tagName === "SELECT") { + clickElement = function() { + // Have to create a custom mousedown event for the + // select click to be handled. Using 'click()' or + //'focus()' will not work. + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent( + "mousedown", + true, + true, + window, + 0, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ); + transclude.dispatchEvent(evt); + }; + + // Selects use a pointer for the cursor. + overlay.css({ cursor: "pointer" }); + } else if (transclude.tagName === "INPUT") { + clickElement = function() { + // An input will become in focus when clicked. + angular.element(transclude).triggerHandler("focus"); + }; + + // Inputs use a text for the cursor. + overlay.css({ cursor: "text" }); + } else { + clickElement = function() { + // Standard element just call click on that element. + angular.element(transclude).triggerHandler("click"); + }; + + // Don't set cursor on other element types. + } + + // Add the click and double click handlers. + var overlayClick = function(evt) { + clickElement(); + evt.preventDefault(); + evt.stopPropagation(); + }; + var overlayDblClick = function(evt) { + // Call the double click handler with in the scope. + scope.$apply(scope.maasDblClickOverlay); + evt.preventDefault(); + evt.stopPropagation(); + }; + + // Enable the handlers if not Firefox. It firefox, then hide + // the overlay as Firefox does not allow sending click events + // to select elements. + if (BrowserService.browser !== "firefox") { + overlay.on("click", overlayClick); + overlay.on("dblclick", overlayDblClick); + } else { + overlay.addClass("ng-hide"); + } + + // Remove the handlers when the scope is destroyed. + scope.$on("$destroy", function() { + overlay.off("click", overlayClick); + overlay.off("dblclick", overlayDblClick); + }); + } + }; } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/default_os_select.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/default_os_select.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/default_os_select.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/default_os_select.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,100 +4,101 @@ * OS/Release select directive. */ - function maasDefaultOsSelect() { - return { - restrict: "A", - scope: { - "osInput": "@maasDefaultOsSelect", - "seriesInput": "@maasDefaultSeriesSelect", - }, - link: function(scope, element) { - var osElement = angular.element(element.find(scope.osInput)); - var seriesElement = angular.element( - element.find(scope.seriesInput)); - if (!osElement || !seriesElement) { - throw new Error("Unable to find os or series elements"); + return { + restrict: "A", + scope: { + osInput: "@maasDefaultOsSelect", + seriesInput: "@maasDefaultSeriesSelect" + }, + link: function(scope, element) { + var osElement = angular.element(element.find(scope.osInput)); + var seriesElement = angular.element(element.find(scope.seriesInput)); + if (!osElement || !seriesElement) { + throw new Error("Unable to find os or series elements"); + } + + var selectVisableOption = function(options) { + var first_option = null; + angular.forEach(options, function(option) { + option = angular.element(option); + if (!option.hasClass("u-hide")) { + if (first_option === null) { + first_option = option; } + } + }); + if (first_option !== null) { + seriesElement.val(first_option.val()); + } + }; - var selectVisableOption = function(options) { - var first_option = null; - angular.forEach(options, function(option) { - option = angular.element(option); - if (!option.hasClass('u-hide')) { - if (first_option === null) { - first_option = option; - } - } - }); - if (first_option !== null) { - seriesElement.val(first_option.val()); - } - }; - - var modifyOption = function(option, newOSValue, initialSkip) { - var selected = false; - var value = option.val(); - var split_value = value.split("/"); - - // If "Default OS" is selected, then - // only show "Default OS Release". - if (newOSValue === '') { - if (value === '') { - option.removeClass('u-hide'); - option.attr('selected', 'selected'); - } else { - option.addClass('u-hide'); - } - } else { - if (split_value[0] === newOSValue) { - option.removeClass('u-hide'); - if (split_value[1] === '' && !initialSkip) { - selected = true; - option.attr('selected', 'selected'); - } - } else { - option.addClass('u-hide'); - } - } - return selected; - }; - - var switchTo = function(newOSValue, initialSkip) { - var options = seriesElement.find('option'); - var selected = false; - angular.forEach(options, function(option) { - var sel = modifyOption( - angular.element(option), newOSValue, initialSkip); - if (selected === false) { - selected = sel; - } - }); - - // We skip selection on first load, as Django will already - // provide the users, current selection. Without this the - // current selection will be clobered. - if (initialSkip) { - return; - } - - // See if a selection was made, if not then we need - // to select the first visible as a default is not - // present. - if (!selected) { - selectVisableOption(options); - } - }; - - // Call switchTo any time the os changes. - osElement.on('change', function(evt) { - switchTo(osElement.val(), false); - }); + var modifyOption = function(option, newOSValue, initialSkip) { + var selected = false; + var value = option.val(); + var split_value = value.split("/"); + + // If "Default OS" is selected, then + // only show "Default OS Release". + if (newOSValue === "") { + if (value === "") { + option.removeClass("u-hide"); + option.attr("selected", "selected"); + } else { + option.addClass("u-hide"); + } + } else { + if (split_value[0] === newOSValue) { + option.removeClass("u-hide"); + if (split_value[1] === "" && !initialSkip) { + selected = true; + option.attr("selected", "selected"); + } + } else { + option.addClass("u-hide"); + } + } + return selected; + }; - // Initialize the options. - switchTo(osElement.val(), true); + var switchTo = function(newOSValue, initialSkip) { + var options = seriesElement.find("option"); + var selected = false; + angular.forEach(options, function(option) { + var sel = modifyOption( + angular.element(option), + newOSValue, + initialSkip + ); + if (selected === false) { + selected = sel; + } + }); + + // We skip selection on first load, as Django will already + // provide the users, current selection. Without this the + // current selection will be clobered. + if (initialSkip) { + return; } - }; + + // See if a selection was made, if not then we need + // to select the first visible as a default is not + // present. + if (!selected) { + selectVisableOption(options); + } + }; + + // Call switchTo any time the os changes. + osElement.on("change", function() { + switchTo(osElement.val(), false); + }); + + // Initialize the options. + switchTo(osElement.val(), true); + } + }; } export default maasDefaultOsSelect; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/enter_blur.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/enter_blur.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/enter_blur.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/enter_blur.js 2019-06-01 02:18:13.000000000 +0000 @@ -7,17 +7,17 @@ */ function maasEnterBlur() { - return { - restrict: "A", - link: function(scope, element, attrs) { - element.bind("keydown keypress", function(evt) { - if(evt.which === 13) { - element.blur(); - evt.preventDefault(); - } - }); + return { + restrict: "A", + link: function(scope, element, attrs) { + element.bind("keydown keypress", function(evt) { + if (evt.which === 13) { + element.blur(); + evt.preventDefault(); } - }; + }); + } + }; } export default maasEnterBlur; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/enter.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/enter.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/enter.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/enter.js 2019-06-01 02:18:13.000000000 +0000 @@ -1,21 +1,21 @@ /* Copyright 2017 Canonical Ltd. This software is licensed under the * GNU Affero General Public License version 3 (see the file LICENSE). -*/ + */ function maasEnter() { - return { - restrict: "A", - link: function (scope, element, attrs) { - element.bind("keydown keypress", function (evt) { - if(evt.which === 13) { - scope.$apply(function () { - scope.$eval(attrs.maasEnter); - }); - evt.preventDefault(); - } - }); + return { + restrict: "A", + link: function(scope, element, attrs) { + element.bind("keydown keypress", function(evt) { + if (evt.which === 13) { + scope.$apply(function() { + scope.$eval(attrs.maasEnter); + }); + evt.preventDefault(); } - }; + }); + } + }; } export default maasEnter; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/error_overlay.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/error_overlay.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/error_overlay.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/error_overlay.js 2019-06-01 02:18:13.000000000 +0000 @@ -8,167 +8,173 @@ */ /* @ngInject */ -export function cacheErrorOverlay($templateCache) { - // Inject the error_overlay.html into the template cache. - $templateCache.put('directive/templates/error_overlay.html', [ - '', - '
', - '
', - '
' - ].join('')); - - // Preload the svg and png error icon. Its possible that it has never been - // loaded by the browser and if the region connection goes down and the - // directive gets shown with an error the icon will be missing. - // - // Note: This is skipped if unit testing because it will throw 404 errors - // continuously. - if (!angular.isDefined(window.jasmine)) { - var image = new Image(); - image.src = "static/assets/images/icons/error.svg"; - image = new Image(); - image.src = "static/assets/images/icons/error.png"; - } +export function cacheErrorOverlay($templateCache, $window) { + // Inject the error_overlay.html into the template cache. + $templateCache.put( + "directive/templates/error_overlay.html", + [ + '", + '
', + "
", + "
" + ].join("") + ); + + // Preload the svg and png error icon. Its possible that it has never been + // loaded by the browser and if the region connection goes down and the + // directive gets shown with an error the icon will be missing. + // + // Note: This is skipped if unit testing because it will throw 404 errors + // continuously. + if (angular.isUndefined($window.jasmine)) { + var image = new Image(); + image.src = "static/assets/images/icons/error.svg"; + image = new Image(); + image.src = "static/assets/images/icons/error.png"; + } } /* @ngInject */ export function maasErrorOverlay( - $window, $timeout, RegionConnection, ErrorService) { - return { - restrict: "A", - transclude: true, - scope: true, - templateUrl: 'directive/templates/error_overlay.html', - link: function(scope, element, attrs) { + $window, + $timeout, + RegionConnection, + ErrorService +) { + return { + restrict: "A", + transclude: true, + scope: true, + templateUrl: "directive/templates/error_overlay.html", + link: function(scope) { + scope.connected = false; + scope.showDisconnected = false; + scope.clientError = false; + scope.wasConnected = false; + + // Holds the promise that sets showDisconnected to true. Will + // be cleared when the scope is destroyed. + var markDisconnected; + + // Returns true when the overlay should be shown. + scope.show = function() { + // Always show if clientError. + if (scope.clientError) { + return true; + } + // Never show if connected. + if (scope.connected) { + return false; + } + // Never been connected then always show. + if (!scope.wasConnected) { + return true; + } + // Not connected. + return scope.showDisconnected; + }; + + // Returns the title for the header. + scope.getTitle = function() { + if (scope.clientError) { + return "Error occurred"; + } else if (scope.wasConnected) { + return "Connection lost, reconnecting..."; + } else { + return "Connecting..."; + } + }; + + // Called to reload the page. + scope.reload = function() { + $window.location.reload(); + }; + + // Called to when the connection status of the region + // changes. Updates the scope connected and error values. + var watchConnection = function() { + // Do nothing if already a client error. + if (scope.clientError) { + return; + } - scope.connected = false; + // Set connected and the time it changed. + var connected = RegionConnection.isConnected(); + if (connected !== scope.connected) { + scope.connected = connected; + if (!connected) { scope.showDisconnected = false; - scope.clientError = false; - scope.wasConnected = false; - // Holds the promise that sets showDisconnected to true. Will - // be cleared when the scope is destroyed. - var markDisconnected; - - // Returns true when the overlay should be shown. - scope.show = function() { - // Always show if clientError. - if (scope.clientError) { - return true; - } - // Never show if connected. - if (scope.connected) { - return false; - } - // Never been connected then always show. - if (!scope.wasConnected) { - return true; - } - // Not connected. - return scope.showDisconnected; - }; - - // Returns the title for the header. - scope.getTitle = function() { - if (scope.clientError) { - return "Error occurred"; - } else if (scope.wasConnected) { - return "Connection lost, reconnecting..."; - } else { - return "Connecting..."; - } - }; - - // Called to reload the page. - scope.reload = function() { - $window.location.reload(); - }; - - // Called to when the connection status of the region - // changes. Updates the scope connected and error values. - var watchConnection = function() { - // Do nothing if already a client error. - if (scope.clientError) { - return; - } - - // Set connected and the time it changed. - var connected = RegionConnection.isConnected(); - if (connected !== scope.connected) { - scope.connected = connected; - if (!connected) { - scope.showDisconnected = false; - - // Show disconnected after 1/2 second. This removes - // the flicker that can occur, if it disconnecets - // and reconnected quickly. - markDisconnected = $timeout(function() { - scope.showDisconnected = true; - markDisconnected = undefined; - }, 500); - } - } - - // Set error and whether of not the connection - // has ever been made. - scope.error = RegionConnection.error; - if (!scope.wasConnected && connected) { - scope.wasConnected = true; - } - }; - - // Watch the isConnected and error value on the - // RegionConnection. - scope.$watch(function() { - return RegionConnection.isConnected(); - }, watchConnection); - scope.$watch(function() { - return RegionConnection.error; - }, watchConnection); - - // Called then the error value on the ErrorService changes. - var watchError = function() { - var error = ErrorService._error; - if (angular.isString(error)) { - scope.clientError = true; - scope.error = ErrorService._error; - } - }; - - // Watch _error on the ErrorService. - scope.$watch(function() { - return ErrorService._error; - }, watchError); - - // Cancel the timeout on scope destroy. - scope.$on("$destroy", function() { - if (angular.isDefined(markDisconnected)) { - $timeout.cancel(markDisconnected); - } - }); + // Show disconnected after 1/2 second. This removes + // the flicker that can occur, if it disconnecets + // and reconnected quickly. + markDisconnected = $timeout(function() { + scope.showDisconnected = true; + markDisconnected = undefined; + }, 500); + } } - }; + + // Set error and whether of not the connection + // has ever been made. + scope.error = RegionConnection.error; + if (!scope.wasConnected && connected) { + scope.wasConnected = true; + } + }; + + // Watch the isConnected and error value on the + // RegionConnection. + scope.$watch(function() { + return RegionConnection.isConnected(); + }, watchConnection); + scope.$watch(function() { + return RegionConnection.error; + }, watchConnection); + + // Called then the error value on the ErrorService changes. + var watchError = function() { + var error = ErrorService._error; + if (angular.isString(error)) { + scope.clientError = true; + scope.error = ErrorService._error; + } + }; + + // Watch _error on the ErrorService. + scope.$watch(function() { + return ErrorService._error; + }, watchError); + + // Cancel the timeout on scope destroy. + scope.$on("$destroy", function() { + if (angular.isDefined(markDisconnected)) { + $timeout.cancel(markDisconnected); + } + }); + } + }; } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/error_toggle.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/error_toggle.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/error_toggle.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/error_toggle.js 2019-06-01 02:18:13.000000000 +0000 @@ -9,60 +9,59 @@ /* @ngInject */ function maasErrorToggle($timeout, RegionConnection, ErrorService) { - return { - restrict: "A", - link: function(scope, element, attrs) { - - // Holds timeout promise for setting ng-hide when - // connection is lost. - var disconnectedPromise; - - // Cancel the disconnected timeout. - var cancelTimeout = function() { - if (angular.isDefined(disconnectedPromise)) { - $timeout.cancel(disconnectedPromise); - disconnectedPromise = undefined; - } - }; - - // Called to when the connection status of the region - // changes or the error on the ErrorService is set. - // The element is shown when connected and no errors. - var watchConnectionAndError = function() { - var connected = RegionConnection.isConnected(); - var error = ErrorService._error; - if (connected && !angular.isString(error)) { - cancelTimeout(); - element.removeClass("ng-hide"); - } else if (angular.isString(error)) { - cancelTimeout(); - element.addClass("ng-hide"); - } else if (!connected) { - // Hide the element after 1/2 second. This stops - // flickering when the connection goes down and - // reconnects quickly. - cancelTimeout(); - disconnectedPromise = $timeout(function() { - element.addClass("ng-hide"); - }, 500); - } - }; - - // Watch the RegionConnection.isConnected() and - // ErrorService._error value. - scope.$watch(function() { - return RegionConnection.isConnected(); - }, watchConnectionAndError); - scope.$watch(function() { - return ErrorService._error; - }, watchConnectionAndError); - - // Cancel disconnect timeout on destroy. - scope.$on("$destroy", function() { - cancelTimeout(); - }); + return { + restrict: "A", + link: function(scope, element, attrs) { + // Holds timeout promise for setting ng-hide when + // connection is lost. + var disconnectedPromise; + + // Cancel the disconnected timeout. + var cancelTimeout = function() { + if (angular.isDefined(disconnectedPromise)) { + $timeout.cancel(disconnectedPromise); + disconnectedPromise = undefined; } - }; + }; + + // Called to when the connection status of the region + // changes or the error on the ErrorService is set. + // The element is shown when connected and no errors. + var watchConnectionAndError = function() { + var connected = RegionConnection.isConnected(); + var error = ErrorService._error; + if (connected && !angular.isString(error)) { + cancelTimeout(); + element.removeClass("ng-hide"); + } else if (angular.isString(error)) { + cancelTimeout(); + element.addClass("ng-hide"); + } else if (!connected) { + // Hide the element after 1/2 second. This stops + // flickering when the connection goes down and + // reconnects quickly. + cancelTimeout(); + disconnectedPromise = $timeout(function() { + element.addClass("ng-hide"); + }, 500); + } + }; + + // Watch the RegionConnection.isConnected() and + // ErrorService._error value. + scope.$watch(function() { + return RegionConnection.isConnected(); + }, watchConnectionAndError); + scope.$watch(function() { + return ErrorService._error; + }, watchConnectionAndError); + + // Cancel disconnect timeout on destroy. + scope.$on("$destroy", function() { + cancelTimeout(); + }); + } + }; } export default maasErrorToggle; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/ipranges.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/ipranges.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/ipranges.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/ipranges.js 2019-06-01 02:18:13.000000000 +0000 @@ -2,171 +2,169 @@ * GNU Affero General Public License version 3 (see the file LICENSE). * * IP Ranges directive. -*/ + */ /* @ngInject */ function maasIpRanges( - IPRangesManager, UsersManager, - ManagerHelperService, ConverterService) { - return { - restrict: "E", - scope: { - subnet: "=", - vlan: "=" - }, - templateUrl: ( - 'static/partials/ipranges.html?v=' + ( - MAAS_config.files_version)), - controller: IpRangesController - }; - - /* @ngInject */ - function IpRangesController($scope) { - $scope.loading = true; - $scope.ipranges = IPRangesManager.getItems(); - $scope.iprangeManager = IPRangesManager; - $scope.newRange = null; + IPRangesManager, + UsersManager, + ManagerHelperService, + ConverterService +) { + return { + restrict: "E", + scope: { + subnet: "=", + vlan: "=" + }, + templateUrl: "static/partials/ipranges.html?v=" + MAAS_config.files_version, + controller: IpRangesController + }; + + /* @ngInject */ + function IpRangesController($scope) { + $scope.loading = true; + $scope.ipranges = IPRangesManager.getItems(); + $scope.iprangeManager = IPRangesManager; + $scope.newRange = null; + $scope.editIPRange = null; + $scope.deleteIPRange = null; + $scope.MAAS_VERSION_NUMBER = IPRangesManager.formatMAASVersionNumber(); + + $scope.RESERVE_RANGE = { + name: "reserve_range", + title: "Reserve range", + selectedTitle: "Reserve range", + objectName: "reserveRange" + }; + + $scope.RESERVE_DYNAMIC_RANGE = { + name: "reserve_dynamic_range", + title: "Reserve dynamic range", + selectedTitle: "Reserve dynamic range", + objectName: "reserveDynamicRange" + }; + + $scope.actionOptions = [$scope.RESERVE_RANGE, $scope.RESERVE_DYNAMIC_RANGE]; + + $scope.actionChanged = function() { + var actionOptionName = $scope.actionOption + ? $scope.actionOption.name + : null; + + if (actionOptionName === "reserve_range") { + $scope.addRange("reserved"); + } + + if (actionOptionName === "reserve_dynamic_range") { + $scope.addRange("dynamic"); + } + }; + + // Return true if the authenticated user is super user. + $scope.isSuperUser = function() { + return UsersManager.isSuperUser(); + }; + + // Called to start adding a new IP range. + $scope.addRange = function(type) { + $scope.newRange = { + type: type, + start_ip: "", + end_ip: "", + comment: "" + }; + if (angular.isObject($scope.subnet)) { + $scope.newRange.subnet = $scope.subnet.id; + } + if (angular.isObject($scope.vlan)) { + $scope.newRange.vlan = $scope.vlan.id; + } + if (type === "dynamic") { + $scope.newRange.comment = "Dynamic"; + } + }; + + // Cancel adding the new IP range. + $scope.cancelAddRange = function() { + $scope.newRange = null; + $scope.actionOption = null; + }; + + // Return true if the IP range can be modified by the + // authenticated user. + $scope.ipRangeCanBeModified = function(range) { + if ($scope.isSuperUser()) { + return true; + } else { + // Can only modify reserved and same user. + return ( + range.type === "reserved" && + range.user === UsersManager.getAuthUser().id + ); + } + }; + + // Return true if the IP range is in edit mode. + $scope.isIPRangeInEditMode = function(range) { + return $scope.editIPRange === range; + }; + + // Toggle edit mode for the IP range. + $scope.ipRangeToggleEditMode = function(range) { + $scope.deleteIPRange = null; + if ($scope.isIPRangeInEditMode(range)) { $scope.editIPRange = null; + } else { + $scope.editIPRange = range; + } + }; + + // Clear edit mode for the IP range. + $scope.ipRangeClearEditMode = function() { + $scope.editIPRange = null; + }; + + // Return true if the IP range is in delete mode. + $scope.isIPRangeInDeleteMode = function(range) { + return $scope.deleteIPRange === range; + }; + + // Enter delete mode for the IP range. + $scope.ipRangeEnterDeleteMode = function(range) { + $scope.editIPRange = null; + $scope.deleteIPRange = range; + }; + + // Exit delete mode for the IP range. + $scope.ipRangeCancelDelete = function() { + $scope.deleteIPRange = null; + }; + + // Perform the delete operation on the IP range. + $scope.ipRangeConfirmDelete = function() { + IPRangesManager.deleteItem($scope.deleteIPRange).then(function() { $scope.deleteIPRange = null; - $scope.MAAS_VERSION_NUMBER - = IPRangesManager.formatMAASVersionNumber(); + }); + }; - $scope.RESERVE_RANGE = { - name: 'reserve_range', - title: 'Reserve range', - selectedTitle: 'Reserve range', - objectName: 'reserveRange' - }; - - $scope.RESERVE_DYNAMIC_RANGE = { - name: 'reserve_dynamic_range', - title: 'Reserve dynamic range', - selectedTitle: 'Reserve dynamic range', - objectName: 'reserveDynamicRange' - }; - - $scope.actionOptions = [ - $scope.RESERVE_RANGE, - $scope.RESERVE_DYNAMIC_RANGE - ]; - - $scope.actionChanged = function() { - var actionOptionName = - $scope.actionOption ? $scope.actionOption.name : null; - - if (actionOptionName === 'reserve_range') { - $scope.addRange('reserved'); - } - - if (actionOptionName === 'reserve_dynamic_range') { - $scope.addRange('dynamic'); - } - }; - - // Return true if the authenticated user is super user. - $scope.isSuperUser = function() { - return UsersManager.isSuperUser(); - }; - - // Called to start adding a new IP range. - $scope.addRange = function(type) { - $scope.newRange = { - type: type, - start_ip: "", - end_ip: "", - comment: "" - }; - if (angular.isObject($scope.subnet)) { - $scope.newRange.subnet = $scope.subnet.id; - } - if (angular.isObject($scope.vlan)) { - $scope.newRange.vlan = $scope.vlan.id; - } - if (type === "dynamic") { - $scope.newRange.comment = "Dynamic"; - } - }; - - // Cancel adding the new IP range. - $scope.cancelAddRange = function() { - $scope.newRange = null; - $scope.actionOption = null; - }; - - // Return true if the IP range can be modified by the - // authenticated user. - $scope.ipRangeCanBeModified = function(range) { - if ($scope.isSuperUser()) { - return true; - } else { - // Can only modify reserved and same user. - return ( - range.type === "reserved" && - range.user === UsersManager.getAuthUser().id); - } - }; - - // Return true if the IP range is in edit mode. - $scope.isIPRangeInEditMode = function(range) { - return $scope.editIPRange === range; - }; - - // Toggle edit mode for the IP range. - $scope.ipRangeToggleEditMode = function(range) { - $scope.deleteIPRange = null; - if ($scope.isIPRangeInEditMode(range)) { - $scope.editIPRange = null; - } else { - $scope.editIPRange = range; - } - }; - - // Clear edit mode for the IP range. - $scope.ipRangeClearEditMode = function() { - $scope.editIPRange = null; - }; - - // Return true if the IP range is in delete mode. - $scope.isIPRangeInDeleteMode = function(range) { - return $scope.deleteIPRange === range; - }; - - // Enter delete mode for the IP range. - $scope.ipRangeEnterDeleteMode = function(range) { - $scope.editIPRange = null; - $scope.deleteIPRange = range; - }; - - // Exit delete mode for the IP range. - $scope.ipRangeCancelDelete = function() { - $scope.deleteIPRange = null; - }; - - // Perform the delete operation on the IP range. - $scope.ipRangeConfirmDelete = function() { - IPRangesManager.deleteItem( - $scope.deleteIPRange).then(function() { - $scope.deleteIPRange = null; - }); - }; - - // Sort ranges by starting IP address. - $scope.ipRangeSort = function(range) { - if (range.start_ip.indexOf(':') !== -1) { - return ConverterService.ipv6Expand(range.start_ip); - } else { - return ConverterService.ipv4ToInteger(range.start_ip); - } - }; - - // Load the required managers. - ManagerHelperService.loadManagers($scope, [ - IPRangesManager, UsersManager]).then( - function() { - $scope.loading = false; - }); - } + // Sort ranges by starting IP address. + $scope.ipRangeSort = function(range) { + if (range.start_ip.indexOf(":") !== -1) { + return ConverterService.ipv6Expand(range.start_ip); + } else { + return ConverterService.ipv4ToInteger(range.start_ip); + } + }; + // Load the required managers. + ManagerHelperService.loadManagers($scope, [ + IPRangesManager, + UsersManager + ]).then(function() { + $scope.loading = false; + }); + } } export default maasIpRanges; diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/login.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/login.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/login.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/login.js 2019-06-01 02:18:13.000000000 +0000 @@ -4,67 +4,67 @@ * Login button for external authentication. */ -const bakery = require('macaroon-bakery'); +const bakery = require("macaroon-bakery"); export function getBakery() { - return function(visitPage) { - return new bakery.Bakery({ - storage: new bakery.BakeryStorage(localStorage, {}), - visitPage: visitPage - }); - }; + return function(visitPage) { + return new bakery.Bakery({ + storage: new bakery.BakeryStorage(localStorage, {}), + visitPage: visitPage + }); + }; } /* @ngInject */ export function externalLogin($window, getBakery) { - return { - restrict: 'E', - scope: {}, - template: [ - '', - ' Go to login page', - '', - '
', - ' Error getting login link:
', - ' {{ errorMessage }}', - '
', - ].join(''), - controller: ExternalLoginController - }; + return { + restrict: "E", + scope: {}, + template: [ + '', + " Go to login page", + "", + '
', + " Error getting login link:
", + " {{ errorMessage }}", + "
" + ].join(""), + controller: ExternalLoginController + }; - /* @ngInject */ - function ExternalLoginController($scope, $element) { - $scope.errorMessage = ''; - $scope.loginURL = '#'; - $scope.externalAuthURL = $element.attr('auth-url'); + /* @ngInject */ + function ExternalLoginController($scope, $element) { + $scope.errorMessage = ""; + $scope.loginURL = "#"; + $scope.externalAuthURL = $element.attr("auth-url"); - const visitPage = function(error) { - $scope.$apply(function() { - $scope.loginURL = error.Info.VisitURL; - $scope.errorMessage = ''; - }); - }; - const bakery = getBakery(visitPage); - const nextPath = $element.attr('next-path'); - bakery.get( - '/MAAS/accounts/discharge-request/', - { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - function(error, response) { - if (response.currentTarget.status != 200) { - $scope.$apply(function() { - $scope.errorMessage = ( - response.currentTarget.responseText); - }); - localStorage.clear(); - } else { - $window.location.replace(nextPath); - } - }); - } + const visitPage = function(error) { + $scope.$apply(function() { + $scope.loginURL = error.Info.VisitURL; + $scope.errorMessage = ""; + }); + }; + const bakery = getBakery(visitPage); + const nextPath = $element.attr("next-path"); + bakery.get( + "/MAAS/accounts/discharge-request/", + { + Accept: "application/json", + "Content-Type": "application/json" + }, + function(error, response) { + if (response.currentTarget.status != 200) { + $scope.$apply(function() { + $scope.errorMessage = response.currentTarget.responseText; + }); + localStorage.clear(); + } else { + $window.location.replace(nextPath); + } + } + ); + } } diff -Nru maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/maas_obj_form.js maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/maas_obj_form.js --- maas-2.6.0~beta2-7695-g691e14ea3/src/maasserver/static/js/angular/directives/maas_obj_form.js 2019-04-27 18:13:25.000000000 +0000 +++ maas-2.6.0-7802-g59416a869/src/maasserver/static/js/angular/directives/maas_obj_form.js 2019-06-01 02:18:13.000000000 +0000 @@ -9,1208 +9,1245 @@ /* @ngInject */ export function maasObjForm(JSONService) { + /* @ngInject */ + function MAASFormController($scope) { + "ngInject"; + this.obj = $scope.obj; + this.manager = $scope.manager; + this.fields = {}; + this.scope = $scope; + this.scope.saving = false; + this.scope.savingKeys = []; + if (angular.isObject(this.scope.obj)) { + this.scope.obj.$maasForm = this; + } - /* @ngInject */ - function MAASFormController($scope) { - 'ngInject'; - this.obj = $scope.obj; - this.manager = $scope.manager; - this.fields = {}; - this.scope = $scope; - this.scope.saving = false; - this.scope.savingKeys = []; - if (angular.isObject(this.scope.obj)) { - this.scope.obj.$maasForm = this; - } - - // Set the managerMethod. - this.managerMethod = $scope.managerMethod; - if (angular.isUndefined(this.managerMethod)) { - this.managerMethod = "updateItem"; - } + // Set the managerMethod. + this.managerMethod = $scope.managerMethod; + if (angular.isUndefined(this.managerMethod)) { + this.managerMethod = "updateItem"; + } - var self = this; - $scope.$watch("obj", function() { - // Update the object when it changes. - self.obj = $scope.obj; - if (angular.isObject(self.obj)) { - self.obj.$maasForm = self; - } - }); - $scope.$on("$destroy", function() { - // Remove the $maasForm from the object when directive is - // deleted. - if (angular.isObject(self.obj)) { - delete self.obj.$maasForm; - } - }); + var self = this; + $scope.$watch("obj", function() { + // Update the object when it changes. + self.obj = $scope.obj; + if (angular.isObject(self.obj)) { + self.obj.$maasForm = self; + } + }); + $scope.$on("$destroy", function() { + // Remove the $maasForm from the object when directive is + // deleted. + if (angular.isObject(self.obj)) { + delete self.obj.$maasForm; + } + }); + } + + // Get the current value for a field in the form. + MAASFormController.prototype.getValue = function(key) { + var field = this.fields[key]; + if (angular.isObject(field) && angular.isObject(field.scope)) { + return field.scope.getValue(); } + }; - // Get the current value for a field in the form. - MAASFormController.prototype.getValue = function(key) { - var field = this.fields[key]; - if (angular.isObject(field) && angular.isObject(field.scope)) { - return field.scope.getValue(); - } - }; + // Update the current value for a field in the form. + MAASFormController.prototype.updateValue = function(key, value) { + var field = this.fields[key]; + if (angular.isObject(field) && angular.isObject(field.scope)) { + return field.scope.updateValue(value); + } + }; - // Update the current value for a field in the form. - MAASFormController.prototype.updateValue = function(key, value) { - var field = this.fields[key]; - if (angular.isObject(field) && angular.isObject(field.scope)) { - return field.scope.updateValue(value); - } - }; + // Clone the current object for this form without the $maasForm + // property set. + MAASFormController.prototype.cloneObject = function() { + if (!angular.isObject(this.obj)) { + return this.obj; + } else { + delete this.obj.$maasForm; + var clonedObj = angular.copy(this.obj); + this.obj.$maasForm = this; + return clonedObj; + } + }; - // Clone the current object for this form without the $maasForm - // property set. - MAASFormController.prototype.cloneObject = function() { - if (!angular.isObject(this.obj)) { - return this.obj; - } else { - delete this.obj.$maasForm; - var clonedObj = angular.copy(this.obj); - this.obj.$maasForm = this; - return clonedObj; - } - }; + // Return true if table form. + MAASFormController.prototype.isTableForm = function() { + if (angular.isUndefined(this.scope.tableForm)) { + // Default is not a table form. + return false; + } else { + return this.scope.tableForm; + } + }; - // Return true if table form. - MAASFormController.prototype.isTableForm = function() { - if (angular.isUndefined(this.scope.tableForm)) { - // Default is not a table form. - return false; - } else { - return this.scope.tableForm; - } - }; + // Return true if the form should be saved on blur. + MAASFormController.prototype.saveOnBlur = function() { + if (angular.isUndefined(this.scope.saveOnBlur)) { + // Default is save on blur. + return true; + } else { + return this.scope.saveOnBlur; + } + }; - // Return true if the form should be saved on blur. - MAASFormController.prototype.saveOnBlur = function() { - if (angular.isUndefined(this.scope.saveOnBlur)) { - // Default is save on blur. - return true; - } else { - return this.scope.saveOnBlur; - } - }; + // Return true if the form is saving this field. + MAASFormController.prototype.isSaving = function(key) { + return this.scope.saving && this.scope.savingKeys.indexOf(key) >= 0; + }; + + // Return true if the input should show the saving spinner. This is + // only show on inputs in forms that are using save on blur. + MAASFormController.prototype.showInputSaving = function(key) { + return this.saveOnBlur() && this.isSaving(key); + }; + + // Return true if any field in the form as an error. + MAASFormController.prototype.hasErrors = function() { + var hasErrors = false; + angular.forEach(this.fields, function(field) { + if (field.scope.hasErrors()) { + hasErrors = true; + } + }); + if (angular.isDefined(this.errorScope)) { + if (this.errorScope.hasErrors()) { + hasErrors = true; + } + } + return hasErrors; + }; - // Return true if the form is saving this field. - MAASFormController.prototype.isSaving = function(key) { - return ( - this.scope.saving && this.scope.savingKeys.indexOf(key) >= 0); - }; + // Called by maas-obj-field to register it as a editable field. + MAASFormController.prototype.registerField = function(key, scope) { + // Store the state of the field and its scope. + this.fields[key] = { + editing: false, + scope: scope + }; + + // Watch for changes on the value of the object. + var self = this; + this.scope.$watch("obj." + key, function() { + if (angular.isObject(self.obj) && !self.fields[key].editing) { + self.fields[key].scope.updateValue(self.obj[key]); + } + }); + + // Return the current value for the field. + if (angular.isObject(this.obj)) { + return this.obj[key]; + } else { + return null; + } + }; - // Return true if the input should show the saving spinner. This is - // only show on inputs in forms that are using save on blur. - MAASFormController.prototype.showInputSaving = function(key) { - return this.saveOnBlur() && this.isSaving(key); - }; + // Called by maas-obj-field to unregister it as a editable field. + MAASFormController.prototype.unregisterField = function(key) { + delete this.fields[key]; + }; + + // Called by maas-obj-field to place field in edit mode. + MAASFormController.prototype.startEditingField = function(key) { + this.fields[key].editing = true; + }; + + // Called by maas-obj-field to end edit mode for the field. + MAASFormController.prototype.stopEditingField = function(key, value) { + var field = this.fields[key]; + + // Do nothing if not save on blur. + if (!this.saveOnBlur()) { + field.editing = false; + return; + } - // Return true if any field in the form as an error. - MAASFormController.prototype.hasErrors = function() { - var hasErrors = false; - angular.forEach(this.fields, function(field) { - if (field.scope.hasErrors()) { - hasErrors = true; - } - }); - if (angular.isDefined(this.errorScope)) { - if (this.errorScope.hasErrors()) { - hasErrors = true; - } - } - return hasErrors; - }; + // Clear errors before saving. + field.scope.clearErrors(); - // Called by maas-obj-field to register it as a editable field. - MAASFormController.prototype.registerField = function(key, scope) { - // Store the state of the field and its scope. - this.fields[key] = { - editing: false, - scope: scope - }; + // Copy the object and update the editing field. + var updatedObj = this.cloneObject(); + updatedObj[key] = value; + if (updatedObj[key] === this.obj[key]) { + // Nothing changed. + field.editing = false; + return; + } - // Watch for changes on the value of the object. - var self = this; - this.scope.$watch("obj." + key, function() { - if (angular.isObject(self.obj) && !self.fields[key].editing) { - self.fields[key].scope.updateValue(self.obj[key]); - } - }); + // Update the item in the manager. + this.scope.saving = true; + this.scope.savingKeys = [key]; + this.updateItem(updatedObj, [key]); + }; + + // Update the item using the manager. + MAASFormController.prototype.updateItem = function(updatedObj, keys) { + var key = keys[0]; + var field = this.fields[key]; + var self = this; + + // Pre-process the updatedObj if one is defined. + if (angular.isFunction(this.scope.preProcess)) { + updatedObj = this.scope.preProcess(updatedObj, keys); + } - // Return the current value for the field. - if (angular.isObject(this.obj)) { - return this.obj[key]; + // Update the item with the manager. + return this.manager[this.managerMethod](updatedObj).then( + function(newObj) { + // Update the value of the element. + field.editing = false; + field.scope.updateValue(newObj[key]); + self.scope.saving = false; + self.scope.savingKeys = []; + if (angular.isFunction(self.scope.afterSave)) { + self.scope.afterSave(newObj); + } + return newObj; + }, + function(error) { + var errorJson = JSONService.tryParse(error); + if (angular.isObject(errorJson)) { + // Add the error to each field it matches. + angular.forEach(errorJson, function(value, key) { + var errorField = self.fields[key]; + if (!angular.isArray(value)) { + value = [value]; + } + + if (angular.isObject(errorField)) { + // Error on a field we know about, place the + // error on that field. + errorField.scope.setErrors(value); + } else { + // Error on a field we don't know about, place + // the error on the editing field. Prefixing + // the error with the field. + if (key !== "__all__") { + value = value.map(function(v) { + return key + ": " + v; + }); + } + field.scope.setErrors(value); + } + }); } else { - return null; + // Add the string error to just the field error. + field.scope.setErrors([error]); } - }; - - // Called by maas-obj-field to unregister it as a editable field. - MAASFormController.prototype.unregisterField = function(key) { - delete this.fields[key]; - }; - - // Called by maas-obj-field to place field in edit mode. - MAASFormController.prototype.startEditingField = function(key) { - this.fields[key].editing = true; - }; - - // Called by maas-obj-field to end edit mode for the field. - MAASFormController.prototype.stopEditingField = function(key, value) { - var field = this.fields[key]; - - // Do nothing if not save on blur. - if (!this.saveOnBlur()) { - field.editing = false; - return; - } - - // Clear errors before saving. - field.scope.clearErrors(); - - // Copy the object and update the editing field. - var updatedObj = this.cloneObject(); - updatedObj[key] = value; - if (updatedObj[key] === this.obj[key]) { - // Nothing changed. - field.editing = false; - return; - } - - // Update the item in the manager. - this.scope.saving = true; - this.scope.savingKeys = [key]; - this.updateItem(updatedObj, [key]); - }; - - // Update the item using the manager. - MAASFormController.prototype.updateItem = function(updatedObj, keys) { - var key = keys[0]; - var field = this.fields[key]; - var self = this; + self.scope.saving = false; + self.scope.savingKeys = []; + return error; + } + ); + }; + + // Called when saveOnBlur is false to save the whole form. + MAASFormController.prototype.saveForm = function() { + var keys = []; + var updatedObj = this.cloneObject(); + angular.forEach(this.fields, function(value, key) { + value.scope.clearErrors(); + var newValue = value.scope.getValue(); + if (angular.isDefined(newValue) && updatedObj[key] !== newValue) { + updatedObj[key] = newValue; + keys.push(key); + } + }); + + // Pre-process the updatedObj if one is defined. + if (angular.isFunction(this.scope.preProcess)) { + updatedObj = this.scope.preProcess(updatedObj, keys); + } - // Pre-process the updatedObj if one is defined. - if (angular.isFunction(this.scope.preProcess)) { - updatedObj = this.scope.preProcess(updatedObj, keys); - } - - // Update the item with the manager. - return this.manager[this.managerMethod]( - updatedObj).then(function(newObj) { - // Update the value of the element. - field.editing = false; - field.scope.updateValue(newObj[key]); - self.scope.saving = false; - self.scope.savingKeys = []; - if (angular.isFunction(self.scope.afterSave)) { - self.scope.afterSave(newObj); - } - return newObj; - }, function(error) { - var errorJson = JSONService.tryParse(error); - if (angular.isObject(errorJson)) { - // Add the error to each field it matches. - angular.forEach(errorJson, function(value, key) { - var errorField = self.fields[key]; - if (!angular.isArray(value)) { - value = [value]; - } - - if (angular.isObject(errorField)) { - // Error on a field we know about, place the - // error on that field. - errorField.scope.setErrors(value); - } else { - // Error on a field we don't know about, place - // the error on the editing field. Prefixing - // the error with the field. - if (key !== "__all__") { - value = value.map(function(v) { - return key + ": " + v; - }); - } - field.scope.setErrors(value); - } - }); - } else { - // Add the string error to just the field error. - field.scope.setErrors([error]); - } - self.scope.saving = false; - self.scope.savingKeys = []; - return error; - }); - }; + // Clear the errors on the errorScope before save. + if (angular.isDefined(this.errorScope)) { + this.errorScope.clearErrors(); + } - // Called when saveOnBlur is false to save the whole form. - MAASFormController.prototype.saveForm = function() { - var keys = []; - var updatedObj = this.cloneObject(); - angular.forEach(this.fields, function(value, key) { - value.scope.clearErrors(); - var newValue = value.scope.getValue(); - if (angular.isDefined(newValue) && - updatedObj[key] !== newValue) { - updatedObj[key] = newValue; - keys.push(key); + var self = this; + this.scope.saving = true; + this.scope.savingKeys = keys; + return this.manager[this.managerMethod](updatedObj).then( + function(newObj) { + self.scope.saving = false; + self.scope.savingKeys = []; + if (angular.isFunction(self.scope.afterSave)) { + self.scope.afterSave(newObj); + } + return newObj; + }, + function(error) { + var errorJson = JSONService.tryParse(error); + if (angular.isObject(errorJson)) { + // Add the error to each field it matches. + angular.forEach(errorJson, function(value, key) { + var errorField = self.fields[key]; + if (!angular.isArray(value)) { + value = [value]; + } + + if (angular.isObject(errorField)) { + // Error on a field we know about, place the + // error on that field. + errorField.scope.setErrors(value); + } else { + if (key !== "__all__") { + value = value.map(function(v) { + return key + ": " + v; + }); + } + // Error on a field we don't know about, place + // the error on errorScope if set. + if (angular.isDefined(self.errorScope)) { + self.errorScope.setErrors(value); + } else { + // No error scope, just log to console. + console.log(value); + } } - }); - - - // Pre-process the updatedObj if one is defined. - if (angular.isFunction(this.scope.preProcess)) { - updatedObj = this.scope.preProcess(updatedObj, keys); - } - - // Clear the errors on the errorScope before save. - if (angular.isDefined(this.errorScope)) { - this.errorScope.clearErrors(); - } - - var self = this; - this.scope.saving = true; - this.scope.savingKeys = keys; - return this.manager[this.managerMethod]( - updatedObj).then(function(newObj) { - self.scope.saving = false; - self.scope.savingKeys = []; - if (angular.isFunction(self.scope.afterSave)) { - self.scope.afterSave(newObj); - } - return newObj; - }, function(error) { - var errorJson = JSONService.tryParse(error); - if (angular.isObject(errorJson)) { - // Add the error to each field it matches. - angular.forEach(errorJson, function(value, key) { - var errorField = self.fields[key]; - if (!angular.isArray(value)) { - value = [value]; - } - - if (angular.isObject(errorField)) { - // Error on a field we know about, place the - // error on that field. - errorField.scope.setErrors(value); - } else { - if (key !== "__all__") { - value = value.map(function(v) { - return key + ": " + v; - }); - } - // Error on a field we don't know about, place - // the error on errorScope if set. - if (angular.isDefined(self.errorScope)) { - self.errorScope.setErrors(value); - } else { - // No error scope, just log to console. - console.log(value); - } - } - }); - } else { - // Add the string error to just the field error. - if (angular.isDefined(self.errorScope)) { - self.errorScope.setErrors([error]); - } else { - // No error scope, just log to console. - console.log(error); - } - } - self.scope.saving = false; - self.scope.savingKeys = []; - return error; - }); - }; - - return { - restrict: "E", - scope: { - obj: "=", - manager: "=", - managerMethod: "@", - preProcess: "=", - afterSave: "=", - tableForm: "=", - saveOnBlur: "=", - inline: "=", - ngDisabled: "&" - }, - transclude: true, - template: ( - '
'), - controller: MAASFormController - }; + }); + } else { + // Add the string error to just the field error. + if (angular.isDefined(self.errorScope)) { + self.errorScope.setErrors([error]); + } else { + // No error scope, just log to console. + console.log(error); + } + } + self.scope.saving = false; + self.scope.savingKeys = []; + return error; + } + ); + }; + + return { + restrict: "E", + scope: { + obj: "=", + manager: "=", + managerMethod: "@", + preProcess: "=", + afterSave: "=", + tableForm: "=", + saveOnBlur: "=", + inline: "=", + ngDisabled: "&" + }, + transclude: true, + template: + '
", + controller: MAASFormController + }; } export function maasObjFieldGroup() { - function MAASGroupController($scope, $timeout) { - 'ngInject'; - this.fields = {}; - this.scope = $scope; - this.scope.saving = false; - this.scope.savingKeys = []; - this.timeout = $timeout; - - var self = this; - this.scope.isEditing = function() { - var editing = false; - angular.forEach(self.fields, function(value) { - if (!editing) { - editing = value.editing; - } - }); - return editing; - }; + function MAASGroupController($scope, $timeout) { + "ngInject"; + this.fields = {}; + this.scope = $scope; + this.scope.saving = false; + this.scope.savingKeys = []; + this.timeout = $timeout; + + var self = this; + this.scope.isEditing = function() { + var editing = false; + angular.forEach(self.fields, function(value) { + if (!editing) { + editing = value.editing; + } + }); + return editing; + }; + } + + // Return true if table form. + MAASGroupController.prototype.isTableForm = function() { + return this.formController.isTableForm(); + }; + + // Return true if should save on blur. + MAASGroupController.prototype.saveOnBlur = function() { + return this.formController.saveOnBlur(); + }; + + // Return true if group is saving. + MAASGroupController.prototype.isSaving = function(key) { + return this.scope.saving && this.scope.savingKeys.indexOf(key) >= 0; + }; + + // Return true if the input should show the saving spinner. This is + // only show on inputs in forms that are using save on blur. + MAASGroupController.prototype.showInputSaving = function(key) { + // In a group we say the entire group is saving, not just that + // one key in the field is being saved. + return this.saveOnBlur() && this.scope.saving; + }; + + // Called by maas-obj-field to register it as a editable field. + MAASGroupController.prototype.registerField = function(key, scope) { + // Store the state of the field and its scope. + this.fields[key] = { + editing: false, + scope: scope + }; + return this.formController.registerField(key, scope); + }; + + // Called by maas-obj-field to unregister it as a editable field. + MAASGroupController.prototype.unregisterField = function(key) { + delete this.fields[key]; + this.formController.unregisterField(key); + }; + + // Called by maas-obj-field to place field in edit mode. + MAASGroupController.prototype.startEditingField = function(key) { + this.fields[key].editing = true; + + // Set all fields in the group as editing in the formController. + var self = this; + angular.forEach(this.fields, function(value, key) { + self.formController.startEditingField(key); + }); + }; + + // Called by maas-obj-field to exit edit mode for the field. + MAASGroupController.prototype.stopEditingField = function(key, value) { + var field = this.fields[key]; + field.editing = false; + + // Exit early if not save on blur. + if (!this.saveOnBlur()) { + return; } - // Return true if table form. - MAASGroupController.prototype.isTableForm = function() { - return this.formController.isTableForm(); - }; - - // Return true if should save on blur. - MAASGroupController.prototype.saveOnBlur = function() { - return this.formController.saveOnBlur(); - }; - - // Return true if group is saving. - MAASGroupController.prototype.isSaving = function(key) { - return ( - this.scope.saving && this.scope.savingKeys.indexOf(key) >= 0); - }; - - // Return true if the input should show the saving spinner. This is - // only show on inputs in forms that are using save on blur. - MAASGroupController.prototype.showInputSaving = function(key) { - // In a group we say the entire group is saving, not just that - // one key in the field is being saved. - return this.saveOnBlur() && this.scope.saving; - }; - - // Called by maas-obj-field to register it as a editable field. - MAASGroupController.prototype.registerField = function(key, scope) { - // Store the state of the field and its scope. - this.fields[key] = { - editing: false, - scope: scope - }; - return this.formController.registerField(key, scope); - }; - - // Called by maas-obj-field to unregister it as a editable field. - MAASGroupController.prototype.unregisterField = function(key) { - delete this.fields[key]; - this.formController.unregisterField(key); - }; - - // Called by maas-obj-field to place field in edit mode. - MAASGroupController.prototype.startEditingField = function(key) { - this.fields[key].editing = true; - - // Set all fields in the group as editing in the formController. - var self = this; - angular.forEach(this.fields, function(value, key) { - self.formController.startEditingField(key); + // Delay the handling of stop to make sure start is not called on + // the next field in the group. + var self = this; + this.timeout(function() { + // If any other fields are in edit mode then nothing to do. + var editing = false; + angular.forEach(self.fields, function(value) { + if (!editing) { + editing = value.editing; + } + }); + if (editing) { + return; + } + + // Copy the object and update the editing fields. + var keys = []; + var changed = false; + var updatedObj = self.formController.cloneObject(); + angular.forEach(self.fields, function(value, key) { + value.scope.clearErrors(); + var newValue = value.scope.getValue(); + if (angular.isDefined(newValue) && updatedObj[key] !== newValue) { + keys.push(key); + updatedObj[key] = newValue; + changed = true; + } + }); + if (!changed) { + return; + } + + // Place the field that actually triggered the update first. + var keyIdx = keys.indexOf(key); + if (keyIdx !== -1) { + keys.splice(keyIdx, 1); + keys.splice(0, 0, key); + } + + // Save the object. + self.scope.saving = true; + self.scope.savingKeys = keys; + self.formController.updateItem(updatedObj, keys).then( + function(obj) { + self.scope.saving = false; + self.scope.savingKeys = []; + return obj; + }, + function(error) { + self.scope.saving = false; + self.scope.savingKeys = []; + return error; + } + ); + }, 10); // Really short has to be next click. + }; + + return { + restrict: "E", + require: ["^^maasObjForm", "maasObjFieldGroup"], + scope: {}, + transclude: true, + template: + '
", + controller: MAASGroupController, + link: { + pre: function(scope, element, attrs, controllers) { + // Set formController on the MAASGroupController to + // point to its parent MAASFormController. This is done in + // pre-link so the controller has the formController before + // registerField is called. + controllers[1].formController = controllers[0]; + + // Set ngDisabled on this scope from the form controller. + scope.ngDisabled = controllers[0].scope.ngDisabled; + + // Set the object to always be the same on the scope. + controllers[0].scope.$watch("obj", function(obj) { + scope.obj = obj; }); - }; - - // Called by maas-obj-field to exit edit mode for the field. - MAASGroupController.prototype.stopEditingField = function(key, value) { - var field = this.fields[key]; - field.editing = false; - - // Exit early if not save on blur. - if (!this.saveOnBlur()) { - return; - } - - // Delay the handling of stop to make sure start is not called on - // the next field in the group. - var self = this; - this.timeout(function() { - // If any other fields are in edit mode then nothing to do. - var editing = false; - angular.forEach(self.fields, function(value) { - if (!editing) { - editing = value.editing; - } - }); - if (editing) { - return; - } - - // Copy the object and update the editing fields. - var keys = []; - var changed = false; - var updatedObj = self.formController.cloneObject(); - angular.forEach(self.fields, function(value, key) { - value.scope.clearErrors(); - var newValue = value.scope.getValue(); - if (angular.isDefined(newValue) && - updatedObj[key] !== newValue) { - keys.push(key); - updatedObj[key] = newValue; - changed = true; - } - }); - if (!changed) { - return; - } - - // Place the field that actually triggered the update first. - var keyIdx = keys.indexOf(key); - if (keyIdx !== -1) { - keys.splice(keyIdx, 1); - keys.splice(0, 0, key); - } - - // Save the object. - self.scope.saving = true; - self.scope.savingKeys = keys; - self.formController.updateItem(updatedObj, keys).then( - function(obj) { - self.scope.saving = false; - self.scope.savingKeys = []; - return obj; - }, function(error) { - self.scope.saving = false; - self.scope.savingKeys = []; - return error; - }); - }, 10); // Really short has to be next click. - }; - - return { - restrict: "E", - require: ["^^maasObjForm", "maasObjFieldGroup"], - scope: {}, - transclude: true, - template: ( - '
'), - controller: MAASGroupController, - link: { - pre: function(scope, element, attrs, controllers) { - // Set formController on the MAASGroupController to - // point to its parent MAASFormController. This is done in - // pre-link so the controller has the formController before - // registerField is called. - controllers[1].formController = controllers[0]; - - // Set ngDisabled on this scope from the form controller. - scope.ngDisabled = controllers[0].scope.ngDisabled; - - // Set the object to always be the same on the scope. - controllers[0].scope.$watch("obj", function(obj) { - scope.obj = obj; - }); - } - } - }; + } + } + }; } /* @ngInject */ export function maasObjField($compile) { - return { - restrict: "E", - require: ["^^maasObjForm", "?^^maasObjFieldGroup"], - scope: { - onChange: "=", - subtleText: "@" - }, - transclude: true, - template: ( - '
'), - link: function(scope, element, attrs, controllers) { - // Select the controller based on which is available. - var controller = controllers[1]; - if (!angular.isObject(controller)) { - controller = controllers[0]; - } + return { + restrict: "E", + require: ["^^maasObjForm", "?^^maasObjFieldGroup"], + scope: { + onChange: "=", + subtleText: "@" + }, + transclude: true, + template: "
", + link: function(scope, element, attrs, controllers) { + // Select the controller based on which is available. + var controller = controllers[1]; + if (!angular.isObject(controller)) { + controller = controllers[0]; + } + + // Set ngDisabled from the parent controller. + scope.ngDisabled = controller.scope.ngDisabled; + + element.addClass("p-form__group"); + if (attrs.subtle !== "false") { + element.addClass("form__group--subtle"); + } + + // type and key required. + var missingAttrs = []; + if (!angular.isString(attrs.type) || attrs.type.length === 0) { + missingAttrs.push("type"); + } + if (!angular.isString(attrs.key) || attrs.key.length === 0) { + missingAttrs.push("key"); + } + if (missingAttrs.length > 0) { + throw new Error( + missingAttrs.join(", ") + " are required on maas-obj-field." + ); + } + if (angular.isString(attrs.disabled)) { + scope.ngDisabled = function() { + return true; + }; + } - // Set ngDisabled from the parent controller. - scope.ngDisabled = controller.scope.ngDisabled; + // Remove transcluded element. + element.find("div").remove(); - element.addClass("p-form__group"); - if (attrs.subtle !== "false") { - element.addClass("form__group--subtle"); - } + // Render the label. + var label = attrs.label || attrs.key; - // type and key required. - var missingAttrs = []; - if (!angular.isString(attrs.type) || attrs.type.length === 0) { - missingAttrs.push("type"); - } - if (!angular.isString(attrs.key) || attrs.key.length === 0) { - missingAttrs.push("key"); - } - if (missingAttrs.length > 0) { - throw new Error( - missingAttrs.join(", ") + - " are required on maas-obj-field."); - } - if (angular.isString(attrs.disabled)) { - scope.ngDisabled = function() { return true; }; + if (attrs.disableLabel !== "true" && !(attrs.type === "hidden")) { + var labelElement = angular.element("